Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,37 @@ Console.WriteLine("Hello, World");
````


## Language Override

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:

```csharp
// begin-snippet: SampleJson(lang=json)
{"hello": "world"}
// end-snippet
```

Renders as:

````markdown
<-- begin-snippet: SampleJson -->
```json
{"hello": "world"}
```
<-- end-snippet -->
````

`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:

```csharp
// begin-snippet: SampleJson(lang=json title=config.json)
{"hello": "world"}
// end-snippet
```

The value must be lowercase alphanumeric.


## More Documentation

* Developer Information<!-- include: doc-index. path: /docs/mdsource/doc-index.include.md -->
Expand Down
31 changes: 31 additions & 0 deletions readme.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,37 @@ Console.WriteLine("Hello, World");
````


## Language Override

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:

```csharp
// begin-snippet: SampleJson(lang=json)
{"hello": "world"}
// end-snippet
```

Renders as:

````markdown
<-- begin-snippet: SampleJson -->
```json
{"hello": "world"}
```
<-- end-snippet -->
````

`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:

```csharp
// begin-snippet: SampleJson(lang=json title=config.json)
{"hello": "world"}
// end-snippet
```

The value must be lowercase alphanumeric.


## More Documentation

include: doc-index
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU1608;NU1109</NoWarn>
<Version>28.0.2</Version>
<Version>28.1.0</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>Markdown, Snippets, mdsnippets, documentation, MarkdownSnippets</PackageTags>
Expand Down
6 changes: 3 additions & 3 deletions src/MarkdownSnippets/Reading/FileSnippetExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,9 @@ static IEnumerable<Snippet> GetSnippets(TextReader stringReader, string path, in

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

if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive))
if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive, out var languageOverride))
{
loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive);
loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive, languageOverride);
continue;
}

Expand Down Expand Up @@ -208,7 +208,7 @@ static Snippet BuildSnippet(string path, LoopStack loopStack, string language, i
key: loopState.Key,
value: value,
path: path,
language: language,
language: loopState.Language ?? language,
expressiveCode: loopState.ExpressiveCode
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/MarkdownSnippets/Reading/LoopStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ public void AppendLine(string line)

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

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

var state = new LoopState(key.ToString(), endFunc, startLine, maxWidth, newLine, expressiveCodeString);
var state = new LoopState(key.ToString(), endFunc, startLine, maxWidth, newLine, expressiveCodeString, languageString);
stack.Push(state);
}

Expand Down
3 changes: 2 additions & 1 deletion src/MarkdownSnippets/Reading/LoopState.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[DebuggerDisplay("Key={Key}")]
class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null)
class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null, string? language = null)
{
public string GetLines()
{
Expand Down Expand Up @@ -88,4 +88,5 @@ void CheckWhiteSpace(CharSpan line, char whiteSpace)
public int StartLine { get; } = startLine;
int newlineCount;
public string? ExpressiveCode { get; } = expressiveCode;
public string? Language { get; } = language;
}
75 changes: 71 additions & 4 deletions src/MarkdownSnippets/Reading/StartEndTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ internal static bool IsStart(
CharSpan path,
out CharSpan currentKey,
[NotNullWhen(true)] out EndFunc? endFunc,
out CharSpan expressiveCode)
out CharSpan expressiveCode,
out CharSpan language)
{
if (IsBeginSnippet(trimmedLine, path, out currentKey, out expressiveCode))
if (IsBeginSnippet(trimmedLine, path, out currentKey, out expressiveCode, out language))
{
endFunc = IsEndSnippet;
return true;
Expand All @@ -27,10 +28,12 @@ internal static bool IsStart(
endFunc = IsEndRegion;
// not supported for regions
expressiveCode = null;
language = null;
return true;
}

expressiveCode = null;
language = null;
endFunc = throwFunc;

return false;
Expand Down Expand Up @@ -80,9 +83,18 @@ internal static bool IsBeginSnippet(
CharSpan line,
CharSpan path,
out CharSpan key,
out CharSpan expressiveCode)
out CharSpan expressiveCode) =>
IsBeginSnippet(line, path, out key, out expressiveCode, out _);

internal static bool IsBeginSnippet(
CharSpan line,
CharSpan path,
out CharSpan key,
out CharSpan expressiveCode,
out CharSpan language)
{
expressiveCode = null;
language = null;
var beginSnippetIndex = IndexOf(line, "begin-snippet: ");
if (beginSnippetIndex == -1)
{
Expand Down Expand Up @@ -115,7 +127,9 @@ internal static bool IsBeginSnippet(
""");
}

expressiveCode = substring[(startArgs + 1)..^1].Trim();
var args = substring[(startArgs + 1)..^1].Trim();
args = ExtractLanguage(args, key, path, line, out language);
expressiveCode = args;
}

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

static CharSpan ExtractLanguage(CharSpan args, scoped CharSpan key, scoped CharSpan path, scoped CharSpan line, out CharSpan language)
{
language = null;
if (!args.StartsWith("lang=", StringComparison.Ordinal))
{
return args;
}

var rest = args[5..];
var end = rest.IndexOf(' ');
CharSpan value;
CharSpan remainder;
if (end == -1)
{
value = rest;
remainder = null;
}
else
{
value = rest[..end];
remainder = rest[(end + 1)..].Trim();
}

if (value.Length == 0)
{
throw new SnippetReadingException(
$"""
lang= must have a value.
Key: {key}
Path: {path}
Line: {line}
""");
}

foreach (var c in value)
{
if (c is < 'a' or > 'z' && c is < '0' or > '9')
{
throw new SnippetReadingException(
$"""
lang value must be lowercase alphanumeric.
Key: {key}
Value: {value.ToString()}
Path: {path}
Line: {line}
""");
}
}

language = value;
return remainder;
}

static int IndexOf(CharSpan line, CharSpan value)
{
if (value.Length > line.Length)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
Key: CodeKey,
Language: json,
Value: {"a": 1},
Error: ,
FileLocation: path.cs(1-3),
IsInError: false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
Key: CodeKey,
Language: json,
Value: {"a": 1},
Error: ,
FileLocation: path.cs(1-3),
IsInError: false
}
]
24 changes: 24 additions & 0 deletions src/Tests/SnippetExtractor/SnippetExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ public Task CanExtractFromXml()
return Verify(snippets);
}

[Fact]
public Task LanguageOverride()
{
var input = """
<!-- begin-snippet: CodeKey (lang=json) -->
{"a": 1}
<!-- end-snippet -->
""";
var snippets = FromText(input);
return Verify(snippets);
}

[Fact]
public Task LanguageOverrideWithExpressiveCode()
{
var input = """
<!-- begin-snippet: CodeKey (lang=json title="config.json") -->
{"a": 1}
<!-- end-snippet -->
""";
var snippets = FromText(input);
return Verify(snippets);
}

static List<Snippet> FromText(string contents)
{
using var reader = new StringReader(contents);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
Type: SnippetReadingException,
Message:
lang= must have a value.
Key: CodeKey
Path: file
Line: <!-- begin-snippet: CodeKey (lang=) -->
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
Type: SnippetReadingException,
Message:
lang value must be lowercase alphanumeric.
Key: CodeKey
Value: C#
Path: file
Line: <!-- begin-snippet: CodeKey (lang=C#) -->
}
30 changes: 30 additions & 0 deletions src/Tests/StartEndTester_IsBeginSnippetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,34 @@ public void CanExtractWithExpressiveCodeWithCsharpCommentTrailingWhitespace()
Assert.Equal("CodeKey", key);
Assert.Equal("""title="Program.cs" {1-3}""", expressive);
}

[Fact]
public void CanExtractLanguageOverride()
{
var isBeginSnippet = StartEndTester.IsBeginSnippet("<!-- begin-snippet: CodeKey (lang=json) -->", "file", out var key, out var expressive, out var language);
Assert.True(isBeginSnippet);
Assert.Equal("CodeKey", key);
Assert.Equal("json", language.ToString());
Assert.Equal(0, expressive.Length);
}

[Fact]
public void CanExtractLanguageOverrideWithExpressiveCode()
{
var isBeginSnippet = StartEndTester.IsBeginSnippet("""<!-- begin-snippet: CodeKey (lang=json title="a.json") -->""", "file", out var key, out var expressive, out var language);
Assert.True(isBeginSnippet);
Assert.Equal("CodeKey", key);
Assert.Equal("json", language.ToString());
Assert.Equal("""title="a.json" """.TrimEnd(), expressive.ToString());
}

[Fact]
public Task ShouldThrowForInvalidLanguageValue() =>
Throws(() =>
StartEndTester.IsBeginSnippet("<!-- begin-snippet: CodeKey (lang=C#) -->", "file", out _, out _, out _));

[Fact]
public Task ShouldThrowForEmptyLanguageValue() =>
Throws(() =>
StartEndTester.IsBeginSnippet("<!-- begin-snippet: CodeKey (lang=) -->", "file", out _, out _, out _));
}
Loading