Skip to content

Commit 3564b77

Browse files
authored
Language Override per snippet (#750)
1 parent 2901b54 commit 3564b77

13 files changed

Lines changed: 233 additions & 11 deletions

readme.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,37 @@ Console.WriteLine("Hello, World");
460460
````
461461

462462

463+
## Language Override
464+
465+
By default the language of a rendered fenced code block is derived from the source file extension (e.g. a snippet extracted from a `.cs` file renders as `csharp`). The language can be overridden per snippet by adding a `lang=` token as the first item inside the parenthesised metadata:
466+
467+
```csharp
468+
// begin-snippet: SampleJson(lang=json)
469+
{"hello": "world"}
470+
// end-snippet
471+
```
472+
473+
Renders as:
474+
475+
````markdown
476+
<-- begin-snippet: SampleJson -->
477+
```json
478+
{"hello": "world"}
479+
```
480+
<-- end-snippet -->
481+
````
482+
483+
`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:
484+
485+
```csharp
486+
// begin-snippet: SampleJson(lang=json title=config.json)
487+
{"hello": "world"}
488+
// end-snippet
489+
```
490+
491+
The value must be lowercase alphanumeric.
492+
493+
463494
## More Documentation
464495

465496
* Developer Information<!-- include: doc-index. path: /docs/mdsource/doc-index.include.md -->

readme.source.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,37 @@ Console.WriteLine("Hello, World");
369369
````
370370

371371

372+
## Language Override
373+
374+
By default the language of a rendered fenced code block is derived from the source file extension (e.g. a snippet extracted from a `.cs` file renders as `csharp`). The language can be overridden per snippet by adding a `lang=` token as the first item inside the parenthesised metadata:
375+
376+
```csharp
377+
// begin-snippet: SampleJson(lang=json)
378+
{"hello": "world"}
379+
// end-snippet
380+
```
381+
382+
Renders as:
383+
384+
````markdown
385+
<-- begin-snippet: SampleJson -->
386+
```json
387+
{"hello": "world"}
388+
```
389+
<-- end-snippet -->
390+
````
391+
392+
`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:
393+
394+
```csharp
395+
// begin-snippet: SampleJson(lang=json title=config.json)
396+
{"hello": "world"}
397+
// end-snippet
398+
```
399+
400+
The value must be lowercase alphanumeric.
401+
402+
372403
## More Documentation
373404

374405
include: doc-index

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<PropertyGroup>
44
<NoWarn>CS1591;NU1608;NU1109</NoWarn>
5-
<Version>28.0.2</Version>
5+
<Version>28.1.0</Version>
66
<LangVersion>preview</LangVersion>
77
<AssemblyVersion>1.0.0</AssemblyVersion>
88
<PackageTags>Markdown, Snippets, mdsnippets, documentation, MarkdownSnippets</PackageTags>

src/MarkdownSnippets/Reading/FileSnippetExtractor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ static IEnumerable<Snippet> GetSnippets(TextReader stringReader, string path, in
154154

155155
var trimmedLine = line.AsSpan().Trim();
156156

157-
if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive))
157+
if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive, out var languageOverride))
158158
{
159-
loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive);
159+
loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive, languageOverride);
160160
continue;
161161
}
162162

@@ -208,7 +208,7 @@ static Snippet BuildSnippet(string path, LoopStack loopStack, string language, i
208208
key: loopState.Key,
209209
value: value,
210210
path: path,
211-
language: language,
211+
language: loopState.Language ?? language,
212212
expressiveCode: loopState.ExpressiveCode
213213
);
214214
}

src/MarkdownSnippets/Reading/LoopStack.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ public void AppendLine(string line)
1515

1616
public void Pop() => stack.Pop();
1717

18-
public void Push(EndFunc endFunc, CharSpan key, int startLine, int maxWidth, string newLine, CharSpan expressiveCode)
18+
public void Push(EndFunc endFunc, CharSpan key, int startLine, int maxWidth, string newLine, CharSpan expressiveCode, CharSpan language)
1919
{
2020
var expressiveCodeString = expressiveCode.Length == 0 ? null : expressiveCode.ToString();
21+
var languageString = language.Length == 0 ? null : language.ToString();
2122

22-
var state = new LoopState(key.ToString(), endFunc, startLine, maxWidth, newLine, expressiveCodeString);
23+
var state = new LoopState(key.ToString(), endFunc, startLine, maxWidth, newLine, expressiveCodeString, languageString);
2324
stack.Push(state);
2425
}
2526

src/MarkdownSnippets/Reading/LoopState.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[DebuggerDisplay("Key={Key}")]
2-
class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null)
2+
class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null, string? language = null)
33
{
44
public string GetLines()
55
{
@@ -88,4 +88,5 @@ void CheckWhiteSpace(CharSpan line, char whiteSpace)
8888
public int StartLine { get; } = startLine;
8989
int newlineCount;
9090
public string? ExpressiveCode { get; } = expressiveCode;
91+
public string? Language { get; } = language;
9192
}

src/MarkdownSnippets/Reading/StartEndTester.cs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ internal static bool IsStart(
1414
CharSpan path,
1515
out CharSpan currentKey,
1616
[NotNullWhen(true)] out EndFunc? endFunc,
17-
out CharSpan expressiveCode)
17+
out CharSpan expressiveCode,
18+
out CharSpan language)
1819
{
19-
if (IsBeginSnippet(trimmedLine, path, out currentKey, out expressiveCode))
20+
if (IsBeginSnippet(trimmedLine, path, out currentKey, out expressiveCode, out language))
2021
{
2122
endFunc = IsEndSnippet;
2223
return true;
@@ -27,10 +28,12 @@ internal static bool IsStart(
2728
endFunc = IsEndRegion;
2829
// not supported for regions
2930
expressiveCode = null;
31+
language = null;
3032
return true;
3133
}
3234

3335
expressiveCode = null;
36+
language = null;
3437
endFunc = throwFunc;
3538

3639
return false;
@@ -80,9 +83,18 @@ internal static bool IsBeginSnippet(
8083
CharSpan line,
8184
CharSpan path,
8285
out CharSpan key,
83-
out CharSpan expressiveCode)
86+
out CharSpan expressiveCode) =>
87+
IsBeginSnippet(line, path, out key, out expressiveCode, out _);
88+
89+
internal static bool IsBeginSnippet(
90+
CharSpan line,
91+
CharSpan path,
92+
out CharSpan key,
93+
out CharSpan expressiveCode,
94+
out CharSpan language)
8495
{
8596
expressiveCode = null;
97+
language = null;
8698
var beginSnippetIndex = IndexOf(line, "begin-snippet: ");
8799
if (beginSnippetIndex == -1)
88100
{
@@ -115,7 +127,9 @@ internal static bool IsBeginSnippet(
115127
""");
116128
}
117129

118-
expressiveCode = substring[(startArgs + 1)..^1].Trim();
130+
var args = substring[(startArgs + 1)..^1].Trim();
131+
args = ExtractLanguage(args, key, path, line, out language);
132+
expressiveCode = args;
119133
}
120134

121135
if (key.Length == 0)
@@ -142,6 +156,59 @@ Key cannot contain whitespace or start/end with symbols.
142156
""");
143157
}
144158

159+
static CharSpan ExtractLanguage(CharSpan args, scoped CharSpan key, scoped CharSpan path, scoped CharSpan line, out CharSpan language)
160+
{
161+
language = null;
162+
if (!args.StartsWith("lang=", StringComparison.Ordinal))
163+
{
164+
return args;
165+
}
166+
167+
var rest = args[5..];
168+
var end = rest.IndexOf(' ');
169+
CharSpan value;
170+
CharSpan remainder;
171+
if (end == -1)
172+
{
173+
value = rest;
174+
remainder = null;
175+
}
176+
else
177+
{
178+
value = rest[..end];
179+
remainder = rest[(end + 1)..].Trim();
180+
}
181+
182+
if (value.Length == 0)
183+
{
184+
throw new SnippetReadingException(
185+
$"""
186+
lang= must have a value.
187+
Key: {key}
188+
Path: {path}
189+
Line: {line}
190+
""");
191+
}
192+
193+
foreach (var c in value)
194+
{
195+
if (c is < 'a' or > 'z' && c is < '0' or > '9')
196+
{
197+
throw new SnippetReadingException(
198+
$"""
199+
lang value must be lowercase alphanumeric.
200+
Key: {key}
201+
Value: {value.ToString()}
202+
Path: {path}
203+
Line: {line}
204+
""");
205+
}
206+
}
207+
208+
language = value;
209+
return remainder;
210+
}
211+
145212
static int IndexOf(CharSpan line, CharSpan value)
146213
{
147214
if (value.Length > line.Length)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
Key: CodeKey,
4+
Language: json,
5+
Value: {"a": 1},
6+
Error: ,
7+
FileLocation: path.cs(1-3),
8+
IsInError: false
9+
}
10+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
Key: CodeKey,
4+
Language: json,
5+
Value: {"a": 1},
6+
Error: ,
7+
FileLocation: path.cs(1-3),
8+
IsInError: false
9+
}
10+
]

src/Tests/SnippetExtractor/SnippetExtractorTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,30 @@ public Task CanExtractFromXml()
204204
return Verify(snippets);
205205
}
206206

207+
[Fact]
208+
public Task LanguageOverride()
209+
{
210+
var input = """
211+
<!-- begin-snippet: CodeKey (lang=json) -->
212+
{"a": 1}
213+
<!-- end-snippet -->
214+
""";
215+
var snippets = FromText(input);
216+
return Verify(snippets);
217+
}
218+
219+
[Fact]
220+
public Task LanguageOverrideWithExpressiveCode()
221+
{
222+
var input = """
223+
<!-- begin-snippet: CodeKey (lang=json title="config.json") -->
224+
{"a": 1}
225+
<!-- end-snippet -->
226+
""";
227+
var snippets = FromText(input);
228+
return Verify(snippets);
229+
}
230+
207231
static List<Snippet> FromText(string contents)
208232
{
209233
using var reader = new StringReader(contents);

0 commit comments

Comments
 (0)