From fddba755cc4d3945611df72ad3a60d8e18fb05ab Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Mon, 28 Apr 2025 12:00:22 -0400 Subject: [PATCH 1/4] Add support for expressive code blocks in snippets This update introduces the handling of expressive code metadata for code snippets, enabling enhanced representation and functionality. Changes include parsing, storing, and rendering expressive attributes in snippets, along with updates to tests and relevant components to validate the new feature. Additionally, a package dependency (Argon) is updated for compatibility. Only works for comment snippets currently. --- src/Directory.Packages.props | 2 +- .../Processing/MarkdownProcessor.cs | 3 +- .../SimpleSnippetMarkdownHandling.cs | 2 +- .../Processing/SnippetMarkdownHandling.cs | 2 +- .../Reading/FileSnippetExtractor.cs | 11 ++--- src/MarkdownSnippets/Reading/LoopStack.cs | 4 +- src/MarkdownSnippets/Reading/LoopState.cs | 3 +- src/MarkdownSnippets/Reading/Snippet.cs | 11 ++++- .../Reading/StartEndTester.cs | 32 ++++++++++---- src/Tests/DirectoryMarkdownProcessorTests.cs | 3 +- .../MarkdownProcessorTests.cs | 9 ++-- ...nHandlingTests.ExpressiveCode.verified.txt | 3 ++ .../SimpleSnippetMarkdownHandlingTests.cs | 15 ++++++- src/Tests/SnippetExtensionsTests.cs | 3 +- ....CanExtractWithExpressiveCode.verified.txt | 10 +++++ .../SnippetExtractor/SnippetExtractorTests.cs | 12 ++++++ src/Tests/SnippetMarkdownHandlingTests.cs | 2 +- .../StartEndTester_IsBeginSnippetTests.cs | 42 +++++++++++++------ 18 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 src/Tests/SimpleSnippetMarkdownHandlingTests.ExpressiveCode.verified.txt create mode 100644 src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithExpressiveCode.verified.txt diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b7e64a04..a35f91ee 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,7 +4,7 @@ true - + diff --git a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs index 01c606ca..67c72e79 100644 --- a/src/MarkdownSnippets/Processing/MarkdownProcessor.cs +++ b/src/MarkdownSnippets/Processing/MarkdownProcessor.cs @@ -353,7 +353,8 @@ Snippet FileToSnippet(string key, string file, string? path) value: text, key: key, language: Path.GetExtension(file)[1..], - path: path); + path: path, + expressiveCode: null); } (string text, int lineCount) ReadNonStartEndLines(string file) diff --git a/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs b/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs index 72814721..79de7db5 100644 --- a/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs +++ b/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs @@ -15,7 +15,7 @@ public static void Append(string key, IEnumerable snippets, Action appendLine, Snippet snippet) { - appendLine($"```{snippet.Language}"); + appendLine($"```{snippet.Language} {snippet.ExpressiveCode}".TrimEnd()); appendLine(snippet.Value); appendLine("```"); } diff --git a/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs b/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs index 2f4f632d..ac358db5 100644 --- a/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs +++ b/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs @@ -86,7 +86,7 @@ string GetSupText(Snippet snippet, string anchor) static void WriteSnippetValueAndLanguage(Action appendLine, Snippet snippet) { - appendLine($"```{snippet.Language}"); + appendLine($"```{snippet.Language} {snippet.ExpressiveCode}".TrimEnd()); appendLine(snippet.Value); appendLine("```"); } diff --git a/src/MarkdownSnippets/Reading/FileSnippetExtractor.cs b/src/MarkdownSnippets/Reading/FileSnippetExtractor.cs index 7c172fb1..e3019568 100644 --- a/src/MarkdownSnippets/Reading/FileSnippetExtractor.cs +++ b/src/MarkdownSnippets/Reading/FileSnippetExtractor.cs @@ -43,7 +43,7 @@ public static async Task AppendUrlAsSnippet(ICollection snippets, strin throw new SnippetException($"Unable to get UrlAsSnippet: {url}"); } - var snippet = Snippet.Build(1, content!.LineCount(), content!, key, GetLanguageFromPath(url), url); + var snippet = Snippet.Build(1, content!.LineCount(), content!, key, GetLanguageFromPath(url), url, null); snippets.Add(snippet); using var reader = new StringReader(content!); @@ -71,7 +71,7 @@ public static void AppendFileAsSnippet(ICollection snippets, string fil { Guard.FileExists(filePath, nameof(filePath)); var text = File.ReadAllText(filePath); - var snippet = Snippet.Build(1, text.LineCount(), text, key, GetLanguageFromPath(filePath), filePath); + var snippet = Snippet.Build(1, text.LineCount(), text, key, GetLanguageFromPath(filePath), filePath, null); snippets.Add(snippet); } @@ -153,9 +153,9 @@ static IEnumerable GetSnippets(TextReader stringReader, string path, in var trimmedLine = line.Trim(); - if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc)) + if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive)) { - loopStack.Push(endFunc, key, index, maxWidth, newLine); + loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive); continue; } @@ -207,7 +207,8 @@ static Snippet BuildSnippet(string path, LoopStack loopStack, string language, i key: loopState.Key, value: value, path: path, - language: language.ToLowerInvariant() + language: language.ToLowerInvariant(), + expressiveCode: loopState.ExpressiveCode ); } } \ No newline at end of file diff --git a/src/MarkdownSnippets/Reading/LoopStack.cs b/src/MarkdownSnippets/Reading/LoopStack.cs index 069ffb56..90483b58 100644 --- a/src/MarkdownSnippets/Reading/LoopStack.cs +++ b/src/MarkdownSnippets/Reading/LoopStack.cs @@ -15,9 +15,9 @@ public void AppendLine(string line) public void Pop() => stack.Pop(); - public void Push(EndFunc endFunc, string key, int startLine, int maxWidth, string newLine) + public void Push(EndFunc endFunc, string key, int startLine, int maxWidth, string newLine, string? block) { - var state = new LoopState(key, endFunc, startLine, maxWidth, newLine); + var state = new LoopState(key, endFunc, startLine, maxWidth, newLine, block); stack.Push(state); } diff --git a/src/MarkdownSnippets/Reading/LoopState.cs b/src/MarkdownSnippets/Reading/LoopState.cs index 4d266235..28b5912b 100644 --- a/src/MarkdownSnippets/Reading/LoopState.cs +++ b/src/MarkdownSnippets/Reading/LoopState.cs @@ -1,5 +1,5 @@ [DebuggerDisplay("Key={Key}")] -class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine) +class LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null) { public string GetLines() { @@ -85,4 +85,5 @@ void CheckWhiteSpace(string line, char whiteSpace) public EndFunc EndFunc { get; } = endFunc; public int StartLine { get; } = startLine; int newlineCount; + public string? ExpressiveCode { get; } = expressiveCode; } \ No newline at end of file diff --git a/src/MarkdownSnippets/Reading/Snippet.cs b/src/MarkdownSnippets/Reading/Snippet.cs index 02813426..52d853bc 100644 --- a/src/MarkdownSnippets/Reading/Snippet.cs +++ b/src/MarkdownSnippets/Reading/Snippet.cs @@ -26,7 +26,7 @@ public static Snippet BuildError(string key, int lineNumberInError, string path, /// /// Initialise a new instance of . /// - public static Snippet Build(int startLine, int endLine, string value, string key, string language, string? path) + public static Snippet Build(int startLine, int endLine, string value, string key, string language, string? path, string? expressiveCode) { Guard.AgainstNullAndEmpty(key, nameof(key)); Guard.AgainstEmpty(path, nameof(path)); @@ -46,6 +46,7 @@ public static Snippet Build(int startLine, int endLine, string value, string key Key = key, language = language, Path = path, + ExpressiveCode = expressiveCode, Error = "" }; } @@ -59,6 +60,12 @@ public static Snippet Build(int startLine, int endLine, string value, string key /// public string Key { get; private init; } = null!; + /// + /// An associated expressive code block with the snippet + /// See https://expressive-code.com/ + /// + public string? ExpressiveCode { get; private init; } + /// /// The language of the snippet, extracted from the file extension of the input file. /// @@ -70,6 +77,7 @@ public string Language return language!; } } + string? language; /// @@ -98,6 +106,7 @@ public string? FileLocation { return null; } + return $"{Path}({StartLine}-{EndLine})"; } } diff --git a/src/MarkdownSnippets/Reading/StartEndTester.cs b/src/MarkdownSnippets/Reading/StartEndTester.cs index 37044a90..ab644cd9 100644 --- a/src/MarkdownSnippets/Reading/StartEndTester.cs +++ b/src/MarkdownSnippets/Reading/StartEndTester.cs @@ -1,7 +1,9 @@ public delegate bool EndFunc(string line); -static class StartEndTester +static partial class StartEndTester { + static readonly Regex keyPatternRegex = new(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?"); + internal static bool IsStartOrEnd(string trimmedLine) => IsBeginSnippet(trimmedLine) || IsEndSnippet(trimmedLine) || @@ -12,21 +14,28 @@ internal static bool IsStart( string trimmedLine, string path, [NotNullWhen(true)] out string? currentKey, - [NotNullWhen(true)] out EndFunc? endFunc) + [NotNullWhen(true)] out EndFunc? endFunc, + out string? expressiveCode + ) { - if (IsBeginSnippet(trimmedLine, path, out currentKey)) + if (IsBeginSnippet(trimmedLine, path, out currentKey, out var block)) { endFunc = IsEndSnippet; + expressiveCode = block; return true; } if (IsStartRegion(trimmedLine, out currentKey)) { endFunc = IsEndRegion; + // not supported for regions + expressiveCode = null; return true; } + expressiveCode = null; endFunc = throwFunc; + return false; } @@ -78,8 +87,11 @@ static bool IsBeginSnippet(string line) internal static bool IsBeginSnippet( string line, string path, - [NotNullWhen(true)] out string? key) + [NotNullWhen(true)] out string? key, + [NotNullWhen(true)] out string? expressiveCode + ) { + expressiveCode = null; var beginSnippetIndex = IndexOf(line, "begin-snippet: "); if (beginSnippetIndex == -1) { @@ -90,8 +102,10 @@ internal static bool IsBeginSnippet( var startIndex = beginSnippetIndex + 15; var substring = line .TrimBackCommentChars(startIndex); - var split = substring.SplitBySpace(); - if (split.Length == 0) + + var match = keyPatternRegex.Match(substring); + + if (match.Length == 0) { throw new SnippetReadingException( $""" @@ -101,7 +115,8 @@ No Key could be derived. """); } - key = split[0]; + var partOne = match.Groups[1].Value; + var split = partOne.SplitBySpace(); if (split.Length != 1) { throw new SnippetReadingException( @@ -112,6 +127,9 @@ Too many parts. """); } + key = split[0]; + expressiveCode = match.Groups[2].Value; + if (KeyValidator.IsValidKey(key.AsSpan())) { return true; diff --git a/src/Tests/DirectoryMarkdownProcessorTests.cs b/src/Tests/DirectoryMarkdownProcessorTests.cs index ed0fb2fa..c29e9519 100644 --- a/src/Tests/DirectoryMarkdownProcessorTests.cs +++ b/src/Tests/DirectoryMarkdownProcessorTests.cs @@ -507,5 +507,6 @@ static Snippet SnippetBuild(string key, string? path = null) => endLine: 2, value: "the code from " + key, key: key, - path: path); + path: path, + expressiveCode: null); } \ No newline at end of file diff --git a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs index 3081308b..1c194807 100644 --- a/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs +++ b/src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs @@ -204,7 +204,8 @@ public Task WithTwoLineSnippet() Snippet """, key: "theKey", - path: "thePath") + path: "thePath", + expressiveCode: null), ]); } @@ -236,7 +237,8 @@ public Task WithMultiLineSnippet() Snippet """, key: "theKey", - path: "thePath") + path: "thePath", + expressiveCode: null) ]); } @@ -669,5 +671,6 @@ static Snippet SnippetBuild(string language, string key) => endLine: 2, value: "Snippet", key: key, - path: "thePath"); + path: "thePath", + expressiveCode: null); } \ No newline at end of file diff --git a/src/Tests/SimpleSnippetMarkdownHandlingTests.ExpressiveCode.verified.txt b/src/Tests/SimpleSnippetMarkdownHandlingTests.ExpressiveCode.verified.txt new file mode 100644 index 00000000..2859d222 --- /dev/null +++ b/src/Tests/SimpleSnippetMarkdownHandlingTests.ExpressiveCode.verified.txt @@ -0,0 +1,3 @@ +```cs title="HelloWorld.cs" {1} +Console.WriteLine("Hello World"); +``` diff --git a/src/Tests/SimpleSnippetMarkdownHandlingTests.cs b/src/Tests/SimpleSnippetMarkdownHandlingTests.cs index de418270..c6675bd9 100644 --- a/src/Tests/SimpleSnippetMarkdownHandlingTests.cs +++ b/src/Tests/SimpleSnippetMarkdownHandlingTests.cs @@ -4,7 +4,20 @@ public Task Append() { var builder = new StringBuilder(); - var snippets = new List {Snippet.Build(1, 2, "theValue", "thekey", "thelanguage", "thePath")}; + var snippets = new List {Snippet.Build(1, 2, "theValue", "thekey", "thelanguage", "thePath", null)}; + using (var writer = new StringWriter(builder)) + { + SimpleSnippetMarkdownHandling.Append("key1", snippets, writer.WriteLine); + } + + return Verify(builder.ToString()); + } + + [Fact] + public Task ExpressiveCode() + { + var builder = new StringBuilder(); + var snippets = new List {Snippet.Build(1, 2, """Console.WriteLine("Hello World");""", "thekey", "cs", "thePath", """title="HelloWorld.cs" {1}""")}; using (var writer = new StringWriter(builder)) { SimpleSnippetMarkdownHandling.Append("key1", snippets, writer.WriteLine); diff --git a/src/Tests/SnippetExtensionsTests.cs b/src/Tests/SnippetExtensionsTests.cs index 9bc6186b..a7cbeb2c 100644 --- a/src/Tests/SnippetExtensionsTests.cs +++ b/src/Tests/SnippetExtensionsTests.cs @@ -30,5 +30,6 @@ static Snippet SnippetBuild(string key, string? path) => endLine: 2, value: "Snippet", key: key, - path: path); + path: path, + expressiveCode: null); } \ No newline at end of file diff --git a/src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithExpressiveCode.verified.txt b/src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithExpressiveCode.verified.txt new file mode 100644 index 00000000..0aca803e --- /dev/null +++ b/src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithExpressiveCode.verified.txt @@ -0,0 +1,10 @@ +[ + { + Key: CodeKey, + Language: cs, + Value: Console.WriteLine("Hello World");, + Error: , + FileLocation: path.cs(1-3), + IsInError: false + } +] \ No newline at end of file diff --git a/src/Tests/SnippetExtractor/SnippetExtractorTests.cs b/src/Tests/SnippetExtractor/SnippetExtractorTests.cs index c2860c59..a5673b4b 100644 --- a/src/Tests/SnippetExtractor/SnippetExtractorTests.cs +++ b/src/Tests/SnippetExtractor/SnippetExtractorTests.cs @@ -288,4 +288,16 @@ the code var snippets = FromText(input); return Verify(snippets); } + + [Fact] + public Task CanExtractWithExpressiveCode() + { + var input = """ + + Console.WriteLine("Hello World"); + + """; + var snippets = FromText(input); + return Verify(snippets); + } } \ No newline at end of file diff --git a/src/Tests/SnippetMarkdownHandlingTests.cs b/src/Tests/SnippetMarkdownHandlingTests.cs index 63613d12..1b080f8f 100644 --- a/src/Tests/SnippetMarkdownHandlingTests.cs +++ b/src/Tests/SnippetMarkdownHandlingTests.cs @@ -71,5 +71,5 @@ public Task AppendHashed() } static List Snippets() => - [Snippet.Build(1, 2, "theValue", "thekey", "thelanguage", Environment.CurrentDirectory)]; + [Snippet.Build(1, 2, "theValue", "thekey", "thelanguage", Environment.CurrentDirectory, expressiveCode: null)]; } \ No newline at end of file diff --git a/src/Tests/StartEndTester_IsBeginSnippetTests.cs b/src/Tests/StartEndTester_IsBeginSnippetTests.cs index c9f646f8..f4ef9ec3 100644 --- a/src/Tests/StartEndTester_IsBeginSnippetTests.cs +++ b/src/Tests/StartEndTester_IsBeginSnippetTests.cs @@ -3,23 +3,23 @@ [Fact] public void CanExtractFromXml() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("CodeKey", key); } [Fact] public Task ShouldThrowForNoKey() => - Throws(() => StartEndTester.IsBeginSnippet("", "file", out _)); + Throws(() => StartEndTester.IsBeginSnippet("", "file", out _, out _)); [Fact] public void ShouldNotThrowForNoKeyWithNoSpace() => - StartEndTester.IsBeginSnippet("", "file", out _); + StartEndTester.IsBeginSnippet("", "file", out _, out _); [Fact] public void CanExtractFromXmlWithMissingSpaces() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("CodeKey", key); } @@ -27,7 +27,7 @@ public void CanExtractFromXmlWithMissingSpaces() [Fact] public void CanExtractFromXmlWithExtraSpaces() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("CodeKey", key); } @@ -35,7 +35,7 @@ public void CanExtractFromXmlWithExtraSpaces() [Fact] public void CanExtractWithNoTrailingCharacters() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("Code_Key", key); } @@ -51,7 +51,7 @@ public void CanExtractWithUnderScores() [Fact] public void CanExtractWithDashes() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("Code-Key", key); } @@ -59,17 +59,17 @@ public void CanExtractWithDashes() [Fact] public Task ShouldThrowForKeyStartingWithSymbol() => Throws(() => - StartEndTester.IsBeginSnippet("", "file", out _)); + StartEndTester.IsBeginSnippet("", "file", out _, out _)); [Fact] public Task ShouldThrowForKeyEndingWithSymbol() => Throws(() => - StartEndTester.IsBeginSnippet("", "file", out _)); + StartEndTester.IsBeginSnippet("", "file", out _, out _)); [Fact] public void CanExtractWithDifferentEndComments() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("/* begin-snippet: CodeKey */", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("/* begin-snippet: CodeKey */", "file", out var key,out _); Assert.True(isBeginSnippet); Assert.Equal("CodeKey", key); } @@ -77,8 +77,26 @@ public void CanExtractWithDifferentEndComments() [Fact] public void CanExtractWithDifferentEndCommentsAndNoSpaces() { - var isBeginSnippet = StartEndTester.IsBeginSnippet("/*begin-snippet: CodeKey */", "file", out var key); + var isBeginSnippet = StartEndTester.IsBeginSnippet("/*begin-snippet: CodeKey */", "file", out var key, out _); Assert.True(isBeginSnippet); Assert.Equal("CodeKey", key); } + + [Fact] + public void CanExtractWithExpressiveCodeWithHtmlSnippet() + { + var isBeginSnippet = StartEndTester.IsBeginSnippet("""""", "file", out var key, out var block); + Assert.True(isBeginSnippet); + Assert.Equal("CodeKey", key); + Assert.Equal("""title="Program.cs" {1-3}""", block); + } + + [Fact] + public void CanExtractWithExpressiveCodeWithCsharpComment() + { + var isBeginSnippet = StartEndTester.IsBeginSnippet("""/*begin-snippet: CodeKey(title="Program.cs" {1-3})*/""", "file", out var key, out var expressive); + Assert.True(isBeginSnippet); + Assert.Equal("CodeKey", key); + Assert.Equal("""title="Program.cs" {1-3}""", expressive); + } } \ No newline at end of file From c9ab3d6ac818937bb98a2c6807460543f716aba1 Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Fri, 2 May 2025 14:20:15 -0400 Subject: [PATCH 2/4] Add ExpressiveCode class and integrate Pattern handling Introduced the `ExpressiveCode` class to centralize regex pattern handling, supporting both .NET 8.0+ generated regex and static regex for older versions. Updated `StartEndTester` to utilize the shared `ExpressiveCode.Instance.Pattern` for consistency and maintainability. Expanded documentation with examples of snippet metadata and its rendering. --- readme.md | 44 +++++++++++++++++++ .../Reading/ExpressiveCode.cs | 17 +++++++ .../Reading/StartEndTester.cs | 4 +- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 src/MarkdownSnippets/Reading/ExpressiveCode.cs diff --git a/readme.md b/readme.md index b391cf15..87f4c5e3 100644 --- a/readme.md +++ b/readme.md @@ -366,6 +366,50 @@ Windows Registry Editor Version 5.00 snippet source | anchor +## Expressive Code / Snippet Metadata + +When defining snippets, you can add additional metadata at the source to the rendered snippet using the following syntax. + +```csharp +// begin-snippet: HelloWorld(title=Program.cs {1}) +Console.WriteLine("Hello, World"); +// end-snippet +``` + +Note the text within the parenthesis; this is metadata we want to add to the rendered Markdown block immediately after the language declaration. +The metadata is stripped and the key remains `HelloWorld`. The feature produces the following output at your target destination (will vary based on your configuration): + +````markdown +<-- begin-snippet: HelloWorld --> +```csharp title=Program.cs {1} +Console.WriteLine("Hello, World"); +``` +<-- end-snippet --> +```` + +This syntax is known as [Expressive Code](https://expressive-code.com/) and is supported in documentation systems such +as [Astro Starlight](https://github.com/withastro/starlight/) but can be installed in any Markdown-powered tool +that supports [reHype](https://github.com/rehypejs/rehype). + +It is important to note, the metadata is not explicitly limited to Expressive code. Any text within the `()` will be rendered after +the language block. This can be useful for adding additional information based on your specific rendering engine. For example, if +you use a presentation tool such as [Sli.dev](https://sli.dev/), you can use this feature to apply [magic-move animations](https://sli.dev/features/shiki-magic-move). + +```csharp +// begin-snippet: EncapsulateVariable({*|2}) +Console.WriteLine("Hello, World"); +// end-snippet +``` + +The above snippet will render as follows: + +````markdown +<-- begin-snippet: EncapsulateVariable --> +```csharp {*|2} +Console.WriteLine("Hello, World"); +``` +<-- end-snippet --> +```` ## More Documentation diff --git a/src/MarkdownSnippets/Reading/ExpressiveCode.cs b/src/MarkdownSnippets/Reading/ExpressiveCode.cs new file mode 100644 index 00000000..9bf0483f --- /dev/null +++ b/src/MarkdownSnippets/Reading/ExpressiveCode.cs @@ -0,0 +1,17 @@ +internal partial class ExpressiveCode +{ + public ExpressiveCode() => +#if NET8_0_OR_GREATER + Pattern = KeyPatternRegex(); +#else + Pattern = KeyPatternRegex; +#endif + public static ExpressiveCode Instance { get; } = new(); + public Regex Pattern { get; } +#if NET8_0_OR_GREATER + [GeneratedRegex(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?")] + protected partial Regex KeyPatternRegex(); +#else + protected static readonly Regex KeyPatternRegex = new(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?"); +#endif +} \ No newline at end of file diff --git a/src/MarkdownSnippets/Reading/StartEndTester.cs b/src/MarkdownSnippets/Reading/StartEndTester.cs index ab644cd9..4b2e7130 100644 --- a/src/MarkdownSnippets/Reading/StartEndTester.cs +++ b/src/MarkdownSnippets/Reading/StartEndTester.cs @@ -2,8 +2,6 @@ static partial class StartEndTester { - static readonly Regex keyPatternRegex = new(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?"); - internal static bool IsStartOrEnd(string trimmedLine) => IsBeginSnippet(trimmedLine) || IsEndSnippet(trimmedLine) || @@ -103,7 +101,7 @@ internal static bool IsBeginSnippet( var substring = line .TrimBackCommentChars(startIndex); - var match = keyPatternRegex.Match(substring); + var match = ExpressiveCode.Instance.Pattern.Match(substring); if (match.Length == 0) { From 7aee95ecfdffbe1d3bf3da3ca4fe53eae00ee857 Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Fri, 2 May 2025 14:31:40 -0400 Subject: [PATCH 3/4] Refactor Regex initialization with conditional compilation. Reorganized Regex initialization using `#if NET8_0_OR_GREATER` to leverage `GeneratedRegex` for .NET 8+ and fallback to static initialization otherwise instance is null. --- src/MarkdownSnippets/Reading/ExpressiveCode.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MarkdownSnippets/Reading/ExpressiveCode.cs b/src/MarkdownSnippets/Reading/ExpressiveCode.cs index 9bf0483f..fd35c34e 100644 --- a/src/MarkdownSnippets/Reading/ExpressiveCode.cs +++ b/src/MarkdownSnippets/Reading/ExpressiveCode.cs @@ -1,5 +1,11 @@ internal partial class ExpressiveCode { +#if NET8_0_OR_GREATER + [GeneratedRegex(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?")] + protected partial Regex KeyPatternRegex(); +#else + protected static readonly Regex KeyPatternRegex = new(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?"); +#endif public ExpressiveCode() => #if NET8_0_OR_GREATER Pattern = KeyPatternRegex(); @@ -8,10 +14,4 @@ public ExpressiveCode() => #endif public static ExpressiveCode Instance { get; } = new(); public Regex Pattern { get; } -#if NET8_0_OR_GREATER - [GeneratedRegex(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?")] - protected partial Regex KeyPatternRegex(); -#else - protected static readonly Regex KeyPatternRegex = new(@"([a-zA-Z0-9\-_]+)(?:\((.*?)\))?"); -#endif } \ No newline at end of file From 274ef54dce1a9749f9f33995348f53b92b24fe17 Mon Sep 17 00:00:00 2001 From: khalidabuhakmeh Date: Fri, 2 May 2025 14:41:16 -0400 Subject: [PATCH 4/4] Refactor snippet language declaration handling. Simplified the logic for generating the language declaration by checking for empty `ExpressiveCode`. No more TrimEnd. --- .../Processing/SimpleSnippetMarkdownHandling.cs | 6 +++++- src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs b/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs index 79de7db5..9409f04a 100644 --- a/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs +++ b/src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs @@ -15,7 +15,11 @@ public static void Append(string key, IEnumerable snippets, Action appendLine, Snippet snippet) { - appendLine($"```{snippet.Language} {snippet.ExpressiveCode}".TrimEnd()); + var declaration = + string.IsNullOrWhiteSpace(snippet.ExpressiveCode) + ? snippet.Language + : $"{snippet.Language} {snippet.ExpressiveCode}"; + appendLine($"```{declaration}"); appendLine(snippet.Value); appendLine("```"); } diff --git a/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs b/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs index ac358db5..e5d71a52 100644 --- a/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs +++ b/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs @@ -86,7 +86,11 @@ string GetSupText(Snippet snippet, string anchor) static void WriteSnippetValueAndLanguage(Action appendLine, Snippet snippet) { - appendLine($"```{snippet.Language} {snippet.ExpressiveCode}".TrimEnd()); + var declaration = + string.IsNullOrWhiteSpace(snippet.ExpressiveCode) + ? snippet.Language + : $"{snippet.Language} {snippet.ExpressiveCode}"; + appendLine($"```{declaration}"); appendLine(snippet.Value); appendLine("```"); }