From ce605f9118500c1efafaf577d38377c842159a83 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 20 May 2025 11:40:57 +0200 Subject: [PATCH] Add glob redirects --- docs/contribute/redirects.md | 42 +++++++++++ .../Builder/RedirectFile.cs | 69 ++++++++++++++++--- ...Elastic.Documentation.Configuration.csproj | 4 +- 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/docs/contribute/redirects.md b/docs/contribute/redirects.md index 7cd26603e..d92714744 100644 --- a/docs/contribute/redirects.md +++ b/docs/contribute/redirects.md @@ -104,3 +104,45 @@ redirects: 'old-anchor': 'active-anchor' 'removed-anchor': ``` + +## Glob pattern redirects + +The redirect system also supports glob patterns for redirecting multiple pages at once. This is useful when moving entire sections or directories. + +### Trailing wildcard redirects + +Use the `**` pattern at the end of a path to match all content under a specific path prefix and redirect it to a new location while preserving the path structure. + +```yaml +redirects: + 'reference/apm/agents/android/**': 'reference/opentelemetry/edot-sdks/android/**' + 'old-section/**': 'new-section/**' +``` + +This redirects: +- `reference/apm/agents/android/setup` → `reference/opentelemetry/edot-sdks/android/setup` +- `reference/apm/agents/android/api/classes` → `reference/opentelemetry/edot-sdks/android/api/classes` + +### Simple glob pattern redirects + +You can use basic glob patterns with `*` (single segment wildcard) and `?` (single character wildcard). + +```yaml +redirects: + 'guides/*/intro': 'tutorials/*/getting-started' + 'api/v1/user?.json': 'api/v2/users' +``` + +For simple cases where the target doesn't contain wildcards, the target is used directly: + +```yaml +redirects: + 'docs/v*/setup': 'documentation/setup-guide' +``` + +### Best practices for glob patterns + +1. Use exact path redirects when possible. +2. Use trailing `**` wildcards for redirecting entire directory trees. +3. Keep glob patterns simple, with wildcards in predictable positions. +4. Test your redirects thoroughly, especially when using complex patterns. diff --git a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs index f9f6b4864..03e133cb3 100644 --- a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using System.Text.RegularExpressions; +using DotNet.Globbing; using Elastic.Documentation.Links; using YamlDotNet.RepresentationModel; @@ -11,6 +13,59 @@ namespace Elastic.Documentation.Configuration.Builder; public record RedirectFile { public Dictionary? Redirects { get; set; } + + public List<(string Pattern, string TargetPrefix)> GlobRedirects { get; set; } = []; + + private readonly Dictionary _compiledPatterns = new(StringComparer.Ordinal); + + private Glob GetCompiledGlob(string pattern) + { + if (_compiledPatterns.TryGetValue(pattern, out var glob)) + return glob; + + var options = new GlobOptions(); + options.Evaluation.CaseInsensitive = true; + glob = Glob.Parse(pattern, options); + _compiledPatterns[pattern] = glob; + return glob; + } + + public string? ResolveRedirect(string path) + { + if (Redirects != null && Redirects.TryGetValue(path, out var linkRedirect) && linkRedirect.To != null) + return linkRedirect.To; + + foreach (var (pattern, targetPrefix) in GlobRedirects) + { + if (pattern.EndsWith("**", StringComparison.Ordinal) && targetPrefix.Contains("**")) + { + var prefix = pattern.AsSpan(0, pattern.Length - 2); + if (path.StartsWith(prefix.ToString(), StringComparison.OrdinalIgnoreCase)) + { + var suffix = path.AsSpan(prefix.Length); + var rewritten = targetPrefix.Replace("**", suffix.ToString()); + return rewritten; + } + } + else if (pattern.Contains('*') || pattern.Contains('?')) + { + var glob = GetCompiledGlob(pattern); + if (glob.IsMatch(path)) + { + if (!targetPrefix.Contains('*') && !targetPrefix.Contains('?')) + return targetPrefix; + return path; + } + } + else if (string.Equals(pattern, path, StringComparison.OrdinalIgnoreCase)) + { + return targetPrefix; + } + } + + return null; + } + private IFileInfo Source { get; init; } private IDocumentationContext Context { get; init; } @@ -47,6 +102,7 @@ public RedirectFile(IFileInfo source, IDocumentationContext context) private static Dictionary? ReadRedirects(YamlStreamReader reader, KeyValuePair entry) { + var globPatterns = new List<(string Pattern, string TargetPrefix)>(); if (!reader.ReadObjectDictionary(entry, out var mapping)) return null; @@ -60,7 +116,8 @@ public RedirectFile(IFileInfo source, IDocumentationContext context) if (entryValue.Value is YamlScalarNode) { var to = reader.ReadString(entryValue); - dictionary.Add(key, + dictionary.Add( + key, !string.IsNullOrEmpty(to) ? to.StartsWith('!') ? new LinkRedirect { To = to.TrimStart('!'), Anchors = LinkRedirect.CatchAllAnchors } @@ -77,7 +134,6 @@ public RedirectFile(IFileInfo source, IDocumentationContext context) if (linkRedirect is not null) dictionary.Add(key, linkRedirect); } - return dictionary; } @@ -112,8 +168,7 @@ public RedirectFile(IFileInfo source, IDocumentationContext context) if (redirect.To is null && redirect.Many is null or { Length: 0 }) return redirect with { To = file }; - return string.IsNullOrEmpty(redirect.To) && redirect.Many is null or { Length: 0 } - ? null : redirect; + return string.IsNullOrEmpty(redirect.To) && redirect.Many is null or { Length: 0 } ? null : redirect; } private static LinkSingleRedirect[]? ReadManyRedirects(YamlStreamReader reader, string file, YamlNode node) @@ -153,10 +208,6 @@ public RedirectFile(IFileInfo source, IDocumentationContext context) if (redirects.Count == 0) return null; - return - [ - ..redirects - .Where(r => r.To is not null && r.Anchors is not null && r.Anchors.Count >= 0) - ]; + return [.. redirects.Where(r => r.To is not null && r.Anchors is not null && r.Anchors.Count >= 0)]; } } diff --git a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj index 2eb80e658..25f880c09 100644 --- a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj +++ b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -11,7 +11,7 @@ - +