diff --git a/docs/syntax/_snippets/reusable-snippet.md b/docs/syntax/_snippets/reusable-snippet.md index 561874d9e..be7c230c8 100644 --- a/docs/syntax/_snippets/reusable-snippet.md +++ b/docs/syntax/_snippets/reusable-snippet.md @@ -1,4 +1,4 @@ -This is a snippet included on "{{page_title}}". +This is a snippet included on "{{context.page_title}}". :::{tip} This is a snippet diff --git a/docs/testing/nested/index.md b/docs/testing/nested/index.md index 3c65e38ad..c65382bcd 100644 --- a/docs/testing/nested/index.md +++ b/docs/testing/nested/index.md @@ -1,8 +1,9 @@ --- sub: x: "Variable" +navigation_title: "Testing nesting and {{x}}" --- -# Testing Nesting +# Testing Nesting and {{x}} The files in this directory are used for testing purposes. Do not edit these files unless you are working on tests. diff --git a/src/Elastic.Markdown/Helpers/Interpolation.cs b/src/Elastic.Markdown/Helpers/Interpolation.cs index 9714ad0de..e6b42e196 100644 --- a/src/Elastic.Markdown/Helpers/Interpolation.cs +++ b/src/Elastic.Markdown/Helpers/Interpolation.cs @@ -2,7 +2,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; +using Elastic.Markdown.Myst; namespace Elastic.Markdown.Helpers; @@ -14,22 +16,60 @@ internal static partial class InterpolationRegex public static class Interpolation { - public static bool ReplaceSubstitutions(this ReadOnlySpan span, IReadOnlyDictionary? properties, out string? replacement) + public static string ReplaceSubstitutions( + this string input, + ParserContext context + ) + { + var span = input.AsSpan(); + if (span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out var replacement)) + return replacement; + return input; + } + + + public static bool ReplaceSubstitutions( + this ReadOnlySpan span, + ParserContext context, + [NotNullWhen(true)] out string? replacement + ) => + span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out replacement); + + public static bool ReplaceSubstitutions( + this ReadOnlySpan span, + IReadOnlyDictionary? properties, + [NotNullWhen(true)] out string? replacement + ) { replacement = null; + if (properties is null || properties.Count == 0) + return false; + if (span.IndexOf("}}") < 0) return false; - if (properties is null || properties.Count == 0) + return span.ReplaceSubstitutions([properties], out replacement); + } + + public static bool ReplaceSubstitutions( + this ReadOnlySpan span, + IReadOnlyDictionary[] properties, + [NotNullWhen(true)] out string? replacement + ) + { + replacement = null; + if (span.IndexOf("}}") < 0) return false; - var substitutions = properties as Dictionary - ?? new Dictionary(properties, StringComparer.OrdinalIgnoreCase); - if (substitutions.Count == 0) + if (properties.Length == 0 || properties.Sum(p => p.Count) == 0) return false; + var lookups = properties + .Select(p => p as Dictionary ?? new Dictionary(p, StringComparer.OrdinalIgnoreCase)) + .Select(d => d.GetAlternateLookup>()) + .ToArray(); + var matchSubs = InterpolationRegex.MatchSubstitutions().EnumerateMatches(span); - var lookup = substitutions.GetAlternateLookup>(); var replaced = false; foreach (var match in matchSubs) @@ -39,14 +79,15 @@ public static bool ReplaceSubstitutions(this ReadOnlySpan span, IReadOnlyD var spanMatch = span.Slice(match.Index, match.Length); var key = spanMatch.Trim(['{', '}']); + foreach (var lookup in lookups) + { + if (!lookup.TryGetValue(key, out var value)) + continue; - if (!lookup.TryGetValue(key, out var value)) - continue; - - replacement ??= span.ToString(); - replacement = replacement.Replace(spanMatch.ToString(), value); - replaced = true; - + replacement ??= span.ToString(); + replacement = replacement.Replace(spanMatch.ToString(), value); + replaced = true; + } } return replaced; diff --git a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs index 1ef151722..89c61ab85 100644 --- a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs @@ -1,6 +1,9 @@ // Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information + +using Elastic.Markdown.Helpers; + namespace Elastic.Markdown.Myst.Directives; public class DropdownBlock(DirectiveBlockParser parser, ParserContext context) : AdmonitionBlock(parser, "dropdown", context); @@ -14,6 +17,11 @@ public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserCon _admonition = admonition; if (_admonition is "admonition") Classes = "plain"; + + var t = Admonition; + var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(t); + Title = title; + } public string Admonition => _admonition; @@ -23,19 +31,7 @@ public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserCon public string? Classes { get; protected set; } public bool? DropdownOpen { get; private set; } - public string Title - { - get - { - var t = Admonition; - var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(t); - if (_admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments)) - title = Arguments; - else if (!string.IsNullOrEmpty(Arguments)) - title += $" {Arguments}"; - return title; - } - } + public string Title { get; private set; } public override void FinalizeAndValidate(ParserContext context) { @@ -43,6 +39,12 @@ public override void FinalizeAndValidate(ParserContext context) DropdownOpen = TryPropBool("open"); if (DropdownOpen.HasValue) Classes = "dropdown"; + + if (_admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments)) + Title = Arguments; + else if (!string.IsNullOrEmpty(Arguments)) + Title += $" {Arguments}"; + Title = Title.ReplaceSubstitutions(context); } } diff --git a/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs b/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs index f884844dc..6b0216c86 100644 --- a/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Diagnostics; +using Elastic.Markdown.Helpers; using Elastic.Markdown.Slices.Directives; namespace Elastic.Markdown.Myst.Directives; @@ -44,7 +45,7 @@ public override void FinalizeAndValidate(ParserContext context) if (string.IsNullOrWhiteSpace(Arguments)) this.EmitError("{tab-item} requires an argument to name the tab."); - Title = Arguments ?? "{undefined}"; + Title = (Arguments ?? "{undefined}").ReplaceSubstitutions(context); Index = Parent!.IndexOf(this); var tabSet = Parent as TabSetBlock; diff --git a/src/Elastic.Markdown/Myst/ParserContext.cs b/src/Elastic.Markdown/Myst/ParserContext.cs index 15e469e05..01d3ddefb 100644 --- a/src/Elastic.Markdown/Myst/ParserContext.cs +++ b/src/Elastic.Markdown/Myst/ParserContext.cs @@ -44,23 +44,29 @@ public ParserContext( Build = context; Configuration = configuration; - foreach (var (key, value) in configuration.Substitutions) - Properties[key.ToLowerInvariant()] = value; - - if (frontMatter?.Properties is { } props) + if (frontMatter?.Properties is not { Count: > 0 }) + Substitutions = configuration.Substitutions; + else { - foreach (var (k, value) in props) + var subs = new Dictionary(configuration.Substitutions); + foreach (var (k, value) in frontMatter.Properties) { var key = k.ToLowerInvariant(); if (configuration.Substitutions.TryGetValue(key, out _)) this.EmitError($"{{{key}}} can not be redeclared in front matter as its a global substitution"); else - Properties[key] = value; + subs[key] = value; } + + Substitutions = subs; } + var contextSubs = new Dictionary(); + if (frontMatter?.Title is { } title) - Properties["page_title"] = title; + contextSubs["context.page_title"] = title; + + ContextSubstitutions = contextSubs; } public ConfigurationFile Configuration { get; } @@ -70,4 +76,7 @@ public ParserContext( public BuildContext Build { get; } public bool SkipValidation { get; init; } public Func? GetDocumentationFile { get; init; } + public IReadOnlyDictionary Substitutions { get; } + public IReadOnlyDictionary ContextSubstitutions { get; } + } diff --git a/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs b/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs index 32d16bcb5..34d140d98 100644 --- a/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs +++ b/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs @@ -6,6 +6,7 @@ using System.Net.Mime; using System.Runtime.CompilerServices; using Elastic.Markdown.Diagnostics; +using Markdig; using Markdig.Helpers; using Markdig.Parsers; using Markdig.Renderers; @@ -98,6 +99,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (slice.PeekCharExtra(1) != match) return false; + if (processor.Context is not ParserContext context) + return false; + Debug.Assert(match is not ('\r' or '\n')); // Match the opened sticks @@ -140,10 +144,15 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var key = content.ToString().Trim(['{', '}']).ToLowerInvariant(); var found = false; var replacement = string.Empty; - if (processor.Context?.Properties.TryGetValue(key, out var value) ?? false) + if (context.Substitutions.TryGetValue(key, out var value)) + { + found = true; + replacement = value; + } + else if (context.ContextSubstitutions.TryGetValue(key, out value)) { found = true; - replacement = value.ToString() ?? string.Empty; + replacement = value; } var start = processor.GetSourcePosition(startPosition, out var line, out var column);