Skip to content

Commit b725538

Browse files
committed
Add support for variable operator syntax
1 parent 4b614a9 commit b725538

File tree

6 files changed

+286
-29
lines changed

6 files changed

+286
-29
lines changed

docs/syntax/substitutions.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ sub:
33
frontmatter_key: "Front Matter Value"
44
a-key-with-dashes: "A key with dashes"
55
version: 7.17.0
6+
hello-world: "Hello world!"
67
---
78

89
# Substitutions
@@ -26,7 +27,7 @@ Doing so will result in a build error.
2627

2728
To use the variables in your files, surround them in curly brackets (`{{variable}}`).
2829

29-
## Example
30+
### Example
3031

3132
Here are some variable substitutions:
3233

@@ -36,6 +37,97 @@ Here are some variable substitutions:
3637
| {{a-key-with-dashes}} | Front Matter |
3738
| {{a-global-variable}} | `docset.yml` |
3839

40+
## Mutations
41+
42+
Substitutions can be mutated using a chain of operators seperated by a pipe (`|`).
43+
44+
````markdown
45+
`{{hello-world | trim | lc | tc}}`
46+
````
47+
48+
Will trim, lowercase and finally titlecase the contents of the 'hello-world' variable.
49+
50+
### Operators
51+
52+
53+
| Operator | Purpose |
54+
|----------|----------------------------------------------------|
55+
| `lc` | LowerCase, |
56+
| `uc` | UpperCase, |
57+
| `tc` | TitleCase, capitalizes all words, |
58+
| `c` | Capitalize the first letter, |
59+
| `kc` | Convert to KebabCase, |
60+
| `sc` | Convert to SnakeCase, |
61+
| `cc` | Convert to CamelCase, |
62+
| `pc` | Convert to PascalCase, |
63+
| `trim` | Trim common non word characters from start and end |
64+
65+
For variables declaring a semantic version or `Major.Minor` the following operations are also exposed
66+
67+
| Operator | Purpose |
68+
|----------|------------------------------------------|
69+
| `M` | Display only the major component |
70+
| `M.x` | Display major component followed by '.x' |
71+
| `M.M` | Display only the major and the minor |
72+
| `M+1` | The next major version |
73+
| `M.M+1` | The next minor version |
74+
75+
### Example
76+
77+
Given the following frontmatter:
78+
79+
```yaml
80+
---
81+
sub:
82+
hello-world: "Hello world!"
83+
---
84+
```
85+
86+
::::{tab-set}
87+
88+
:::{tab-item} Output
89+
90+
* Lowercase: {{hello-world | lc}}
91+
* Uppercase: {{hello-world | uc}}
92+
* TitleCase: {{hello-world | tc}}
93+
* kebab-case: {{hello-world | kc}}
94+
* camelCase: {{hello-world | tc | cc}}
95+
* PascalCase: {{hello-world | pc}}
96+
* SnakeCase: {{hello-world | sc}}
97+
* CapitalCase (chained): {{hello-world | lc | c}}
98+
* Trim: {{hello-world | trim}}
99+
* M.x: {{version.stack | M.x }}
100+
* M.M: {{version.stack | M.M }}
101+
* M: {{version.stack | M }}
102+
* M+1: {{version.stack | M+1 }}
103+
* M+1 | M.M: {{version.stack | M+1 | M.M }}
104+
* M.M+1: {{version.stack | M.M+1 }}
105+
106+
:::
107+
108+
:::{tab-item} Markdown
109+
110+
````markdown
111+
* Lowercase: {{hello-world | lc}}
112+
* Uppercase: {{hello-world | uc}}
113+
* TitleCase: {{hello-world | tc}}
114+
* kebab-case: {{hello-world | kc}}
115+
* camelCase: {{hello-world | tc | cc}}
116+
* PascalCase: {{hello-world | pc}}
117+
* SnakeCase: {{hello-world | sc}}
118+
* CapitalCase (chained): {{hello-world | lc | c}}
119+
* Trim: {{hello-world | trim}}
120+
* M.x: {{version.stack | M.x }}
121+
* M.M: {{version.stack | M.M }}
122+
* M: {{version.stack | M }}
123+
* M+1: {{version.stack | M+1 }}
124+
* M+1 | M.M: {{version.stack | M+1 | M.M }}
125+
* M.M+1: {{version.stack | M.M+1 }}
126+
````
127+
:::
128+
129+
::::
130+
39131
## Code blocks
40132

41133
Substitutions are supported in code blocks but are disabled by default. Enable substitutions by adding `subs=true` to the code block.

docs/syntax/version-variables.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@ Besides the current version, the following suffixes are available:
1010

1111
| Version substitution | result | purpose |
1212
|--------------------------------------|-----------------------------------|-----------------------------------------|
13-
| `{{versions.stack}}` | {{version.stack}} | Current version |
14-
| `{{versions.stack.major_minor}}` | {{version.stack.major_minor}} | Current `MAJOR.MINOR` |
15-
| `{{versions.stack.major_x}}` | {{version.stack.major_x}} | Current `MAJOR.X` |
16-
| `{{versions.stack.major_component}}` | {{version.stack.major_component}} | Current major component |
17-
| `{{versions.stack.next_major}}` | {{version.stack.next_major}} | The next major version |
18-
| `{{versions.stack.next_minor}}` | {{version.stack.next_minor}} | The next minor version |
19-
| `{{versions.stack.base}}` | {{version.stack.base}} | The first version on the new doc system |
13+
| `{{version.stack}}` | {{version.stack}} | Current version |
14+
| `{{version.stack.base}}` | {{version.stack.base}} | The first version on the new doc system |
2015

16+
## Formatting
17+
18+
Using specialized [mutation operators](substitutions.md#mutations) versions
19+
can be printed in any kind of ways.
20+
21+
22+
| Version substitution | result |
23+
|------------------------|-----------|
24+
| `{{version.stack| M.M}}` | {{version.stack|M.M}} |
25+
| `{{version.stack.base | M }}` | {{version.stack.base | M }} |
26+
| `{{version.stack | M+1 | M }}` | {{version.stack | M+1 | M }} |
27+
| `{{version.stack.base | M.M+1 }}` | {{version.stack.base | M.M+1 }} |
2128

2229
## Available versioning schemes.
2330

src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -163,27 +163,11 @@ public ConfigurationFile(IDocumentationContext context, VersionsConfiguration ve
163163
foreach (var (id, system) in versionsConfig.VersioningSystems)
164164
{
165165
var name = id.ToStringFast(true);
166-
var current = system.Current;
167166
var key = $"version.{name}";
168167
_substitutions[key] = system.Current;
169168

170169
key = $"version.{name}.base";
171170
_substitutions[key] = system.Base;
172-
173-
key = $"version.{name}.major_minor";
174-
_substitutions[key] = $"{current.Major:N0}.{current.Minor:N0}";
175-
176-
key = $"version.{name}.major_x";
177-
_substitutions[key] = $"{current.Major:N0}.x";
178-
179-
key = $"version.{name}.major_component";
180-
_substitutions[key] = $"{current.Major:N0}";
181-
182-
key = $"version.{name}.next_minor";
183-
_substitutions[key] = new SemVersion(current.Major, current.Minor + 1, current.Patch, current.Prerelease, current.Metadata);
184-
185-
key = $"version.{name}.next_major";
186-
_substitutions[key] = new SemVersion(current.Major + 1, current.Minor, current.Patch, current.Prerelease, current.Metadata);
187171
}
188172

189173
var toc = new TableOfContentsConfiguration(this, sourceFile, ScopeDirectory, _context, 0, "");

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

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,130 @@
33
// See the LICENSE file in the project root for more information
44

55
using System.Buffers;
6+
using System.ComponentModel.DataAnnotations;
67
using System.Diagnostics;
8+
using System.Globalization;
9+
using System.Text;
10+
using System.Text.Json;
11+
using Elastic.Documentation;
712
using Elastic.Markdown.Diagnostics;
813
using Markdig.Helpers;
914
using Markdig.Parsers;
1015
using Markdig.Renderers;
1116
using Markdig.Renderers.Html;
1217
using Markdig.Syntax;
1318
using Markdig.Syntax.Inlines;
19+
using NetEscapades.EnumGenerators;
1420

1521
namespace Elastic.Markdown.Myst.InlineParsers.Substitution;
1622

1723
[DebuggerDisplay("{GetType().Name} Line: {Line}, Found: {Found}, Replacement: {Replacement}")]
18-
public class SubstitutionLeaf(string content, bool found, string replacement) : CodeInline(content)
24+
public class SubstitutionLeaf(string content, bool found, string replacement)
25+
: CodeInline(content)
1926
{
2027
public bool Found { get; } = found;
2128
public string Replacement { get; } = replacement;
29+
public IReadOnlyCollection<SubstitutionMutation>? Mutations { get; set; }
30+
}
31+
32+
[EnumExtensions]
33+
public enum SubstitutionMutation
34+
{
35+
[Display(Name = "M")] MajorComponent,
36+
[Display(Name = "M.x")] MajorX,
37+
[Display(Name = "M.M")] MajorMinor,
38+
[Display(Name = "M+1")] IncreaseMajor,
39+
[Display(Name = "M.M+1")] IncreaseMinor,
40+
[Display(Name = "lc")] LowerCase,
41+
[Display(Name = "uc")] UpperCase,
42+
[Display(Name = "tc")] TitleCase,
43+
[Display(Name = "c")] Capitalize,
44+
[Display(Name = "kc")] KebabCase,
45+
[Display(Name = "sc")] SnakeCase,
46+
[Display(Name = "cc")] CamelCase,
47+
[Display(Name = "pc")] PascalCase,
48+
[Display(Name = "trim")] Trim
2249
}
2350

2451
public class SubstitutionRenderer : HtmlObjectRenderer<SubstitutionLeaf>
2552
{
26-
protected override void Write(HtmlRenderer renderer, SubstitutionLeaf obj) =>
27-
renderer.Write(obj.Found ? obj.Replacement : obj.Content);
53+
protected override void Write(HtmlRenderer renderer, SubstitutionLeaf leaf)
54+
{
55+
if (!leaf.Found)
56+
{
57+
_ = renderer.Write(leaf.Content);
58+
return;
59+
}
60+
61+
var replacement = leaf.Replacement;
62+
if (leaf.Mutations is null or { Count: 0 })
63+
{
64+
_ = renderer.Write(replacement);
65+
return;
66+
}
67+
68+
foreach (var mutation in leaf.Mutations)
69+
{
70+
var (success, update) = mutation switch
71+
{
72+
SubstitutionMutation.MajorComponent => TryGetVersion(replacement, v => $"{v.Major}"),
73+
SubstitutionMutation.MajorX => TryGetVersion(replacement, v => $"{v.Major}.x"),
74+
SubstitutionMutation.MajorMinor => TryGetVersion(replacement, v => $"{v.Major}.{v.Minor}"),
75+
SubstitutionMutation.IncreaseMajor => TryGetVersion(replacement, v => $"{v.Major + 1}.0.0"),
76+
SubstitutionMutation.IncreaseMinor => TryGetVersion(replacement, v => $"{v.Major}.{v.Minor + 1}.0"),
77+
SubstitutionMutation.LowerCase => (true, replacement.ToLowerInvariant()),
78+
SubstitutionMutation.UpperCase => (true, replacement.ToUpperInvariant()),
79+
SubstitutionMutation.Capitalize => (true, Capitalize(replacement)),
80+
SubstitutionMutation.KebabCase => (true, ToKebabCase(replacement)),
81+
SubstitutionMutation.CamelCase => (true, ToCamelCase(replacement)),
82+
SubstitutionMutation.PascalCase => (true, ToPascalCase(replacement)),
83+
SubstitutionMutation.SnakeCase => (true, ToSnakeCase(replacement)),
84+
SubstitutionMutation.TitleCase => (true, TitleCase(replacement)),
85+
SubstitutionMutation.Trim => (true, Trim(replacement)),
86+
_ => throw new Exception($"encountered an unknown mutation '{mutation.ToStringFast(true)}'")
87+
};
88+
if (!success)
89+
{
90+
_ = renderer.Write(leaf.Content);
91+
return;
92+
}
93+
replacement = update;
94+
}
95+
_ = renderer.Write(replacement);
96+
}
97+
98+
private static string ToCamelCase(string str) => JsonNamingPolicy.CamelCase.ConvertName(str.Replace(" ", string.Empty));
99+
private static string ToSnakeCase(string str) => JsonNamingPolicy.SnakeCaseLower.ConvertName(str).Replace(" ", string.Empty);
100+
private static string ToKebabCase(string str) => JsonNamingPolicy.KebabCaseLower.ConvertName(str).Replace(" ", string.Empty);
101+
private static string ToPascalCase(string str) => TitleCase(str).Replace(" ", string.Empty);
102+
103+
private static string TitleCase(string str) => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(str);
104+
105+
private static string Trim(string str) =>
106+
str.AsSpan().Trim(['!', ' ', '\t', '\r', '\n', '.', ',', ')', '(', ':', ';', '<', '>', '[', ']']).ToString();
107+
108+
private static string Capitalize(string input) =>
109+
input switch
110+
{
111+
null => string.Empty,
112+
"" => string.Empty,
113+
_ => string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1))
114+
};
115+
116+
private (bool, string) TryGetVersion(string version, Func<SemVersion, string> mutate)
117+
{
118+
if (!SemVersion.TryParse(version, out var v) && !SemVersion.TryParse(version + ".0", out v))
119+
return (false, string.Empty);
120+
121+
return (true, mutate(v));
122+
}
28123
}
29124

30125
public class SubstitutionParser : InlineParser
31126
{
32127
public SubstitutionParser() => OpeningCharacters = ['{'];
33128

34-
private readonly SearchValues<char> _values = SearchValues.Create(['\r', '\n', ' ', '\t', '}']);
129+
private readonly SearchValues<char> _values = SearchValues.Create(['\r', '\n', '\t', '}']);
35130

36131
public override bool Match(InlineProcessor processor, ref StringSlice slice)
37132
{
@@ -84,6 +179,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
84179
var key = content.ToString().Trim(['{', '}']).ToLowerInvariant();
85180
var found = false;
86181
var replacement = string.Empty;
182+
var components = key.Split('|');
183+
if (components.Length > 1)
184+
key = components[0].Trim();
185+
87186
if (context.Substitutions.TryGetValue(key, out var value))
88187
{
89188
found = true;
@@ -100,7 +199,6 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
100199
var start = processor.GetSourcePosition(startPosition, out var line, out var column);
101200
var end = processor.GetSourcePosition(slice.Start);
102201
var sourceSpan = new SourceSpan(start, end);
103-
104202
var substitutionLeaf = new SubstitutionLeaf(content.ToString(), found, replacement)
105203
{
106204
Delimiter = '{',
@@ -109,8 +207,31 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
109207
Column = column,
110208
DelimiterCount = openSticks
111209
};
210+
112211
if (!found)
113212
processor.EmitError(line + 1, column + 3, substitutionLeaf.Span.Length - 3, $"Substitution key {{{key}}} is undefined");
213+
else
214+
{
215+
List<SubstitutionMutation>? mutations = null;
216+
if (components.Length >= 10)
217+
processor.EmitError(line + 1, column + 3, substitutionLeaf.Span.Length - 3, $"Substitution key {{{key}}} defines too many mutations, none will be applied");
218+
else if (components.Length > 1)
219+
{
220+
foreach (var c in components[1..])
221+
{
222+
if (SubstitutionMutationExtensions.TryParse(c.Trim(), out var mutation, true, true))
223+
{
224+
mutations ??= [];
225+
mutations.Add(mutation);
226+
}
227+
else
228+
processor.EmitError(line + 1, column + 3, substitutionLeaf.Span.Length - 3, $"Mutation '{c}' on {{{key}}} is undefined");
229+
}
230+
}
231+
232+
substitutionLeaf.Mutations = mutations;
233+
}
234+
114235

115236
if (processor.TrackTrivia)
116237
{

0 commit comments

Comments
 (0)