From b54aa8e6c659ee32efeee070add033bc688783dd Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Sun, 4 Oct 2026 15:03:14 +0200 Subject: [PATCH 1/5] fix(components): keep TypeTable rows that share a name TypeTable tracked registered rows by Name, so a second row with the same name (an overload, or two rows describing one thing) was dropped without a warning. Rows are now tracked by their TypeRow component. --- .../Content/TypeRow.razor | 2 +- .../Content/TypeTable.razor | 6 +++-- tests/ShellDocs.Tests/TypeTableRenderTests.cs | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 tests/ShellDocs.Tests/TypeTableRenderTests.cs diff --git a/src/ShellDocs.Components/Content/TypeRow.razor b/src/ShellDocs.Components/Content/TypeRow.razor index 5487905..5a92075 100644 --- a/src/ShellDocs.Components/Content/TypeRow.razor +++ b/src/ShellDocs.Components/Content/TypeRow.razor @@ -11,6 +11,6 @@ protected override void OnInitialized() { - Parent?.Register(new TypeRowInfo(Name, Type, Default, Description, Required)); + Parent?.Register(this, new TypeRowInfo(Name, Type, Default, Description, Required)); } } diff --git a/src/ShellDocs.Components/Content/TypeTable.razor b/src/ShellDocs.Components/Content/TypeTable.razor index 36f5afc..25cedf1 100644 --- a/src/ShellDocs.Components/Content/TypeTable.razor +++ b/src/ShellDocs.Components/Content/TypeTable.razor @@ -66,10 +66,12 @@ [Parameter] public RenderFragment? ChildContent { get; set; } private readonly List _rows = new(); + private readonly HashSet _registered = new(); - internal void Register(TypeRowInfo info) + // Keyed by the row component, not its Name: rows may share a name (overloads). + internal void Register(TypeRow row, TypeRowInfo info) { - if (_rows.Any(r => r.Name == info.Name)) return; + if (!_registered.Add(row)) return; _rows.Add(info); StateHasChanged(); } diff --git a/tests/ShellDocs.Tests/TypeTableRenderTests.cs b/tests/ShellDocs.Tests/TypeTableRenderTests.cs new file mode 100644 index 0000000..948708b --- /dev/null +++ b/tests/ShellDocs.Tests/TypeTableRenderTests.cs @@ -0,0 +1,24 @@ +using ShellDocs.Components; +using ShellDocs.Components.Content; +using Xunit; + +namespace ShellDocs.Tests; + +public class TypeTableRenderTests +{ + private static Task Render(string markdown) + => new ComponentRenderHarness(o => + o.ContentRoot = Path.Combine(Path.GetTempPath(), "shelldocs-no-content-" + Guid.NewGuid().ToString("N"))) + .RenderAsync(new() { ["Markdown"] = markdown }); + + [Fact] + public async Task RowsThatShareAName_AreAllRendered() + { + var html = await Render( + "\n\n" + + "\n"); + + Assert.Contains("One image.", html); + Assert.Contains("An image per theme.", html); + } +} From de6bd4f358eaa60e5b219bce3856890f1aee8145 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Sun, 4 Oct 2026 15:03:54 +0200 Subject: [PATCH 2/5] fix(markdown): read fenced blocks the way CommonMark does The slot extractor only matched three-backtick fences, so a longer fence showing a shorter one was paired with the wrong closing line: the next razor:preview rendered as plain code and tags inside the example became empty placeholders. Fences are now three or more backticks or tildes, closed by a run of the same character at least as long. Empty fences, longer closing fences and trailing spaces are handled. --- src/ShellDocs.Markdown/SlotExtractor.cs | 6 +- tests/ShellDocs.Tests/FenceScannerTests.cs | 81 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/ShellDocs.Tests/FenceScannerTests.cs diff --git a/src/ShellDocs.Markdown/SlotExtractor.cs b/src/ShellDocs.Markdown/SlotExtractor.cs index dc1d20a..a976c47 100644 --- a/src/ShellDocs.Markdown/SlotExtractor.cs +++ b/src/ShellDocs.Markdown/SlotExtractor.cs @@ -7,8 +7,12 @@ internal class SlotExtractor { private readonly TypeRegistry _registry; + /* CommonMark fenced block: a run of three or more backticks or tildes, closed by + a run of the same character at least as long. A longer fence can show a + shorter one in its body, so the two must not be paired with each other. */ private static readonly Regex FenceBlock = new( - @"^(?[ \t]*)```(?[^\n\r]*)\r?\n(?[\s\S]*?)\r?\n\1```(?=\r?\n|$)", + @"^(?[ \t]*)(?`{3,}|(?~{3,}))(?(?(tilde)(?!~)[^\r\n]*|[^`\r\n]*))\r?\n" + + @"(?:(?[\s\S]*?)\r?\n)??\k\k(?(tilde)~*|`*)[ \t]*(?=\r?\n|$)", RegexOptions.Multiline | RegexOptions.Compiled); private static readonly Regex OpeningTag = new( diff --git a/tests/ShellDocs.Tests/FenceScannerTests.cs b/tests/ShellDocs.Tests/FenceScannerTests.cs new file mode 100644 index 0000000..192a6b4 --- /dev/null +++ b/tests/ShellDocs.Tests/FenceScannerTests.cs @@ -0,0 +1,81 @@ +using ShellDocs.Markdown; +using Xunit; + +namespace ShellDocs.Tests; + +// The slot extractor has to agree with Markdig about where a fenced block starts +// and ends, or everything after a misread fence is masked or scanned wrongly. +public class FenceScannerTests +{ + public class Callout { } + public class Button { } + + private static MarkdownRenderer Renderer() => new(new TypeRegistry().Register().Register