Skip to content

Commit 2254d10

Browse files
authored
Add interpolation support and unify substitution logic (#201)
Introduced interpolation functionality for handling string replacements in Markdown. Refactored and centralized substitution logic into a reusable helper, replacing redundant implementations. Updated navigation title and code block parsing to leverage the new interpolation system, ensuring consistency and cleaner code.
1 parent ddd64a5 commit 2254d10

File tree

8 files changed

+122
-39
lines changed

8 files changed

+122
-39
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Text.RegularExpressions;
6+
7+
namespace Elastic.Markdown.Helpers;
8+
9+
internal static partial class InterpolationRegex
10+
{
11+
[GeneratedRegex(@"\{\{[^\r\n}]+?\}\}", RegexOptions.IgnoreCase, "en-US")]
12+
public static partial Regex MatchSubstitutions();
13+
}
14+
15+
public static class Interpolation
16+
{
17+
public static bool ReplaceSubstitutions(this ReadOnlySpan<char> span, Dictionary<string, string>? properties, out string? replacement)
18+
{
19+
replacement = null;
20+
var substitutions = properties ?? new();
21+
if (substitutions.Count == 0)
22+
return false;
23+
24+
var matchSubs = InterpolationRegex.MatchSubstitutions().EnumerateMatches(span);
25+
var lookup = substitutions.GetAlternateLookup<ReadOnlySpan<char>>();
26+
27+
var replaced = false;
28+
foreach (var match in matchSubs)
29+
{
30+
if (match.Length == 0)
31+
continue;
32+
33+
var spanMatch = span.Slice(match.Index, match.Length);
34+
var key = spanMatch.Trim(['{', '}']);
35+
36+
if (!lookup.TryGetValue(key, out var value))
37+
continue;
38+
39+
replacement ??= span.ToString();
40+
replacement = replacement.Replace(spanMatch.ToString(), value);
41+
replaced = true;
42+
43+
}
44+
45+
return replaced;
46+
}
47+
}

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information
44
using System.IO.Abstractions;
55
using Elastic.Markdown.Diagnostics;
6+
using Elastic.Markdown.Helpers;
67
using Elastic.Markdown.IO.Navigation;
78
using Elastic.Markdown.Myst;
89
using Elastic.Markdown.Myst.Directives;
@@ -102,6 +103,24 @@ private void ReadDocumentInstructions(MarkdownDocument document)
102103
YamlFrontMatter = ReadYamlFrontMatter(document, raw);
103104
Title = YamlFrontMatter.Title;
104105
NavigationTitle = YamlFrontMatter.NavigationTitle;
106+
if (!string.IsNullOrEmpty(NavigationTitle))
107+
{
108+
var props = MarkdownParser.Configuration.Substitutions;
109+
var properties = YamlFrontMatter.Properties;
110+
if (properties is { Count: >= 0 } local)
111+
{
112+
var allProperties = new Dictionary<string, string>(local);
113+
foreach (var (key, value) in props)
114+
allProperties[key] = value;
115+
if (NavigationTitle.AsSpan().ReplaceSubstitutions(allProperties, out var replacement))
116+
NavigationTitle = replacement;
117+
}
118+
else
119+
{
120+
if (NavigationTitle.AsSpan().ReplaceSubstitutions(properties, out var replacement))
121+
NavigationTitle = replacement;
122+
}
123+
}
105124
}
106125
else
107126
{

src/Elastic.Markdown/Myst/CodeBlocks/CallOutParser.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,4 @@ public static partial class CallOutParser
1313

1414
[GeneratedRegex(@"^.+\S+.*?\s(?:\/\/|#)\s[^""]+$", RegexOptions.IgnoreCase, "en-US")]
1515
public static partial Regex MathInlineAnnotation();
16-
17-
[GeneratedRegex(@"\{\{[^\r\n}]+?\}\}", RegexOptions.IgnoreCase, "en-US")]
18-
public static partial Regex MatchSubstitutions();
1916
}

src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Text.RegularExpressions;
66
using Elastic.Markdown.Diagnostics;
7+
using Elastic.Markdown.Helpers;
78
using Markdig.Helpers;
89
using Markdig.Parsers;
910
using Markdig.Syntax;
@@ -87,7 +88,7 @@ public override bool Close(BlockProcessor processor, Block block)
8788
var line = lines.Lines[index];
8889
var span = line.Slice.AsSpan();
8990

90-
if (ReplaceSubstitutions(context, span, out var replacement))
91+
if (span.ReplaceSubstitutions(context.FrontMatter?.Properties, out var replacement))
9192
{
9293
var s = new StringSlice(replacement);
9394
lines.Lines[index] = new StringLine(ref s);
@@ -139,37 +140,6 @@ public override bool Close(BlockProcessor processor, Block block)
139140
return base.Close(processor, block);
140141
}
141142

142-
private static bool ReplaceSubstitutions(ParserContext context, ReadOnlySpan<char> span, out string? replacement)
143-
{
144-
replacement = null;
145-
var substitutions = context.FrontMatter?.Properties ?? new();
146-
if (substitutions.Count == 0)
147-
return false;
148-
149-
var matchSubs = CallOutParser.MatchSubstitutions().EnumerateMatches(span);
150-
151-
var replaced = false;
152-
foreach (var match in matchSubs)
153-
{
154-
if (match.Length == 0)
155-
continue;
156-
157-
var spanMatch = span.Slice(match.Index, match.Length);
158-
var key = spanMatch.Trim(['{', '}']);
159-
160-
// TODO: alternate lookup using span in c# 9
161-
if (substitutions.TryGetValue(key.ToString(), out var value))
162-
{
163-
replacement ??= span.ToString();
164-
replacement = replacement.Replace(spanMatch.ToString(), value);
165-
replaced = true;
166-
}
167-
168-
}
169-
170-
return replaced;
171-
}
172-
173143
private static CallOut? EnumerateAnnotations(Regex.ValueMatchEnumerator matches,
174144
ref ReadOnlySpan<char> span,
175145
ref int callOutIndex,

src/Elastic.Markdown/Myst/FrontMatter/FrontMatterParser.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public class YamlFrontMatter
1818
[YamlMember(Alias = "sub")]
1919
public Dictionary<string, string>? Properties { get; set; }
2020

21-
2221
[YamlMember(Alias = "applies")]
2322
public Deployment? AppliesTo { get; set; }
2423
}

src/Elastic.Markdown/Myst/MarkdownParser.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ public class MarkdownParser(
5353
.DisableHtml()
5454
.Build();
5555

56+
public ConfigurationFile Configuration { get; } = configuration;
57+
5658
public Task<MarkdownDocument> MinimalParseAsync(IFileInfo path, Cancel ctx)
5759
{
58-
var context = new ParserContext(this, path, null, Context, configuration)
60+
var context = new ParserContext(this, path, null, Context, Configuration)
5961
{
6062
SkipValidation = true,
6163
GetDocumentationFile = getDocumentationFile
@@ -65,7 +67,7 @@ public Task<MarkdownDocument> MinimalParseAsync(IFileInfo path, Cancel ctx)
6567

6668
public Task<MarkdownDocument> ParseAsync(IFileInfo path, YamlFrontMatter? matter, Cancel ctx)
6769
{
68-
var context = new ParserContext(this, path, matter, Context, configuration)
70+
var context = new ParserContext(this, path, matter, Context, Configuration)
6971
{
7072
GetDocumentationFile = getDocumentationFile
7173
};
@@ -96,7 +98,7 @@ private async Task<MarkdownDocument> ParseAsync(
9698

9799
public MarkdownDocument Parse(string yaml, IFileInfo parent, YamlFrontMatter? matter)
98100
{
99-
var context = new ParserContext(this, parent, matter, Context, configuration)
101+
var context = new ParserContext(this, parent, matter, Context, Configuration)
100102
{
101103
GetDocumentationFile = getDocumentationFile
102104
};

tests/Elastic.Markdown.Tests/FrontMatter/YamlFrontMatterTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,18 @@ public void WarnsOfNoTitle() =>
4848
Collector.Diagnostics.Should().NotBeEmpty()
4949
.And.Contain(d => d.Message.Contains("Missing yaml front-matter block defining a title"));
5050
}
51+
52+
public class NavigationTitleSupportReplacements(ITestOutputHelper output) : DirectiveTest(output,
53+
"""
54+
---
55+
title: Elastic Docs v3
56+
navigation_title: "Documentation Guide: {{key}}"
57+
sub:
58+
key: "value"
59+
---
60+
"""
61+
)
62+
{
63+
[Fact]
64+
public void ReadsNavigationTitle() => File.NavigationTitle.Should().Be("Documentation Guide: value");
65+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Helpers;
6+
using FluentAssertions;
7+
8+
namespace Elastic.Markdown.Tests.Interpolation;
9+
10+
public class InterpolationTests
11+
{
12+
[Fact]
13+
public void ReplacesVariables()
14+
{
15+
var span = "My text {{with-variables}} {{not-defined}}".AsSpan();
16+
var replacements = new Dictionary<string, string> { { "with-variables", "With Variables" } };
17+
var replaced = span.ReplaceSubstitutions(replacements, out var replacement);
18+
19+
replaced.Should().BeTrue();
20+
replacement.Should().Be("My text With Variables {{not-defined}}");
21+
}
22+
23+
[Fact]
24+
public void OnlyReplacesDefinedVariables()
25+
{
26+
var span = "My text {{not-defined}}".AsSpan();
27+
var replacements = new Dictionary<string, string> { { "with-variables", "With Variables" } };
28+
var replaced = span.ReplaceSubstitutions(replacements, out var replacement);
29+
30+
replaced.Should().BeFalse();
31+
// no need to allocate replacement we can continue with span
32+
replacement.Should().BeNull();
33+
}
34+
}

0 commit comments

Comments
 (0)