Skip to content

Commit 47c7663

Browse files
authored
Support substitutions in directive titles (admonitions, tabs) (#459)
* Support substitutions in directive titles (admonitions, tabs) * fix formatting and use context.page_title * Fix tests
1 parent b2db28c commit 47c7663

File tree

7 files changed

+101
-38
lines changed

7 files changed

+101
-38
lines changed

docs/syntax/_snippets/reusable-snippet.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
This is a snippet included on "{{page_title}}".
1+
This is a snippet included on "{{context.page_title}}".
22

33
:::{tip}
44
This is a snippet

docs/testing/nested/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
---
22
sub:
33
x: "Variable"
4+
navigation_title: "Testing nesting and {{x}}"
45
---
5-
# Testing Nesting
6+
# Testing Nesting and {{x}}
67

78
The files in this directory are used for testing purposes. Do not edit these files unless you are working on tests.
89

src/Elastic.Markdown/Helpers/Interpolation.cs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Diagnostics.CodeAnalysis;
56
using System.Text.RegularExpressions;
7+
using Elastic.Markdown.Myst;
68

79
namespace Elastic.Markdown.Helpers;
810

@@ -14,22 +16,60 @@ internal static partial class InterpolationRegex
1416

1517
public static class Interpolation
1618
{
17-
public static bool ReplaceSubstitutions(this ReadOnlySpan<char> span, IReadOnlyDictionary<string, string>? properties, out string? replacement)
19+
public static string ReplaceSubstitutions(
20+
this string input,
21+
ParserContext context
22+
)
23+
{
24+
var span = input.AsSpan();
25+
if (span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out var replacement))
26+
return replacement;
27+
return input;
28+
}
29+
30+
31+
public static bool ReplaceSubstitutions(
32+
this ReadOnlySpan<char> span,
33+
ParserContext context,
34+
[NotNullWhen(true)] out string? replacement
35+
) =>
36+
span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out replacement);
37+
38+
public static bool ReplaceSubstitutions(
39+
this ReadOnlySpan<char> span,
40+
IReadOnlyDictionary<string, string>? properties,
41+
[NotNullWhen(true)] out string? replacement
42+
)
1843
{
1944
replacement = null;
45+
if (properties is null || properties.Count == 0)
46+
return false;
47+
2048
if (span.IndexOf("}}") < 0)
2149
return false;
2250

23-
if (properties is null || properties.Count == 0)
51+
return span.ReplaceSubstitutions([properties], out replacement);
52+
}
53+
54+
public static bool ReplaceSubstitutions(
55+
this ReadOnlySpan<char> span,
56+
IReadOnlyDictionary<string, string>[] properties,
57+
[NotNullWhen(true)] out string? replacement
58+
)
59+
{
60+
replacement = null;
61+
if (span.IndexOf("}}") < 0)
2462
return false;
2563

26-
var substitutions = properties as Dictionary<string, string>
27-
?? new Dictionary<string, string>(properties, StringComparer.OrdinalIgnoreCase);
28-
if (substitutions.Count == 0)
64+
if (properties.Length == 0 || properties.Sum(p => p.Count) == 0)
2965
return false;
3066

67+
var lookups = properties
68+
.Select(p => p as Dictionary<string, string> ?? new Dictionary<string, string>(p, StringComparer.OrdinalIgnoreCase))
69+
.Select(d => d.GetAlternateLookup<ReadOnlySpan<char>>())
70+
.ToArray();
71+
3172
var matchSubs = InterpolationRegex.MatchSubstitutions().EnumerateMatches(span);
32-
var lookup = substitutions.GetAlternateLookup<ReadOnlySpan<char>>();
3373

3474
var replaced = false;
3575
foreach (var match in matchSubs)
@@ -39,14 +79,15 @@ public static bool ReplaceSubstitutions(this ReadOnlySpan<char> span, IReadOnlyD
3979

4080
var spanMatch = span.Slice(match.Index, match.Length);
4181
var key = spanMatch.Trim(['{', '}']);
82+
foreach (var lookup in lookups)
83+
{
84+
if (!lookup.TryGetValue(key, out var value))
85+
continue;
4286

43-
if (!lookup.TryGetValue(key, out var value))
44-
continue;
45-
46-
replacement ??= span.ToString();
47-
replacement = replacement.Replace(spanMatch.ToString(), value);
48-
replaced = true;
49-
87+
replacement ??= span.ToString();
88+
replacement = replacement.Replace(spanMatch.ToString(), value);
89+
replaced = true;
90+
}
5091
}
5192

5293
return replaced;

src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Licensed to Elasticsearch B.V under one or more agreements.
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Helpers;
6+
47
namespace Elastic.Markdown.Myst.Directives;
58

69
public class DropdownBlock(DirectiveBlockParser parser, ParserContext context) : AdmonitionBlock(parser, "dropdown", context);
@@ -14,6 +17,11 @@ public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserCon
1417
_admonition = admonition;
1518
if (_admonition is "admonition")
1619
Classes = "plain";
20+
21+
var t = Admonition;
22+
var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(t);
23+
Title = title;
24+
1725
}
1826

1927
public string Admonition => _admonition;
@@ -23,26 +31,20 @@ public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserCon
2331
public string? Classes { get; protected set; }
2432
public bool? DropdownOpen { get; private set; }
2533

26-
public string Title
27-
{
28-
get
29-
{
30-
var t = Admonition;
31-
var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(t);
32-
if (_admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments))
33-
title = Arguments;
34-
else if (!string.IsNullOrEmpty(Arguments))
35-
title += $" {Arguments}";
36-
return title;
37-
}
38-
}
34+
public string Title { get; private set; }
3935

4036
public override void FinalizeAndValidate(ParserContext context)
4137
{
4238
CrossReferenceName = Prop("name");
4339
DropdownOpen = TryPropBool("open");
4440
if (DropdownOpen.HasValue)
4541
Classes = "dropdown";
42+
43+
if (_admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments))
44+
Title = Arguments;
45+
else if (!string.IsNullOrEmpty(Arguments))
46+
Title += $" {Arguments}";
47+
Title = Title.ReplaceSubstitutions(context);
4648
}
4749
}
4850

src/Elastic.Markdown/Myst/Directives/TabSetBlock.cs

Lines changed: 2 additions & 1 deletion
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

55
using Elastic.Markdown.Diagnostics;
6+
using Elastic.Markdown.Helpers;
67
using Elastic.Markdown.Slices.Directives;
78

89
namespace Elastic.Markdown.Myst.Directives;
@@ -44,7 +45,7 @@ public override void FinalizeAndValidate(ParserContext context)
4445
if (string.IsNullOrWhiteSpace(Arguments))
4546
this.EmitError("{tab-item} requires an argument to name the tab.");
4647

47-
Title = Arguments ?? "{undefined}";
48+
Title = (Arguments ?? "{undefined}").ReplaceSubstitutions(context);
4849
Index = Parent!.IndexOf(this);
4950

5051
var tabSet = Parent as TabSetBlock;

src/Elastic.Markdown/Myst/ParserContext.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,29 @@ public ParserContext(
4444
Build = context;
4545
Configuration = configuration;
4646

47-
foreach (var (key, value) in configuration.Substitutions)
48-
Properties[key.ToLowerInvariant()] = value;
49-
50-
if (frontMatter?.Properties is { } props)
47+
if (frontMatter?.Properties is not { Count: > 0 })
48+
Substitutions = configuration.Substitutions;
49+
else
5150
{
52-
foreach (var (k, value) in props)
51+
var subs = new Dictionary<string, string>(configuration.Substitutions);
52+
foreach (var (k, value) in frontMatter.Properties)
5353
{
5454
var key = k.ToLowerInvariant();
5555
if (configuration.Substitutions.TryGetValue(key, out _))
5656
this.EmitError($"{{{key}}} can not be redeclared in front matter as its a global substitution");
5757
else
58-
Properties[key] = value;
58+
subs[key] = value;
5959
}
60+
61+
Substitutions = subs;
6062
}
6163

64+
var contextSubs = new Dictionary<string, string>();
65+
6266
if (frontMatter?.Title is { } title)
63-
Properties["page_title"] = title;
67+
contextSubs["context.page_title"] = title;
68+
69+
ContextSubstitutions = contextSubs;
6470
}
6571

6672
public ConfigurationFile Configuration { get; }
@@ -70,4 +76,7 @@ public ParserContext(
7076
public BuildContext Build { get; }
7177
public bool SkipValidation { get; init; }
7278
public Func<IFileInfo, DocumentationFile?>? GetDocumentationFile { get; init; }
79+
public IReadOnlyDictionary<string, string> Substitutions { get; }
80+
public IReadOnlyDictionary<string, string> ContextSubstitutions { get; }
81+
7382
}

src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Mime;
77
using System.Runtime.CompilerServices;
88
using Elastic.Markdown.Diagnostics;
9+
using Markdig;
910
using Markdig.Helpers;
1011
using Markdig.Parsers;
1112
using Markdig.Renderers;
@@ -98,6 +99,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
9899
if (slice.PeekCharExtra(1) != match)
99100
return false;
100101

102+
if (processor.Context is not ParserContext context)
103+
return false;
104+
101105
Debug.Assert(match is not ('\r' or '\n'));
102106

103107
// Match the opened sticks
@@ -140,10 +144,15 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
140144
var key = content.ToString().Trim(['{', '}']).ToLowerInvariant();
141145
var found = false;
142146
var replacement = string.Empty;
143-
if (processor.Context?.Properties.TryGetValue(key, out var value) ?? false)
147+
if (context.Substitutions.TryGetValue(key, out var value))
148+
{
149+
found = true;
150+
replacement = value;
151+
}
152+
else if (context.ContextSubstitutions.TryGetValue(key, out value))
144153
{
145154
found = true;
146-
replacement = value.ToString() ?? string.Empty;
155+
replacement = value;
147156
}
148157

149158
var start = processor.GetSourcePosition(startPosition, out var line, out var column);

0 commit comments

Comments
 (0)