Skip to content

Commit 20a4d81

Browse files
authored
Add global substitutions support to Markdown processing (#143)
1 parent cc8fdf5 commit 20a4d81

File tree

10 files changed

+129
-19
lines changed

10 files changed

+129
-19
lines changed

.github/workflows/pr.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@ env:
1515
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
1616

1717
jobs:
18-
docs:
19-
runs-on: ubuntu-latest
20-
steps:
21-
- uses: actions/checkout@v4
22-
- name: Build documentation
23-
uses: elastic/docs-builder@main
24-
2518
build:
2619
runs-on: ubuntu-latest
2720
steps:
@@ -39,4 +32,8 @@ jobs:
3932

4033
- name: Publish AOT
4134
run: ./build.sh publishbinaries
42-
35+
36+
# we run our artifact directly please use the prebuild
37+
# elastic/docs-builder@main GitHub Action for all other repositories!
38+
- name: Build documentation
39+
run: .artifacts/publish/docs-builder/release/docs-builder

docs/source/docset.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ external_hosts:
1717
- palletsprojects.com
1818
exclude:
1919
- '_*.md'
20+
subs:
21+
a-global-variable: "This was defined in docset.yml"
2022
toc:
2123
- file: index.md
2224
- folder: migration

docs/source/syntax/substitutions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,32 @@ sub:
66
version: 7.17.0
77
---
88

9+
Substitutions can be defined in two places:
10+
11+
1. In the `frontmatter` YAML within a file.
12+
2. Globally for all files in `docset.yml`
13+
14+
In both cases the yaml to define them is as followed:
15+
16+
17+
```yaml
18+
subs:
19+
key: value
20+
another-var: Another Value
21+
```
22+
23+
If a substitution is defined globally it may not be redefined (shaded) in a files `frontmatter`.
24+
Doing so will result in a build error.
25+
26+
## Example
27+
928
Here are some variable substitutions:
1029

1130
| Variable | Defined in |
1231
|-----------------------|--------------|
1332
| {{frontmatter_key}} | Front Matter |
1433
| {{a-key-with-dashes}} | Front Matter |
34+
| {{a-global-variable}} | `docset.yml` |
1535

1636
Substitutions should work in code blocks too.
1737

src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,15 @@ public static void EmitWarning(this InlineProcessor processor, int line, int col
4747
context.Build.Collector.Channel.Write(d);
4848
}
4949

50-
public static void EmitError(this ParserContext context, int line, int column, int length, string message, Exception? e = null)
50+
public static void EmitError(this ParserContext context, string message, Exception? e = null)
5151
{
5252
if (context.SkipValidation)
5353
return;
5454
var d = new Diagnostic
5555
{
5656
Severity = Severity.Error,
5757
File = context.Path.FullName,
58-
Column = column,
59-
Line = line,
6058
Message = message + (e != null ? Environment.NewLine + e : string.Empty),
61-
Length = length
6259
};
6360
context.Build.Collector.Channel.Write(d);
6461
}

src/Elastic.Markdown/IO/ConfigurationFile.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public record ConfigurationFile : DocumentationFile
2929
"github.com",
3030
};
3131

32+
private readonly Dictionary<string, string> _substitutions = new(StringComparer.OrdinalIgnoreCase);
33+
public IReadOnlyDictionary<string, string> Substitutions => _substitutions;
34+
3235
public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildContext context)
3336
: base(sourceFile, rootPath)
3437
{
@@ -70,6 +73,9 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
7073
.Select(Glob.Parse)
7174
.ToArray();
7275
break;
76+
case "subs":
77+
_substitutions = ReadDictionary(entry);
78+
break;
7379
case "external_hosts":
7480
var hosts = ReadStringArray(entry)
7581
.ToArray();
@@ -148,6 +154,27 @@ private List<ITocItem> ReadChildren(KeyValuePair<YamlNode, YamlNode> entry, stri
148154
return null;
149155
}
150156

157+
private Dictionary<string, string> ReadDictionary(KeyValuePair<YamlNode, YamlNode> entry)
158+
{
159+
var dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
160+
if (entry.Value is not YamlMappingNode mapping)
161+
{
162+
var key = ((YamlScalarNode)entry.Key).Value;
163+
EmitWarning($"'{key}' is not a dictionary");
164+
return dictionary;
165+
}
166+
foreach (var entryValue in mapping.Children)
167+
{
168+
if (entryValue.Key is not YamlScalarNode scalar || scalar.Value is null)
169+
continue;
170+
var key = scalar.Value;
171+
var value = ReadString(entryValue);
172+
if (value is not null)
173+
dictionary.Add(key, value);
174+
}
175+
return dictionary;
176+
}
177+
151178
private string? ReadFolder(KeyValuePair<YamlNode, YamlNode> entry, string parentPath, out bool found)
152179
{
153180
found = false;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private void ExtractInclusionPath(ParserContext context)
5757
var includePath = Arguments;
5858
if (string.IsNullOrWhiteSpace(includePath))
5959
{
60-
context.EmitError(Line, Column, $"```{{{Directive}}}".Length, "include requires an argument.");
60+
this.EmitError("include requires an argument.");
6161
return;
6262
}
6363

src/Elastic.Markdown/Myst/ParserContext.cs

Lines changed: 11 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 System.IO.Abstractions;
6+
using Elastic.Markdown.Diagnostics;
67
using Elastic.Markdown.IO;
78
using Elastic.Markdown.Myst.FrontMatter;
89
using Markdig;
@@ -41,10 +42,19 @@ public ParserContext(MarkdownParser markdownParser,
4142
Build = context;
4243
Configuration = configuration;
4344

45+
foreach (var (key, value) in configuration.Substitutions)
46+
Properties[key] = value;
47+
4448
if (frontMatter?.Properties is { } props)
4549
{
4650
foreach (var (key, value) in props)
47-
Properties[key] = value;
51+
{
52+
if (configuration.Substitutions.TryGetValue(key, out _))
53+
this.EmitError($"{{{key}}} can not be redeclared in front matter as its a global substitution");
54+
else
55+
Properties[key] = value;
56+
}
57+
4858
}
4959

5060
if (frontMatter?.Title is { } title)

tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ public abstract class InlineTest : IAsyncLifetime
7878
protected DocumentationSet Set { get; }
7979

8080

81-
protected InlineTest(ITestOutputHelper output, [LanguageInjection("markdown")] string content)
81+
protected InlineTest(
82+
ITestOutputHelper output,
83+
[LanguageInjection("markdown")] string content,
84+
Dictionary<string, string>? globalVariables = null)
8285
{
8386
var logger = new TestLoggerFactory(output);
8487
FileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
@@ -102,7 +105,7 @@ protected InlineTest(ITestOutputHelper output, [LanguageInjection("markdown")] s
102105
AddToFileSystem(FileSystem);
103106

104107
var root = FileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/source"));
105-
FileSystem.GenerateDocSetYaml(root);
108+
FileSystem.GenerateDocSetYaml(root, globalVariables);
106109

107110
Collector = new TestDiagnosticsCollector(logger);
108111
var context = new BuildContext(FileSystem)

tests/Elastic.Markdown.Tests/Inline/SubstitutionTest.cs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ not a comment
2222
{
2323

2424
[Fact]
25-
public void GeneratesAttributesInHtml() =>
25+
public void ReplacesSubsFromFrontMatter() =>
2626
Html.Should().Contain(
2727
"""Hello World!<br />"""
2828
).And.Contain(
@@ -48,7 +48,7 @@ not a {substitution}
4848
{
4949

5050
[Fact]
51-
public void GeneratesAttributesInHtml() =>
51+
public void PreservesSingleBracket() =>
5252
Html.Should().Contain(
5353
"""Hello World!<br />"""
5454
).And.Contain(
@@ -87,3 +87,49 @@ public void ReplacesSubsInCode() =>
8787
Html.Should().Contain("7.17.0");
8888
}
8989

90+
91+
public class SupportsSubstitutionsFromDocSet(ITestOutputHelper output) : InlineTest(output,
92+
"""
93+
---
94+
sub:
95+
hello-world: "Hello World!"
96+
---
97+
The following should be subbed: {{hello-world}}
98+
The following should be subbed as well: {{global-var}}
99+
"""
100+
, new() { { "global-var", "A variable from docset.yml" } }
101+
)
102+
{
103+
104+
[Fact]
105+
public void EmitsGlobalVariable() =>
106+
Html.Should().Contain("Hello World!<br />")
107+
.And.NotContain("{{hello-world}}")
108+
.And.Contain("A variable from docset.yml")
109+
.And.NotContain("{{global-var}}");
110+
}
111+
112+
113+
public class CanNotShadeGlobalVariables(ITestOutputHelper output) : InlineTest(output,
114+
"""
115+
---
116+
sub:
117+
hello-world: "Hello World!"
118+
---
119+
The following should be subbed: {{hello-world}}
120+
The following should be subbed as well: {{hello-world}}
121+
"""
122+
, new() { { "hello-world", "A variable from docset.yml" } }
123+
)
124+
{
125+
126+
[Fact]
127+
public void OnlySeesGlobalVariable() =>
128+
Html.Should().NotContain("Hello World!<br />")
129+
.And.NotContain("{{hello-world}}")
130+
.And.Contain("A variable from docset.yml");
131+
132+
[Fact]
133+
public void HasError() => Collector.Diagnostics.Should().HaveCount(1)
134+
.And.Contain(d => d.Message.Contains("{hello-world} can not be redeclared in front matter as its a global substitution"));
135+
}

tests/Elastic.Markdown.Tests/MockFileSystemExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Elastic.Markdown.Tests;
99

1010
public static class MockFileSystemExtensions
1111
{
12-
public static void GenerateDocSetYaml(this MockFileSystem fileSystem, IDirectoryInfo root)
12+
public static void GenerateDocSetYaml(this MockFileSystem fileSystem, IDirectoryInfo root, Dictionary<string, string>? globalVariables = null)
1313
{
1414
// language=yaml
1515
var yaml = new StringWriter();
@@ -21,6 +21,14 @@ public static void GenerateDocSetYaml(this MockFileSystem fileSystem, IDirectory
2121
var relative = fileSystem.Path.GetRelativePath(root.FullName, markdownFile);
2222
yaml.WriteLine($" - file: {relative}");
2323
}
24+
25+
if (globalVariables is not null)
26+
{
27+
yaml.WriteLine($"subs:");
28+
foreach (var (key, value) in globalVariables)
29+
yaml.WriteLine($" {key}: {value}");
30+
}
31+
2432
fileSystem.AddFile(Path.Combine(root.FullName, "docset.yml"), new MockFileData(yaml.ToString()));
2533
}
2634

0 commit comments

Comments
 (0)