mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 05:44:50 +00:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3535701d70 | ||
|
|
c41b389053 | ||
|
|
09a4b81a6e | ||
|
|
7b14e2e091 | ||
|
|
1e17dcdd08 | ||
|
|
40e5ab1514 | ||
|
|
2953b026fc | ||
|
|
42ab98968d | ||
|
|
b15cf582a5 | ||
|
|
61e9be290b | ||
|
|
a9ce0eb438 | ||
|
|
023d93c091 | ||
|
|
bbefce3b1f | ||
|
|
0d6343b421 | ||
|
|
f4effc25c0 | ||
|
|
7a83a1fd3d | ||
|
|
8269ff1af5 | ||
|
|
0e6d0f4cb2 | ||
|
|
8484420b72 | ||
|
|
c82a36884d | ||
|
|
da3d7f4f3a | ||
|
|
eceb70c16a | ||
|
|
7a9c192d7d | ||
|
|
8cfa0cf0ae | ||
|
|
a82c3bd705 | ||
|
|
ecfda373b9 | ||
|
|
d8f69218db | ||
|
|
adfcf42529 | ||
|
|
dab1ca5483 | ||
|
|
55f770cc07 | ||
|
|
8b84542527 | ||
|
|
086440bcd3 | ||
|
|
97470bd61f | ||
|
|
90c73b7754 | ||
|
|
ee403ce28f | ||
|
|
8b403918b9 | ||
|
|
39b07d6bc5 | ||
|
|
fb3fe8b261 | ||
|
|
abb19ecf37 | ||
|
|
9dac60df73 | ||
|
|
148278417f | ||
|
|
5b32391348 | ||
|
|
5528023158 | ||
|
|
f93b9d79d9 | ||
|
|
d53fd0e870 | ||
|
|
c488aca96c | ||
|
|
9b3f442765 | ||
|
|
7b6d659bbd | ||
|
|
bc8ba4fecb | ||
|
|
d87bb7292d | ||
|
|
118d28f886 | ||
|
|
3e0c72f043 | ||
|
|
f2590e7b80 | ||
|
|
88c5b5cb41 | ||
|
|
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.
|
||||
|
||||
|
||||
18
readme.md
18
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">
|
||||
|
||||
@@ -14,7 +14,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
|
||||
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
|
||||
- Checkout [Markdown Editor v2 for Visual Studio 2022](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2) powered by Markdig!
|
||||
- Converter to **HTML**
|
||||
- Passing more than **600+ tests** from the latest [CommonMark specs (0.30)](http://spec.commonmark.org/)
|
||||
- Passing more than **600+ tests** from the latest [CommonMark specs (0.31.2)](http://spec.commonmark.org/)
|
||||
- Includes all the core elements of CommonMark:
|
||||
- including **GFM fenced code blocks**.
|
||||
- **Extensible** architecture
|
||||
@@ -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)
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
|
||||
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
|
||||
<PackageReference Include="Markdown" Version="2.2.1" />
|
||||
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.506101" />
|
||||
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.512801" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Markdig\Markdig.csproj" />
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<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>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,5 +533,28 @@ namespace Markdig.Tests.Specs.AutoLinks
|
||||
|
||||
TestParser.TestSpec("<http://foö.bar.`baz>`", "<p><a href=\"http://xn--fo-gka.bar.%60baz\">http://foö.bar.`baz</a>`</p>", "autolinks|advanced", context: "Example 25\nSection Extensions / AutoLinks / Unicode support\n");
|
||||
}
|
||||
|
||||
// Unicode punctuation characters are not allowed, but symbols are.
|
||||
// Note that this does _not_ exactly match CommonMark's "Unicode punctuation character" definition.
|
||||
[Test]
|
||||
public void ExtensionsAutoLinksUnicodeSupport_Example026()
|
||||
{
|
||||
// Example 26
|
||||
// Section: Extensions / AutoLinks / Unicode support
|
||||
//
|
||||
// The following Markdown:
|
||||
// http://☃.net?☃ // OtherSymbol
|
||||
//
|
||||
// http://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol
|
||||
//
|
||||
// http://‰.net?‰ // OtherPunctuation
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p><a href="http://xn--n3h.net?%E2%98%83">http://☃.net?☃</a> // OtherSymbol</p>
|
||||
// <p><a href="http://xn--ji8h.net?%F0%9F%8D%89">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>
|
||||
// <p>http://‰.net?‰ // OtherPunctuation</p>
|
||||
|
||||
TestParser.TestSpec("http://☃.net?☃ // OtherSymbol\n\nhttp://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol\n\nhttp://‰.net?‰ // OtherPunctuation", "<p><a href=\"http://xn--n3h.net?%E2%98%83\">http://☃.net?☃</a> // OtherSymbol</p>\n<p><a href=\"http://xn--ji8h.net?%F0%9F%8D%89\">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>\n<p>http://‰.net?‰ // OtherPunctuation</p>", "autolinks|advanced", context: "Example 26\nSection Extensions / AutoLinks / Unicode support\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,4 +303,19 @@ This will therefore be seen as an autolink and not as code inline.
|
||||
<http://foö.bar.`baz>`
|
||||
.
|
||||
<p><a href="http://xn--fo-gka.bar.%60baz">http://foö.bar.`baz</a>`</p>
|
||||
````````````````````````````````
|
||||
|
||||
Unicode punctuation characters are not allowed, but symbols are.
|
||||
Note that this does _not_ exactly match CommonMark's "Unicode punctuation character" definition.
|
||||
|
||||
```````````````````````````````` example
|
||||
http://☃.net?☃ // OtherSymbol
|
||||
|
||||
http://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol
|
||||
|
||||
http://‰.net?‰ // OtherPunctuation
|
||||
.
|
||||
<p><a href="http://xn--n3h.net?%E2%98%83">http://☃.net?☃</a> // OtherSymbol</p>
|
||||
<p><a href="http://xn--ji8h.net?%F0%9F%8D%89">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>
|
||||
<p>http://‰.net?‰ // OtherPunctuation</p>
|
||||
````````````````````````````````
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
---
|
||||
title: CommonMark Spec
|
||||
author: John MacFarlane
|
||||
version: '0.30'
|
||||
date: '2021-06-19'
|
||||
license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)'
|
||||
version: '0.31.2'
|
||||
date: '2024-01-28'
|
||||
license: '[CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)'
|
||||
...
|
||||
|
||||
# Introduction
|
||||
@@ -14,7 +14,7 @@ Markdown is a plain text format for writing structured documents,
|
||||
based on conventions for indicating formatting in email
|
||||
and usenet posts. It was developed by John Gruber (with
|
||||
help from Aaron Swartz) and released in 2004 in the form of a
|
||||
[syntax description](http://daringfireball.net/projects/markdown/syntax)
|
||||
[syntax description](https://daringfireball.net/projects/markdown/syntax)
|
||||
and a Perl script (`Markdown.pl`) for converting Markdown to
|
||||
HTML. In the next decade, dozens of implementations were
|
||||
developed in many languages. Some extended the original
|
||||
@@ -34,10 +34,10 @@ As Gruber writes:
|
||||
> Markdown-formatted document should be publishable as-is, as
|
||||
> plain text, without looking like it's been marked up with tags
|
||||
> or formatting instructions.
|
||||
> (<http://daringfireball.net/projects/markdown/>)
|
||||
> (<https://daringfireball.net/projects/markdown/>)
|
||||
|
||||
The point can be illustrated by comparing a sample of
|
||||
[AsciiDoc](http://www.methods.co.nz/asciidoc/) with
|
||||
[AsciiDoc](https://asciidoc.org/) with
|
||||
an equivalent sample of Markdown. Here is a sample of
|
||||
AsciiDoc from the AsciiDoc manual:
|
||||
|
||||
@@ -103,7 +103,7 @@ source, not just in the processed document.
|
||||
## Why is a spec needed?
|
||||
|
||||
John Gruber's [canonical description of Markdown's
|
||||
syntax](http://daringfireball.net/projects/markdown/syntax)
|
||||
syntax](https://daringfireball.net/projects/markdown/syntax)
|
||||
does not specify the syntax unambiguously. Here are some examples of
|
||||
questions it does not answer:
|
||||
|
||||
@@ -316,9 +316,9 @@ A line containing no characters, or a line containing only spaces
|
||||
|
||||
The following definitions of character classes will be used in this spec:
|
||||
|
||||
A [Unicode whitespace character](@) is
|
||||
any code point in the Unicode `Zs` general category, or a tab (`U+0009`),
|
||||
line feed (`U+000A`), form feed (`U+000C`), or carriage return (`U+000D`).
|
||||
A [Unicode whitespace character](@) is a character in the Unicode `Zs` general
|
||||
category, or a tab (`U+0009`), line feed (`U+000A`), form feed (`U+000C`), or
|
||||
carriage return (`U+000D`).
|
||||
|
||||
[Unicode whitespace](@) is a sequence of one or more
|
||||
[Unicode whitespace characters].
|
||||
@@ -337,9 +337,8 @@ is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`,
|
||||
`[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B–0060),
|
||||
`{`, `|`, `}`, or `~` (U+007B–007E).
|
||||
|
||||
A [Unicode punctuation character](@) is an [ASCII
|
||||
punctuation character] or anything in
|
||||
the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`.
|
||||
A [Unicode punctuation character](@) is a character in the Unicode `P`
|
||||
(puncuation) or `S` (symbol) general categories.
|
||||
|
||||
## Tabs
|
||||
|
||||
@@ -579,9 +578,9 @@ raw HTML:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://example.com?find=\*>
|
||||
<https://example.com?find=\*>
|
||||
.
|
||||
<p><a href="http://example.com?find=%5C*">http://example.com?find=\*</a></p>
|
||||
<p><a href="https://example.com?find=%5C*">https://example.com?find=\*</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -1964,7 +1963,7 @@ has been found, the code block contains all of the lines after the
|
||||
opening code fence until the end of the containing block (or
|
||||
document). (An alternative spec would require backtracking in the
|
||||
event that a closing code fence is not found. But this makes parsing
|
||||
much less efficient, and there seems to be no real down side to the
|
||||
much less efficient, and there seems to be no real downside to the
|
||||
behavior described here.)
|
||||
|
||||
A fenced code block may interrupt a paragraph, and does not require
|
||||
@@ -2403,7 +2402,7 @@ followed by one of the strings (case-insensitive) `address`,
|
||||
`h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `head`, `header`, `hr`,
|
||||
`html`, `iframe`, `legend`, `li`, `link`, `main`, `menu`, `menuitem`,
|
||||
`nav`, `noframes`, `ol`, `optgroup`, `option`, `p`, `param`,
|
||||
`section`, `source`, `summary`, `table`, `tbody`, `td`,
|
||||
`search`, `section`, `summary`, `table`, `tbody`, `td`,
|
||||
`tfoot`, `th`, `thead`, `title`, `tr`, `track`, `ul`, followed
|
||||
by a space, a tab, the end of the line, the string `>`, or
|
||||
the string `/>`.\
|
||||
@@ -4115,7 +4114,7 @@ The following rules define [list items]:
|
||||
blocks *Bs* starting with a character other than a space or tab, and *M* is
|
||||
a list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces of indentation,
|
||||
then the result of prepending *M* and the following spaces to the first line
|
||||
of Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a
|
||||
of *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a
|
||||
list item with *Bs* as its contents. The type of the list item
|
||||
(bullet or ordered) is determined by the type of its list marker.
|
||||
If the list item is ordered, then it is also assigned a start
|
||||
@@ -5350,11 +5349,11 @@ by itself should be a paragraph followed by a nested sublist.
|
||||
Since it is well established Markdown practice to allow lists to
|
||||
interrupt paragraphs inside list items, the [principle of
|
||||
uniformity] requires us to allow this outside list items as
|
||||
well. ([reStructuredText](http://docutils.sourceforge.net/rst.html)
|
||||
well. ([reStructuredText](https://docutils.sourceforge.net/rst.html)
|
||||
takes a different approach, requiring blank lines before lists
|
||||
even inside other list items.)
|
||||
|
||||
In order to solve of unwanted lists in paragraphs with
|
||||
In order to solve the problem of unwanted lists in paragraphs with
|
||||
hard-wrapped numerals, we allow only lists starting with `1` to
|
||||
interrupt paragraphs. Thus,
|
||||
|
||||
@@ -6055,18 +6054,18 @@ But this is an HTML tag:
|
||||
And this is code:
|
||||
|
||||
```````````````````````````````` example
|
||||
`<http://foo.bar.`baz>`
|
||||
`<https://foo.bar.`baz>`
|
||||
.
|
||||
<p><code><http://foo.bar.</code>baz>`</p>
|
||||
<p><code><https://foo.bar.</code>baz>`</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
But this is an autolink:
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://foo.bar.`baz>`
|
||||
<https://foo.bar.`baz>`
|
||||
.
|
||||
<p><a href="http://foo.bar.%60baz">http://foo.bar.`baz</a>`</p>
|
||||
<p><a href="https://foo.bar.%60baz">https://foo.bar.`baz</a>`</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -6099,7 +6098,7 @@ closing backtick strings to be equal in length:
|
||||
## Emphasis and strong emphasis
|
||||
|
||||
John Gruber's original [Markdown syntax
|
||||
description](http://daringfireball.net/projects/markdown/syntax#em) says:
|
||||
description](https://daringfireball.net/projects/markdown/syntax#em) says:
|
||||
|
||||
> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
|
||||
> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML
|
||||
@@ -6201,7 +6200,7 @@ Here are some examples of delimiter runs.
|
||||
(The idea of distinguishing left-flanking and right-flanking
|
||||
delimiter runs based on the character before and the character
|
||||
after comes from Roopesh Chander's
|
||||
[vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags).
|
||||
[vfmd](https://web.archive.org/web/20220608143320/http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags).
|
||||
vfmd uses the terminology "emphasis indicator string" instead of "delimiter
|
||||
run," and its rules for distinguishing left- and right-flanking runs
|
||||
are a bit more complex than the ones given here.)
|
||||
@@ -6343,6 +6342,21 @@ Unicode nonbreaking spaces count as whitespace, too:
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
Unicode symbols count as punctuation, too:
|
||||
|
||||
```````````````````````````````` example
|
||||
*$*alpha.
|
||||
|
||||
*£*bravo.
|
||||
|
||||
*€*charlie.
|
||||
.
|
||||
<p>*$*alpha.</p>
|
||||
<p>*£*bravo.</p>
|
||||
<p>*€*charlie.</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
Intraword emphasis with `*` is permitted:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -7428,16 +7442,16 @@ _a `_`_
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
**a<http://foo.bar/?q=**>
|
||||
**a<https://foo.bar/?q=**>
|
||||
.
|
||||
<p>**a<a href="http://foo.bar/?q=**">http://foo.bar/?q=**</a></p>
|
||||
<p>**a<a href="https://foo.bar/?q=**">https://foo.bar/?q=**</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
__a<http://foo.bar/?q=__>
|
||||
__a<https://foo.bar/?q=__>
|
||||
.
|
||||
<p>__a<a href="http://foo.bar/?q=__">http://foo.bar/?q=__</a></p>
|
||||
<p>__a<a href="https://foo.bar/?q=__">https://foo.bar/?q=__</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -7685,13 +7699,13 @@ A link can contain fragment identifiers and queries:
|
||||
```````````````````````````````` example
|
||||
[link](#fragment)
|
||||
|
||||
[link](http://example.com#fragment)
|
||||
[link](https://example.com#fragment)
|
||||
|
||||
[link](http://example.com?foo=3#frag)
|
||||
[link](https://example.com?foo=3#frag)
|
||||
.
|
||||
<p><a href="#fragment">link</a></p>
|
||||
<p><a href="http://example.com#fragment">link</a></p>
|
||||
<p><a href="http://example.com?foo=3#frag">link</a></p>
|
||||
<p><a href="https://example.com#fragment">link</a></p>
|
||||
<p><a href="https://example.com?foo=3#frag">link</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -7935,9 +7949,9 @@ and autolinks over link grouping:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
[foo<http://example.com/?search=](uri)>
|
||||
[foo<https://example.com/?search=](uri)>
|
||||
.
|
||||
<p>[foo<a href="http://example.com/?search=%5D(uri)">http://example.com/?search=](uri)</a></p>
|
||||
<p>[foo<a href="https://example.com/?search=%5D(uri)">https://example.com/?search=](uri)</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8091,11 +8105,11 @@ and autolinks over link grouping:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
[foo<http://example.com/?search=][ref]>
|
||||
[foo<https://example.com/?search=][ref]>
|
||||
|
||||
[ref]: /uri
|
||||
.
|
||||
<p>[foo<a href="http://example.com/?search=%5D%5Bref%5D">http://example.com/?search=][ref]</a></p>
|
||||
<p>[foo<a href="https://example.com/?search=%5D%5Bref%5D">https://example.com/?search=][ref]</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8295,7 +8309,7 @@ A [collapsed reference link](@)
|
||||
consists of a [link label] that [matches] a
|
||||
[link reference definition] elsewhere in the
|
||||
document, followed by the string `[]`.
|
||||
The contents of the first link label are parsed as inlines,
|
||||
The contents of the link label are parsed as inlines,
|
||||
which are used as the link's text. The link's URI and title are
|
||||
provided by the matching reference link definition. Thus,
|
||||
`[foo][]` is equivalent to `[foo][foo]`.
|
||||
@@ -8348,7 +8362,7 @@ A [shortcut reference link](@)
|
||||
consists of a [link label] that [matches] a
|
||||
[link reference definition] elsewhere in the
|
||||
document and is not followed by `[]` or a link label.
|
||||
The contents of the first link label are parsed as inlines,
|
||||
The contents of the link label are parsed as inlines,
|
||||
which are used as the link's text. The link's URI and title
|
||||
are provided by the matching link reference definition.
|
||||
Thus, `[foo]` is equivalent to `[foo][]`.
|
||||
@@ -8435,7 +8449,7 @@ following closing bracket:
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
Full and compact references take precedence over shortcut
|
||||
Full and collapsed references take precedence over shortcut
|
||||
references:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -8771,9 +8785,9 @@ Here are some valid autolinks:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://foo.bar.baz/test?q=hello&id=22&boolean>
|
||||
<https://foo.bar.baz/test?q=hello&id=22&boolean>
|
||||
.
|
||||
<p><a href="http://foo.bar.baz/test?q=hello&id=22&boolean">http://foo.bar.baz/test?q=hello&id=22&boolean</a></p>
|
||||
<p><a href="https://foo.bar.baz/test?q=hello&id=22&boolean">https://foo.bar.baz/test?q=hello&id=22&boolean</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8813,9 +8827,9 @@ with their syntax:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://../>
|
||||
<https://../>
|
||||
.
|
||||
<p><a href="http://../">http://../</a></p>
|
||||
<p><a href="https://../">https://../</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8829,18 +8843,18 @@ with their syntax:
|
||||
Spaces are not allowed in autolinks:
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://foo.bar/baz bim>
|
||||
<https://foo.bar/baz bim>
|
||||
.
|
||||
<p><http://foo.bar/baz bim></p>
|
||||
<p><https://foo.bar/baz bim></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
Backslash-escapes do not work inside autolinks:
|
||||
|
||||
```````````````````````````````` example
|
||||
<http://example.com/\[\>
|
||||
<https://example.com/\[\>
|
||||
.
|
||||
<p><a href="http://example.com/%5C%5B%5C">http://example.com/\[\</a></p>
|
||||
<p><a href="https://example.com/%5C%5B%5C">https://example.com/\[\</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8892,9 +8906,9 @@ These are not autolinks:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
< http://foo.bar >
|
||||
< https://foo.bar >
|
||||
.
|
||||
<p>< http://foo.bar ></p>
|
||||
<p>< https://foo.bar ></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8913,9 +8927,9 @@ These are not autolinks:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
http://example.com
|
||||
https://example.com
|
||||
.
|
||||
<p>http://example.com</p>
|
||||
<p>https://example.com</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -8977,10 +8991,9 @@ A [closing tag](@) consists of the string `</`, a
|
||||
[tag name], optional spaces, tabs, and up to one line ending, and the character
|
||||
`>`.
|
||||
|
||||
An [HTML comment](@) consists of `<!--` + *text* + `-->`,
|
||||
where *text* does not start with `>` or `->`, does not end with `-`,
|
||||
and does not contain `--`. (See the
|
||||
[HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).)
|
||||
An [HTML comment](@) consists of `<!-->`, `<!--->`, or `<!--`, a string of
|
||||
characters not including the string `-->`, and `-->` (see the
|
||||
[HTML spec](https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state)).
|
||||
|
||||
A [processing instruction](@)
|
||||
consists of the string `<?`, a string
|
||||
@@ -9119,30 +9132,20 @@ Illegal attributes in closing tag:
|
||||
Comments:
|
||||
|
||||
```````````````````````````````` example
|
||||
foo <!-- this is a
|
||||
comment - with hyphen -->
|
||||
foo <!-- this is a --
|
||||
comment - with hyphens -->
|
||||
.
|
||||
<p>foo <!-- this is a
|
||||
comment - with hyphen --></p>
|
||||
<p>foo <!-- this is a --
|
||||
comment - with hyphens --></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
foo <!-- not a comment -- two hyphens -->
|
||||
.
|
||||
<p>foo <!-- not a comment -- two hyphens --></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
Not comments:
|
||||
|
||||
```````````````````````````````` example
|
||||
foo <!--> foo -->
|
||||
|
||||
foo <!-- foo--->
|
||||
foo <!---> foo -->
|
||||
.
|
||||
<p>foo <!--> foo --></p>
|
||||
<p>foo <!-- foo---></p>
|
||||
<p>foo <!--> foo --></p>
|
||||
<p>foo <!---> foo --></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -9671,7 +9674,7 @@ through the stack for an opening `[` or `![` delimiter.
|
||||
delimiter from the stack, and return a literal text node `]`.
|
||||
|
||||
- If we find one and it's active, then we parse ahead to see if
|
||||
we have an inline link/image, reference link/image, compact reference
|
||||
we have an inline link/image, reference link/image, collapsed reference
|
||||
link/image, or shortcut reference link/image.
|
||||
|
||||
+ If we don't, then we remove the opening delimiter from the
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,6 +123,8 @@ namespace Markdig.Tests.Specs.EmphasisExtra
|
||||
public class TestExtensionsEmphasisOnHtmlEntities
|
||||
{
|
||||
// ## Emphasis on Html Entities
|
||||
//
|
||||
// Note that Unicode symbols are treated as punctuation, which are not allowed to open the emphasis unless they are preceded by a space.
|
||||
[Test]
|
||||
public void ExtensionsEmphasisOnHtmlEntities_Example006()
|
||||
{
|
||||
@@ -132,14 +134,14 @@ namespace Markdig.Tests.Specs.EmphasisExtra
|
||||
// The following Markdown:
|
||||
// This is text MyBrand ^®^ and MyTrademark ^™^
|
||||
// This is text MyBrand^®^ and MyTrademark^™^
|
||||
// This is text MyBrand~®~ and MyCopyright^©^
|
||||
// This is text MyBrand ~®~ and MyCopyright ^©^
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>
|
||||
// This is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>
|
||||
// This is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>
|
||||
// This is text MyBrand^®^ and MyTrademark^TM^
|
||||
// This is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>
|
||||
|
||||
TestParser.TestSpec("This is text MyBrand ^®^ and MyTrademark ^™^\nThis is text MyBrand^®^ and MyTrademark^™^\nThis is text MyBrand~®~ and MyCopyright^©^", "<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>\nThis is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>\nThis is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>", "emphasisextras|advanced", context: "Example 6\nSection Extensions / Emphasis on Html Entities\n");
|
||||
TestParser.TestSpec("This is text MyBrand ^®^ and MyTrademark ^™^\nThis is text MyBrand^®^ and MyTrademark^™^\nThis is text MyBrand ~®~ and MyCopyright ^©^", "<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>\nThis is text MyBrand^®^ and MyTrademark^TM^\nThis is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>", "emphasisextras|advanced", context: "Example 6\nSection Extensions / Emphasis on Html Entities\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,16 +52,17 @@ Marked text can be used to specify that a text has been marked in a document. T
|
||||
.
|
||||
<p><mark>Marked text</mark></p>
|
||||
````````````````````````````````
|
||||
|
||||
## Emphasis on Html Entities
|
||||
|
||||
Note that Unicode symbols are treated as punctuation, which are not allowed to open the emphasis unless they are preceded by a space.
|
||||
|
||||
```````````````````````````````` example
|
||||
This is text MyBrand ^®^ and MyTrademark ^™^
|
||||
This is text MyBrand^®^ and MyTrademark^™^
|
||||
This is text MyBrand~®~ and MyCopyright^©^
|
||||
This is text MyBrand ~®~ and MyCopyright ^©^
|
||||
.
|
||||
<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>
|
||||
This is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>
|
||||
This is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>
|
||||
This is text MyBrand^®^ and MyTrademark^TM^
|
||||
This is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Markdig.Tests.Specs.Math
|
||||
//
|
||||
// ## Math Inline
|
||||
//
|
||||
// Allows to define a mathematic block embraced by `$...$`
|
||||
// Allows to define a mathematic inline block embraced by `$...$`
|
||||
[Test]
|
||||
public void ExtensionsMathInline_Example001()
|
||||
{
|
||||
@@ -25,12 +25,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $math block$
|
||||
// This is a $math inline$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span></p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is a $math block$", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 1\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $math inline$", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 1\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// Or by `$$...$$` embracing it by:
|
||||
@@ -41,12 +41,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $$math block$$
|
||||
// This is a $$math inline$$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span></p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is a $$math block$$", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 2\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $$math inline$$", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 2\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// Newlines inside an inline math are not allowed:
|
||||
@@ -58,13 +58,13 @@ namespace Markdig.Tests.Specs.Math
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is not a $$math
|
||||
// block$$ and? this is a $$math block$$
|
||||
// inline$$ and? this is a $$math inline$$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is not a $$math
|
||||
// block$$ and? this is a <span class="math">\(math block\)</span></p>
|
||||
// inline$$ and? this is a <span class="math">\(math inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is not a $$math \nblock$$ and? this is a $$math block$$", "<p>This is not a $$math\nblock$$ and? this is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 3\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is not a $$math \ninline$$ and? this is a $$math inline$$", "<p>This is not a $$math\ninline$$ and? this is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 3\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -75,13 +75,13 @@ namespace Markdig.Tests.Specs.Math
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is not a $math
|
||||
// block$ and? this is a $math block$
|
||||
// inline$ and? this is a $math inline$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is not a $math
|
||||
// block$ and? this is a <span class="math">\(math block\)</span></p>
|
||||
// inline$ and? this is a <span class="math">\(math inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is not a $math \nblock$ and? this is a $math block$", "<p>This is not a $math\nblock$ and? this is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 4\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is not a $math \ninline$ and? this is a $math inline$", "<p>This is not a $math\ninline$ and? this is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 4\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// An opening `$` can be followed by a space if the closing is also preceded by a space `$`:
|
||||
@@ -92,12 +92,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $ math block $
|
||||
// This is a $ math inline $
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span></p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is a $ math block $", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 5\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $ math inline $", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 5\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -107,12 +107,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $ math block $ after
|
||||
// This is a $ math inline $ after
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span> after</p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span> after</p>
|
||||
|
||||
TestParser.TestSpec("This is a $ math block $ after", "<p>This is a <span class=\"math\">\\(math block\\)</span> after</p>", "mathematics|advanced", context: "Example 6\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $ math inline $ after", "<p>This is a <span class=\"math\">\\(math inline\\)</span> after</p>", "mathematics|advanced", context: "Example 6\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -122,12 +122,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $$ math block $$ after
|
||||
// This is a $$ math inline $$ after
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span> after</p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span> after</p>
|
||||
|
||||
TestParser.TestSpec("This is a $$ math block $$ after", "<p>This is a <span class=\"math\">\\(math block\\)</span> after</p>", "mathematics|advanced", context: "Example 7\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $$ math inline $$ after", "<p>This is a <span class=\"math\">\\(math inline\\)</span> after</p>", "mathematics|advanced", context: "Example 7\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -137,12 +137,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a not $ math block$ because there is not a whitespace before the closing
|
||||
// This is a not $ math inline$ because there is not a whitespace before the closing
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a not $ math block$ because there is not a whitespace before the closing</p>
|
||||
// <p>This is a not $ math inline$ because there is not a whitespace before the closing</p>
|
||||
|
||||
TestParser.TestSpec("This is a not $ math block$ because there is not a whitespace before the closing", "<p>This is a not $ math block$ because there is not a whitespace before the closing</p>", "mathematics|advanced", context: "Example 8\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a not $ math inline$ because there is not a whitespace before the closing", "<p>This is a not $ math inline$ because there is not a whitespace before the closing</p>", "mathematics|advanced", context: "Example 8\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// For the opening `$` it requires a space or a punctuation before (but cannot be used within a word):
|
||||
@@ -153,12 +153,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is not a m$ath block$
|
||||
// This is not a m$ath inline$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is not a m$ath block$</p>
|
||||
// <p>This is not a m$ath inline$</p>
|
||||
|
||||
TestParser.TestSpec("This is not a m$ath block$", "<p>This is not a m$ath block$</p>", "mathematics|advanced", context: "Example 9\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is not a m$ath inline$", "<p>This is not a m$ath inline$</p>", "mathematics|advanced", context: "Example 9\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
|
||||
@@ -169,12 +169,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is not a $math bloc$k
|
||||
// This is not a $math inlin$e
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is not a $math bloc$k</p>
|
||||
// <p>This is not a $math inlin$e</p>
|
||||
|
||||
TestParser.TestSpec("This is not a $math bloc$k", "<p>This is not a $math bloc$k</p>", "mathematics|advanced", context: "Example 10\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is not a $math inlin$e", "<p>This is not a $math inlin$e</p>", "mathematics|advanced", context: "Example 10\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
|
||||
@@ -201,12 +201,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $math \$ block$
|
||||
// This is a $math \$ inline$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math \$ block\)</span></p>
|
||||
// <p>This is a <span class="math">\(math \$ inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is a $math \\$ block$", "<p>This is a <span class=\"math\">\\(math \\$ block\\)</span></p>", "mathematics|advanced", context: "Example 12\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $math \\$ inline$", "<p>This is a <span class=\"math\">\\(math \\$ inline\\)</span></p>", "mathematics|advanced", context: "Example 12\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// At most, two `$` will be matched for the opening and closing:
|
||||
@@ -217,12 +217,12 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $$$math block$$$
|
||||
// This is a $$$math inline$$$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\($math block$\)</span></p>
|
||||
// <p>This is a <span class="math">\($math inline$\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is a $$$math block$$$", "<p>This is a <span class=\"math\">\\($math block$\\)</span></p>", "mathematics|advanced", context: "Example 13\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $$$math inline$$$", "<p>This is a <span class=\"math\">\\($math inline$\\)</span></p>", "mathematics|advanced", context: "Example 13\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// Regular text can come both before and after the math inline
|
||||
@@ -233,15 +233,15 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is a $math block$ with text on both sides.
|
||||
// This is a $math inline$ with text on both sides.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is a <span class="math">\(math block\)</span> with text on both sides.</p>
|
||||
// <p>This is a <span class="math">\(math inline\)</span> with text on both sides.</p>
|
||||
|
||||
TestParser.TestSpec("This is a $math block$ with text on both sides.", "<p>This is a <span class=\"math\">\\(math block\\)</span> with text on both sides.</p>", "mathematics|advanced", context: "Example 14\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is a $math inline$ with text on both sides.", "<p>This is a <span class=\"math\">\\(math inline\\)</span> with text on both sides.</p>", "mathematics|advanced", context: "Example 14\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// A mathematic block takes precedence over standard emphasis `*` `_`:
|
||||
// A mathematic inline block takes precedence over standard emphasis `*` `_`:
|
||||
[Test]
|
||||
public void ExtensionsMathInline_Example015()
|
||||
{
|
||||
@@ -249,15 +249,15 @@ namespace Markdig.Tests.Specs.Math
|
||||
// Section: Extensions / Math Inline
|
||||
//
|
||||
// The following Markdown:
|
||||
// This is *a $math* block$
|
||||
// This is *a $math* inline$
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>This is *a <span class="math">\(math* block\)</span></p>
|
||||
// <p>This is *a <span class="math">\(math* inline\)</span></p>
|
||||
|
||||
TestParser.TestSpec("This is *a $math* block$", "<p>This is *a <span class=\"math\">\\(math* block\\)</span></p>", "mathematics|advanced", context: "Example 15\nSection Extensions / Math Inline\n");
|
||||
TestParser.TestSpec("This is *a $math* inline$", "<p>This is *a <span class=\"math\">\\(math* inline\\)</span></p>", "mathematics|advanced", context: "Example 15\nSection Extensions / Math Inline\n");
|
||||
}
|
||||
|
||||
// An opening $$ at the beginning of a line should not be interpreted as a Math block:
|
||||
// An opening $$ at the beginning of a line should not be interpreted as a Math inline:
|
||||
[Test]
|
||||
public void ExtensionsMathInline_Example016()
|
||||
{
|
||||
|
||||
@@ -4,79 +4,79 @@ Adds support for mathematics spans:
|
||||
|
||||
## Math Inline
|
||||
|
||||
Allows to define a mathematic block embraced by `$...$`
|
||||
Allows to define a mathematic inline block embraced by `$...$`
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $math block$
|
||||
This is a $math inline$
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span></p>
|
||||
<p>This is a <span class="math">\(math inline\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
Or by `$$...$$` embracing it by:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $$math block$$
|
||||
This is a $$math inline$$
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span></p>
|
||||
<p>This is a <span class="math">\(math inline\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
Newlines inside an inline math are not allowed:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a $$math
|
||||
block$$ and? this is a $$math block$$
|
||||
inline$$ and? this is a $$math inline$$
|
||||
.
|
||||
<p>This is not a $$math
|
||||
block$$ and? this is a <span class="math">\(math block\)</span></p>
|
||||
inline$$ and? this is a <span class="math">\(math inline\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a $math
|
||||
block$ and? this is a $math block$
|
||||
inline$ and? this is a $math inline$
|
||||
.
|
||||
<p>This is not a $math
|
||||
block$ and? this is a <span class="math">\(math block\)</span></p>
|
||||
inline$ and? this is a <span class="math">\(math inline\)</span></p>
|
||||
````````````````````````````````
|
||||
An opening `$` can be followed by a space if the closing is also preceded by a space `$`:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $ math block $
|
||||
This is a $ math inline $
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span></p>
|
||||
<p>This is a <span class="math">\(math inline\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $ math block $ after
|
||||
This is a $ math inline $ after
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span> after</p>
|
||||
<p>This is a <span class="math">\(math inline\)</span> after</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $$ math block $$ after
|
||||
This is a $$ math inline $$ after
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span> after</p>
|
||||
<p>This is a <span class="math">\(math inline\)</span> after</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a not $ math block$ because there is not a whitespace before the closing
|
||||
This is a not $ math inline$ because there is not a whitespace before the closing
|
||||
.
|
||||
<p>This is a not $ math block$ because there is not a whitespace before the closing</p>
|
||||
<p>This is a not $ math inline$ because there is not a whitespace before the closing</p>
|
||||
````````````````````````````````
|
||||
|
||||
For the opening `$` it requires a space or a punctuation before (but cannot be used within a word):
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a m$ath block$
|
||||
This is not a m$ath inline$
|
||||
.
|
||||
<p>This is not a m$ath block$</p>
|
||||
<p>This is not a m$ath inline$</p>
|
||||
````````````````````````````````
|
||||
|
||||
For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a $math bloc$k
|
||||
This is not a $math inlin$e
|
||||
.
|
||||
<p>This is not a $math bloc$k</p>
|
||||
<p>This is not a $math inlin$e</p>
|
||||
````````````````````````````````
|
||||
|
||||
For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
|
||||
@@ -90,34 +90,34 @@ This is should not match a 16$ or a $15
|
||||
A `$` can be escaped between a math inline block by using the escape `\\`
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $math \$ block$
|
||||
This is a $math \$ inline$
|
||||
.
|
||||
<p>This is a <span class="math">\(math \$ block\)</span></p>
|
||||
<p>This is a <span class="math">\(math \$ inline\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
At most, two `$` will be matched for the opening and closing:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $$$math block$$$
|
||||
This is a $$$math inline$$$
|
||||
.
|
||||
<p>This is a <span class="math">\($math block$\)</span></p>
|
||||
<p>This is a <span class="math">\($math inline$\)</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
Regular text can come both before and after the math inline
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $math block$ with text on both sides.
|
||||
This is a $math inline$ with text on both sides.
|
||||
.
|
||||
<p>This is a <span class="math">\(math block\)</span> with text on both sides.</p>
|
||||
<p>This is a <span class="math">\(math inline\)</span> with text on both sides.</p>
|
||||
````````````````````````````````
|
||||
A mathematic block takes precedence over standard emphasis `*` `_`:
|
||||
A mathematic inline block takes precedence over standard emphasis `*` `_`:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is *a $math* block$
|
||||
This is *a $math* inline$
|
||||
.
|
||||
<p>This is *a <span class="math">\(math* block\)</span></p>
|
||||
<p>This is *a <span class="math">\(math* inline\)</span></p>
|
||||
````````````````````````````````
|
||||
An opening $$ at the beginning of a line should not be interpreted as a Math block:
|
||||
An opening $$ at the beginning of a line should not be interpreted as a Math inline:
|
||||
|
||||
```````````````````````````````` example
|
||||
$$ math $$ starting at a line
|
||||
|
||||
24
src/Markdig.Tests/TestAutoLinks.cs
Normal file
24
src/Markdig.Tests/TestAutoLinks.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class TestAutoLinks
|
||||
{
|
||||
[Test]
|
||||
[TestCase("https://localhost", "<p><a href=\"https://localhost\">https://localhost</a></p>")]
|
||||
[TestCase("http://localhost", "<p><a href=\"http://localhost\">http://localhost</a></p>")]
|
||||
[TestCase("https://l", "<p><a href=\"https://l\">https://l</a></p>")]
|
||||
[TestCase("www.l", "<p><a href=\"http://www.l\">www.l</a></p>")]
|
||||
[TestCase("https://localhost:5000", "<p><a href=\"https://localhost:5000\">https://localhost:5000</a></p>")]
|
||||
[TestCase("www.l:5000", "<p><a href=\"http://www.l:5000\">www.l:5000</a></p>")]
|
||||
public void TestLinksWithAllowDomainWithoutPeriod(string markdown, string expected)
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoLinks(new AutoLinkOptions { AllowDomainWithoutPeriod = true })
|
||||
.Build();
|
||||
var html = Markdown.ToHtml(markdown, pipeline);
|
||||
|
||||
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
|
||||
}
|
||||
}
|
||||
@@ -19,18 +19,32 @@ public class TestCharHelper
|
||||
'{', '|', '}', '~'
|
||||
};
|
||||
|
||||
// A Unicode punctuation character is an ASCII punctuation character or anything in the general Unicode categories
|
||||
// Pc, Pd, Pe, Pf, Pi, Po, or Ps.
|
||||
private static readonly HashSet<UnicodeCategory> s_punctuationCategories = new()
|
||||
{
|
||||
// A Unicode punctuation character is a character in the Unicode P (punctuation) or S (symbol) general categories.
|
||||
private static readonly HashSet<UnicodeCategory> s_punctuationCategories =
|
||||
[
|
||||
UnicodeCategory.ConnectorPunctuation,
|
||||
UnicodeCategory.DashPunctuation,
|
||||
UnicodeCategory.OpenPunctuation,
|
||||
UnicodeCategory.ClosePunctuation,
|
||||
UnicodeCategory.FinalQuotePunctuation,
|
||||
UnicodeCategory.InitialQuotePunctuation,
|
||||
UnicodeCategory.FinalQuotePunctuation,
|
||||
UnicodeCategory.OtherPunctuation,
|
||||
UnicodeCategory.OpenPunctuation
|
||||
};
|
||||
UnicodeCategory.MathSymbol,
|
||||
UnicodeCategory.CurrencySymbol,
|
||||
UnicodeCategory.ModifierSymbol,
|
||||
UnicodeCategory.OtherSymbol,
|
||||
];
|
||||
|
||||
private static readonly HashSet<UnicodeCategory> s_punctuationWithoutSymbolsCategories =
|
||||
[
|
||||
UnicodeCategory.ConnectorPunctuation,
|
||||
UnicodeCategory.DashPunctuation,
|
||||
UnicodeCategory.OpenPunctuation,
|
||||
UnicodeCategory.ClosePunctuation,
|
||||
UnicodeCategory.InitialQuotePunctuation,
|
||||
UnicodeCategory.FinalQuotePunctuation,
|
||||
UnicodeCategory.OtherPunctuation,
|
||||
];
|
||||
|
||||
private static bool ExpectedIsPunctuation(char c)
|
||||
{
|
||||
@@ -39,23 +53,98 @@ public class TestCharHelper
|
||||
: s_punctuationCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c));
|
||||
}
|
||||
|
||||
private static bool ExpectedIsPunctuationWithoutSymbols(char c)
|
||||
{
|
||||
return c <= 127
|
||||
? s_asciiPunctuation.Contains(c)
|
||||
: s_punctuationWithoutSymbolsCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c));
|
||||
}
|
||||
|
||||
private static bool ExpectedIsWhitespace(char c)
|
||||
{
|
||||
// A Unicode whitespace character is any code point in the Unicode Zs general category,
|
||||
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
|
||||
return c == '\t' || c == '\n' || c == '\u000C' || c == '\r' ||
|
||||
return c == '\t' || c == '\n' || c == '\f' || c == '\r' ||
|
||||
CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAcrossTab()
|
||||
{
|
||||
Assert.False(CharHelper.IsAcrossTab(0));
|
||||
Assert.True(CharHelper.IsAcrossTab(1));
|
||||
Assert.True(CharHelper.IsAcrossTab(2));
|
||||
Assert.True(CharHelper.IsAcrossTab(3));
|
||||
Assert.False(CharHelper.IsAcrossTab(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddTab()
|
||||
{
|
||||
Assert.AreEqual(4, CharHelper.AddTab(0));
|
||||
Assert.AreEqual(4, CharHelper.AddTab(1));
|
||||
Assert.AreEqual(4, CharHelper.AddTab(2));
|
||||
Assert.AreEqual(4, CharHelper.AddTab(3));
|
||||
Assert.AreEqual(8, CharHelper.AddTab(4));
|
||||
Assert.AreEqual(8, CharHelper.AddTab(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsWhitespace()
|
||||
{
|
||||
for (int i = char.MinValue; i <= char.MaxValue; i++)
|
||||
{
|
||||
char c = (char)i;
|
||||
Test(
|
||||
ExpectedIsWhitespace,
|
||||
CharHelper.IsWhitespace);
|
||||
|
||||
Assert.AreEqual(ExpectedIsWhitespace(c), CharHelper.IsWhitespace(c));
|
||||
}
|
||||
Test(
|
||||
ExpectedIsWhitespace,
|
||||
CharHelper.WhitespaceChars.Contains);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsWhiteSpaceOrZero()
|
||||
{
|
||||
Test(
|
||||
c => ExpectedIsWhitespace(c) || c == 0,
|
||||
CharHelper.IsWhiteSpaceOrZero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAsciiPunctuation()
|
||||
{
|
||||
Test(
|
||||
c => char.IsAscii(c) && ExpectedIsPunctuation(c),
|
||||
CharHelper.IsAsciiPunctuation);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAsciiPunctuationOrZero()
|
||||
{
|
||||
Test(
|
||||
c => char.IsAscii(c) && (ExpectedIsPunctuation(c) || c == 0),
|
||||
CharHelper.IsAsciiPunctuationOrZero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsSpaceOrPunctuationForGFMAutoLink()
|
||||
{
|
||||
Test(
|
||||
c => c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuationWithoutSymbols(c),
|
||||
CharHelper.IsSpaceOrPunctuationForGFMAutoLink);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InvalidAutoLinkCharacters()
|
||||
{
|
||||
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
|
||||
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
|
||||
// zero or more characters other than ASCII control characters, space, <, and >.
|
||||
//
|
||||
// 2.1 Characters and lines
|
||||
// An ASCII control character is a character between U+0000–1F (both including) or U+007F.
|
||||
Test(
|
||||
c => c != 0 && c is < (char)0x20 or ' ' or '<' or '>' or '\u007F',
|
||||
CharHelper.InvalidAutoLinkCharacters.Contains);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -76,15 +165,98 @@ public class TestCharHelper
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsSpaceOrPunctuation()
|
||||
public void IsControl()
|
||||
{
|
||||
Test(
|
||||
char.IsControl,
|
||||
CharHelper.IsControl);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAlpha()
|
||||
{
|
||||
Test(
|
||||
c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'),
|
||||
CharHelper.IsAlpha);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAlphaUpper()
|
||||
{
|
||||
Test(
|
||||
c => c >= 'A' && c <= 'Z',
|
||||
CharHelper.IsAlphaUpper);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsAlphaNumeric()
|
||||
{
|
||||
Test(
|
||||
c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'),
|
||||
CharHelper.IsAlphaNumeric);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsDigit()
|
||||
{
|
||||
Test(
|
||||
c => c >= '0' && c <= '9',
|
||||
CharHelper.IsDigit);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsNewLineOrLineFeed()
|
||||
{
|
||||
Test(
|
||||
c => c is '\r' or '\n',
|
||||
CharHelper.IsNewLineOrLineFeed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsSpaceOrTab()
|
||||
{
|
||||
Test(
|
||||
c => c is ' ' or '\t',
|
||||
CharHelper.IsSpaceOrTab);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEscapableSymbol()
|
||||
{
|
||||
Test(
|
||||
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~•".Contains,
|
||||
CharHelper.IsEscapableSymbol);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEmailUsernameSpecialChar()
|
||||
{
|
||||
Test(
|
||||
".!#$%&'*+/=?^_`{|}~-+.~".Contains,
|
||||
CharHelper.IsEmailUsernameSpecialChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsEmailUsernameSpecialCharOrDigit()
|
||||
{
|
||||
Test(
|
||||
c => CharHelper.IsDigit(c) || ".!#$%&'*+/=?^_`{|}~-+.~".Contains(c),
|
||||
CharHelper.IsEmailUsernameSpecialCharOrDigit);
|
||||
}
|
||||
|
||||
private static void Test(Func<char, bool> expected, Func<char, bool> actual)
|
||||
{
|
||||
for (int i = char.MinValue; i <= char.MaxValue; i++)
|
||||
{
|
||||
char c = (char)i;
|
||||
|
||||
bool expected = c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuation(c);
|
||||
bool expectedResult = expected(c);
|
||||
bool actualResult = actual(c);
|
||||
|
||||
Assert.AreEqual(expected, CharHelper.IsSpaceOrPunctuation(c));
|
||||
if (expectedResult != actualResult)
|
||||
{
|
||||
Assert.AreEqual(expectedResult, actualResult, $"Char: '{c}' ({i})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/Markdig.Tests/TestHtmlCodeBlocks.cs
Normal file
35
src/Markdig.Tests/TestHtmlCodeBlocks.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
public class TestHtmlCodeBlocks
|
||||
{
|
||||
// Start condition: line begins with the string < or </ followed by one of the strings (case-insensitive)
|
||||
// {list of all tags}, followed by a space, a tab, the end of the line, the string >, or the string />.
|
||||
public static string[] KnownSimpleHtmlTags =>
|
||||
[
|
||||
"address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", "col", "colgroup", "dd", "details",
|
||||
"dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "iframe", "legend", "li", "link",
|
||||
"main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param",
|
||||
"search", "section", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul",
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[TestCaseSource(nameof(KnownSimpleHtmlTags))]
|
||||
public void TestKnownTags(string tag)
|
||||
{
|
||||
MarkdownDocument document = Markdown.Parse(
|
||||
$"""
|
||||
Hello
|
||||
<{tag} />
|
||||
World
|
||||
""".ReplaceLineEndings("\n"));
|
||||
|
||||
HtmlBlock[] htmlBlocks = document.Descendants<HtmlBlock>().ToArray();
|
||||
|
||||
Assert.AreEqual(1, htmlBlocks.Length);
|
||||
Assert.AreEqual(7, htmlBlocks[0].Span.Start);
|
||||
Assert.AreEqual(10 + tag.Length, htmlBlocks[0].Span.Length);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -12,10 +12,47 @@ public sealed class TestPipeTable
|
||||
[TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)]
|
||||
public void TestTableBug(string markdown, int tableCount = 1)
|
||||
{
|
||||
MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
|
||||
MarkdownDocument document =
|
||||
Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
|
||||
|
||||
Table[] tables = document.Descendants().OfType<Table>().ToArray();
|
||||
|
||||
Assert.AreEqual(tableCount, tables.Length);
|
||||
}
|
||||
|
||||
[TestCase("A | B\r\n---|---", new[] {50.0f, 50.0f})]
|
||||
[TestCase("A | B\r\n-|---", new[] {25.0f, 75.0f})]
|
||||
[TestCase("A | B\r\n-|---\r\nA | B\r\n---|---", new[] {25.0f, 75.0f})]
|
||||
[TestCase("A | B\r\n---|---|---", new[] {33.33f, 33.33f, 33.33f})]
|
||||
[TestCase("A | B\r\n---|---|---|", new[] {33.33f, 33.33f, 33.33f})]
|
||||
public void TestColumnWidthByHeaderLines(string markdown, float[] expectedWidth)
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = true})
|
||||
.Build();
|
||||
var document = Markdown.Parse(markdown, pipeline);
|
||||
var table = document.Descendants().OfType<Table>().FirstOrDefault();
|
||||
Assert.IsNotNull(table);
|
||||
var actualWidths = table.ColumnDefinitions.Select(x => x.Width).ToList();
|
||||
Assert.AreEqual(actualWidths.Count, expectedWidth.Length);
|
||||
for (int i = 0; i < expectedWidth.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(actualWidths[i], expectedWidth[i], 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColumnWidthIsNotSetWithoutConfigurationFlag()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = false})
|
||||
.Build();
|
||||
var document = Markdown.Parse("| A | B | C |\r\n|---|---|---|", pipeline);
|
||||
var table = document.Descendants().OfType<Table>().FirstOrDefault();
|
||||
Assert.IsNotNull(table);
|
||||
foreach (var column in table.ColumnDefinitions)
|
||||
{
|
||||
Assert.AreEqual(0, column.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,14 @@ public class TestPlainText
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(/* markdownText: */ "```\nConsole.WriteLine(\"Hello, World!\");\n```", /* expected: */ "Console.WriteLine(\"Hello, World!\");\n")]
|
||||
public void TestPlainCodeBlock(string markdownText, string expected)
|
||||
{
|
||||
var actual = Markdown.ToPlainText(markdownText);
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")]
|
||||
[TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public class ApiController : Controller
|
||||
{
|
||||
[HttpGet()]
|
||||
[Route("")]
|
||||
public string Empty()
|
||||
public new string Empty()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class Startup
|
||||
if (env.IsEnvironment("Development"))
|
||||
{
|
||||
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
|
||||
builder.AddApplicationInsightsSettings(developerMode: true);
|
||||
builder.AddApplicationInsightsSettings(connectionString: null, developerMode: true);
|
||||
}
|
||||
|
||||
builder.AddEnvironmentVariables();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -202,20 +203,17 @@ public class AbbreviationParser : BlockParser
|
||||
while (index <= contentNew.End)
|
||||
{
|
||||
var c = contentNew.PeekCharAbsolute(index);
|
||||
if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.IsAlphaNumeric())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.IsWhitespace())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!c.IsAsciiPunctuationOrZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -22,7 +22,7 @@ public class AlertBlock : QuoteBlock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the kind of the alert block (e.g `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`)
|
||||
/// Gets or sets the kind of the alert block (e.g `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`).
|
||||
/// </summary>
|
||||
public StringSlice Kind { get; set; }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
@@ -50,6 +49,7 @@ public class AlertBlockRenderer : HtmlObjectRenderer<AlertBlock>
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
}
|
||||
|
||||
renderer.EnsureLine();
|
||||
}
|
||||
|
||||
@@ -61,7 +61,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>",
|
||||
|
||||
@@ -6,7 +6,6 @@ using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
@@ -16,6 +15,9 @@ namespace Markdig.Extensions.Alerts;
|
||||
/// <seealso cref="InlineParser" />
|
||||
public class AlertInlineParser : InlineParser
|
||||
{
|
||||
private static readonly TransformedStringCache s_alertTypeClassCache = new(
|
||||
type => $"markdown-alert-{type.ToLowerInvariant()}");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AlertInlineParser"/> class.
|
||||
/// </summary>
|
||||
@@ -26,27 +28,31 @@ public class AlertInlineParser : InlineParser
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
if (slice.PeekChar() != '!')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
var saved = slice;
|
||||
var c = slice.NextChar();
|
||||
if (c != '!')
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
}
|
||||
StringSlice saved = slice;
|
||||
|
||||
c = slice.NextChar(); // Skip !
|
||||
slice.SkipChar(); // Skip [
|
||||
char c = slice.NextChar(); // Skip !
|
||||
|
||||
var start = slice.Start;
|
||||
var end = start;
|
||||
while (c.IsAlphaUpper())
|
||||
int start = slice.Start;
|
||||
int end = start;
|
||||
while (c.IsAlpha())
|
||||
{
|
||||
end = slice.Start;
|
||||
c = slice.NextChar();
|
||||
@@ -76,17 +82,17 @@ public class AlertInlineParser : InlineParser
|
||||
end = slice.Start;
|
||||
if (c == '\n')
|
||||
{
|
||||
slice.NextChar(); // Skip \n
|
||||
slice.SkipChar(); // Skip \n
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == '\n')
|
||||
{
|
||||
slice.NextChar(); // Skip \n
|
||||
slice.SkipChar(); // Skip \n
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (!c.IsSpaceOrTab())
|
||||
else if (!c.IsSpaceOrTab())
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
@@ -103,8 +109,9 @@ public class AlertInlineParser : InlineParser
|
||||
Column = quoteBlock.Column,
|
||||
};
|
||||
|
||||
alertBlock.GetAttributes().AddClass("markdown-alert");
|
||||
alertBlock.GetAttributes().AddClass($"markdown-alert-{alertType.ToString().ToLowerInvariant()}");
|
||||
HtmlAttributes attributes = alertBlock.GetAttributes();
|
||||
attributes.AddClass("markdown-alert");
|
||||
attributes.AddClass(s_alertTypeClassCache.Get(alertType.AsSpan()));
|
||||
|
||||
// Replace the quote block with the alert block
|
||||
var parentQuoteBlock = quoteBlock.Parent!;
|
||||
|
||||
@@ -22,6 +22,8 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
private static readonly StripRendererCache _rendererCache = new();
|
||||
|
||||
private readonly AutoIdentifierOptions _options;
|
||||
private readonly ProcessInlineDelegate _processInlinesBegin;
|
||||
private readonly ProcessInlineDelegate _processInlinesEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
|
||||
@@ -30,6 +32,8 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
public AutoIdentifierExtension(AutoIdentifierOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_processInlinesBegin = DocumentOnProcessInlinesBegin;
|
||||
_processInlinesEnd = HeadingBlock_ProcessInlinesEnd;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
@@ -85,19 +89,19 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
{
|
||||
dictionary = new Dictionary<string, HeadingLinkReferenceDefinition>();
|
||||
doc.SetData(this, dictionary);
|
||||
doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
|
||||
doc.ProcessInlinesBegin += _processInlinesBegin;
|
||||
}
|
||||
dictionary[text] = linkRef;
|
||||
}
|
||||
|
||||
// Then we register after inline have been processed to actually generate the proper #id
|
||||
headingBlock.ProcessInlinesEnd += HeadingBlock_ProcessInlinesEnd;
|
||||
headingBlock.ProcessInlinesEnd += _processInlinesEnd;
|
||||
}
|
||||
|
||||
private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline? inline)
|
||||
{
|
||||
var doc = processor.Document;
|
||||
doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin;
|
||||
doc.ProcessInlinesBegin -= _processInlinesBegin;
|
||||
var dictionary = (Dictionary<string, HeadingLinkReferenceDefinition>)doc.GetData(this)!;
|
||||
foreach (var keyPair in dictionary)
|
||||
{
|
||||
@@ -117,7 +121,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
/// Callback when there is a reference to found to a heading.
|
||||
/// Note that reference are only working if they are declared after.
|
||||
/// </summary>
|
||||
private Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child)
|
||||
private static Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child)
|
||||
{
|
||||
var headingRef = (HeadingLinkReferenceDefinition) linkRef;
|
||||
return new LinkInline()
|
||||
|
||||
@@ -22,4 +22,9 @@ public class AutoLinkOptions
|
||||
/// Should a www link be prefixed with https:// instead of http:// (false by default)
|
||||
/// </summary>
|
||||
public bool UseHttpsForWWWLinks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should auto-linking allow a domain with no period, e.g. https://localhost (false by default)
|
||||
/// </summary>
|
||||
public bool AllowDomainWithoutPeriod { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Markdig.Extensions.AutoLinks;
|
||||
|
||||
@@ -31,186 +33,171 @@ public class AutoLinkParser : InlineParser
|
||||
'w', // for www.
|
||||
];
|
||||
|
||||
_listOfCharCache = new ListOfCharCache();
|
||||
_validPreviousCharacters = SearchValues.Create(options.ValidPreviousCharacters);
|
||||
}
|
||||
|
||||
public readonly AutoLinkOptions Options;
|
||||
|
||||
private readonly ListOfCharCache _listOfCharCache;
|
||||
private readonly SearchValues<char> _validPreviousCharacters;
|
||||
|
||||
// This is a particularly expensive parser as it gets called for many common letters.
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Previous char must be a whitespace or a punctuation
|
||||
var previousChar = slice.PeekCharExtra(-1);
|
||||
if (!previousChar.IsWhiteSpaceOrZero() && Options.ValidPreviousCharacters.IndexOf(previousChar) == -1)
|
||||
if (!previousChar.IsWhiteSpaceOrZero() && !_validPreviousCharacters.Contains(previousChar))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> span = slice.AsSpan();
|
||||
|
||||
Debug.Assert(span[0] is 'h' or 'f' or 'm' or 't' or 'w');
|
||||
|
||||
// Precheck URL
|
||||
bool mayBeValid = span.Length >= 4 && span[0] switch
|
||||
{
|
||||
'h' => span.StartsWith("https://", StringComparison.Ordinal) || span.StartsWith("http://", StringComparison.Ordinal),
|
||||
'w' => span.StartsWith("www.", StringComparison.Ordinal), // We won't match http:/www. or /www.xxx
|
||||
'f' => span.StartsWith("ftp://", StringComparison.Ordinal),
|
||||
'm' => span.StartsWith("mailto:", StringComparison.Ordinal),
|
||||
_ => span.StartsWith("tel:", StringComparison.Ordinal),
|
||||
};
|
||||
|
||||
return mayBeValid && MatchCore(processor, ref slice);
|
||||
}
|
||||
|
||||
private bool MatchCore(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
char c = slice.CurrentChar;
|
||||
var startPosition = slice.Start;
|
||||
|
||||
// We don't bother disposing the builder as it'll realistically never grow beyond the initial stack size.
|
||||
var pendingEmphasis = new ValueStringBuilder(stackalloc char[32]);
|
||||
|
||||
// Check that an autolink is possible in the current context
|
||||
if (!IsAutoLinkValidInCurrentContext(processor, ref pendingEmphasis))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse URL
|
||||
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
|
||||
if (pendingEmphasis.Length > 0)
|
||||
{
|
||||
for (int i = link.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (pendingEmphasis.AsSpan().Contains(link[i]))
|
||||
{
|
||||
slice.Start--;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i < link.Length - 1)
|
||||
{
|
||||
link = link.Substring(0, i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int domainOffset = 0;
|
||||
|
||||
var c = slice.CurrentChar;
|
||||
// Precheck URL
|
||||
// Post-check URL
|
||||
switch (c)
|
||||
{
|
||||
case 'h':
|
||||
if (slice.MatchLowercase("ttp://", 1))
|
||||
if (string.Equals(link, "http://", StringComparison.Ordinal) ||
|
||||
string.Equals(link, "https://", StringComparison.Ordinal))
|
||||
{
|
||||
domainOffset = 7; // http://
|
||||
return false;
|
||||
}
|
||||
else if (slice.MatchLowercase("ttps://", 1))
|
||||
{
|
||||
domainOffset = 8; // https://
|
||||
}
|
||||
else return false;
|
||||
domainOffset = link[4] == 's' ? 8 : 7; // https:// or http://
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
domainOffset = 4; // www.
|
||||
break;
|
||||
|
||||
case 'f':
|
||||
if (!slice.MatchLowercase("tp://", 1))
|
||||
if (string.Equals(link, "ftp://", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
domainOffset = 6; // ftp://
|
||||
break;
|
||||
case 'm':
|
||||
if (!slice.MatchLowercase("ailto:", 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 't':
|
||||
if (!slice.MatchLowercase("el:", 1))
|
||||
if (string.Equals(link, "tel", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
domainOffset = 4;
|
||||
break;
|
||||
case 'w':
|
||||
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
|
||||
|
||||
case 'm':
|
||||
int atIndex = link.IndexOf('@');
|
||||
if (atIndex == -1 ||
|
||||
atIndex == 7) // mailto:@ - no email part
|
||||
{
|
||||
return false;
|
||||
}
|
||||
domainOffset = 4; // www.
|
||||
domainOffset = atIndex + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
List<char> pendingEmphasis = _listOfCharCache.Get();
|
||||
try
|
||||
// Do not need to check if a telephone number is a valid domain
|
||||
if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset, Options.AllowDomainWithoutPeriod))
|
||||
{
|
||||
// Check that an autolink is possible in the current context
|
||||
if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse URL
|
||||
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
|
||||
if (pendingEmphasis.Count > 0)
|
||||
{
|
||||
for (int i = link.Length - 1; i >= 0; i--)
|
||||
{
|
||||
if (pendingEmphasis.Contains(link[i]))
|
||||
{
|
||||
slice.Start--;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (i < link.Length - 1)
|
||||
{
|
||||
link = link.Substring(0, i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-check URL
|
||||
switch (c)
|
||||
{
|
||||
case 'h':
|
||||
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 't':
|
||||
if (string.Equals(link, "tel", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
int atIndex = link.IndexOf('@');
|
||||
if (atIndex == -1 ||
|
||||
atIndex == 7) // mailto:@ - no email part
|
||||
{
|
||||
return false;
|
||||
}
|
||||
domainOffset = atIndex + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Do not need to check if a telephone number is a valid domain
|
||||
if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var inline = new LinkInline()
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
|
||||
},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
|
||||
IsClosed = true,
|
||||
IsAutoLink = true,
|
||||
};
|
||||
|
||||
var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content
|
||||
skipFromBeginning = c == 't' ? 4 : skipFromBeginning; // See above but for tel:
|
||||
|
||||
inline.Span.End = inline.Span.Start + link.Length - 1;
|
||||
inline.UrlSpan = inline.Span;
|
||||
inline.AppendChild(new LiteralInline()
|
||||
{
|
||||
Span = inline.Span,
|
||||
Line = line,
|
||||
Column = column,
|
||||
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
|
||||
IsClosed = true
|
||||
});
|
||||
processor.Inline = inline;
|
||||
|
||||
if (Options.OpenInNewWindow)
|
||||
{
|
||||
inline.GetAttributes().AddPropertyIfNotExist("target", "_blank");
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
|
||||
var inline = new LinkInline()
|
||||
{
|
||||
_listOfCharCache.Release(pendingEmphasis);
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
|
||||
},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
|
||||
IsClosed = true,
|
||||
IsAutoLink = true,
|
||||
};
|
||||
|
||||
int skipFromBeginning = c switch
|
||||
{
|
||||
'm' => 7, // For mailto: skip "mailto:" for content
|
||||
't' => 4, // Same but for tel:
|
||||
_ => 0
|
||||
};
|
||||
|
||||
inline.Span.End = inline.Span.Start + link.Length - 1;
|
||||
inline.UrlSpan = inline.Span;
|
||||
inline.AppendChild(new LiteralInline()
|
||||
{
|
||||
Span = inline.Span,
|
||||
Line = line,
|
||||
Column = column,
|
||||
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
|
||||
IsClosed = true
|
||||
});
|
||||
processor.Inline = inline;
|
||||
|
||||
if (Options.OpenInNewWindow)
|
||||
{
|
||||
inline.GetAttributes().AddPropertyIfNotExist("target", "_blank");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List<char> pendingEmphasis)
|
||||
private static bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, ref ValueStringBuilder pendingEmphasis)
|
||||
{
|
||||
// Case where there is a pending HtmlInline <a>
|
||||
var currentInline = processor.Inline;
|
||||
@@ -257,9 +244,9 @@ public class AutoLinkParser : InlineParser
|
||||
// Record all pending characters for emphasis
|
||||
if (currentInline is EmphasisDelimiterInline emphasisDelimiter)
|
||||
{
|
||||
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
|
||||
if (!pendingEmphasis.AsSpan().Contains(emphasisDelimiter.DelimiterChar))
|
||||
{
|
||||
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
|
||||
pendingEmphasis.Append(emphasisDelimiter.DelimiterChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,12 +255,4 @@ public class AutoLinkParser : InlineParser
|
||||
|
||||
return countBrackets <= 0;
|
||||
}
|
||||
|
||||
private sealed class ListOfCharCache : DefaultObjectCache<List<char>>
|
||||
{
|
||||
protected override void Reset(List<char> instance)
|
||||
{
|
||||
instance.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -124,7 +124,7 @@ public class GenericAttributesParser : InlineParser
|
||||
var start = line.Start;
|
||||
// Get all non-whitespace characters following a #
|
||||
// But stop if we found a } or \0
|
||||
while (c != '}' && c != '\0' && !c.IsWhitespace())
|
||||
while (c != '}' && !c.IsWhiteSpaceOrZero())
|
||||
{
|
||||
c = line.NextChar();
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class GlobalizationExtension : IMarkdownExtension
|
||||
}
|
||||
|
||||
int rune = c;
|
||||
if (CharHelper.IsHighSurrogate(c) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1]))
|
||||
if (char.IsHighSurrogate(c) && i < slice.End && char.IsLowSurrogate(slice[i + 1]))
|
||||
{
|
||||
Debug.Assert(char.IsSurrogatePair(c, slice[i + 1]));
|
||||
rune = char.ConvertToUtf32(c, slice[i + 1]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -55,15 +55,15 @@ public class HostProviderBuilder
|
||||
return new DelegateProvider(hostPrefix, handler, allowFullScreen, iframeClass);
|
||||
}
|
||||
|
||||
internal static Dictionary<string, IHostProvider> KnownHosts { get; }
|
||||
= new Dictionary<string, IHostProvider>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"),
|
||||
["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
|
||||
["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
|
||||
["Yandex"] = Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"),
|
||||
["Odnoklassniki"] = Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"),
|
||||
};
|
||||
internal static readonly IHostProvider[] KnownHosts =
|
||||
[
|
||||
Create("www.youtube.com", YouTubeShort, iframeClass: "youtubeshort"),
|
||||
Create("www.youtube.com", YouTube, iframeClass: "youtube"),
|
||||
Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
|
||||
Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
|
||||
Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"),
|
||||
Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"),
|
||||
];
|
||||
|
||||
#region Known providers
|
||||
|
||||
@@ -92,6 +92,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(
|
||||
|
||||
@@ -81,7 +81,7 @@ public class MediaOptions
|
||||
{".au", "audio/basic"},
|
||||
{".wav", "audio/x-wav"},
|
||||
};
|
||||
Hosts = new List<IHostProvider>(HostProviderBuilder.KnownHosts.Values);
|
||||
Hosts = new List<IHostProvider>(HostProviderBuilder.KnownHosts);
|
||||
}
|
||||
|
||||
public string Width { get; set; }
|
||||
|
||||
@@ -43,7 +43,7 @@ public class GridTableParser : BlockParser
|
||||
}
|
||||
|
||||
// Parse a column alignment
|
||||
if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign))
|
||||
if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign, out _))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -33,4 +33,11 @@ public class PipeTableOptions
|
||||
/// in all other rows (default behavior).
|
||||
/// </summary>
|
||||
public bool UseHeaderForColumnCount { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether column widths should be inferred based on the number of dashes
|
||||
/// in the header separator row. Each column's width will be proportional to the dash count in its respective column.
|
||||
/// </summary>
|
||||
public bool InferColumnWidthsFromSeparator { get; set; }
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -467,9 +481,10 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align)
|
||||
private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align, out int delimiterCount)
|
||||
{
|
||||
align = 0;
|
||||
delimiterCount = 0;
|
||||
var literal = inline as LiteralInline;
|
||||
if (literal is null)
|
||||
{
|
||||
@@ -478,7 +493,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
|
||||
// Work on a copy of the slice
|
||||
var line = literal.Content;
|
||||
if (TableHelper.ParseColumnHeader(ref line, '-', out align))
|
||||
if (TableHelper.ParseColumnHeader(ref line, '-', out align, out delimiterCount))
|
||||
{
|
||||
if (line.CurrentChar != '\0')
|
||||
{
|
||||
@@ -493,7 +508,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
private List<TableColumnDefinition>? FindHeaderRow(List<Inline> delimiters)
|
||||
{
|
||||
bool isValidRow = false;
|
||||
List<TableColumnDefinition>? aligns = null;
|
||||
int totalDelimiterCount = 0;
|
||||
List<TableColumnDefinition>? columnDefinitions = null;
|
||||
for (int i = 0; i < delimiters.Count; i++)
|
||||
{
|
||||
if (!IsLine(delimiters[i]))
|
||||
@@ -515,18 +531,19 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
|
||||
// Check the left side of a `|` delimiter
|
||||
TableColumnAlign? align = null;
|
||||
int delimiterCount = 0;
|
||||
if (delimiter.PreviousSibling != null &&
|
||||
!(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) && // ignore parsed whitespace
|
||||
!ParseHeaderString(delimiter.PreviousSibling, out align))
|
||||
!ParseHeaderString(delimiter.PreviousSibling, out align, out delimiterCount))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Create aligns until we may have a header row
|
||||
|
||||
aligns ??= new List<TableColumnDefinition>();
|
||||
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
columnDefinitions ??= new List<TableColumnDefinition>();
|
||||
totalDelimiterCount += delimiterCount;
|
||||
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
|
||||
|
||||
// If this is the last delimiter, we need to check the right side of the `|` delimiter
|
||||
if (nextDelimiter is null)
|
||||
@@ -542,13 +559,13 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ParseHeaderString(nextSibling, out align))
|
||||
if (!ParseHeaderString(nextSibling, out align, out delimiterCount))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
totalDelimiterCount += delimiterCount;
|
||||
isValidRow = true;
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -562,7 +579,27 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
break;
|
||||
}
|
||||
|
||||
return isValidRow ? aligns : null;
|
||||
// calculate the width of the columns in percent based on the delimiter count
|
||||
if (!isValidRow || columnDefinitions == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Options.InferColumnWidthsFromSeparator)
|
||||
{
|
||||
foreach (var columnDefinition in columnDefinitions)
|
||||
{
|
||||
columnDefinition.Width = (columnDefinition.Width * 100) / totalDelimiterCount;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var columnDefinition in columnDefinitions)
|
||||
{
|
||||
columnDefinition.Width = 0;
|
||||
}
|
||||
}
|
||||
return columnDefinitions;
|
||||
}
|
||||
|
||||
private static bool IsLine(Inline inline)
|
||||
|
||||
@@ -17,12 +17,13 @@ public static class TableHelper
|
||||
/// <param name="slice">The text slice.</param>
|
||||
/// <param name="delimiterChar">The delimiter character (either `-` or `=`).</param>
|
||||
/// <param name="align">The alignment of the column.</param>
|
||||
/// <param name="delimiterCount">The number of delimiters.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful
|
||||
/// </returns>
|
||||
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align)
|
||||
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align, out int delimiterCount)
|
||||
{
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out delimiterCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -37,7 +38,7 @@ public static class TableHelper
|
||||
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align)
|
||||
{
|
||||
delimiterChar = '\0';
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,10 +50,10 @@ public static class TableHelper
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successful
|
||||
/// </returns>
|
||||
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align)
|
||||
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align, out int delimiterCount)
|
||||
{
|
||||
align = null;
|
||||
|
||||
delimiterCount = 0;
|
||||
slice.TrimStart();
|
||||
var c = slice.CurrentChar;
|
||||
bool hasLeft = false;
|
||||
@@ -80,7 +81,8 @@ public static class TableHelper
|
||||
}
|
||||
|
||||
// We expect at least one `-` delimiter char
|
||||
if (slice.CountAndSkipChar(delimiterChar) == 0)
|
||||
delimiterCount = slice.CountAndSkipChar(delimiterChar);
|
||||
if (delimiterCount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public class YamlFrontMatterParser : BlockParser
|
||||
|
||||
// If three dashes (optionally followed by whitespace)
|
||||
// this is a YAML front matter block
|
||||
if (count == 3 && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
|
||||
if (count == 3 && c.IsWhiteSpaceOrZero() && line.TrimEnd())
|
||||
{
|
||||
bool hasFullYamlFrontMatter = false;
|
||||
// We make sure that there is a closing frontmatter somewhere in the document
|
||||
@@ -146,7 +146,7 @@ public class YamlFrontMatterParser : BlockParser
|
||||
|
||||
// If we have a closing fence, close it and discard the current line
|
||||
// The line must contain only fence characters and optional following whitespace.
|
||||
if (count == 3 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
|
||||
if (count == 3 && !processor.IsCodeIndent && c.IsWhiteSpaceOrZero() && line.TrimEnd())
|
||||
{
|
||||
block.UpdateSpanEnd(line.Start - 1);
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
global using System;
|
||||
global using System.Collections.Frozen;
|
||||
global using System.Collections.Generic;
|
||||
@@ -2,6 +2,7 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -19,16 +20,53 @@ public static class CharHelper
|
||||
|
||||
public const string ReplacementCharString = "\uFFFD";
|
||||
|
||||
private const char HighSurrogateStart = '\ud800';
|
||||
private const char HighSurrogateEnd = '\udbff';
|
||||
private const char LowSurrogateStart = '\udc00';
|
||||
private const char LowSurrogateEnd = '\udfff';
|
||||
private const string EmailUsernameSpecialChars = ".!#$%&'*+/=?^_`{|}~-+.~";
|
||||
|
||||
// We don't support LCDM
|
||||
private static readonly Dictionary<char, int> romanMap = new Dictionary<char, int>(6) {
|
||||
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 },
|
||||
{ 'I', 1 }, { 'V', 5 }, { 'X', 10 }
|
||||
};
|
||||
// 2.1 Characters and lines
|
||||
// A Unicode whitespace character is any code point in the Unicode Zs general category,
|
||||
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
|
||||
// CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
|
||||
private const string AsciiWhitespaceChars = "\t\n\f\r ";
|
||||
internal const string WhitespaceChars = AsciiWhitespaceChars + "\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000";
|
||||
|
||||
// 2.1 Characters and lines
|
||||
// An ASCII punctuation character is
|
||||
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F),
|
||||
// :, ;, <, =, >, ?, @ (U+003A–0040),
|
||||
// [, \, ], ^, _, ` (U+005B–0060),
|
||||
// {, |, }, or ~ (U+007B–007E).
|
||||
private const string AsciiPunctuationChars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
||||
|
||||
// Unicode P (punctuation) categories.
|
||||
private const int UnicodePunctuationCategoryMask =
|
||||
1 << (int)UnicodeCategory.ConnectorPunctuation |
|
||||
1 << (int)UnicodeCategory.DashPunctuation |
|
||||
1 << (int)UnicodeCategory.OpenPunctuation |
|
||||
1 << (int)UnicodeCategory.ClosePunctuation |
|
||||
1 << (int)UnicodeCategory.InitialQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.FinalQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.OtherPunctuation;
|
||||
|
||||
private const int UnicodePunctuationOrSpaceCategoryMask =
|
||||
UnicodePunctuationCategoryMask |
|
||||
1 << (int)UnicodeCategory.SpaceSeparator;
|
||||
|
||||
// 2.1 Characters and lines
|
||||
// A Unicode punctuation character is a character in the Unicode P (punctuation) or S (symbol) general categories.
|
||||
private const int CommonMarkPunctuationCategoryMask =
|
||||
UnicodePunctuationCategoryMask |
|
||||
1 << (int)UnicodeCategory.MathSymbol |
|
||||
1 << (int)UnicodeCategory.CurrencySymbol |
|
||||
1 << (int)UnicodeCategory.ModifierSymbol |
|
||||
1 << (int)UnicodeCategory.OtherSymbol;
|
||||
|
||||
// We're not currently using these SearchValues instances for vectorized IndexOfAny-like searches, but for their efficient single Contains(char) checks.
|
||||
private static readonly SearchValues<char> s_emailUsernameSpecialChar = SearchValues.Create(EmailUsernameSpecialChars);
|
||||
private static readonly SearchValues<char> s_emailUsernameSpecialCharOrDigit = SearchValues.Create(EmailUsernameSpecialChars + "0123456789");
|
||||
private static readonly SearchValues<char> s_asciiPunctuationChars = SearchValues.Create(AsciiPunctuationChars);
|
||||
private static readonly SearchValues<char> s_asciiPunctuationCharsOrZero = SearchValues.Create(AsciiPunctuationChars + '\0');
|
||||
private static readonly SearchValues<char> s_asciiPunctuationOrWhitespaceCharsOrZero = SearchValues.Create(AsciiPunctuationChars + AsciiWhitespaceChars + '\0');
|
||||
private static readonly SearchValues<char> s_escapableSymbolChars = SearchValues.Create("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~•");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsPunctuationException(char c) =>
|
||||
@@ -101,8 +139,8 @@ public static class CharHelper
|
||||
int result = 0;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
var candidate = romanMap[text[i]];
|
||||
if ((uint)(i + 1) < text.Length && candidate < romanMap[text[i + 1]])
|
||||
int candidate = RomanToArabic(text[i]);
|
||||
if ((uint)(i + 1) < text.Length && candidate < RomanToArabic(text[i + 1]))
|
||||
{
|
||||
result -= candidate;
|
||||
}
|
||||
@@ -112,6 +150,20 @@ public static class CharHelper
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
// We don't support LCDM
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static int RomanToArabic(char c)
|
||||
{
|
||||
Debug.Assert(IsRomanLetterPartial(c));
|
||||
|
||||
return (c | 0x20) switch
|
||||
{
|
||||
'i' => 1,
|
||||
'v' => 5,
|
||||
_ => 10
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -134,33 +186,47 @@ public static class CharHelper
|
||||
// 2.1 Characters and lines
|
||||
// A Unicode whitespace character is any code point in the Unicode Zs general category,
|
||||
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
|
||||
if (c <= ' ')
|
||||
if (c < '\u00A0')
|
||||
{
|
||||
const long Mask =
|
||||
(1L << ' ') |
|
||||
(1L << '\t') |
|
||||
(1L << '\n') |
|
||||
(1L << '\f') |
|
||||
(1L << '\r');
|
||||
|
||||
return (Mask & (1L << c)) != 0;
|
||||
// Matches any of "\t\n\f\r ". See comments in HexConverter.IsHexChar for how these checks work:
|
||||
// https://github.com/dotnet/runtime/blob/a2e1d21bb4faf914363968b812c990329ba92d8e/src/libraries/Common/src/System/HexConverter.cs#L392-L415
|
||||
// https://gist.github.com/MihaZupan/b93ba180c2b5fbaaed993db2ade76b49
|
||||
ulong shift = 30399299632234496UL << c;
|
||||
ulong mask = (ulong)c - 64;
|
||||
return (long)(shift & mask) < 0;
|
||||
}
|
||||
|
||||
return c >= '\u00A0' && IsWhitespaceRare(c);
|
||||
return IsWhitespaceRare(c);
|
||||
}
|
||||
|
||||
static bool IsWhitespaceRare(char c)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsWhiteSpaceOrZero(this char c)
|
||||
{
|
||||
if (c < '\u00A0')
|
||||
{
|
||||
// return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
|
||||
// Matches any of "\0\t\n\f\r ".
|
||||
ulong shift = 9253771336487010304UL << c;
|
||||
ulong mask = (ulong)c - 64;
|
||||
return (long)(shift & mask) < 0;
|
||||
}
|
||||
|
||||
if (c < 5760)
|
||||
{
|
||||
return c == '\u00A0';
|
||||
}
|
||||
else
|
||||
{
|
||||
return c <= 12288 &&
|
||||
(c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288);
|
||||
}
|
||||
return IsWhitespaceRare(c);
|
||||
}
|
||||
|
||||
private static bool IsWhitespaceRare(char c)
|
||||
{
|
||||
Debug.Assert(c >= '\u00A0');
|
||||
|
||||
// return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
|
||||
|
||||
if (c < 5760)
|
||||
{
|
||||
return c == '\u00A0';
|
||||
}
|
||||
else
|
||||
{
|
||||
return c <= 12288 &&
|
||||
(c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +240,7 @@ public static class CharHelper
|
||||
public static bool IsEscapableSymbol(this char c)
|
||||
{
|
||||
// char.IsSymbol also works with Unicode symbols that cannot be escaped based on the specification.
|
||||
return (c > ' ' && c < '0') || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z' && c < 127) || c == '•';
|
||||
}
|
||||
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsWhiteSpaceOrZero(this char c)
|
||||
{
|
||||
return IsZero(c) || IsWhitespace(c);
|
||||
return s_escapableSymbolChars.Contains(c);
|
||||
}
|
||||
|
||||
// Check if a char is a space or a punctuation
|
||||
@@ -194,52 +254,46 @@ public static class CharHelper
|
||||
else if (c <= 127)
|
||||
{
|
||||
space = c == '\0';
|
||||
punctuation = c == '\0' || IsAsciiPunctuation(c);
|
||||
punctuation = IsAsciiPunctuationOrZero(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
// A Unicode punctuation character is an ASCII punctuation character
|
||||
// or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps.
|
||||
const int PunctuationCategoryMask =
|
||||
1 << (int)UnicodeCategory.ConnectorPunctuation |
|
||||
1 << (int)UnicodeCategory.DashPunctuation |
|
||||
1 << (int)UnicodeCategory.OpenPunctuation |
|
||||
1 << (int)UnicodeCategory.ClosePunctuation |
|
||||
1 << (int)UnicodeCategory.InitialQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.FinalQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.OtherPunctuation;
|
||||
|
||||
space = false;
|
||||
punctuation = (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
punctuation = (CommonMarkPunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Same as CheckUnicodeCategory
|
||||
internal static bool IsSpaceOrPunctuation(this char c)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsSpaceOrPunctuationForGFMAutoLink(char c)
|
||||
{
|
||||
if (IsWhitespace(c))
|
||||
// Github Flavored Markdown's allowed set of domain characters differs from CommonMark's "punctuation" definition.
|
||||
// CommonMark also counts symbols as punctuation, but GitHub will render e.g. http://☃.net as an autolink, despite
|
||||
// the snowman emoji falling under the OtherSymbol (So) category.
|
||||
if (c <= 127)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (c <= 127)
|
||||
{
|
||||
return c == '\0' || IsAsciiPunctuation(c);
|
||||
return s_asciiPunctuationOrWhitespaceCharsOrZero.Contains(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int PunctuationCategoryMask =
|
||||
1 << (int)UnicodeCategory.ConnectorPunctuation |
|
||||
1 << (int)UnicodeCategory.DashPunctuation |
|
||||
1 << (int)UnicodeCategory.OpenPunctuation |
|
||||
1 << (int)UnicodeCategory.ClosePunctuation |
|
||||
1 << (int)UnicodeCategory.InitialQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.FinalQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.OtherPunctuation;
|
||||
return NonAscii(c);
|
||||
|
||||
return (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
static bool NonAscii(char c) =>
|
||||
(UnicodePunctuationOrSpaceCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
|
||||
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
|
||||
// zero or more characters other than ASCII control characters, space, <, and >.
|
||||
//
|
||||
// 2.1 Characters and lines
|
||||
// An ASCII control character is a character between U+0000–1F (both including) or U+007F.
|
||||
internal static readonly SearchValues<char> InvalidAutoLinkCharacters = SearchValues.Create(
|
||||
// 0 is excluded because it can be slightly more expensive for SearchValues to handle, and we've already removed it from the input text.
|
||||
"\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F" +
|
||||
"\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" +
|
||||
" <>\u007F");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsNewLineOrLineFeed(this char c)
|
||||
{
|
||||
@@ -279,7 +333,7 @@ public static class CharHelper
|
||||
{
|
||||
// 2.3 Insecure characters
|
||||
// For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD).
|
||||
return c == '\0' ? '\ufffd' : c;
|
||||
return c == '\0' ? ReplacementChar : c;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -306,46 +360,33 @@ public static class CharHelper
|
||||
return (uint)(c - '0') <= ('9' - '0');
|
||||
}
|
||||
|
||||
public static bool IsAsciiPunctuation(this char c)
|
||||
{
|
||||
// 2.1 Characters and lines
|
||||
// An ASCII punctuation character is
|
||||
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+0021–2F),
|
||||
// :, ;, <, =, >, ?, @ (U+003A–0040),
|
||||
// [, \, ], ^, _, ` (U+005B–0060),
|
||||
// {, |, }, or ~ (U+007B–007E).
|
||||
return c <= 127 && (
|
||||
IsInInclusiveRange(c, 33, 47) ||
|
||||
IsInInclusiveRange(c, 58, 64) ||
|
||||
IsInInclusiveRange(c, 91, 96) ||
|
||||
IsInInclusiveRange(c, 123, 126));
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsAsciiPunctuationOrZero(this char c) =>
|
||||
s_asciiPunctuationCharsOrZero.Contains(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsEmailUsernameSpecialChar(char c)
|
||||
{
|
||||
return ".!#$%&'*+/=?^_`{|}~-+.~".IndexOf(c) >= 0;
|
||||
}
|
||||
public static bool IsAsciiPunctuation(this char c) =>
|
||||
s_asciiPunctuationChars.Contains(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsHighSurrogate(char c)
|
||||
{
|
||||
return IsInInclusiveRange(c, HighSurrogateStart, HighSurrogateEnd);
|
||||
}
|
||||
public static bool IsEmailUsernameSpecialChar(char c) =>
|
||||
s_emailUsernameSpecialChar.Contains(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsLowSurrogate(char c)
|
||||
{
|
||||
return IsInInclusiveRange(c, LowSurrogateStart, LowSurrogateEnd);
|
||||
}
|
||||
internal static bool IsEmailUsernameSpecialCharOrDigit(char c) =>
|
||||
s_emailUsernameSpecialCharOrDigit.Contains(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsInInclusiveRange(char c, char min, char max)
|
||||
=> (uint)(c - min) <= (uint)(max - min);
|
||||
public static bool IsHighSurrogate(char c) =>
|
||||
char.IsHighSurrogate(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsInInclusiveRange(int value, uint min, uint max)
|
||||
=> ((uint)value - min) <= (max - min);
|
||||
public static bool IsLowSurrogate(char c) =>
|
||||
char.IsLowSurrogate(c);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool IsInInclusiveRange(int value, uint min, uint max) =>
|
||||
((uint)value - min) <= (max - min);
|
||||
|
||||
public static bool IsRightToLeft(int c)
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ public static class CharNormalizer
|
||||
}
|
||||
|
||||
// This table was generated by the app UnicodeNormDApp
|
||||
private static readonly Dictionary<char, string> CodeToAscii = new(1269)
|
||||
private static readonly FrozenDictionary<char, string> CodeToAscii = new Dictionary<char, string>(1269)
|
||||
{
|
||||
{'Ḋ', "D"},
|
||||
{'Ḍ', "D"},
|
||||
@@ -1291,5 +1291,5 @@ public static class CharNormalizer
|
||||
{'|', "|"},
|
||||
{'}', "}"},
|
||||
{'~', "~"},
|
||||
};
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Markdig.Helpers;
|
||||
@@ -17,7 +16,7 @@ public sealed class CharacterMap<T> where T : class
|
||||
{
|
||||
private readonly SearchValues<char> _values;
|
||||
private readonly T[] _asciiMap;
|
||||
private readonly Dictionary<uint, T>? _nonAsciiMap;
|
||||
private readonly FrozenDictionary<uint, T>? _nonAsciiMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterMap{T}"/> class.
|
||||
@@ -39,6 +38,7 @@ public sealed class CharacterMap<T> where T : class
|
||||
Array.Sort(OpeningCharacters);
|
||||
|
||||
_asciiMap = new T[128];
|
||||
Dictionary<uint, T>? nonAsciiMap = null;
|
||||
|
||||
foreach (var state in maps)
|
||||
{
|
||||
@@ -49,16 +49,21 @@ public sealed class CharacterMap<T> where T : class
|
||||
}
|
||||
else
|
||||
{
|
||||
_nonAsciiMap ??= new Dictionary<uint, T>();
|
||||
nonAsciiMap ??= [];
|
||||
|
||||
if (!_nonAsciiMap.ContainsKey(openingChar))
|
||||
if (!nonAsciiMap.ContainsKey(openingChar))
|
||||
{
|
||||
_nonAsciiMap[openingChar] = state.Value;
|
||||
nonAsciiMap[openingChar] = state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_values = SearchValues.Create(OpeningCharacters);
|
||||
|
||||
if (nonAsciiMap is not null)
|
||||
{
|
||||
_nonAsciiMap = nonAsciiMap.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
@@ -393,40 +394,52 @@ public static class HtmlHelper
|
||||
|
||||
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
{
|
||||
// https://spec.commonmark.org/0.31.2/#raw-html
|
||||
// An HTML comment consists of <!-->, <!--->, or
|
||||
// <!--, a string of characters not including the string -->, and -->.
|
||||
|
||||
// The caller already checked <!-
|
||||
Debug.Assert(text.CurrentChar == '-' && text.PeekCharExtra(-1) == '!' && text.PeekCharExtra(-2) == '<');
|
||||
|
||||
var c = text.NextChar();
|
||||
if (c != '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
builder.Append('-');
|
||||
builder.Append('-');
|
||||
if (text.PeekChar() == '>')
|
||||
|
||||
c = text.NextChar();
|
||||
|
||||
if (c == '>')
|
||||
{
|
||||
// <!--> is considered valid.
|
||||
builder.Append("-->");
|
||||
text.SkipChar();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == '-' && text.PeekChar() == '>')
|
||||
{
|
||||
// <!---> is also considered valid.
|
||||
builder.Append("--->");
|
||||
text.SkipChar();
|
||||
text.SkipChar();
|
||||
return true;
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> slice = text.AsSpan();
|
||||
|
||||
const string EndOfComment = "-->";
|
||||
|
||||
int endOfComment = slice.IndexOf(EndOfComment, StringComparison.Ordinal);
|
||||
if (endOfComment < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var countHyphen = 0;
|
||||
while (true)
|
||||
{
|
||||
c = text.NextChar();
|
||||
if (c == '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (countHyphen == 2)
|
||||
{
|
||||
if (c == '>')
|
||||
{
|
||||
builder.Append('>');
|
||||
text.SkipChar();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
countHyphen = c == '-' ? countHyphen + 1 : 0;
|
||||
builder.Append(c);
|
||||
}
|
||||
builder.Append("--");
|
||||
builder.Append(slice.Slice(0, endOfComment + EndOfComment.Length));
|
||||
text.Start += endOfComment + EndOfComment.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Syntax;
|
||||
@@ -166,7 +168,7 @@ public static class LinkHelper
|
||||
if (!c.IsAlpha())
|
||||
{
|
||||
// We may have an email char?
|
||||
if (c.IsDigit() || CharHelper.IsEmailUsernameSpecialChar(c))
|
||||
if (CharHelper.IsEmailUsernameSpecialCharOrDigit(c))
|
||||
{
|
||||
state = -1;
|
||||
}
|
||||
@@ -286,40 +288,34 @@ public static class LinkHelper
|
||||
}
|
||||
else
|
||||
{
|
||||
// scan an uri
|
||||
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:)
|
||||
// followed by zero or more characters other than ASCII whitespace and control characters, <, and >.
|
||||
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
|
||||
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
|
||||
// zero or more characters other than ASCII control characters, space, <, and >.
|
||||
// If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space).
|
||||
//
|
||||
// 2.1 Characters and lines
|
||||
// An ASCII control character is a character between U+0000–1F (both including) or U+007F.
|
||||
|
||||
while (true)
|
||||
text.SkipChar();
|
||||
ReadOnlySpan<char> slice = text.AsSpan();
|
||||
|
||||
Debug.Assert(!slice.Contains('\0'));
|
||||
|
||||
// This set of invalid characters includes '>'.
|
||||
int end = slice.IndexOfAny(CharHelper.InvalidAutoLinkCharacters);
|
||||
|
||||
if ((uint)end < (uint)slice.Length && slice[end] == '>')
|
||||
{
|
||||
c = text.NextChar();
|
||||
if (c == '\0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == '>')
|
||||
{
|
||||
text.SkipChar();
|
||||
link = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Chars valid for both scheme and email
|
||||
if (c <= 127)
|
||||
{
|
||||
if (c > ' ' && c != '>')
|
||||
{
|
||||
builder.Append(c);
|
||||
}
|
||||
else break;
|
||||
}
|
||||
else if (!c.IsSpaceOrPunctuation())
|
||||
{
|
||||
builder.Append(c);
|
||||
}
|
||||
else break;
|
||||
// We've found '>' and all characters before it are valid.
|
||||
#if NET
|
||||
link = string.Concat(builder.AsSpan(), slice.Slice(0, end));
|
||||
builder.Dispose();
|
||||
#else
|
||||
builder.Append(slice.Slice(0, end));
|
||||
link = builder.ToString();
|
||||
#endif
|
||||
text.Start += end + 1; // +1 to skip '>'
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,87 +541,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 +620,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 +720,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 +739,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -814,20 +777,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 +869,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 +888,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -961,20 +926,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))
|
||||
@@ -1036,7 +1002,7 @@ public static class LinkHelper
|
||||
return c == '\0' || c.IsSpaceOrTab() || c.IsControl() || (isAutoLink && c == '<'); // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
|
||||
}
|
||||
|
||||
public static bool IsValidDomain(string link, int prefixLength)
|
||||
public static bool IsValidDomain(string link, int prefixLength, bool allowDomainWithoutPeriod = false)
|
||||
{
|
||||
// https://github.github.com/gfm/#extended-www-autolink
|
||||
// A valid domain consists of alphanumeric characters, underscores (_), hyphens (-) and periods (.).
|
||||
@@ -1049,22 +1015,22 @@ public static class LinkHelper
|
||||
bool segmentHasCharacters = false;
|
||||
int lastUnderscoreSegment = -1;
|
||||
|
||||
for (int i = prefixLength; i < link.Length; i++)
|
||||
for (int i = prefixLength; (uint)i < (uint)link.Length; i++)
|
||||
{
|
||||
char c = link[i];
|
||||
|
||||
if (c == '.') // New segment
|
||||
{
|
||||
if (!segmentHasCharacters)
|
||||
return false;
|
||||
|
||||
segmentCount++;
|
||||
segmentHasCharacters = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!c.IsAlphaNumeric())
|
||||
{
|
||||
if (c == '.') // New segment
|
||||
{
|
||||
if (!segmentHasCharacters)
|
||||
return false;
|
||||
|
||||
segmentCount++;
|
||||
segmentHasCharacters = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain name
|
||||
break;
|
||||
|
||||
@@ -1072,7 +1038,7 @@ public static class LinkHelper
|
||||
{
|
||||
lastUnderscoreSegment = segmentCount;
|
||||
}
|
||||
else if (c != '-' && c.IsSpaceOrPunctuation())
|
||||
else if (c != '-' && CharHelper.IsSpaceOrPunctuationForGFMAutoLink(c))
|
||||
{
|
||||
// An invalid character has been found
|
||||
return false;
|
||||
@@ -1082,7 +1048,7 @@ public static class LinkHelper
|
||||
segmentHasCharacters = true;
|
||||
}
|
||||
|
||||
return segmentCount != 1 && // At least one dot was present
|
||||
return (segmentCount != 1 || allowDomainWithoutPeriod) && // At least one dot was present
|
||||
segmentHasCharacters && // Last segment has valid characters
|
||||
segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain
|
||||
}
|
||||
@@ -1159,7 +1125,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 +1265,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
|
||||
|
||||
@@ -231,7 +231,7 @@ public struct StringSlice : ICharIterator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Peeks a character at the specified offset from the current begining of the slice
|
||||
/// Peeks a character at the specified offset from the current beginning of the slice
|
||||
/// without using the range <see cref="Start"/> or <see cref="End"/>, returns `\0` if outside the <see cref="Text"/>.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset.</param>
|
||||
@@ -291,7 +291,7 @@ public struct StringSlice : ICharIterator
|
||||
var c = Text[i];
|
||||
if (c.IsWhitespace())
|
||||
{
|
||||
if (c == '\0' || c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n'))
|
||||
if (c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
<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>
|
||||
<PackageReleaseNotes>https://github.com/xoofx/markdig/blob/master/changelog.md</PackageReleaseNotes>
|
||||
<PackageLicenseExpression>BSD-2-Clause</PackageLicenseExpression>
|
||||
<PackageReadmeFile>readme.md</PackageReadmeFile>
|
||||
<PackageIcon>markdig.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/lunet-io/markdig</PackageProjectUrl>
|
||||
<PackageProjectUrl>https://github.com/xoofx/markdig</PackageProjectUrl>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>12</LangVersion>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -24,8 +24,8 @@
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
using Markdig.Helpers;
|
||||
@@ -19,13 +19,13 @@ namespace Markdig;
|
||||
/// </summary>
|
||||
public static class Markdown
|
||||
{
|
||||
public static string Version =>
|
||||
s_version ??= typeof(Markdown).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
|
||||
|
||||
private static string? s_version;
|
||||
[field: MaybeNull]
|
||||
public static string Version => field ??= typeof(Markdown).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
|
||||
|
||||
internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build();
|
||||
private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build();
|
||||
|
||||
[field: MaybeNull]
|
||||
private static MarkdownPipeline DefaultTrackTriviaPipeline => field ??= new MarkdownPipelineBuilder().EnableTrackTrivia().Build();
|
||||
|
||||
private static MarkdownPipeline GetPipeline(MarkdownPipeline? pipeline, string markdown)
|
||||
{
|
||||
@@ -90,8 +90,8 @@ public static class Markdown
|
||||
/// <param name="markdown">A Markdown text.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <param name="context">A parser context used for the parsing.</param>
|
||||
/// <returns>The result of the conversion</returns>
|
||||
/// <exception cref="ArgumentNullException">if markdown variable is null</exception>
|
||||
/// <returns>The HTML string.</returns>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="markdown"/> is null.</exception>
|
||||
public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null)
|
||||
{
|
||||
if (markdown is null) ThrowHelper.ArgumentNullException_markdown();
|
||||
@@ -108,8 +108,8 @@ public static class Markdown
|
||||
/// </summary>
|
||||
/// <param name="document">A Markdown document.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <returns>The result of the conversion</returns>
|
||||
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
|
||||
/// <returns>The HTML string.</returns>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="document"/> is null.</exception>
|
||||
public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pipeline = null)
|
||||
{
|
||||
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
|
||||
@@ -131,8 +131,8 @@ public static class Markdown
|
||||
/// <param name="document">A Markdown document.</param>
|
||||
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <returns>The result of the conversion</returns>
|
||||
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
|
||||
/// <returns>The HTML string.</returns>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="document"/> is null.</exception>
|
||||
public static void ToHtml(this MarkdownDocument document, TextWriter writer, MarkdownPipeline? pipeline = null)
|
||||
{
|
||||
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
|
||||
@@ -165,11 +165,7 @@ public static class Markdown
|
||||
|
||||
var document = MarkdownParser.Parse(markdown, pipeline, context);
|
||||
|
||||
using var rentedRenderer = pipeline.RentHtmlRenderer(writer);
|
||||
HtmlRenderer renderer = rentedRenderer.Instance;
|
||||
|
||||
renderer.Render(document);
|
||||
writer.Flush();
|
||||
ToHtml(document, writer, pipeline);
|
||||
|
||||
return document;
|
||||
}
|
||||
@@ -206,7 +202,7 @@ public static class Markdown
|
||||
{
|
||||
if (markdown is null) ThrowHelper.ArgumentNullException_markdown();
|
||||
|
||||
MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null;
|
||||
MarkdownPipeline? pipeline = trackTrivia ? DefaultTrackTriviaPipeline : null;
|
||||
|
||||
return Parse(markdown, pipeline);
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public sealed class MarkdownPipeline
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct RentedHtmlRenderer : IDisposable
|
||||
internal readonly ref struct RentedHtmlRenderer : IDisposable
|
||||
{
|
||||
private readonly HtmlRendererCache _cache;
|
||||
public readonly HtmlRenderer Instance;
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Markdig;
|
||||
/// <remarks>NOTE: A pipeline is not thread-safe.</remarks>
|
||||
public class MarkdownPipelineBuilder
|
||||
{
|
||||
private MarkdownPipeline? pipeline;
|
||||
private MarkdownPipeline? _pipeline;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MarkdownPipeline" /> class.
|
||||
@@ -95,9 +95,9 @@ public class MarkdownPipelineBuilder
|
||||
/// <exception cref="InvalidOperationException">An extension cannot be null</exception>
|
||||
public MarkdownPipeline Build()
|
||||
{
|
||||
if (pipeline != null)
|
||||
if (_pipeline != null)
|
||||
{
|
||||
return pipeline;
|
||||
return _pipeline;
|
||||
}
|
||||
|
||||
// TODO: Review the whole initialization process for extensions
|
||||
@@ -115,7 +115,7 @@ public class MarkdownPipelineBuilder
|
||||
extension.Setup(this);
|
||||
}
|
||||
|
||||
pipeline = new MarkdownPipeline(
|
||||
_pipeline = new MarkdownPipeline(
|
||||
new OrderedList<IMarkdownExtension>(Extensions),
|
||||
new BlockParserList(BlockParsers),
|
||||
new InlineParserList(InlineParsers),
|
||||
@@ -125,6 +125,6 @@ public class MarkdownPipelineBuilder
|
||||
PreciseSourceLocation = PreciseSourceLocation,
|
||||
TrackTrivia = TrackTrivia,
|
||||
};
|
||||
return pipeline;
|
||||
return _pipeline;
|
||||
}
|
||||
}
|
||||
@@ -322,7 +322,7 @@ public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T :
|
||||
|
||||
if (fence.OpeningFencedCharCount <= closingCount &&
|
||||
!processor.IsCodeIndent &&
|
||||
(c == '\0' || c.IsWhitespace()) &&
|
||||
c.IsWhiteSpaceOrZero() &&
|
||||
line.TrimEnd())
|
||||
{
|
||||
block.UpdateSpanEnd(startBeforeTrim - 1);
|
||||
|
||||
@@ -139,9 +139,7 @@ public class HtmlBlockParser : BlockParser
|
||||
c = line.NextChar();
|
||||
}
|
||||
|
||||
if (
|
||||
!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhitespace() ||
|
||||
c == '\0'))
|
||||
if (!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhiteSpaceOrZero()))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
@@ -297,7 +295,7 @@ public class HtmlBlockParser : BlockParser
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
private static readonly CompactPrefixTree<int> HtmlTags = new(66, 94, 83)
|
||||
private static readonly CompactPrefixTree<int> HtmlTags = new(67, 96, 86)
|
||||
{
|
||||
{ "address", 0 },
|
||||
{ "article", 1 },
|
||||
@@ -364,6 +362,7 @@ public class HtmlBlockParser : BlockParser
|
||||
{ "title", 62 },
|
||||
{ "tr", 63 },
|
||||
{ "track", 64 },
|
||||
{ "ul", 65 }
|
||||
{ "ul", 65 },
|
||||
{ "search", 66 },
|
||||
};
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if !NETSTANDARD2_1_OR_GREATER
|
||||
#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
|
||||
38
src/Markdig/Polyfills/FrozenCollections.cs
Normal file
38
src/Markdig/Polyfills/FrozenCollections.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
namespace System.Collections.Frozen;
|
||||
|
||||
// We're using a polyfill instead of conditionally referencing the package as the package is untested on older TFMs, and
|
||||
// brings in a reference to System.Runtime.CompilerServices.Unsafe, which conflicts with our polyfills of that type.
|
||||
|
||||
internal sealed class FrozenDictionary<TKey, TValue> : Dictionary<TKey, TValue>
|
||||
{
|
||||
public FrozenDictionary(Dictionary<TKey, TValue> dictionary) : base(dictionary) { }
|
||||
}
|
||||
|
||||
internal static class FrozenDictionaryExtensions
|
||||
{
|
||||
public static FrozenDictionary<TKey, TValue> ToFrozenDictionary<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
|
||||
{
|
||||
return new FrozenDictionary<TKey, TValue>(dictionary);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FrozenSet<T> : HashSet<T>
|
||||
{
|
||||
public FrozenSet(HashSet<T> set, IEqualityComparer<T> comparer) : base(set, comparer) { }
|
||||
}
|
||||
|
||||
internal static class FrozenSetExtensions
|
||||
{
|
||||
public static FrozenSet<T> ToFrozenSet<T>(this HashSet<T> set, IEqualityComparer<T> comparer)
|
||||
{
|
||||
return new FrozenSet<T>(set, comparer);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -18,6 +18,9 @@ internal sealed class NotNullWhenAttribute : Attribute
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class AllowNullAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
|
||||
internal sealed class MaybeNullAttribute : Attribute { }
|
||||
#endif
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
|
||||
@@ -26,6 +26,8 @@ internal static class SearchValues
|
||||
|
||||
internal abstract class SearchValues<T>
|
||||
{
|
||||
public abstract bool Contains(T value);
|
||||
|
||||
public abstract int IndexOfAny(ReadOnlySpan<char> span);
|
||||
|
||||
public abstract int IndexOfAnyExcept(ReadOnlySpan<char> span);
|
||||
@@ -52,6 +54,10 @@ internal sealed class PreNet8CompatSearchValues : SearchValues<char>
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override bool Contains(char value) =>
|
||||
value < 128 ? _ascii[value] : (_nonAscii is { } nonAscii && nonAscii.Contains(value));
|
||||
|
||||
public override int IndexOfAny(ReadOnlySpan<char> span)
|
||||
{
|
||||
if (_nonAscii is null)
|
||||
|
||||
23
src/Markdig/Polyfills/SpanExtensions.cs
Normal file
23
src/Markdig/Polyfills/SpanExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if NET462 || NETSTANDARD2_0
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System;
|
||||
|
||||
internal static class SpanExtensions
|
||||
{
|
||||
public static bool StartsWith(this ReadOnlySpan<char> span, string prefix, StringComparison comparisonType)
|
||||
{
|
||||
Debug.Assert(comparisonType is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return
|
||||
span.Length >= prefix.Length &&
|
||||
span.Slice(0, prefix.Length).Equals(prefix.AsSpan(), comparisonType);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -2,15 +2,19 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if NETSTANDARD2_1
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
#if NETSTANDARD2_1
|
||||
internal static class Unsafe
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T As<T>(object o) where T : class
|
||||
[return: NotNullIfNotNull(nameof(o))]
|
||||
public static T? As<T>(object? o) where T : class
|
||||
{
|
||||
return (T)o;
|
||||
return (T?)o;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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;
|
||||
using Markdig.Syntax;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Markdig.Renderers.Html;
|
||||
|
||||
@@ -13,8 +15,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 +25,48 @@ 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);
|
||||
|
||||
[field: MaybeNull]
|
||||
private FrozenSet<string> SpecialBlockMapping
|
||||
{
|
||||
get
|
||||
{
|
||||
return field ?? CreateNew();
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
FrozenSet<string> CreateNew()
|
||||
{
|
||||
HashSet<string> set = [.. BlocksAsDiv, .. BlockMapping.Keys];
|
||||
return field = set.ToFrozenSet(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 is FencedCodeBlock { Info: string info } && SpecialBlockMapping.Contains(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 +76,7 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
renderer.Write("</").Write(htmlBlock).WriteLine(">");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -75,7 +100,7 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
renderer.WriteRaw('>');
|
||||
}
|
||||
|
||||
renderer.WriteLeafRawLines(obj, true, true);
|
||||
renderer.WriteLeafRawLines(obj, true, renderer.EnableHtmlEscape);
|
||||
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
|
||||
@@ -286,7 +286,7 @@ public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
{
|
||||
scoped ReadOnlySpan<char> chars;
|
||||
|
||||
if (CharHelper.IsHighSurrogate(c) && (uint)(i + 1) < (uint)content.Length)
|
||||
if (char.IsHighSurrogate(c) && (uint)(i + 1) < (uint)content.Length)
|
||||
{
|
||||
chars = stackalloc char[] { c, content[i + 1] };
|
||||
i++;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 System.Diagnostics.CodeAnalysis;
|
||||
@@ -174,10 +174,14 @@ public abstract class TextRendererBase<T> : TextRendererBase where T : TextRende
|
||||
|
||||
public void PopIndent()
|
||||
{
|
||||
// TODO: Check
|
||||
indents.RemoveAt(indents.Count - 1);
|
||||
if (this.indents.Count > 0)
|
||||
indents.RemoveAt(indents.Count - 1);
|
||||
else
|
||||
throw new InvalidOperationException("No indent to pop");
|
||||
}
|
||||
|
||||
public void ClearIndent() => indents.Clear();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private protected void WriteIndent()
|
||||
{
|
||||
@@ -220,12 +224,12 @@ public abstract class TextRendererBase<T> : TextRendererBase where T : TextRende
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal T Write(char c, int count)
|
||||
{
|
||||
WriteIndent();
|
||||
|
||||
WriteIndent();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Writer.Write(c);
|
||||
}
|
||||
}
|
||||
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public static class CharIteratorHelper
|
||||
var c = iterator.CurrentChar;
|
||||
bool hasWhitespaces = false;
|
||||
lastLine = NewLine.None;
|
||||
while (c != '\0' && c.IsWhitespace())
|
||||
while (c.IsWhitespace())
|
||||
{
|
||||
if (c == '\n' || c == '\r')
|
||||
{
|
||||
|
||||
@@ -143,7 +143,7 @@ public abstract class MarkdownObject : IMarkdownObject
|
||||
private protected T? GetTrivia<T>() where T : class
|
||||
{
|
||||
object? trivia = _attachedDatas?.Trivia;
|
||||
return trivia is null ? null : Unsafe.As<T>(trivia);
|
||||
return Unsafe.As<T>(trivia);
|
||||
}
|
||||
|
||||
private protected T GetOrSetTrivia<T>() where T : class, new()
|
||||
@@ -153,7 +153,7 @@ public abstract class MarkdownObject : IMarkdownObject
|
||||
return Unsafe.As<T>(storage.Trivia);
|
||||
}
|
||||
|
||||
private class DataEntriesAndTrivia
|
||||
private sealed class DataEntriesAndTrivia
|
||||
{
|
||||
private struct DataEntry(object key, object value)
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ public class QuoteBlock : ContainerBlock
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia per line of this QuoteBlock.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
|
||||
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
|
||||
/// </summary>
|
||||
public List<QuoteBlockLine> QuoteLines => Trivia;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -78,7 +78,7 @@ class Program
|
||||
//var newValues = new Dictionary<int, char>(values.Count)
|
||||
//{
|
||||
// {15, 'a'}
|
||||
//}
|
||||
//}.ToFrozenDictionary();
|
||||
Trace.WriteLine($"CodeToAscii = new Dictionary<char, string>({values.Count})");
|
||||
Trace.WriteLine("{");
|
||||
foreach (var pair in values)
|
||||
@@ -86,7 +86,7 @@ class Program
|
||||
var escape = pair.Value.Replace("\\", @"\\").Replace("\"", "\\\"");
|
||||
Trace.WriteLine($" {{'{pair.Key}',\"{escape}\"}},");
|
||||
}
|
||||
Trace.WriteLine("};");
|
||||
Trace.WriteLine("}.ToFrozenDictionary();");
|
||||
|
||||
//Trace.WriteLine("count: " + count);
|
||||
//Trace.WriteLine("max: " + max);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"rollForward": "latestMajor",
|
||||
"version": "9.0.100",
|
||||
"rollForward": "latestMinor",
|
||||
"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