-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for inline anchors in Markdown parsing
Introduced a parser for inline anchors using `$$$` syntax and updated related components to handle and render them as HTML anchor tags. Enhanced heading slug generation to exclude inline anchors, and added comprehensive tests to ensure correct behavior.
- Loading branch information
Showing
8 changed files
with
251 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// Licensed to Elasticsearch B.V under one or more agreements. | ||
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information | ||
|
||
using Slugify; | ||
|
||
namespace Elastic.Markdown.Helpers; | ||
|
||
public static class SlugExtensions | ||
{ | ||
private static readonly SlugHelper _slugHelper = new(); | ||
|
||
|
||
public static string Slugify(this string? text) => _slugHelper.GenerateSlug(text); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
src/Elastic.Markdown/Myst/InlineParsers/InlineAnchorParser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// Licensed to Elasticsearch B.V under one or more agreements. | ||
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information | ||
|
||
using Elastic.Markdown.Helpers; | ||
using Markdig; | ||
using Markdig.Extensions.SmartyPants; | ||
using Markdig.Helpers; | ||
using Markdig.Parsers; | ||
using Markdig.Parsers.Inlines; | ||
using Markdig.Renderers; | ||
using Markdig.Renderers.Html; | ||
using Markdig.Renderers.Html.Inlines; | ||
using Markdig.Syntax.Inlines; | ||
|
||
namespace Elastic.Markdown.Myst.InlineParsers; | ||
|
||
public static class InlineAnchorBuilderExtensions | ||
{ | ||
public static MarkdownPipelineBuilder UseInlineAnchors(this MarkdownPipelineBuilder pipeline) | ||
{ | ||
pipeline.Extensions.AddIfNotAlready<InlineAnchorBuilderExtension>(); | ||
return pipeline; | ||
} | ||
} | ||
|
||
public class InlineAnchorBuilderExtension : IMarkdownExtension | ||
{ | ||
public void Setup(MarkdownPipelineBuilder pipeline) => | ||
pipeline.InlineParsers.InsertAfter<EmphasisInlineParser>(new InlineAnchorParser()); | ||
|
||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) => | ||
renderer.ObjectRenderers.InsertAfter<EmphasisInlineRenderer>(new InlineAnchorRenderer()); | ||
} | ||
|
||
public class InlineAnchorParser : InlineParser | ||
{ | ||
public InlineAnchorParser() | ||
{ | ||
OpeningCharacters = ['$']; | ||
} | ||
|
||
public override bool Match(InlineProcessor processor, ref StringSlice slice) | ||
{ | ||
var startPosition = processor.GetSourcePosition(slice.Start, out var line, out var column); | ||
var c = slice.CurrentChar; | ||
|
||
var span = slice.AsSpan(); | ||
if (!span.StartsWith("$$$")) return false; | ||
|
||
var closingStart = span[3..].IndexOf('$'); | ||
if (closingStart <= 0) | ||
return false; | ||
|
||
//not ending with three dollar signs | ||
if (!span[(closingStart+3)..].StartsWith("$$$")) | ||
return false; | ||
|
||
processor.Inline = new InlineAnchor { Anchor = span[3..(closingStart+3)].ToString().Slugify() }; | ||
|
||
var sliceEnd = slice.Start + closingStart + 6; | ||
while (slice.Start != sliceEnd) | ||
slice.SkipChar(); | ||
|
||
return true; | ||
} | ||
|
||
|
||
} | ||
|
||
public class InlineAnchor : LeafInline | ||
{ | ||
public required string Anchor { get; init; } | ||
} | ||
|
||
public class InlineAnchorRenderer : HtmlObjectRenderer<InlineAnchor> | ||
{ | ||
protected override void Write(HtmlRenderer renderer, InlineAnchor obj) => | ||
renderer.Write("<a id=\"").Write(obj.Anchor).Write("\"></a>"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
tests/Elastic.Markdown.Tests/Inline/InlineAnchorTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// Licensed to Elasticsearch B.V under one or more agreements. | ||
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. | ||
// See the LICENSE file in the project root for more information | ||
|
||
using Elastic.Markdown.Myst.InlineParsers; | ||
using FluentAssertions; | ||
using Markdig.Syntax; | ||
using Xunit.Abstractions; | ||
|
||
namespace Elastic.Markdown.Tests.Inline; | ||
|
||
public class InlineAnchorTests(ITestOutputHelper output) : LeafTest<InlineAnchor>(output, | ||
""" | ||
this is regular text and this $$$is-an-inline-anchor$$$ and this continues to be regular text | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void ParsesBlock() | ||
{ | ||
Block.Should().NotBeNull(); | ||
Block!.Anchor.Should().Be("is-an-inline-anchor"); | ||
} | ||
|
||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Contain( | ||
"""<p>this is regular text and this <a id="is-an-inline-anchor"></a> and this continues to be regular text</p>""" | ||
); | ||
} | ||
|
||
public class InlineAnchorAtStartTests(ITestOutputHelper output) : LeafTest<InlineAnchor>(output, | ||
""" | ||
$$$is-an-inline-anchor$$$ and this continues to be regular text | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void ParsesBlock() | ||
{ | ||
Block.Should().NotBeNull(); | ||
Block!.Anchor.Should().Be("is-an-inline-anchor"); | ||
} | ||
|
||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Be( | ||
"""<p><a id="is-an-inline-anchor"></a> and this continues to be regular text</p>""" | ||
); | ||
} | ||
|
||
public class InlineAnchorAtEndTests(ITestOutputHelper output) : LeafTest<InlineAnchor>(output, | ||
""" | ||
this is regular text and this $$$is-an-inline-anchor$$$ | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void ParsesBlock() | ||
{ | ||
Block.Should().NotBeNull(); | ||
Block!.Anchor.Should().Be("is-an-inline-anchor"); | ||
} | ||
|
||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Contain( | ||
"""<p>this is regular text and this <a id="is-an-inline-anchor"></a></p>""" | ||
); | ||
} | ||
|
||
public class BadStartInlineAnchorTests(ITestOutputHelper output) : BlockTest<ParagraphBlock>(output, | ||
""" | ||
this is regular text and this $$is-an-inline-anchor$$$ | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Contain( | ||
"""<p>this is regular text and this $$is-an-inline-anchor$$$</p>""" | ||
); | ||
} | ||
|
||
public class BadEndInlineAnchorTests(ITestOutputHelper output) : BlockTest<ParagraphBlock>(output, | ||
""" | ||
this is regular text and this $$$is-an-inline-anchor$$ | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Contain( | ||
"""<p>this is regular text and this $$$is-an-inline-anchor$$</p>""" | ||
); | ||
} | ||
|
||
public class InlineAnchorInHeading(ITestOutputHelper output) : BlockTest<HeadingBlock>(output, | ||
""" | ||
## Hello world $$$my-anchor$$$ | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Be( | ||
""" | ||
<section id="hello-world"><h2>Hello world <a id="my-anchor"></a><a class="headerlink" href="#hello-world" title="Link to this heading">¶</a> | ||
</h2> | ||
</section> | ||
""".TrimEnd() | ||
); | ||
} | ||
|
||
public class ExplicitSlugInHeader(ITestOutputHelper output) : BlockTest<HeadingBlock>(output, | ||
""" | ||
## Hello world [#my-anchor] | ||
""" | ||
) | ||
{ | ||
[Fact] | ||
public void GeneratesAttributesInHtml() => | ||
// language=html | ||
Html.Should().Be( | ||
""" | ||
<section id="my-anchor"><h2>Hello world <a class="headerlink" href="#my-anchor" title="Link to this heading">¶</a> | ||
</h2> | ||
</section> | ||
""".TrimEnd() | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters