mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 13:54:44 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5528023158 | ||
|
|
f93b9d79d9 | ||
|
|
d53fd0e870 | ||
|
|
c488aca96c | ||
|
|
9b3f442765 | ||
|
|
7b6d659bbd | ||
|
|
bc8ba4fecb | ||
|
|
d87bb7292d | ||
|
|
118d28f886 | ||
|
|
3e0c72f043 | ||
|
|
f2590e7b80 | ||
|
|
d1233ffe66 | ||
|
|
ab8e85b06e | ||
|
|
90bc15c016 | ||
|
|
7f604bef30 | ||
|
|
54783b8f65 | ||
|
|
ad0770a594 | ||
|
|
90365bfeee | ||
|
|
c35f7fff17 | ||
|
|
fdaef77474 | ||
|
|
733c028311 | ||
|
|
bc41b0c2a3 | ||
|
|
a8de2087d8 | ||
|
|
2cff6c5194 | ||
|
|
5e4a917dbd | ||
|
|
aff8a6823a | ||
|
|
b8a3c270cc | ||
|
|
68659f4037 | ||
|
|
e92a8097d0 | ||
|
|
57fad6fc1a | ||
|
|
260f4d5acc | ||
|
|
102d02a6c1 | ||
|
|
5ae8ab7a74 | ||
|
|
eb28f76588 | ||
|
|
d0311b4cea | ||
|
|
a11899a350 | ||
|
|
40781737c3 | ||
|
|
455f8f333d | ||
|
|
98a060f2a3 | ||
|
|
49cf59b819 | ||
|
|
310a55c724 | ||
|
|
f734e91568 | ||
|
|
090e6d791a | ||
|
|
41bdb0f0ab | ||
|
|
b27ef11240 | ||
|
|
dfa2c94b88 | ||
|
|
89330f3524 | ||
|
|
1a1bbecc46 | ||
|
|
68bd3074b2 | ||
|
|
e486903687 | ||
|
|
8e22754db4 | ||
|
|
93d88ab994 | ||
|
|
000393f46a | ||
|
|
a5796890e1 | ||
|
|
c19ba5b0eb | ||
|
|
03390e4f71 | ||
|
|
42bd65caaf | ||
|
|
b7ae04bdba | ||
|
|
391f376fa2 | ||
|
|
f9e96bc9c9 | ||
|
|
c75a11ec32 | ||
|
|
fd226d53e9 | ||
|
|
7132584996 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -13,6 +13,9 @@ jobs:
|
||||
build:
|
||||
uses: xoofx/.github/.github/workflows/dotnet.yml@main
|
||||
with:
|
||||
dotnet-version: '6.0 8.0'
|
||||
dotnet-version: |
|
||||
6.0
|
||||
8.0
|
||||
9.0
|
||||
secrets:
|
||||
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Extensions and Parsers
|
||||
|
||||
Markdig was [implemented in such a way](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
|
||||
Markdig was [implemented in such a way](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
|
||||
|
||||
The basic mechanism for extension of Markdig is the `IMarkdownExtension` interface, which allows any implementing class to be registered with the pipeline builder and thus to directly modify the collections of `BlockParser` and `InlineParser` objects which end up in the pipeline.
|
||||
|
||||
|
||||
16
readme.md
16
readme.md
@@ -1,4 +1,4 @@
|
||||
# Markdig [](https://github.com/lunet-io/markdig/actions) [](https://coveralls.io/github/xoofx/markdig?branch=master) [](https://www.nuget.org/packages/Markdig/) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
|
||||
# Markdig [](https://github.com/xoofx/markdig/actions/workflows/ci.yml) [](https://coveralls.io/github/xoofx/markdig?branch=master) [](https://www.nuget.org/packages/Markdig/) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
|
||||
|
||||
<img align="right" width="160px" height="160px" src="img/markdig.png">
|
||||
|
||||
@@ -48,7 +48,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
|
||||
- [**Emoji**](src/Markdig.Tests/Specs/EmojiSpecs.md) support (inspired from [Markdown-it](https://markdown-it.github.io/))
|
||||
- [**SmartyPants**](src/Markdig.Tests/Specs/SmartyPantsSpecs.md) (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
|
||||
- [**Bootstrap**](src/Markdig.Tests/Specs/BootstrapSpecs.md) class (to output bootstrap class)
|
||||
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://knsv.github.io/mermaid/) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
|
||||
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://mermaid.js.org) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
|
||||
- [**YAML Front Matter**](src/Markdig.Tests/Specs/YamlSpecs.md) to parse without evaluating the front matter and to discard it from the HTML output (typically used for previewing without the front matter in MarkdownEditor)
|
||||
- [**JIRA links**](src/Markdig.Tests/Specs/JiraLinks.md) to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker)
|
||||
- Starting with Markdig version `0.20.0+`, Markdig is compatible only with `NETStandard 2.0`, `NETStandard 2.1`, `NETCoreApp 2.1` and `NETCoreApp 3.1`.
|
||||
@@ -70,7 +70,7 @@ If you are looking for support for an old .NET Framework 3.5 or 4.0, you can dow
|
||||
|
||||
While there is not yet a dedicated documentation, you can find from the [specs documentation](src/Markdig.Tests/Specs/readme.md) how to use these extensions.
|
||||
|
||||
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
|
||||
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
|
||||
|
||||
## Download
|
||||
|
||||
@@ -144,12 +144,12 @@ AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
|
||||
- Markdig is roughly **x100 times faster than MarkdownSharp**
|
||||
- **20% faster than the reference cmark C implementation**
|
||||
|
||||
## Sponsors
|
||||
|
||||
## Donate
|
||||
Supports this project with a monthly donation and help me continue improving it. \[[Become a sponsor](https://github.com/sponsors/xoofx)\]
|
||||
|
||||
If you are using this library and find it useful for your project, please consider a donation for it!
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
|
||||
[<img src="https://github.com/lilith.png?size=200" width="64px;" style="border-radius: 50%" alt="lilith"/>](https://github.com/lilith) Lilith River, author of [Imageflow Server, an easy on-demand
|
||||
image editing, optimization, and delivery server](https://github.com/imazen/imageflow-server)
|
||||
|
||||
## Credits
|
||||
|
||||
@@ -164,4 +164,4 @@ Some decoding part (e.g HTML [EntityHelper.cs](https://github.com/lunet-io/markd
|
||||
Thanks to the work done by @clarkd on the JIRA Link extension (https://github.com/clarkd/MarkdigJiraLinker), now included with this project!
|
||||
## Author
|
||||
|
||||
Alexandre MUTEL aka [xoofx](http://xoofx.com)
|
||||
Alexandre MUTEL aka [xoofx](http://xoofx.github.io)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
<StartupObject>Markdig.Tests.Program</StartupObject>
|
||||
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net8.0\SpecFileGen.dll</SpecExecutable>
|
||||
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net8.0\SpecFileGen.timestamp</SpecTimestamp>
|
||||
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\$(TargetFramework)\SpecFileGen.dll</SpecExecutable>
|
||||
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\$(TargetFramework)\SpecFileGen.timestamp</SpecTimestamp>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -317,4 +317,73 @@ $$
|
||||
Assert.That(paragraph.Inline.Span.Start == paragraph.Inline.FirstChild.Span.Start);
|
||||
Assert.That(paragraph.Inline.Span.End == paragraph.Inline.LastChild.Span.End);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGridTableShortLine()
|
||||
{
|
||||
var input = @"
|
||||
+--+
|
||||
| |
|
||||
+-";
|
||||
|
||||
var expected = @"<table>
|
||||
<col style=""width:100%"" />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseGridTables().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefinitionListInListItemWithBlankLine()
|
||||
{
|
||||
var input = @"
|
||||
-
|
||||
|
||||
term
|
||||
: definition
|
||||
";
|
||||
|
||||
var expected = @"<ul>
|
||||
<li>
|
||||
<dl>
|
||||
<dt>term</dt>
|
||||
<dd>definition</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseDefinitionLists().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlertWithinAlertOrNestedBlock()
|
||||
{
|
||||
var input = @"
|
||||
>[!NOTE]
|
||||
[!NOTE]
|
||||
The second one is not a note.
|
||||
|
||||
>>[!NOTE]
|
||||
Also not a note.
|
||||
";
|
||||
|
||||
var expected = @"<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p>[!NOTE]
|
||||
The second one is not a note.</p>
|
||||
</div>
|
||||
<blockquote>
|
||||
<blockquote>
|
||||
<p>[!NOTE]
|
||||
Also not a note.</p>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks().Build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Markdig.Tests.Specs.Diagrams
|
||||
//
|
||||
// ## Mermaid diagrams
|
||||
//
|
||||
// Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
|
||||
// Using a fenced code block with the `mermaid` language info will output a `<pre class='mermaid'>` block (which is the default for other code block):
|
||||
[Test]
|
||||
public void ExtensionsMermaidDiagrams_Example001()
|
||||
{
|
||||
@@ -34,14 +34,14 @@ namespace Markdig.Tests.Specs.Diagrams
|
||||
// ```
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <div class="mermaid">graph TD;
|
||||
// <pre class="mermaid">graph TD;
|
||||
// A-->B;
|
||||
// A-->C;
|
||||
// B-->D;
|
||||
// C-->D;
|
||||
// </div>
|
||||
// </pre>
|
||||
|
||||
TestParser.TestSpec("```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```", "<div class=\"mermaid\">graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n</div>", "diagrams|advanced", context: "Example 1\nSection Extensions / Mermaid diagrams\n");
|
||||
TestParser.TestSpec("```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```", "<pre class=\"mermaid\">graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n</pre>", "diagrams|advanced", context: "Example 1\nSection Extensions / Mermaid diagrams\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Adds support for diagrams extension:
|
||||
|
||||
## Mermaid diagrams
|
||||
|
||||
Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
|
||||
Using a fenced code block with the `mermaid` language info will output a `<pre class='mermaid'>` block (which is the default for other code block):
|
||||
|
||||
```````````````````````````````` example
|
||||
```mermaid
|
||||
@@ -15,12 +15,12 @@ graph TD;
|
||||
C-->D;
|
||||
```
|
||||
.
|
||||
<div class="mermaid">graph TD;
|
||||
<pre class="mermaid">graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
</div>
|
||||
</pre>
|
||||
````````````````````````````````
|
||||
|
||||
## nomnoml diagrams
|
||||
|
||||
@@ -81,6 +81,22 @@ public class TestLinkHelper
|
||||
Assert.AreEqual(' ', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTitleMultiline()
|
||||
{
|
||||
var text = new StringSlice("'this\ris\r\na\ntitle'");
|
||||
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
|
||||
Assert.AreEqual("this\ris\r\na\ntitle", title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTitleMultilineWithSpaceAndBackslash()
|
||||
{
|
||||
var text = new StringSlice("'a\n\\ \\\ntitle'");
|
||||
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
|
||||
Assert.AreEqual("a\n\\ \\\ntitle", title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlAndTitle()
|
||||
{
|
||||
@@ -230,6 +246,13 @@ public class TestLinkHelper
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestlLinkReferenceDefinitionInvalid()
|
||||
{
|
||||
var text = new StringSlice("[foo]: /url (title) x\n");
|
||||
Assert.False(LinkHelper.TryParseLinkReferenceDefinition(ref text, out _, out _, out _, out _, out _, out _));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAutoLinkUrlSimple()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ public class TestMediaLinks
|
||||
[Test]
|
||||
[TestCase("", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"https://sample.com/video.mp4\"></source></video></p>\n")]
|
||||
[TestCase("", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n")]
|
||||
[TestCase(@"", "<p><iframe src=\"https://www.youtube.com/embed/6BUptHVuvyI\" class=\"youtubeshort\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
|
||||
[TestCase(@"", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
|
||||
[TestCase("", "<p><iframe src=\"https://music.yandex.ru/iframe/#track/4402274/411845/\" class=\"yandex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n")]
|
||||
[TestCase("", "<p><iframe src=\"https://player.vimeo.com/video/8607834\" class=\"vimeo\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
|
||||
@@ -33,7 +34,7 @@ public class TestMediaLinks
|
||||
public void TestBuiltInHosts(string markdown, string expected)
|
||||
{
|
||||
string html = Markdown.ToHtml(markdown, GetPipeline());
|
||||
Assert.AreEqual(html, expected);
|
||||
Assert.AreEqual(expected, html);
|
||||
}
|
||||
|
||||
[TestCase("",
|
||||
@@ -43,7 +44,7 @@ public class TestMediaLinks
|
||||
public void TestBuiltInHostsWithRelativePaths(string markdown, string expected)
|
||||
{
|
||||
string html = Markdown.ToHtml(markdown, GetPipeline());
|
||||
Assert.AreEqual(html, expected);
|
||||
Assert.AreEqual(expected, html);
|
||||
}
|
||||
|
||||
private class TestHostProvider : IHostProvider
|
||||
|
||||
@@ -9,6 +9,13 @@ namespace Markdig.Tests;
|
||||
[TestFixture]
|
||||
public class TestPlayParser
|
||||
{
|
||||
|
||||
[Test]
|
||||
public void TestInvalidSetext()
|
||||
{
|
||||
TestParser.TestSpec("test\n===n", "<p>test\n===n</p>", "advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBugWithEmphasisAndTable()
|
||||
{
|
||||
@@ -39,6 +46,14 @@ public class TestPlayParser
|
||||
Assert.AreEqual("/yoyo", link?.Url);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinkWithMultipleBackslashesInTitle()
|
||||
{
|
||||
var doc = Markdown.Parse(@"[link](/uri '\\\\127.0.0.1')");
|
||||
var link = doc.Descendants<LinkInline>().FirstOrDefault();
|
||||
Assert.AreEqual(@"\\127.0.0.1", link?.Title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestListBug2()
|
||||
{
|
||||
|
||||
@@ -160,6 +160,17 @@ literal ( 0, 8) 8-8
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasis4()
|
||||
{
|
||||
Check("**foo*", @"
|
||||
paragraph ( 0, 0) 0-5
|
||||
literal ( 0, 0) 0-0
|
||||
emphasis ( 0, 1) 1-5
|
||||
literal ( 0, 2) 2-4
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasisFalse()
|
||||
{
|
||||
@@ -522,13 +533,17 @@ literal ( 1, 6) 8-9
|
||||
[Test]
|
||||
public void TestAbbreviations()
|
||||
{
|
||||
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @"
|
||||
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML\r\n\r\nHTML abbreviation at the beginning of a line", @"
|
||||
paragraph ( 2, 0) 38-102
|
||||
container ( 2, 0) 38-102
|
||||
literal ( 2, 0) 38-66
|
||||
abbreviation ( 2,29) 67-70
|
||||
literal ( 2,33) 71-98
|
||||
abbreviation ( 2,61) 99-102
|
||||
paragraph ( 4, 0) 107-150
|
||||
container ( 4, 0) 107-150
|
||||
abbreviation ( 4, 0) 107-110
|
||||
literal ( 4, 4) 111-150
|
||||
", "abbreviations");
|
||||
}
|
||||
|
||||
@@ -698,7 +713,7 @@ literal ( 0, 2) 2-3
|
||||
[Test]
|
||||
public void TestMathematicsInline()
|
||||
{
|
||||
// 01 23456789AB
|
||||
// 01 23456789ABCDEF
|
||||
Check("0\n012 $abcd$ 321", @"
|
||||
paragraph ( 0, 0) 0-15
|
||||
literal ( 0, 0) 0-0
|
||||
@@ -707,6 +722,13 @@ literal ( 1, 0) 2-5
|
||||
math ( 1, 4) 6-11
|
||||
attributes ( 0, 0) 0--1
|
||||
literal ( 1,10) 12-15
|
||||
", "mathematics");
|
||||
|
||||
// 012345678
|
||||
Check("$ abcd $", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
math ( 0, 0) 0-7
|
||||
attributes ( 0, 0) 0--1
|
||||
", "mathematics");
|
||||
}
|
||||
|
||||
@@ -789,6 +811,29 @@ literal ( 4, 2) 13-13
|
||||
", "pipetables");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPipeTable3()
|
||||
{
|
||||
// 01234 5678 9ABCD
|
||||
Check("|a|b\n-|-\n0|1|\n", @"
|
||||
table ( 0, 0) 0-12
|
||||
tablerow ( 0, 1) 1-3
|
||||
tablecell ( 0, 1) 1-1
|
||||
paragraph ( 0, 1) 1-1
|
||||
literal ( 0, 1) 1-1
|
||||
tablecell ( 0, 3) 3-3
|
||||
paragraph ( 0, 3) 3-3
|
||||
literal ( 0, 3) 3-3
|
||||
tablerow ( 2, 0) 9-11
|
||||
tablecell ( 2, 0) 9-9
|
||||
paragraph ( 2, 0) 9-9
|
||||
literal ( 2, 0) 9-9
|
||||
tablecell ( 2, 2) 11-11
|
||||
paragraph ( 2, 2) 11-11
|
||||
literal ( 2, 2) 11-11
|
||||
", "pipetables");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCode()
|
||||
{
|
||||
|
||||
@@ -107,7 +107,7 @@ public class TestYamlFrontMatterExtension
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Pass("No exception parsing and iterating through YAML front matter block lines");
|
||||
// No exception parsing and iterating through YAML front matter block lines
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ public class AbbreviationParser : BlockParser
|
||||
{
|
||||
var literal = (LiteralInline)processor.Inline!;
|
||||
var originalLiteral = literal;
|
||||
var originalSpanEnd = literal.Span.End;
|
||||
|
||||
ContainerInline? container = null;
|
||||
|
||||
@@ -171,7 +172,7 @@ public class AbbreviationParser : BlockParser
|
||||
// Process the remaining literal
|
||||
literal = new LiteralInline()
|
||||
{
|
||||
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
|
||||
Span = new SourceSpan(abbrInline.Span.End + 1, originalSpanEnd),
|
||||
Line = line,
|
||||
Column = column + match.Length,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ public class AlertBlockRenderer : HtmlObjectRenderer<AlertBlock>
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
}
|
||||
|
||||
renderer.EnsureLine();
|
||||
}
|
||||
|
||||
@@ -61,7 +62,14 @@ public class AlertBlockRenderer : HtmlObjectRenderer<AlertBlock>
|
||||
/// <param name="kind">The kind of the alert to render</param>
|
||||
public static void DefaultRenderKind(HtmlRenderer renderer, StringSlice kind)
|
||||
{
|
||||
string? html = kind.AsSpan() switch
|
||||
if (kind.Length >= 16)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<char> upperKind = stackalloc char[kind.Length];
|
||||
kind.AsSpan().ToUpperInvariant(upperKind);
|
||||
string? html = upperKind switch
|
||||
{
|
||||
"NOTE" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>",
|
||||
"TIP" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>",
|
||||
|
||||
@@ -29,7 +29,8 @@ public class AlertInlineParser : InlineParser
|
||||
// We expect the alert to be the first child of a quote block. Example:
|
||||
// > [!NOTE]
|
||||
// > This is a note
|
||||
if (processor.Block is not ParagraphBlock paragraphBlock || paragraphBlock.Parent is not QuoteBlock quoteBlock || paragraphBlock.Inline?.FirstChild != null)
|
||||
if (processor.Block is not ParagraphBlock paragraphBlock || paragraphBlock.Parent is not QuoteBlock quoteBlock || paragraphBlock.Inline?.FirstChild != null
|
||||
|| quoteBlock is AlertBlock || quoteBlock.Parent is not MarkdownDocument)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -46,7 +47,7 @@ public class AlertInlineParser : InlineParser
|
||||
|
||||
var start = slice.Start;
|
||||
var end = start;
|
||||
while (c.IsAlphaUpper())
|
||||
while (c.IsAlpha())
|
||||
{
|
||||
end = slice.Start;
|
||||
c = slice.NextChar();
|
||||
@@ -86,7 +87,7 @@ public class AlertInlineParser : InlineParser
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (!c.IsSpaceOrTab())
|
||||
else if (!c.IsSpaceOrTab())
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Linq;
|
||||
using Markdig.Extensions.Alerts;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
@@ -24,10 +26,22 @@ public class BootstrapExtension : IMarkdownExtension
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is HtmlRenderer htmlRenderer)
|
||||
{
|
||||
var alertRenderer = htmlRenderer.ObjectRenderers.OfType<AlertBlockRenderer>().FirstOrDefault();
|
||||
if (alertRenderer == null)
|
||||
{
|
||||
alertRenderer = new AlertBlockRenderer();
|
||||
renderer.ObjectRenderers.InsertBefore<QuoteBlockRenderer>(new AlertBlockRenderer());
|
||||
}
|
||||
|
||||
alertRenderer.RenderKind = (_, _) => { };
|
||||
}
|
||||
}
|
||||
|
||||
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
|
||||
{
|
||||
Span<char> upperKind = new char[16];
|
||||
foreach (var node in document.Descendants())
|
||||
{
|
||||
if (node.IsInline)
|
||||
@@ -43,6 +57,28 @@ public class BootstrapExtension : IMarkdownExtension
|
||||
{
|
||||
node.GetAttributes().AddClass("table");
|
||||
}
|
||||
else if (node is AlertBlock alertBlock) // Needs to be before QuoteBlock
|
||||
{
|
||||
var attributes = node.GetAttributes();
|
||||
attributes.AddClass("alert");
|
||||
attributes.AddProperty("role", "alert");
|
||||
if (alertBlock.Kind.Length <= upperKind.Length)
|
||||
{
|
||||
alertBlock.Kind.AsSpan().ToUpperInvariant(upperKind);
|
||||
attributes.AddClass(upperKind.Slice(0, alertBlock.Kind.Length) switch
|
||||
{
|
||||
"NOTE" => "alert-primary",
|
||||
"TIP" => "alert-success",
|
||||
"IMPORTANT" => "alert-info",
|
||||
"WARNING" => "alert-warning",
|
||||
"CAUTION" => "alert-danger",
|
||||
_ => "alert-dark",
|
||||
});
|
||||
}
|
||||
|
||||
var lastParagraph = alertBlock.Descendants().OfType<ParagraphBlock>().LastOrDefault();
|
||||
lastParagraph?.GetAttributes().AddClass("mb-0");
|
||||
}
|
||||
else if (node is QuoteBlock)
|
||||
{
|
||||
node.GetAttributes().AddClass("blockquote");
|
||||
|
||||
@@ -30,7 +30,11 @@ public class CustomContainerExtension : IMarkdownExtension
|
||||
{
|
||||
if (delimiterCount == 2 && emphasisChar == ':')
|
||||
{
|
||||
return new CustomContainerInline();
|
||||
return new CustomContainerInline
|
||||
{
|
||||
DelimiterChar = ':',
|
||||
DelimiterCount = 2
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -105,13 +105,20 @@ public class DefinitionListParser : BlockParser
|
||||
{
|
||||
var index = previousParent.IndexOf(paragraphBlock) - 1;
|
||||
if (index < 0) return null;
|
||||
var lastBlock = previousParent[index];
|
||||
if (lastBlock is BlankLineBlock)
|
||||
switch (previousParent[index])
|
||||
{
|
||||
lastBlock = previousParent[index - 1];
|
||||
previousParent.RemoveAt(index);
|
||||
case DefinitionList definitionList:
|
||||
return definitionList;
|
||||
|
||||
case BlankLineBlock:
|
||||
if (index > 0 && previousParent[index - 1] is DefinitionList definitionList2)
|
||||
{
|
||||
previousParent.RemoveAt(index);
|
||||
return definitionList2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return lastBlock as DefinitionList;
|
||||
return null;
|
||||
}
|
||||
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Renderers;
|
||||
@@ -22,9 +22,8 @@ public class DiagramExtension : IMarkdownExtension
|
||||
if (renderer is HtmlRenderer htmlRenderer)
|
||||
{
|
||||
var codeRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>()!;
|
||||
// TODO: Add other well known diagram languages
|
||||
codeRenderer.BlocksAsDiv.Add("mermaid");
|
||||
codeRenderer.BlockMapping["mermaid"] = "pre";
|
||||
codeRenderer.BlocksAsDiv.Add("nomnoml");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ public class FootnoteParser : BlockParser
|
||||
{
|
||||
Label = label,
|
||||
LabelSpan = labelSpan,
|
||||
Column = processor.Column,
|
||||
Span = new SourceSpan(processor.Start, processor.Line.End),
|
||||
};
|
||||
|
||||
// Maintain a list of all footnotes at document level
|
||||
@@ -74,6 +76,7 @@ public class FootnoteParser : BlockParser
|
||||
{
|
||||
CreateLinkInline = CreateLinkToFootnote,
|
||||
Line = processor.LineIndex,
|
||||
Column = saved,
|
||||
Span = new SourceSpan(start, processor.Start - 2), // account for ]:
|
||||
LabelSpan = labelSpan,
|
||||
Label = label
|
||||
|
||||
@@ -152,7 +152,7 @@ public class MathInlineParser : InlineParser
|
||||
// Create a new MathInline
|
||||
var inline = new MathInline()
|
||||
{
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(end)),
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Delimiter = match,
|
||||
|
||||
@@ -58,6 +58,7 @@ public class HostProviderBuilder
|
||||
internal static Dictionary<string, IHostProvider> KnownHosts { get; }
|
||||
= new Dictionary<string, IHostProvider>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["YouTubeShort"] = Create("www.youtube.com", YouTubeShort, iframeClass: "youtubeshort"),
|
||||
["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"),
|
||||
["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
|
||||
["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
|
||||
@@ -92,6 +93,19 @@ public class HostProviderBuilder
|
||||
);
|
||||
}
|
||||
|
||||
private static string? YouTubeShort(Uri uri)
|
||||
{
|
||||
string uriPath = uri.AbsolutePath;
|
||||
bool isYouTubeShort = uriPath.StartsWith("/shorts/", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isYouTubeShort)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var shortId = uriPath.Substring("/shorts/".Length).Split('?').FirstOrDefault(); // the format might be "/shorts/6BUptHVuvyI?feature=share"
|
||||
return BuildYouTubeIframeUrl(shortId, null);
|
||||
}
|
||||
|
||||
private static string? YouTubeShortened(Uri uri)
|
||||
{
|
||||
return BuildYouTubeIframeUrl(
|
||||
|
||||
@@ -135,6 +135,7 @@ public class GridTableParser : BlockParser
|
||||
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
|
||||
{
|
||||
var lineStart = line.Start;
|
||||
var lineEnd = line.End;
|
||||
isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '=';
|
||||
hasRowSpan = false;
|
||||
foreach (var columnSlice in columns)
|
||||
@@ -142,7 +143,7 @@ public class GridTableParser : BlockParser
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
line.Start = lineStart + columnSlice.Start + 1;
|
||||
line.End = lineStart + columnSlice.End - 1;
|
||||
line.End = Math.Min(lineStart + columnSlice.End - 1, lineEnd);
|
||||
line.Trim();
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
|
||||
{
|
||||
@@ -256,7 +257,7 @@ public class GridTableParser : BlockParser
|
||||
{
|
||||
sliceForCell.End = line.Start + columnEnd - 1;
|
||||
}
|
||||
else if (line.PeekCharExtra(line.End) == '|')
|
||||
else if (line.PeekCharExtra(line.End - line.Start) == '|')
|
||||
{
|
||||
sliceForCell.End = line.End - 1;
|
||||
}
|
||||
|
||||
@@ -280,6 +280,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
tableState.EndOfLines.Add(endOfTable);
|
||||
}
|
||||
|
||||
int lastPipePos = 0;
|
||||
|
||||
// Cell loop
|
||||
// Reconstruct the table from the delimiters
|
||||
TableRow? row = null;
|
||||
@@ -302,6 +304,12 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
if (pipeSeparator != null && (delimiter.PreviousSibling is null || delimiter.PreviousSibling is LineBreakInline))
|
||||
{
|
||||
delimiter.Remove();
|
||||
if (table.Span.IsEmpty)
|
||||
{
|
||||
table.Span = delimiter.Span;
|
||||
table.Line = delimiter.Line;
|
||||
table.Column = delimiter.Column;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -354,6 +362,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
// If the delimiter is a pipe, we need to remove it from the tree
|
||||
// so that previous loop looking for a parent will not go further on subsequent cells
|
||||
delimiter.Remove();
|
||||
lastPipePos = delimiter.Span.End;
|
||||
}
|
||||
|
||||
// We trim whitespace at the beginning and ending of the cell
|
||||
@@ -421,6 +430,11 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPipePos > table.Span.End)
|
||||
{
|
||||
table.UpdateSpanEnd(lastPipePos);
|
||||
}
|
||||
|
||||
// Once we are done with the cells, we can remove all end of lines in the table tree
|
||||
foreach (var endOfLine in tableState.EndOfLines)
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed class FastStringWriter : TextWriter
|
||||
public override string NewLine
|
||||
{
|
||||
get => _newLine;
|
||||
set => _newLine = value ?? Environment.NewLine;
|
||||
set => base.NewLine = _newLine = value ?? Environment.NewLine;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -545,87 +545,70 @@ public static class LinkHelper
|
||||
enclosingCharacter = c;
|
||||
var closingQuote = c == '(' ? ')' : c;
|
||||
bool hasEscape = false;
|
||||
// -1: undefined
|
||||
// 0: has only spaces
|
||||
// 1: has other characters
|
||||
int hasOnlyWhiteSpacesSinceLastLine = -1;
|
||||
while (true)
|
||||
bool isLineBlank = false; // the first line is never blank
|
||||
while ((c = text.NextChar()) != '\0')
|
||||
{
|
||||
c = text.NextChar();
|
||||
|
||||
if (c == '\r' || c == '\n')
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
|
||||
if (isLineBlank)
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine == 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
hasOnlyWhiteSpacesSinceLastLine = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasEscape)
|
||||
{
|
||||
hasEscape = false;
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
if (c == '\r' && text.PeekChar() == '\n')
|
||||
{
|
||||
buffer.Append('\n');
|
||||
text.SkipChar();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\0')
|
||||
{
|
||||
break;
|
||||
isLineBlank = true;
|
||||
}
|
||||
|
||||
if (c == closingQuote)
|
||||
else if (hasEscape)
|
||||
{
|
||||
if (hasEscape)
|
||||
hasEscape = false;
|
||||
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append(closingQuote);
|
||||
hasEscape = false;
|
||||
continue;
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
}
|
||||
else if (c == closingQuote)
|
||||
{
|
||||
// Skip last quote
|
||||
text.SkipChar();
|
||||
goto ReturnValid;
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
continue;
|
||||
isLineBlank = false;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
if (c.IsSpaceOrTab())
|
||||
else
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine < 0)
|
||||
if (isLineBlank && !c.IsSpaceOrTab())
|
||||
{
|
||||
hasOnlyWhiteSpacesSinceLastLine = 1;
|
||||
isLineBlank = false;
|
||||
}
|
||||
}
|
||||
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
|
||||
{
|
||||
hasOnlyWhiteSpacesSinceLastLine = 0;
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
buffer.Append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.Dispose();
|
||||
title = null;
|
||||
return false;
|
||||
|
||||
ReturnValid:
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseTitleTrivia<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
|
||||
@@ -641,87 +624,70 @@ public static class LinkHelper
|
||||
enclosingCharacter = c;
|
||||
var closingQuote = c == '(' ? ')' : c;
|
||||
bool hasEscape = false;
|
||||
// -1: undefined
|
||||
// 0: has only spaces
|
||||
// 1: has other characters
|
||||
int hasOnlyWhiteSpacesSinceLastLine = -1;
|
||||
while (true)
|
||||
bool isLineBlank = false; // the first line is never blank
|
||||
while ((c = text.NextChar()) != '\0')
|
||||
{
|
||||
c = text.NextChar();
|
||||
|
||||
if (c == '\r' || c == '\n')
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
|
||||
if (isLineBlank)
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine == 1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
hasOnlyWhiteSpacesSinceLastLine = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasEscape)
|
||||
{
|
||||
hasEscape = false;
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
if (c == '\r' && text.PeekChar() == '\n')
|
||||
{
|
||||
buffer.Append('\n');
|
||||
text.SkipChar();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\0')
|
||||
{
|
||||
break;
|
||||
isLineBlank = true;
|
||||
}
|
||||
|
||||
if (c == closingQuote)
|
||||
else if (hasEscape)
|
||||
{
|
||||
if (hasEscape)
|
||||
hasEscape = false;
|
||||
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append(closingQuote);
|
||||
hasEscape = false;
|
||||
continue;
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
}
|
||||
else if (c == closingQuote)
|
||||
{
|
||||
// Skip last quote
|
||||
text.SkipChar();
|
||||
goto ReturnValid;
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
continue;
|
||||
isLineBlank = false;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
if (c.IsSpaceOrTab())
|
||||
else
|
||||
{
|
||||
if (hasOnlyWhiteSpacesSinceLastLine < 0)
|
||||
if (isLineBlank && !c.IsSpaceOrTab())
|
||||
{
|
||||
hasOnlyWhiteSpacesSinceLastLine = 1;
|
||||
isLineBlank = false;
|
||||
}
|
||||
}
|
||||
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
|
||||
{
|
||||
hasOnlyWhiteSpacesSinceLastLine = 0;
|
||||
}
|
||||
|
||||
buffer.Append(c);
|
||||
buffer.Append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer.Dispose();
|
||||
title = null;
|
||||
return false;
|
||||
|
||||
ReturnValid:
|
||||
title = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseUrl<T>(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator
|
||||
@@ -758,12 +724,15 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
if (hasEscape)
|
||||
{
|
||||
buffer.Append('\\');
|
||||
hasEscape = false;
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
continue;
|
||||
@@ -774,8 +743,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -814,20 +781,21 @@ public static class LinkHelper
|
||||
|
||||
if (!isAutoLink)
|
||||
{
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
if (hasEscape)
|
||||
{
|
||||
buffer.Append('\\');
|
||||
hasEscape = false;
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an escape
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
c = text.NextChar();
|
||||
continue;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
}
|
||||
|
||||
if (IsEndOfUri(c, isAutoLink))
|
||||
@@ -905,12 +873,15 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
if (hasEscape)
|
||||
{
|
||||
buffer.Append('\\');
|
||||
hasEscape = false;
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
continue;
|
||||
@@ -921,8 +892,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -961,20 +930,21 @@ public static class LinkHelper
|
||||
|
||||
if (!isAutoLink)
|
||||
{
|
||||
if (hasEscape && !c.IsAsciiPunctuation())
|
||||
if (hasEscape)
|
||||
{
|
||||
buffer.Append('\\');
|
||||
hasEscape = false;
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
buffer.Append('\\');
|
||||
}
|
||||
}
|
||||
|
||||
// If we have an escape
|
||||
if (c == '\\')
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
c = text.NextChar();
|
||||
continue;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
}
|
||||
|
||||
if (IsEndOfUri(c, isAutoLink))
|
||||
@@ -1159,7 +1129,7 @@ public static class LinkHelper
|
||||
c = text.NextChar();
|
||||
}
|
||||
|
||||
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
|
||||
if (c != '\0' && c != '\n' && c != '\r')
|
||||
{
|
||||
// If we were able to parse the url but the title doesn't end with space,
|
||||
// we are still returning a valid definition
|
||||
@@ -1299,7 +1269,7 @@ public static class LinkHelper
|
||||
c = text.NextChar();
|
||||
}
|
||||
|
||||
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
|
||||
if (c != '\0' && c != '\n' && c != '\r')
|
||||
{
|
||||
// If we were able to parse the url but the title doesn't end with space,
|
||||
// we are still returning a valid definition
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Copyright>Alexandre Mutel</Copyright>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Authors>Alexandre Mutel</Authors>
|
||||
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
<CheckEolTargetFramework>false</CheckEolTargetFramework>
|
||||
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
|
||||
<PackageReleaseNotes>https://github.com/lunet-io/markdig/blob/master/changelog.md</PackageReleaseNotes>
|
||||
|
||||
@@ -302,14 +302,13 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
var openDelimitercount = openDelimiter.DelimiterCount;
|
||||
var closeDelimitercount = closeDelimiter.DelimiterCount;
|
||||
|
||||
emphasis!.Span.Start = openDelimiter.Span.Start;
|
||||
emphasis!.Span.Start = openDelimiter.Span.Start + openDelimitercount - delimiterDelta;
|
||||
emphasis.Line = openDelimiter.Line;
|
||||
emphasis.Column = openDelimiter.Column;
|
||||
emphasis.Column = openDelimiter.Column + openDelimitercount - delimiterDelta;
|
||||
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
|
||||
|
||||
openDelimiter.Content.Start += delimiterDelta;
|
||||
openDelimiter.Span.Start += delimiterDelta;
|
||||
openDelimiter.Column += delimiterDelta;
|
||||
openDelimiter.Span.End -= delimiterDelta;
|
||||
openDelimiter.Content.End -= delimiterDelta;
|
||||
closeDelimiter.Content.Start += delimiterDelta;
|
||||
closeDelimiter.Span.Start += delimiterDelta;
|
||||
closeDelimiter.Column += delimiterDelta;
|
||||
|
||||
@@ -137,6 +137,9 @@ public class LinkInlineParser : InlineParser
|
||||
if (linkRef.CreateLinkInline != null)
|
||||
{
|
||||
link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild);
|
||||
link.Span = new SourceSpan(parent.Span.Start, endPosition);
|
||||
link.Line = parent.Line;
|
||||
link.Column = parent.Column;
|
||||
}
|
||||
|
||||
// Create a default link if the callback was not found
|
||||
@@ -145,8 +148,8 @@ public class LinkInlineParser : InlineParser
|
||||
// Inline Link
|
||||
var linkInline = new LinkInline()
|
||||
{
|
||||
Url = HtmlHelper.Unescape(linkRef.Url),
|
||||
Title = HtmlHelper.Unescape(linkRef.Title),
|
||||
Url = HtmlHelper.Unescape(linkRef.Url, removeBackSlash: false),
|
||||
Title = HtmlHelper.Unescape(linkRef.Title, removeBackSlash: false),
|
||||
Label = label,
|
||||
LabelSpan = labelSpan,
|
||||
UrlSpan = linkRef.UrlSpan,
|
||||
@@ -256,8 +259,8 @@ public class LinkInlineParser : InlineParser
|
||||
// Inline Link
|
||||
link = new LinkInline()
|
||||
{
|
||||
Url = HtmlHelper.Unescape(url),
|
||||
Title = HtmlHelper.Unescape(title),
|
||||
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
|
||||
Title = HtmlHelper.Unescape(title, removeBackSlash: false),
|
||||
IsImage = openParent.IsImage,
|
||||
LabelSpan = openParent.LabelSpan,
|
||||
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
|
||||
@@ -382,11 +385,11 @@ public class LinkInlineParser : InlineParser
|
||||
return new LinkInline()
|
||||
{
|
||||
TriviaBeforeUrl = wsBeforeLink,
|
||||
Url = HtmlHelper.Unescape(url),
|
||||
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
|
||||
UnescapedUrl = unescapedUrl,
|
||||
UrlHasPointyBrackets = urlHasPointyBrackets,
|
||||
TriviaAfterUrl = wsAfterLink,
|
||||
Title = HtmlHelper.Unescape(title),
|
||||
Title = HtmlHelper.Unescape(title, removeBackSlash: false),
|
||||
UnescapedTitle = unescapedTitle,
|
||||
TitleEnclosingCharacter = titleEnclosingCharacter,
|
||||
TriviaAfterTitle = wsAfterTitle,
|
||||
|
||||
@@ -171,13 +171,9 @@ public class ParagraphBlockParser : BlockParser
|
||||
{
|
||||
count = line.CountAndSkipChar(headingChar);
|
||||
|
||||
if (line.IsEmpty)
|
||||
{
|
||||
return headingChar;
|
||||
}
|
||||
|
||||
while (line.NextChar().IsSpaceOrTab())
|
||||
while (line.CurrentChar.IsSpaceOrTab())
|
||||
{
|
||||
line.NextChar();
|
||||
}
|
||||
|
||||
if (line.IsEmpty)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Parsers;
|
||||
@@ -13,8 +13,6 @@ namespace Markdig.Renderers.Html;
|
||||
/// <seealso cref="HtmlObjectRenderer{CodeBlock}" />
|
||||
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
{
|
||||
private HashSet<string>? _blocksAsDiv;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
|
||||
/// </summary>
|
||||
@@ -25,23 +23,32 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
/// <summary>
|
||||
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
|
||||
/// </summary>
|
||||
public HashSet<string> BlocksAsDiv => _blocksAsDiv ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
public HashSet<string> BlocksAsDiv { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a map of custom block mapping to render as custom blocks instead of pre/code blocks.
|
||||
/// For example defining {"mermaid", "pre"} will render a block with info `mermaid` as a `pre` block but without the code HTML element.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> BlockMapping { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
|
||||
{
|
||||
renderer.EnsureLine();
|
||||
|
||||
if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info))
|
||||
if ((obj as FencedCodeBlock)?.Info is string info && (BlocksAsDiv.Contains(info) || BlockMapping.ContainsKey(info)))
|
||||
{
|
||||
var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ??
|
||||
FencedCodeBlockParser.DefaultInfoPrefix;
|
||||
|
||||
var htmlBlock = BlockMapping.TryGetValue(info, out var blockType) ? blockType : "div";
|
||||
|
||||
// We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block
|
||||
// NOTE that we are allocating a closure here
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<div")
|
||||
renderer.WriteRaw('<');
|
||||
renderer.Write(htmlBlock)
|
||||
.WriteAttributes(obj.TryGetAttributes(),
|
||||
cls => cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls)
|
||||
.WriteRaw('>');
|
||||
@@ -51,7 +58,7 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
renderer.Write("</").Write(htmlBlock).WriteLine(">");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -28,15 +28,15 @@ public abstract class MarkdownObjectRenderer<TRenderer, TObject> : IMarkdownObje
|
||||
|
||||
public virtual void Write(RendererBase renderer, MarkdownObject obj)
|
||||
{
|
||||
var htmlRenderer = (TRenderer)renderer;
|
||||
var typedRenderer = (TRenderer)renderer;
|
||||
var typedObj = (TObject)obj;
|
||||
|
||||
if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj))
|
||||
if (_tryWriters is not null && TryWrite(typedRenderer, typedObj))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Write(htmlRenderer, typedObj);
|
||||
Write(typedRenderer, typedObj);
|
||||
}
|
||||
|
||||
private bool TryWrite(TRenderer renderer, TObject obj)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"version": "9.0.100",
|
||||
"rollForward": "latestMajor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
This file is licensed under the BSD-Clause 2 license. 
|
||||
See the license.txt file in the project root for more information.</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue"><Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/NUnitProvider/SetCurrentDirectoryTo/@EntryValue">TestFolder</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Autolink/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Inlines/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
Reference in New Issue
Block a user