mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 05:44:50 +00:00
Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682c727288 | ||
|
|
ec2eef25b2 | ||
|
|
6261660d37 | ||
|
|
6d1fa96389 | ||
|
|
47c4e9b1e2 | ||
|
|
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 | ||
|
|
f48331d6c7 | ||
|
|
6549d3b726 | ||
|
|
d434f00355 | ||
|
|
b62a12d32d | ||
|
|
3c7edaa82d | ||
|
|
fcb56fb037 | ||
|
|
50bc6cadfc | ||
|
|
201aa4ef73 | ||
|
|
adce9797d6 | ||
|
|
fb71dff0ec | ||
|
|
399570941a | ||
|
|
2f903697e2 | ||
|
|
eb8fe15679 | ||
|
|
8f008e45ca | ||
|
|
0014ec4138 | ||
|
|
2ca05ccad7 | ||
|
|
6a15c804bc | ||
|
|
0446959623 | ||
|
|
e6afddbaa0 | ||
|
|
a377239e91 | ||
|
|
35aa304faf | ||
|
|
e4568979ec | ||
|
|
3470ec0d54 | ||
|
|
113ef7f215 | ||
|
|
4cb4b68883 | ||
|
|
64ae344b74 | ||
|
|
b5f3c9fc67 | ||
|
|
8a88fd0557 | ||
|
|
cc7623989d | ||
|
|
b6a7acf5fc | ||
|
|
804a6f0dbc | ||
|
|
342e264988 | ||
|
|
f52ecee0b9 | ||
|
|
a092ec23b3 | ||
|
|
6f1dce6306 | ||
|
|
040a778d87 | ||
|
|
2ae2cf9263 | ||
|
|
ba1e562d2f | ||
|
|
65a02e44ec | ||
|
|
e78833ae30 | ||
|
|
2ab716bec1 | ||
|
|
feeb1867ce | ||
|
|
f3aa7e73e3 | ||
|
|
dce5572356 | ||
|
|
dbbabd2221 | ||
|
|
22145c2fb0 | ||
|
|
2517003edc | ||
|
|
50a3d02c2c | ||
|
|
40fb2b8249 | ||
|
|
5c54968807 | ||
|
|
58ea46d58b | ||
|
|
f557e57ab1 | ||
|
|
87aa32e1bd | ||
|
|
4f1cb9da08 | ||
|
|
5cff880c90 | ||
|
|
c7aec822b0 | ||
|
|
b0bde46cc1 | ||
|
|
7803417e5c | ||
|
|
047c4cbcbb | ||
|
|
e4f57ca21e | ||
|
|
1f1364e69b | ||
|
|
4eea9db35c | ||
|
|
cce7284b84 | ||
|
|
8e1e0b9bf3 | ||
|
|
7d40bc118b | ||
|
|
dba94a2371 | ||
|
|
6d75eed3bb | ||
|
|
ccb75fd5f0 | ||
|
|
06eb6ba774 | ||
|
|
f15e9f020e | ||
|
|
a70ca6304f | ||
|
|
d26822be05 | ||
|
|
5e3416f8b7 | ||
|
|
012a57d361 | ||
|
|
053a18c684 | ||
|
|
13265453ac | ||
|
|
8ea0783834 | ||
|
|
3d29430337 | ||
|
|
81bc58c6c9 | ||
|
|
bfe3800130 | ||
|
|
b7cb169fd3 | ||
|
|
512b28256a | ||
|
|
cd5d11eeff | ||
|
|
a9118774a8 |
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -11,22 +11,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Build, Test, Pack, Publish
|
||||
shell: bash
|
||||
run: |
|
||||
dotnet tool install -g dotnet-releaser
|
||||
dotnet-releaser run --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" src/dotnet-releaser.toml
|
||||
uses: xoofx/.github/.github/workflows/dotnet.yml@main
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.0
|
||||
8.0
|
||||
9.0
|
||||
secrets:
|
||||
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
|
||||
7
.github/workflows/nuget_org_only.config
vendored
Normal file
7
.github/workflows/nuget_org_only.config
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -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.
|
||||
|
||||
|
||||
20
readme.md
20
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">
|
||||
|
||||
@@ -12,9 +12,9 @@ You can **try Markdig online** and compare it to other implementations on [babel
|
||||
|
||||
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
|
||||
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
|
||||
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
|
||||
- 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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsPackable>false</IsPackable>
|
||||
@@ -19,12 +19,12 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.1" />
|
||||
<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="2.0.226801" />
|
||||
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.512801" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Markdig\Markdig.csproj" />
|
||||
|
||||
5
src/Markdig.Tests/GlobalUsings.cs
Normal file
5
src/Markdig.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
// 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.
|
||||
|
||||
global using Assert = NUnit.Framework.Legacy.ClassicAssert;
|
||||
@@ -1,19 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.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)\net6.0\SpecFileGen.dll</SpecExecutable>
|
||||
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.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.1.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
@@ -198,9 +199,9 @@ $$
|
||||
<div class=""math"">
|
||||
\begin{align}
|
||||
\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\
|
||||
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
|
||||
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
|
||||
& = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\
|
||||
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
|
||||
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
|
||||
& \approx \frac{73}{12}\left(1 - \frac{1}{2\cdot73^2}\right)
|
||||
\end{align}
|
||||
</div>
|
||||
@@ -291,4 +292,98 @@ $$
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"https://www.foo.bar\">www.foo.bar</a></p>", httpsPipeline);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RootInlineHasCorrectSourceSpan()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
|
||||
pipeline.TrackTrivia = true;
|
||||
|
||||
var document = Markdown.Parse("0123456789\n", pipeline);
|
||||
|
||||
var expectedSourceSpan = new SourceSpan(0, 10);
|
||||
Assert.That(((LeafBlock)document.LastChild).Inline.Span == expectedSourceSpan);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RootInlineInTableCellHasCorrectSourceSpan()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseAdvancedExtensions().Build();
|
||||
pipeline.TrackTrivia = true;
|
||||
|
||||
var document = Markdown.Parse("| a | b |\n| --- | --- |\n| <span id=\"dest\"></span><span id=\"DEST\"></span>*dest*<br/> | \\[in\\] The address of the result of the operation.<br/> |", pipeline);
|
||||
|
||||
var paragraph = (ParagraphBlock)((TableCell)((TableRow)((Table)document.LastChild).LastChild).First()).LastChild;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
179
src/Markdig.Tests/Specs/AlertBlockSpecs.generated.cs
Normal file
179
src/Markdig.Tests/Specs/AlertBlockSpecs.generated.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
// --------------------------------
|
||||
// Alert Blocks
|
||||
// --------------------------------
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests.Specs.AlertBlocks
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestExtensionsAlertBlocks
|
||||
{
|
||||
// # Extensions
|
||||
//
|
||||
// This section describes the different extensions supported:
|
||||
//
|
||||
// ## Alert Blocks
|
||||
//
|
||||
// This is supporting the [GitHub Alert blocks](https://github.com/orgs/community/discussions/16925)
|
||||
[Test]
|
||||
public void ExtensionsAlertBlocks_Example001()
|
||||
{
|
||||
// Example 1
|
||||
// Section: Extensions / Alert Blocks
|
||||
//
|
||||
// The following Markdown:
|
||||
// > [!NOTE]
|
||||
// > Highlights information that users should take into account, even when skimming.
|
||||
//
|
||||
// > [!TIP]
|
||||
// > Optional information to help a user be more successful.
|
||||
//
|
||||
// > [!IMPORTANT]
|
||||
// > Crucial information necessary for users to succeed.
|
||||
//
|
||||
// > [!WARNING]
|
||||
// > Critical content demanding immediate user attention due to potential risks.
|
||||
//
|
||||
// > [!CAUTION]
|
||||
// > Negative potential consequences of an action.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <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>Highlights information that users should take into account, even when skimming.</p>
|
||||
// </div>
|
||||
// <div class="markdown-alert markdown-alert-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>
|
||||
// <p>Optional information to help a user be more successful.</p>
|
||||
// </div>
|
||||
// <div class="markdown-alert markdown-alert-important">
|
||||
// <p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Important</p>
|
||||
// <p>Crucial information necessary for users to succeed.</p>
|
||||
// </div>
|
||||
// <div class="markdown-alert markdown-alert-warning">
|
||||
// <p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Warning</p>
|
||||
// <p>Critical content demanding immediate user attention due to potential risks.</p>
|
||||
// </div>
|
||||
// <div class="markdown-alert markdown-alert-caution">
|
||||
// <p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>Caution</p>
|
||||
// <p>Negative potential consequences of an action.</p>
|
||||
// </div>
|
||||
|
||||
TestParser.TestSpec("> [!NOTE] \n> Highlights information that users should take into account, even when skimming.\n\n> [!TIP]\n> Optional information to help a user be more successful.\n\n> [!IMPORTANT] \n> Crucial information necessary for users to succeed.\n\n> [!WARNING] \n> Critical content demanding immediate user attention due to potential risks.\n\n> [!CAUTION]\n> Negative potential consequences of an action.", "<div class=\"markdown-alert markdown-alert-note\">\n<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>\n<p>Highlights information that users should take into account, even when skimming.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-tip\">\n<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>\n<p>Optional information to help a user be more successful.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-important\">\n<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Important</p>\n<p>Crucial information necessary for users to succeed.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-warning\">\n<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>\n<p>Critical content demanding immediate user attention due to potential risks.</p>\n</div>\n<div class=\"markdown-alert markdown-alert-caution\">\n<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>\n<p>Negative potential consequences of an action.</p>\n</div>", "advanced", context: "Example 1\nSection Extensions / Alert Blocks\n");
|
||||
}
|
||||
|
||||
// Example with code blocks and mix formatting:
|
||||
[Test]
|
||||
public void ExtensionsAlertBlocks_Example002()
|
||||
{
|
||||
// Example 2
|
||||
// Section: Extensions / Alert Blocks
|
||||
//
|
||||
// The following Markdown:
|
||||
// > [!NOTE]
|
||||
// > Highlights information that users should take into account, even when skimming.
|
||||
// > Testing rendering for multiple lines
|
||||
// > ```csharp
|
||||
// > var test = "I can also add code to panels
|
||||
// > ```
|
||||
// > `Inline code testing`
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <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>Highlights information that users should take into account, even when skimming.
|
||||
// Testing rendering for multiple lines</p>
|
||||
// <pre><code class="language-csharp">var test = "I can also add code to panels
|
||||
// </code></pre>
|
||||
// <p><code>Inline code testing</code></p>
|
||||
// </div>
|
||||
|
||||
TestParser.TestSpec("> [!NOTE]\n> Highlights information that users should take into account, even when skimming.\n> Testing rendering for multiple lines\n> ```csharp\n> var test = \"I can also add code to panels\n> ```\n> `Inline code testing`", "<div class=\"markdown-alert markdown-alert-note\">\n<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>\n<p>Highlights information that users should take into account, even when skimming.\nTesting rendering for multiple lines</p>\n<pre><code class=\"language-csharp\">var test = "I can also add code to panels\n</code></pre>\n<p><code>Inline code testing</code></p>\n</div>", "advanced", context: "Example 2\nSection Extensions / Alert Blocks\n");
|
||||
}
|
||||
|
||||
// Multiline:
|
||||
[Test]
|
||||
public void ExtensionsAlertBlocks_Example003()
|
||||
{
|
||||
// Example 3
|
||||
// Section: Extensions / Alert Blocks
|
||||
//
|
||||
// The following Markdown:
|
||||
// > [!NOTE]
|
||||
// > Highlights information that users should take into account, even when skimming.
|
||||
// >
|
||||
// > Testing rendering for multiple lines
|
||||
// >
|
||||
// > `Inline code testing`
|
||||
// >
|
||||
// > Other line
|
||||
// >
|
||||
// > > Nested quote
|
||||
// > >
|
||||
// > > Final nested quote line
|
||||
// >
|
||||
// > Final line of alert
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <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>Highlights information that users should take into account, even when skimming.</p>
|
||||
// <p>Testing rendering for multiple lines</p>
|
||||
// <p><code>Inline code testing</code></p>
|
||||
// <p>Other line</p>
|
||||
// <blockquote>
|
||||
// <p>Nested quote</p>
|
||||
// <p>Final nested quote line</p>
|
||||
// </blockquote>
|
||||
// <p>Final line of alert</p>
|
||||
// </div>
|
||||
|
||||
TestParser.TestSpec("> [!NOTE]\n> Highlights information that users should take into account, even when skimming.\n> \n> Testing rendering for multiple lines\n> \n> `Inline code testing`\n> \n> Other line\n> \n> > Nested quote\n> >\n> > Final nested quote line\n> \n> Final line of alert", "<div class=\"markdown-alert markdown-alert-note\">\n<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>\n<p>Highlights information that users should take into account, even when skimming.</p>\n<p>Testing rendering for multiple lines</p>\n<p><code>Inline code testing</code></p>\n<p>Other line</p>\n<blockquote>\n<p>Nested quote</p>\n<p>Final nested quote line</p>\n</blockquote>\n<p>Final line of alert</p>\n</div>", "advanced", context: "Example 3\nSection Extensions / Alert Blocks\n");
|
||||
}
|
||||
|
||||
// An alert inline (e.g `[!NOTE]`) must come first in a quote block, and must be followed by optional spaces with a new line. If no new lines are found, it will not be considered as an alert block.
|
||||
//
|
||||
// Followed by space and new line:
|
||||
[Test]
|
||||
public void ExtensionsAlertBlocks_Example004()
|
||||
{
|
||||
// Example 4
|
||||
// Section: Extensions / Alert Blocks
|
||||
//
|
||||
// The following Markdown:
|
||||
// > [!NOTE] This is invalid because no new line
|
||||
// > Highlights information that users should take into account, even when skimming.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <blockquote>
|
||||
// <p>[!NOTE] This is invalid because no new line
|
||||
// Highlights information that users should take into account, even when skimming.</p>
|
||||
// </blockquote>
|
||||
|
||||
TestParser.TestSpec("> [!NOTE] This is invalid because no new line\n> Highlights information that users should take into account, even when skimming.", "<blockquote>\n<p>[!NOTE] This is invalid because no new line\nHighlights information that users should take into account, even when skimming.</p>\n</blockquote>", "advanced", context: "Example 4\nSection Extensions / Alert Blocks\n");
|
||||
}
|
||||
|
||||
// Must come first in a quote block:
|
||||
[Test]
|
||||
public void ExtensionsAlertBlocks_Example005()
|
||||
{
|
||||
// Example 5
|
||||
// Section: Extensions / Alert Blocks
|
||||
//
|
||||
// The following Markdown:
|
||||
// > This is not a [!NOTE]
|
||||
// > Highlights information that users should take into account, even when skimming.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <blockquote>
|
||||
// <p>This is not a [!NOTE]
|
||||
// Highlights information that users should take into account, even when skimming.</p>
|
||||
// </blockquote>
|
||||
|
||||
TestParser.TestSpec("> This is not a [!NOTE]\n> Highlights information that users should take into account, even when skimming.", "<blockquote>\n<p>This is not a [!NOTE]\nHighlights information that users should take into account, even when skimming.</p>\n</blockquote>", "advanced", context: "Example 5\nSection Extensions / Alert Blocks\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/Markdig.Tests/Specs/AlertBlockSpecs.md
Normal file
127
src/Markdig.Tests/Specs/AlertBlockSpecs.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Extensions
|
||||
|
||||
This section describes the different extensions supported:
|
||||
|
||||
## Alert Blocks
|
||||
|
||||
This is supporting the [GitHub Alert blocks](https://github.com/orgs/community/discussions/16925)
|
||||
|
||||
```````````````````````````````` example
|
||||
> [!NOTE]
|
||||
> Highlights information that users should take into account, even when skimming.
|
||||
|
||||
> [!TIP]
|
||||
> Optional information to help a user be more successful.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Crucial information necessary for users to succeed.
|
||||
|
||||
> [!WARNING]
|
||||
> Critical content demanding immediate user attention due to potential risks.
|
||||
|
||||
> [!CAUTION]
|
||||
> Negative potential consequences of an action.
|
||||
.
|
||||
<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>Highlights information that users should take into account, even when skimming.</p>
|
||||
</div>
|
||||
<div class="markdown-alert markdown-alert-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>
|
||||
<p>Optional information to help a user be more successful.</p>
|
||||
</div>
|
||||
<div class="markdown-alert markdown-alert-important">
|
||||
<p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Important</p>
|
||||
<p>Crucial information necessary for users to succeed.</p>
|
||||
</div>
|
||||
<div class="markdown-alert markdown-alert-warning">
|
||||
<p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Warning</p>
|
||||
<p>Critical content demanding immediate user attention due to potential risks.</p>
|
||||
</div>
|
||||
<div class="markdown-alert markdown-alert-caution">
|
||||
<p class="markdown-alert-title"><svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>Caution</p>
|
||||
<p>Negative potential consequences of an action.</p>
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
Example with code blocks and mix formatting:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
> [!NOTE]
|
||||
> Highlights information that users should take into account, even when skimming.
|
||||
> Testing rendering for multiple lines
|
||||
> ```csharp
|
||||
> var test = "I can also add code to panels
|
||||
> ```
|
||||
> `Inline code testing`
|
||||
.
|
||||
<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>Highlights information that users should take into account, even when skimming.
|
||||
Testing rendering for multiple lines</p>
|
||||
<pre><code class="language-csharp">var test = "I can also add code to panels
|
||||
</code></pre>
|
||||
<p><code>Inline code testing</code></p>
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
Multiline:
|
||||
|
||||
```````````````````````````````` example
|
||||
> [!NOTE]
|
||||
> Highlights information that users should take into account, even when skimming.
|
||||
>
|
||||
> Testing rendering for multiple lines
|
||||
>
|
||||
> `Inline code testing`
|
||||
>
|
||||
> Other line
|
||||
>
|
||||
> > Nested quote
|
||||
> >
|
||||
> > Final nested quote line
|
||||
>
|
||||
> Final line of alert
|
||||
.
|
||||
<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>Highlights information that users should take into account, even when skimming.</p>
|
||||
<p>Testing rendering for multiple lines</p>
|
||||
<p><code>Inline code testing</code></p>
|
||||
<p>Other line</p>
|
||||
<blockquote>
|
||||
<p>Nested quote</p>
|
||||
<p>Final nested quote line</p>
|
||||
</blockquote>
|
||||
<p>Final line of alert</p>
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
An alert inline (e.g `[!NOTE]`) must come first in a quote block, and must be followed by optional spaces with a new line. If no new lines are found, it will not be considered as an alert block.
|
||||
|
||||
Followed by space and new line:
|
||||
|
||||
```````````````````````````````` example
|
||||
> [!NOTE] This is invalid because no new line
|
||||
> Highlights information that users should take into account, even when skimming.
|
||||
.
|
||||
<blockquote>
|
||||
<p>[!NOTE] This is invalid because no new line
|
||||
Highlights information that users should take into account, even when skimming.</p>
|
||||
</blockquote>
|
||||
````````````````````````````````
|
||||
|
||||
Must come first in a quote block:
|
||||
|
||||
```````````````````````````````` example
|
||||
> This is not a [!NOTE]
|
||||
> Highlights information that users should take into account, even when skimming.
|
||||
.
|
||||
<blockquote>
|
||||
<p>This is not a [!NOTE]
|
||||
Highlights information that users should take into account, even when skimming.</p>
|
||||
</blockquote>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -279,7 +279,7 @@ namespace Markdig.Tests.Specs.Math
|
||||
{
|
||||
// ## Math Block
|
||||
//
|
||||
// The match block can spawn on multiple lines by having a $$ starting on a line.
|
||||
// The math block can spawn on multiple lines by having a $$ starting on a line.
|
||||
// It is working as a fenced code block.
|
||||
[Test]
|
||||
public void ExtensionsMathBlock_Example017()
|
||||
|
||||
@@ -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
|
||||
@@ -127,7 +127,7 @@ $$ math $$ starting at a line
|
||||
|
||||
## Math Block
|
||||
|
||||
The match block can spawn on multiple lines by having a $$ starting on a line.
|
||||
The math block can spawn on multiple lines by having a $$ starting on a line.
|
||||
It is working as a fenced code block.
|
||||
|
||||
```````````````````````````````` example
|
||||
|
||||
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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// 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.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
[TestFixture]
|
||||
@@ -18,4 +21,17 @@ public partial class TestEmphasisPlus
|
||||
{
|
||||
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <em><strong>Strong emphasis</strong></em> normal</p>", "");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OpenEmphasisHasConvenientContentStringSlice()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().Build();
|
||||
|
||||
var document = Markdown.Parse("test*test", pipeline);
|
||||
|
||||
var emphasisDelimiterLiteral = (LiteralInline)((ParagraphBlock)document.LastChild).Inline.ElementAt(1);
|
||||
Assert.That(emphasisDelimiterLiteral.Content.Text == "test*test");
|
||||
Assert.That(emphasisDelimiterLiteral.Content.Start == 4);
|
||||
Assert.That(emphasisDelimiterLiteral.Content.End == 4);
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
@@ -96,26 +112,26 @@ public class TestLinkHelper
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlAndTitleEmpty()
|
||||
public void TestUrlEmptyAndTitleNull()
|
||||
{
|
||||
// 01234
|
||||
var text = new StringSlice(@"(<>)A");
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(null, title);
|
||||
Assert.AreEqual(new SourceSpan(1, 2), linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlAndTitleEmpty2()
|
||||
public void TestUrlEmptyAndTitleNull2()
|
||||
{
|
||||
// 012345
|
||||
var text = new StringSlice(@"( <> )A");
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(null, title);
|
||||
Assert.AreEqual(new SourceSpan(2, 3), linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
@@ -142,7 +158,7 @@ public class TestLinkHelper
|
||||
var text = new StringSlice(@"()A");
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(null, title);
|
||||
Assert.AreEqual(SourceSpan.Empty, linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
@@ -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,9 +34,19 @@ 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("",
|
||||
"<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"./video.mp4\"></source></video></p>\n")]
|
||||
[TestCase("",
|
||||
"<p><audio width=\"500\" controls=\"\"><source type=\"audio/mpeg\" src=\"./audio.mp3\"></source></audio></p>\n")]
|
||||
public void TestBuiltInHostsWithRelativePaths(string markdown, string expected)
|
||||
{
|
||||
string html = Markdown.ToHtml(markdown, GetPipeline());
|
||||
Assert.AreEqual(expected, html);
|
||||
}
|
||||
|
||||
private class TestHostProvider : IHostProvider
|
||||
{
|
||||
public string Class { get; } = "regex";
|
||||
|
||||
@@ -51,6 +51,16 @@ public class TestNormalize
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyntaxHeadlineLevel7()
|
||||
{
|
||||
AssertSyntax("####### Headline", new HeadingBlock(null) {
|
||||
HeaderChar = '#',
|
||||
Level = 7,
|
||||
Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")),
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SyntaxParagraph()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -67,6 +67,60 @@ literal ( 2, 0) 12-21
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParagraphWithEndNewLine()
|
||||
{
|
||||
Check("0123456789\n", @"
|
||||
paragraph ( 0, 0) 0-10
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
", trackTrivia: true);
|
||||
|
||||
Check("0123456789\r", @"
|
||||
paragraph ( 0, 0) 0-10
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
", trackTrivia: true);
|
||||
|
||||
Check("0123456789\r\n", @"
|
||||
paragraph ( 0, 0) 0-11
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-11
|
||||
", trackTrivia: true);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestMultipleParagraphsWithEndNewLine()
|
||||
{
|
||||
Check("0123456789\n\n0123456789\n\n", @"
|
||||
paragraph ( 0, 0) 0-10
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
paragraph ( 2, 0) 12-22
|
||||
literal ( 2, 0) 12-21
|
||||
linebreak ( 2,10) 22-22
|
||||
", trackTrivia: true);
|
||||
|
||||
Check("0123456789\r\r0123456789\r\r", @"
|
||||
paragraph ( 0, 0) 0-10
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
paragraph ( 2, 0) 12-22
|
||||
literal ( 2, 0) 12-21
|
||||
linebreak ( 2,10) 22-22
|
||||
", trackTrivia: true);
|
||||
|
||||
Check("0123456789\r\n\r\n0123456789\r\n\r\n", @"
|
||||
paragraph ( 0, 0) 0-11
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-11
|
||||
paragraph ( 2, 0) 14-25
|
||||
literal ( 2, 0) 14-23
|
||||
linebreak ( 2,10) 24-25
|
||||
", trackTrivia: true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasis()
|
||||
{
|
||||
@@ -106,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()
|
||||
{
|
||||
@@ -468,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");
|
||||
}
|
||||
|
||||
@@ -644,14 +713,22 @@ literal ( 0, 2) 2-3
|
||||
[Test]
|
||||
public void TestMathematicsInline()
|
||||
{
|
||||
// 01 23456789AB
|
||||
Check("0\n012 $abcd$", @"
|
||||
paragraph ( 0, 0) 0-11
|
||||
// 01 23456789ABCDEF
|
||||
Check("0\n012 $abcd$ 321", @"
|
||||
paragraph ( 0, 0) 0-15
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -734,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()
|
||||
{
|
||||
@@ -825,9 +925,10 @@ literal ( 8, 2) 77-92
|
||||
");
|
||||
}
|
||||
|
||||
private static void Check(string text, string expectedResult, string extensions = null)
|
||||
private static void Check(string text, string expectedResult, string extensions = null, bool trackTrivia = false)
|
||||
{
|
||||
var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation();
|
||||
pipelineBuilder.TrackTrivia = trackTrivia;
|
||||
if (extensions != null)
|
||||
{
|
||||
pipelineBuilder.Configure(extensions);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
using Markdig.Helpers;
|
||||
@@ -180,4 +181,62 @@ public class TestStringSliceList
|
||||
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStringLineGroupCharIteratorAtCapacity()
|
||||
{
|
||||
string str = "ABCDEFGHI";
|
||||
var text = new StringLineGroup(1)
|
||||
{
|
||||
// Will store the following line at capacity
|
||||
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
|
||||
};
|
||||
|
||||
var iterator = text.ToCharIterator();
|
||||
var chars = ToString(iterator);
|
||||
TextAssert.AreEqual("ABC\r\n", chars.ToString());
|
||||
TextAssert.AreEqual("ABC", text.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStringLineGroupCharIteratorForcingIncreaseCapacity()
|
||||
{
|
||||
string str = "ABCDEFGHI";
|
||||
var text = new StringLineGroup(1)
|
||||
{
|
||||
// Will store the following line at capacity
|
||||
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
|
||||
|
||||
// Will force increase capacity to 2 and store the line at capacity
|
||||
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 3, End = 3 },
|
||||
};
|
||||
|
||||
var iterator = text.ToCharIterator();
|
||||
var chars = ToString(iterator);
|
||||
TextAssert.AreEqual("ABC\r\nD\r\n", chars.ToString());
|
||||
TextAssert.AreEqual("ABC\r\nD", text.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStringLineGroup_EnumeratorReturnsRealLines()
|
||||
{
|
||||
string str = "A\r\n";
|
||||
var text = new StringLineGroup(4)
|
||||
{
|
||||
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 0 }
|
||||
};
|
||||
|
||||
var enumerator = ((IEnumerable)text).GetEnumerator();
|
||||
Assert.True(enumerator.MoveNext());
|
||||
StringLine currentLine = (StringLine)enumerator.Current;
|
||||
TextAssert.AreEqual("A", currentLine.ToString());
|
||||
Assert.False(enumerator.MoveNext());
|
||||
|
||||
var nonBoxedEnumerator = text.GetEnumerator();
|
||||
|
||||
Assert.True(nonBoxedEnumerator.MoveNext());
|
||||
currentLine = (StringLine)nonBoxedEnumerator.Current;
|
||||
TextAssert.AreEqual("A", currentLine.ToString());
|
||||
Assert.False(nonBoxedEnumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,39 @@ public class TestYamlFrontMatterExtension
|
||||
ObjectRenderers = new ObjectRendererCollection();
|
||||
}
|
||||
|
||||
#pragma warning disable CS0067 // ObjectWriteBefore/ObjectWriteAfter is never used
|
||||
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
|
||||
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
public ObjectRendererCollection ObjectRenderers { get; }
|
||||
public object Render(MarkdownObject markdownObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("---\nkey1: value1\nkey2: value2\n---\n\n# Content\n")]
|
||||
[TestCase("---\nkey1: value1\nkey2: value2\nkey3: value3\nkey4: value4\nkey5: value5\nkey6: value6\nkey7: value7\nkey8: value8\n---\n\n# Content\n")]
|
||||
public void FrontMatterBlockLinesCharIterator(string value)
|
||||
{
|
||||
var builder = new MarkdownPipelineBuilder();
|
||||
builder.Extensions.Add(new YamlFrontMatterExtension());
|
||||
var markdownDocument = Markdown.Parse(value, builder.Build());
|
||||
|
||||
var yamlBlocks = markdownDocument.Descendants<YamlFrontMatterBlock>();
|
||||
Assert.True(yamlBlocks.Any());
|
||||
|
||||
foreach (var yamlBlock in yamlBlocks)
|
||||
{
|
||||
var iterator = yamlBlock.Lines.ToCharIterator();
|
||||
while(iterator.CurrentChar != '\0')
|
||||
{
|
||||
iterator.NextChar();
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<AssemblyName>Markdig.WebApp</AssemblyName>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -14,7 +14,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -20,7 +20,7 @@ public class AbbreviationParser : BlockParser
|
||||
/// </summary>
|
||||
public AbbreviationParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '*' };
|
||||
OpeningCharacters = ['*'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -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;
|
||||
|
||||
33
src/Markdig/Extensions/Alerts/AlertBlock.cs
Normal file
33
src/Markdig/Extensions/Alerts/AlertBlock.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
/// <summary>
|
||||
/// A block representing an alert quote block.
|
||||
/// </summary>
|
||||
public class AlertBlock : QuoteBlock
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of this block.
|
||||
/// </summary>
|
||||
/// <param name="kind"></param>
|
||||
public AlertBlock(StringSlice kind) : base(null)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the kind of the alert block (e.g `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`).
|
||||
/// </summary>
|
||||
public StringSlice Kind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trivia space after the kind.
|
||||
/// </summary>
|
||||
public StringSlice TriviaSpaceAfterKind { get; set; }
|
||||
}
|
||||
86
src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs
Normal file
86
src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
/// <summary>
|
||||
/// A HTML renderer for a <see cref="AlertBlock"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="HtmlObjectRenderer{AlertBlock}" />
|
||||
public class AlertBlockRenderer : HtmlObjectRenderer<AlertBlock>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of this renderer.
|
||||
/// </summary>
|
||||
public AlertBlockRenderer()
|
||||
{
|
||||
RenderKind = DefaultRenderKind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets of sets a delegate to render the kind of the alert.
|
||||
/// </summary>
|
||||
public Action<HtmlRenderer, StringSlice> RenderKind { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Write(HtmlRenderer renderer, AlertBlock obj)
|
||||
{
|
||||
renderer.EnsureLine();
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.Write("<div");
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.WriteLine('>');
|
||||
}
|
||||
|
||||
RenderKind(renderer, obj.Kind);
|
||||
|
||||
var savedImplicitParagraph = renderer.ImplicitParagraph;
|
||||
renderer.ImplicitParagraph = false;
|
||||
renderer.WriteChildren(obj);
|
||||
renderer.ImplicitParagraph = savedImplicitParagraph;
|
||||
if (renderer.EnableHtmlForBlock)
|
||||
{
|
||||
renderer.WriteLine("</div>");
|
||||
}
|
||||
|
||||
renderer.EnsureLine();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders the kind of the alert.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The HTML renderer.</param>
|
||||
/// <param name="kind">The kind of the alert to render</param>
|
||||
public static void DefaultRenderKind(HtmlRenderer renderer, StringSlice kind)
|
||||
{
|
||||
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>",
|
||||
"IMPORTANT" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Important</p>",
|
||||
"WARNING" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>",
|
||||
"CAUTION" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (html is not null)
|
||||
{
|
||||
renderer.WriteLine(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Markdig/Extensions/Alerts/AlertExtension.cs
Normal file
44
src/Markdig/Extensions/Alerts/AlertExtension.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers.Inlines;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
/// <summary>
|
||||
/// Extension for adding alerts to a Markdown pipeline.
|
||||
/// </summary>
|
||||
public class AlertExtension : IMarkdownExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the delegate to render the kind of the alert.
|
||||
/// </summary>
|
||||
public Action<HtmlRenderer, StringSlice>? RenderKind { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
var inlineParser = pipeline.InlineParsers.Find<AlertInlineParser>();
|
||||
if (inlineParser == null)
|
||||
{
|
||||
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new AlertInlineParser());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var blockRenderer = renderer.ObjectRenderers.FindExact<AlertBlockRenderer>();
|
||||
if (blockRenderer == null)
|
||||
{
|
||||
renderer.ObjectRenderers.InsertBefore<QuoteBlockRenderer>(new AlertBlockRenderer()
|
||||
{
|
||||
RenderKind = RenderKind ?? AlertBlockRenderer.DefaultRenderKind
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
134
src/Markdig/Extensions/Alerts/AlertInlineParser.cs
Normal file
134
src/Markdig/Extensions/Alerts/AlertInlineParser.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
/// <summary>
|
||||
/// An inline parser for an alert inline (e.g. `[!NOTE]`).
|
||||
/// </summary>
|
||||
/// <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>
|
||||
public AlertInlineParser()
|
||||
{
|
||||
OpeningCharacters = ['['];
|
||||
}
|
||||
|
||||
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 ||
|
||||
quoteBlock is AlertBlock ||
|
||||
quoteBlock.Parent is not MarkdownDocument)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StringSlice saved = slice;
|
||||
|
||||
slice.SkipChar(); // Skip [
|
||||
char c = slice.NextChar(); // Skip !
|
||||
|
||||
int start = slice.Start;
|
||||
int end = start;
|
||||
while (c.IsAlpha())
|
||||
{
|
||||
end = slice.Start;
|
||||
c = slice.NextChar();
|
||||
}
|
||||
|
||||
// We need at least one character
|
||||
if (c != ']' || start == end)
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
}
|
||||
|
||||
var alertType = new StringSlice(slice.Text, start, end);
|
||||
c = slice.NextChar(); // Skip ]
|
||||
|
||||
start = slice.Start;
|
||||
while (true)
|
||||
{
|
||||
if (c == '\0' || c == '\n' || c == '\r')
|
||||
{
|
||||
end = slice.Start;
|
||||
if (c == '\r')
|
||||
{
|
||||
c = slice.NextChar(); // Skip \r
|
||||
if (c == '\0' || c == '\n')
|
||||
{
|
||||
end = slice.Start;
|
||||
if (c == '\n')
|
||||
{
|
||||
slice.SkipChar(); // Skip \n
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == '\n')
|
||||
{
|
||||
slice.SkipChar(); // Skip \n
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (!c.IsSpaceOrTab())
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
}
|
||||
|
||||
c = slice.NextChar();
|
||||
}
|
||||
|
||||
var alertBlock = new AlertBlock(alertType)
|
||||
{
|
||||
Span = quoteBlock.Span,
|
||||
TriviaSpaceAfterKind = new StringSlice(slice.Text, start, end),
|
||||
Line = quoteBlock.Line,
|
||||
Column = quoteBlock.Column,
|
||||
};
|
||||
|
||||
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!;
|
||||
var indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock);
|
||||
parentQuoteBlock[indexOfQuoteBlock] = alertBlock;
|
||||
|
||||
while (quoteBlock.Count > 0)
|
||||
{
|
||||
var block = quoteBlock[0];
|
||||
quoteBlock.RemoveAt(0);
|
||||
alertBlock.Add(block);
|
||||
}
|
||||
|
||||
// Workaround to replace the parent container
|
||||
// Experimental API, so we are keeping it internal for now until we are sure it's the way we want to go
|
||||
processor.ReplaceParentContainer(quoteBlock, alertBlock);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.IO;
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers;
|
||||
@@ -20,8 +18,12 @@ namespace Markdig.Extensions.AutoIdentifiers;
|
||||
public class AutoIdentifierExtension : IMarkdownExtension
|
||||
{
|
||||
private const string AutoIdentifierKey = "AutoIdentifier";
|
||||
private readonly AutoIdentifierOptions options;
|
||||
private readonly StripRendererCache rendererCache = new StripRendererCache();
|
||||
|
||||
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.
|
||||
@@ -29,7 +31,9 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
/// <param name="options">The options.</param>
|
||||
public AutoIdentifierExtension(AutoIdentifierOptions options)
|
||||
{
|
||||
this.options = options;
|
||||
_options = options;
|
||||
_processInlinesBegin = DocumentOnProcessInlinesBegin;
|
||||
_processInlinesEnd = HeadingBlock_ProcessInlinesEnd;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
@@ -68,7 +72,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
}
|
||||
|
||||
// If the AutoLink options is set, we register a LinkReferenceDefinition at the document level
|
||||
if ((options & AutoIdentifierOptions.AutoLink) != 0)
|
||||
if ((_options & AutoIdentifierOptions.AutoLink) != 0)
|
||||
{
|
||||
var headingLine = headingBlock.Lines.Lines[0];
|
||||
|
||||
@@ -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()
|
||||
@@ -157,16 +161,17 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
}
|
||||
|
||||
// Use internally a HtmlRenderer to strip links from a heading
|
||||
var stripRenderer = rendererCache.Get();
|
||||
var stripRenderer = _rendererCache.Get();
|
||||
|
||||
stripRenderer.Render(headingBlock.Inline);
|
||||
var headingText = stripRenderer.Writer.ToString()!;
|
||||
rendererCache.Release(stripRenderer);
|
||||
ReadOnlySpan<char> rawHeadingText = ((FastStringWriter)stripRenderer.Writer).AsSpan();
|
||||
|
||||
// Urilize the link
|
||||
headingText = (options & AutoIdentifierOptions.GitHub) != 0
|
||||
? LinkHelper.UrilizeAsGfm(headingText)
|
||||
: LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
|
||||
string headingText = (_options & AutoIdentifierOptions.GitHub) != 0
|
||||
? LinkHelper.UrilizeAsGfm(rawHeadingText)
|
||||
: LinkHelper.Urilize(rawHeadingText, (_options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
|
||||
|
||||
_rendererCache.Release(stripRenderer);
|
||||
|
||||
// If the heading is empty, use the word "section" instead
|
||||
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
|
||||
@@ -197,7 +202,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
{
|
||||
protected override HtmlRenderer NewInstance()
|
||||
{
|
||||
var headingWriter = new StringWriter();
|
||||
var headingWriter = new FastStringWriter();
|
||||
var stripRenderer = new HtmlRenderer(headingWriter)
|
||||
{
|
||||
// Set to false both to avoid having any HTML tags in the output
|
||||
@@ -209,7 +214,9 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
|
||||
protected override void Reset(HtmlRenderer instance)
|
||||
{
|
||||
instance.Reset();
|
||||
instance.ResetInternal();
|
||||
|
||||
((FastStringWriter)instance.Writer).Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,9 @@ namespace Markdig.Extensions.AutoLinks;
|
||||
/// Extension to automatically create <see cref="LinkInline"/> when a link url http: or mailto: is found.
|
||||
/// </summary>
|
||||
/// <seealso cref="IMarkdownExtension" />
|
||||
public class AutoLinkExtension : IMarkdownExtension
|
||||
public class AutoLinkExtension(AutoLinkOptions? options) : IMarkdownExtension
|
||||
{
|
||||
public readonly AutoLinkOptions Options;
|
||||
|
||||
public AutoLinkExtension(AutoLinkOptions? options)
|
||||
{
|
||||
Options = options ?? new AutoLinkOptions();
|
||||
}
|
||||
public readonly AutoLinkOptions Options = options ?? new AutoLinkOptions();
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,195 +24,180 @@ public class AutoLinkParser : InlineParser
|
||||
{
|
||||
Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
OpeningCharacters = new char[]
|
||||
{
|
||||
OpeningCharacters =
|
||||
[
|
||||
'h', // for http:// and https://
|
||||
'f', // for ftp://
|
||||
'm', // for mailto:
|
||||
't', // for tel:
|
||||
'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;
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ public class CustomContainerParser : FencedBlockParserBase<CustomContainer>
|
||||
/// </summary>
|
||||
public CustomContainerParser()
|
||||
{
|
||||
OpeningCharacters = new [] {':'};
|
||||
OpeningCharacters = [':'];
|
||||
|
||||
// We don't need a prefix
|
||||
InfoPrefix = null;
|
||||
|
||||
@@ -18,7 +18,7 @@ public class DefinitionListParser : BlockParser
|
||||
/// </summary>
|
||||
public DefinitionListParser()
|
||||
{
|
||||
OpeningCharacters = new [] {':', '~'};
|
||||
OpeningCharacters = [':', '~'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,6 +1786,6 @@ public class EmojiMapping
|
||||
ThrowHelper.ArgumentException(string.Format("Smiley {0} is already present in the emoji mapping", smiley.Key));
|
||||
}
|
||||
|
||||
OpeningCharacters = new List<char>(firstChars).ToArray();
|
||||
OpeningCharacters = [.. firstChars];
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class FigureBlockParser : BlockParser
|
||||
/// </summary>
|
||||
public FigureBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '^' };
|
||||
OpeningCharacters = ['^'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
@@ -19,7 +19,7 @@ public class FooterBlockParser : BlockParser
|
||||
/// </summary>
|
||||
public FooterBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'^'};
|
||||
OpeningCharacters = ['^'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
@@ -22,7 +22,7 @@ public class FootnoteParser : BlockParser
|
||||
|
||||
public FootnoteParser()
|
||||
{
|
||||
OpeningCharacters = new [] {'['};
|
||||
OpeningCharacters = ['['];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -49,7 +49,7 @@ public class FootnoteParser : BlockParser
|
||||
|
||||
// Advance the column
|
||||
int deltaColumn = processor.Start - start;
|
||||
processor.Column = processor.Column + deltaColumn;
|
||||
processor.Column += deltaColumn;
|
||||
|
||||
processor.NextChar(); // Skip ':'
|
||||
|
||||
@@ -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
|
||||
@@ -170,10 +173,8 @@ public class FootnoteParser : BlockParser
|
||||
paragraphBlock = new ParagraphBlock();
|
||||
footnote.Add(paragraphBlock);
|
||||
}
|
||||
if (paragraphBlock.Inline == null)
|
||||
{
|
||||
paragraphBlock.Inline = new ContainerInline();
|
||||
}
|
||||
|
||||
paragraphBlock.Inline ??= new ContainerInline();
|
||||
|
||||
foreach (var link in footnote.Links)
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ public class GenericAttributesParser : InlineParser
|
||||
/// </summary>
|
||||
public GenericAttributesParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '{' };
|
||||
OpeningCharacters = ['{'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -136,10 +136,7 @@ public class GenericAttributesParser : InlineParser
|
||||
var text = slice.Text.Substring(start, end - start + 1);
|
||||
if (isClass)
|
||||
{
|
||||
if (classes is null)
|
||||
{
|
||||
classes = new List<string>();
|
||||
}
|
||||
classes ??= new List<string>();
|
||||
classes.Add(text);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -20,7 +20,7 @@ public class MathBlockParser : FencedBlockParserBase<MathBlock>
|
||||
/// </summary>
|
||||
public MathBlockParser()
|
||||
{
|
||||
OpeningCharacters = new [] {'$'};
|
||||
OpeningCharacters = ['$'];
|
||||
// We expect to match only a $$, no less, no more
|
||||
MinimumMatchCount = 2;
|
||||
MaximumMatchCount = 2;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class MathInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public MathInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '$' };
|
||||
OpeningCharacters = ['$'];
|
||||
DefaultClass = "math";
|
||||
}
|
||||
|
||||
@@ -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(slice.End)),
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Delimiter = match,
|
||||
|
||||
@@ -11,23 +11,19 @@ namespace Markdig.Extensions.MediaLinks;
|
||||
|
||||
public class HostProviderBuilder
|
||||
{
|
||||
private sealed class DelegateProvider : IHostProvider
|
||||
private sealed class DelegateProvider(
|
||||
string hostPrefix,
|
||||
Func<Uri, string?> handler,
|
||||
bool allowFullscreen = true,
|
||||
string? className = null) : IHostProvider
|
||||
{
|
||||
public DelegateProvider(string hostPrefix, Func<Uri, string?> handler, bool allowFullscreen = true, string? className = null)
|
||||
{
|
||||
HostPrefix = hostPrefix;
|
||||
Delegate = handler;
|
||||
AllowFullScreen = allowFullscreen;
|
||||
Class = className;
|
||||
}
|
||||
public string HostPrefix { get; } = hostPrefix;
|
||||
|
||||
public string HostPrefix { get; }
|
||||
public Func<Uri, string?> Delegate { get; } = handler;
|
||||
|
||||
public Func<Uri, string?> Delegate { get; }
|
||||
public bool AllowFullScreen { get; } = allowFullscreen;
|
||||
|
||||
public bool AllowFullScreen { get; }
|
||||
|
||||
public string? Class { get; }
|
||||
public string? Class { get; } = className;
|
||||
|
||||
public bool TryHandle(Uri mediaUri, bool isSchemaRelative, [NotNullWhen(true)] out string? iframeUrl)
|
||||
{
|
||||
@@ -59,19 +55,19 @@ 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
|
||||
|
||||
private static readonly string[] SplitAnd = { "&" };
|
||||
private static readonly string[] SplitAnd = ["&"];
|
||||
private static string[] SplitQuery(Uri uri)
|
||||
{
|
||||
var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1);
|
||||
@@ -96,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(
|
||||
|
||||
@@ -50,27 +50,32 @@ public class MediaLinkExtension : IMarkdownExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
var url = linkInline.Url;
|
||||
bool isSchemaRelative = false;
|
||||
// Only process absolute Uri
|
||||
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out Uri? uri) || !uri.IsAbsoluteUri)
|
||||
|
||||
// force // schema to an absolute url
|
||||
if (url.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
// see https://tools.ietf.org/html/rfc3986#section-4.2
|
||||
// since relative uri doesn't support many properties, "http" is used as a placeholder here.
|
||||
if (linkInline.Url.StartsWith("//", StringComparison.Ordinal) && Uri.TryCreate("http:" + linkInline.Url, UriKind.Absolute, out uri))
|
||||
url = "https:" + url;
|
||||
isSchemaRelative = true;
|
||||
}
|
||||
|
||||
// Make sure we have a valid absolute/relative url
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri? uri)) // || !uri.IsAbsoluteUri)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// iFrame has to be absolute path
|
||||
if (uri.IsAbsoluteUri)
|
||||
{
|
||||
if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline))
|
||||
{
|
||||
isSchemaRelative = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// audio/video has can have relative path
|
||||
if (TryGuessAudioVideoFile(uri, isSchemaRelative, renderer, linkInline))
|
||||
{
|
||||
return true;
|
||||
@@ -93,7 +98,10 @@ public class MediaLinkExtension : IMarkdownExtension
|
||||
|
||||
private bool TryGuessAudioVideoFile(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
|
||||
string path = uri.IsAbsoluteUri
|
||||
? uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)
|
||||
: uri.ToString();
|
||||
|
||||
// Otherwise try to detect if we have an audio/video from the file extension
|
||||
var lastDot = path.LastIndexOf('.');
|
||||
if (lastDot >= 0 &&
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -17,7 +17,7 @@ public class NoFollowLinksExtension : IMarkdownExtension
|
||||
|
||||
public NoFollowLinksExtension()
|
||||
{
|
||||
_referralLinksExtension = new ReferralLinksExtension(new[] { "nofollow" });
|
||||
_referralLinksExtension = new ReferralLinksExtension(["nofollow"]);
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
|
||||
@@ -19,7 +19,7 @@ public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
|
||||
/// </summary>
|
||||
public SmartyPantsInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'};
|
||||
OpeningCharacters = ['\'', '"', '<', '>', '.', '-'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class GridTableParser : BlockParser
|
||||
{
|
||||
public GridTableParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '+' };
|
||||
OpeningCharacters = ['+'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -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,9 +143,9 @@ 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() || !IsRowSeperator(line))
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
|
||||
{
|
||||
hasRowSpan = true;
|
||||
columnSlice.CurrentCell.RowSpan++;
|
||||
@@ -158,7 +159,7 @@ public class GridTableParser : BlockParser
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRowSeperator(StringSlice slice)
|
||||
private static bool IsRowSeparator(StringSlice slice)
|
||||
{
|
||||
char c = slice.CurrentChar;
|
||||
do
|
||||
@@ -256,14 +257,14 @@ 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;
|
||||
}
|
||||
}
|
||||
sliceForCell.TrimEnd();
|
||||
|
||||
if (!isRowLine || !IsRowSeperator(sliceForCell))
|
||||
if (!isRowLine || !IsRowSeparator(sliceForCell))
|
||||
{
|
||||
if (columnSlice.CurrentCell is null)
|
||||
{
|
||||
@@ -273,10 +274,7 @@ public class GridTableParser : BlockParser
|
||||
ColumnIndex = i
|
||||
};
|
||||
|
||||
if (columnSlice.BlockProcessor is null)
|
||||
{
|
||||
columnSlice.BlockProcessor = processor.CreateChild();
|
||||
}
|
||||
columnSlice.BlockProcessor ??= processor.CreateChild();
|
||||
|
||||
// Ensure that the BlockParser is aware that the TableCell is the top-level container
|
||||
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
|
||||
|
||||
@@ -10,21 +10,15 @@ namespace Markdig.Extensions.Tables;
|
||||
/// <summary>
|
||||
/// Internal state used by the <see cref="GridTableParser"/>
|
||||
/// </summary>
|
||||
internal sealed class GridTableState
|
||||
internal sealed class GridTableState(int start, bool expectRow)
|
||||
{
|
||||
public GridTableState(int start, bool expectRow)
|
||||
{
|
||||
Start = start;
|
||||
ExpectRow = expectRow;
|
||||
}
|
||||
|
||||
public int Start { get; }
|
||||
public int Start { get; } = start;
|
||||
|
||||
public StringLineGroup Lines;
|
||||
|
||||
public List<ColumnSlice>? ColumnSlices { get; private set; }
|
||||
|
||||
public bool ExpectRow { get; }
|
||||
public bool ExpectRow { get; } = expectRow;
|
||||
|
||||
public int StartRowGroup { get; set; }
|
||||
|
||||
@@ -45,26 +39,18 @@ internal sealed class GridTableState
|
||||
ColumnSlices.Add(new ColumnSlice(start, end, align));
|
||||
}
|
||||
|
||||
public sealed class ColumnSlice
|
||||
public sealed class ColumnSlice(int start, int end, TableColumnAlign? align)
|
||||
{
|
||||
public ColumnSlice(int start, int end, TableColumnAlign? align)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Align = align;
|
||||
CurrentColumnSpan = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index position of this column (after the |)
|
||||
/// </summary>
|
||||
public int Start { get; }
|
||||
public int Start { get; } = start;
|
||||
|
||||
public int End { get; }
|
||||
public int End { get; } = end;
|
||||
|
||||
public TableColumnAlign? Align { get; }
|
||||
public TableColumnAlign? Align { get; } = align;
|
||||
|
||||
public int CurrentColumnSpan { get; set; }
|
||||
public int CurrentColumnSpan { get; set; } = -1;
|
||||
|
||||
public int PreviousColumnSpan { get; set; }
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public class PipeTableBlockParser : BlockParser
|
||||
/// </summary>
|
||||
public PipeTableBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'-'};
|
||||
OpeningCharacters = ['-'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -25,12 +25,12 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipeTableParser" /> class.
|
||||
/// </summary>
|
||||
/// <param name="lineBreakParser">The linebreak parser to use</param>
|
||||
/// <param name="lineBreakParser">The line break parser to use</param>
|
||||
/// <param name="options">The options.</param>
|
||||
public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions? options = null)
|
||||
{
|
||||
this.lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser));
|
||||
OpeningCharacters = new[] { '|', '\n', '\r' };
|
||||
OpeningCharacters = ['|', '\n', '\r'];
|
||||
Options = options ?? new PipeTableOptions();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -443,6 +457,11 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
var paragraph = (ParagraphBlock) cell[0];
|
||||
state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true);
|
||||
if (paragraph.Inline?.LastChild is not null)
|
||||
{
|
||||
paragraph.Inline.Span.End = paragraph.Inline.LastChild.Span.End;
|
||||
paragraph.UpdateSpanEnd(paragraph.Inline.LastChild.Span.End);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear cells when we are done
|
||||
@@ -462,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)
|
||||
{
|
||||
@@ -473,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')
|
||||
{
|
||||
@@ -488,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]))
|
||||
@@ -510,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)
|
||||
@@ -537,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;
|
||||
}
|
||||
|
||||
@@ -557,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)
|
||||
@@ -632,10 +674,10 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
|
||||
public int LineIndex { get; set; }
|
||||
|
||||
public List<Inline> ColumnAndLineDelimiters { get; } = new();
|
||||
public List<Inline> ColumnAndLineDelimiters { get; } = [];
|
||||
|
||||
public List<TableCell> Cells { get; } = new();
|
||||
public List<TableCell> Cells { get; } = [];
|
||||
|
||||
public List<Inline> EndOfLines { get; } = new();
|
||||
public List<Inline> EndOfLines { get; } = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class TaskListInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public TaskListInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'['};
|
||||
OpeningCharacters = ['['];
|
||||
ListClass = "contains-task-list";
|
||||
ListItemClass = "task-list-item";
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class YamlFrontMatterParser : BlockParser
|
||||
/// </summary>
|
||||
public YamlFrontMatterParser()
|
||||
{
|
||||
this.OpeningCharacters = new[] { '-' };
|
||||
OpeningCharacters = ['-'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
@@ -7,14 +7,9 @@ using Markdig.Syntax;
|
||||
namespace Markdig.Helpers;
|
||||
|
||||
// Used to avoid the overhead of type covariance checks
|
||||
internal readonly struct BlockWrapper : IEquatable<BlockWrapper>
|
||||
internal readonly struct BlockWrapper(Block block) : IEquatable<BlockWrapper>
|
||||
{
|
||||
public readonly Block Block;
|
||||
|
||||
public BlockWrapper(Block block)
|
||||
{
|
||||
Block = block;
|
||||
}
|
||||
public readonly Block Block = block;
|
||||
|
||||
public static implicit operator Block(BlockWrapper wrapper) => wrapper.Block;
|
||||
|
||||
@@ -22,7 +17,7 @@ internal readonly struct BlockWrapper : IEquatable<BlockWrapper>
|
||||
|
||||
public bool Equals(BlockWrapper other) => ReferenceEquals(Block, other.Block);
|
||||
|
||||
public override bool Equals(object obj) => Block.Equals(obj);
|
||||
public override bool Equals(object? obj) => Block.Equals(obj);
|
||||
|
||||
public override int GetHashCode() => Block.GetHashCode();
|
||||
}
|
||||
|
||||
@@ -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,26 +20,65 @@ 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";
|
||||
|
||||
private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' };
|
||||
// 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) =>
|
||||
c is '−' or '-' or '†' or '‡';
|
||||
|
||||
public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose)
|
||||
{
|
||||
pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation);
|
||||
c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation);
|
||||
|
||||
var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc);
|
||||
var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c);
|
||||
var prevIsExcepted = prevIsPunctuation && IsPunctuationException(pc);
|
||||
var nextIsExcepted = nextIsPunctuation && IsPunctuationException(c);
|
||||
|
||||
// A left-flanking delimiter run is a delimiter run that is
|
||||
// (1) not followed by Unicode whitespace, and either
|
||||
@@ -99,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;
|
||||
}
|
||||
@@ -110,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)]
|
||||
@@ -126,72 +180,67 @@ public static class CharHelper
|
||||
return (column & (TabSize - 1)) != 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Contains(this char[] charList, char c)
|
||||
{
|
||||
foreach (char ch in charList)
|
||||
{
|
||||
if (ch == c)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsWhitespace(this char c)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsControl(this char c)
|
||||
{
|
||||
return c < ' ' || char.IsControl(c);
|
||||
return char.IsControl(c);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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
|
||||
@@ -205,48 +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.
|
||||
space = false;
|
||||
UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
punctuation = category == UnicodeCategory.ConnectorPunctuation
|
||||
|| category == UnicodeCategory.DashPunctuation
|
||||
|| category == UnicodeCategory.OpenPunctuation
|
||||
|| category == UnicodeCategory.ClosePunctuation
|
||||
|| category == UnicodeCategory.InitialQuotePunctuation
|
||||
|| category == UnicodeCategory.FinalQuotePunctuation
|
||||
|| category == UnicodeCategory.OtherPunctuation;
|
||||
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
|
||||
{
|
||||
var category = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
return category == UnicodeCategory.ConnectorPunctuation
|
||||
|| category == UnicodeCategory.DashPunctuation
|
||||
|| category == UnicodeCategory.OpenPunctuation
|
||||
|| category == UnicodeCategory.ClosePunctuation
|
||||
|| category == UnicodeCategory.InitialQuotePunctuation
|
||||
|| category == UnicodeCategory.FinalQuotePunctuation
|
||||
|| category == UnicodeCategory.OtherPunctuation;
|
||||
return NonAscii(c);
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -286,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)]
|
||||
@@ -313,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)
|
||||
{
|
||||
@@ -729,11 +763,11 @@ public static class CharHelper
|
||||
}
|
||||
|
||||
// Used by ListExtraItemParser to format numbers from 1 - 26
|
||||
private static readonly string[] smallNumberStringCache = {
|
||||
private static readonly string[] smallNumberStringCache = [
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
|
||||
"20", "21", "22", "23", "24", "25", "26",
|
||||
};
|
||||
];
|
||||
|
||||
internal static string SmallNumberToString(int number)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -2,14 +2,9 @@
|
||||
// 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.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
using System.Numerics;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
#endif
|
||||
|
||||
namespace Markdig.Helpers;
|
||||
|
||||
@@ -19,13 +14,9 @@ namespace Markdig.Helpers;
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public sealed class CharacterMap<T> where T : class
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
private readonly Vector128<byte> _asciiBitmap;
|
||||
#endif
|
||||
|
||||
private readonly T[] asciiMap;
|
||||
private readonly Dictionary<uint, T>? nonAsciiMap;
|
||||
private readonly BoolVector128 isOpeningCharacter;
|
||||
private readonly SearchValues<char> _values;
|
||||
private readonly T[] _asciiMap;
|
||||
private readonly FrozenDictionary<uint, T>? _nonAsciiMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterMap{T}"/> class.
|
||||
@@ -35,64 +26,44 @@ public sealed class CharacterMap<T> where T : class
|
||||
public CharacterMap(IEnumerable<KeyValuePair<char, T>> maps)
|
||||
{
|
||||
if (maps is null) ThrowHelper.ArgumentNullException(nameof(maps));
|
||||
|
||||
var charSet = new HashSet<char>();
|
||||
int maxChar = 0;
|
||||
|
||||
foreach (var map in maps)
|
||||
{
|
||||
var openingChar = map.Key;
|
||||
charSet.Add(openingChar);
|
||||
|
||||
if (openingChar < 128)
|
||||
{
|
||||
maxChar = Math.Max(maxChar, openingChar);
|
||||
|
||||
if (openingChar == 0)
|
||||
{
|
||||
ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
nonAsciiMap ??= new Dictionary<uint, T>();
|
||||
}
|
||||
charSet.Add(map.Key);
|
||||
}
|
||||
|
||||
OpeningCharacters = charSet.ToArray();
|
||||
OpeningCharacters = [.. charSet];
|
||||
Array.Sort(OpeningCharacters);
|
||||
|
||||
asciiMap = new T[maxChar + 1];
|
||||
_asciiMap = new T[128];
|
||||
Dictionary<uint, T>? nonAsciiMap = null;
|
||||
|
||||
foreach (var state in maps)
|
||||
{
|
||||
char openingChar = state.Key;
|
||||
if (openingChar < 128)
|
||||
{
|
||||
asciiMap[openingChar] ??= state.Value;
|
||||
isOpeningCharacter.Set(openingChar);
|
||||
_asciiMap[openingChar] ??= state.Value;
|
||||
}
|
||||
else if (!nonAsciiMap!.ContainsKey(openingChar))
|
||||
else
|
||||
{
|
||||
nonAsciiMap[openingChar] = state.Value;
|
||||
nonAsciiMap ??= [];
|
||||
|
||||
if (!nonAsciiMap.ContainsKey(openingChar))
|
||||
{
|
||||
nonAsciiMap[openingChar] = state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
if (nonAsciiMap is null)
|
||||
_values = SearchValues.Create(OpeningCharacters);
|
||||
|
||||
if (nonAsciiMap is not null)
|
||||
{
|
||||
long bitmap_0_3 = 0;
|
||||
long bitmap_4_7 = 0;
|
||||
|
||||
foreach (char openingChar in OpeningCharacters)
|
||||
{
|
||||
int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3);
|
||||
if (position < 64) bitmap_0_3 |= 1L << position;
|
||||
else bitmap_4_7 |= 1L << (position - 64);
|
||||
}
|
||||
|
||||
_asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte();
|
||||
_nonAsciiMap = nonAsciiMap.ToFrozenDictionary();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,7 +81,7 @@ public sealed class CharacterMap<T> where T : class
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
T[] asciiMap = this.asciiMap;
|
||||
T[] asciiMap = _asciiMap;
|
||||
if (openingChar < (uint)asciiMap.Length)
|
||||
{
|
||||
return asciiMap[openingChar];
|
||||
@@ -118,13 +89,12 @@ public sealed class CharacterMap<T> where T : class
|
||||
else
|
||||
{
|
||||
T? map = null;
|
||||
nonAsciiMap?.TryGetValue(openingChar, out map);
|
||||
_nonAsciiMap?.TryGetValue(openingChar, out map);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Searches for an opening character from a registered parser in the specified string.
|
||||
/// </summary>
|
||||
@@ -132,167 +102,20 @@ public sealed class CharacterMap<T> where T : class
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int IndexOfOpeningCharacter(string text, int start, int end)
|
||||
{
|
||||
Debug.Assert(text is not null);
|
||||
Debug.Assert(start >= 0 && end >= 0);
|
||||
Debug.Assert(end - start + 1 >= 0);
|
||||
Debug.Assert(end - start + 1 <= text.Length);
|
||||
|
||||
if (nonAsciiMap is null)
|
||||
ReadOnlySpan<char> span = text.AsSpan(start, end - start + 1);
|
||||
|
||||
int index = span.IndexOfAny(_values);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
if (Ssse3.IsSupported && BitConverter.IsLittleEndian)
|
||||
{
|
||||
// Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm
|
||||
// Optimized for sets in the [1, 127] range
|
||||
|
||||
int lengthMinusOne = end - start;
|
||||
int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128<short>.Count - 1);
|
||||
int finalStart = start + charsToProcessVectorized;
|
||||
|
||||
if (start < finalStart)
|
||||
{
|
||||
ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start);
|
||||
Vector128<byte> bitmap = _asciiBitmap;
|
||||
do
|
||||
{
|
||||
// Load 32 bytes (16 chars) into two Vector128<short>s (chars)
|
||||
// Drop the high byte of each char
|
||||
// Pack the remaining bytes into a single Vector128<byte>
|
||||
Vector128<byte> input = Sse2.PackUnsignedSaturate(
|
||||
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref textStartRef)),
|
||||
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref textStartRef, Vector128<short>.Count))));
|
||||
|
||||
// Extract the higher nibble of each character ((input >> 4) & 0xF)
|
||||
Vector128<byte> higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF));
|
||||
|
||||
// Lookup the matching higher nibble for each character based on the lower nibble
|
||||
// PSHUFB will set the result to 0 for any non-ASCII (> 127) character
|
||||
Vector128<byte> bitsets = Ssse3.Shuffle(bitmap, input);
|
||||
|
||||
// Calculate a bitmask (1 << (higherNibble % 8)) for each character
|
||||
Vector128<byte> bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles);
|
||||
|
||||
// Check which characters are present in the set
|
||||
// We are relying on bitsets being zero for non-ASCII characters
|
||||
Vector128<byte> result = Sse2.And(bitsets, bitmask);
|
||||
|
||||
if (!result.Equals(Vector128<byte>.Zero))
|
||||
{
|
||||
int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128<byte>.Zero));
|
||||
return start + BitOperations.TrailingZeroCount((uint)resultMask);
|
||||
}
|
||||
|
||||
start += 2 * Vector128<short>.Count;
|
||||
textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128<short>.Count);
|
||||
}
|
||||
while (start != finalStart);
|
||||
}
|
||||
}
|
||||
|
||||
ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference());
|
||||
for (; start <= end; start++)
|
||||
{
|
||||
if (IntPtr.Size == 4)
|
||||
{
|
||||
uint c = Unsafe.Add(ref textRef, start);
|
||||
if (c < 128 && isOpeningCharacter[c])
|
||||
{
|
||||
return start;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong c = Unsafe.Add(ref textRef, start);
|
||||
if (c < 128 && isOpeningCharacter[c])
|
||||
{
|
||||
return start;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
unsafe
|
||||
{
|
||||
fixed (char* pText = text)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
char c = pText[i];
|
||||
if (c < 128 && isOpeningCharacter[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
index += start;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IndexOfOpeningCharacterNonAscii(text, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
private int IndexOfOpeningCharacterNonAscii(string text, int start, int end)
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference());
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
char c = Unsafe.Add(ref textRef, i);
|
||||
if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
#else
|
||||
unsafe
|
||||
{
|
||||
fixed (char* pText = text)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
char c = pText[i];
|
||||
if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe struct BoolVector128
|
||||
{
|
||||
private fixed bool values[128];
|
||||
|
||||
public void Set(char c)
|
||||
{
|
||||
Debug.Assert(c < 128);
|
||||
values[c] = true;
|
||||
}
|
||||
|
||||
public readonly bool this[uint c]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
Debug.Assert(c < 128);
|
||||
return values[c];
|
||||
}
|
||||
}
|
||||
public readonly bool this[ulong c]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
Debug.Assert(c < 128 && IntPtr.Size == 8);
|
||||
return values[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Markdig.Helpers;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,41 +59,31 @@ public static class EntityHelper
|
||||
/// <returns>The unicode character set or <c>null</c> if the entity was not recognized.</returns>
|
||||
public static string DecodeEntity(int utf32)
|
||||
{
|
||||
if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343))
|
||||
if (utf32 == 0 || !UnicodeUtility.IsValidUnicodeScalar((uint)utf32))
|
||||
return CharHelper.ReplacementCharString;
|
||||
|
||||
if (utf32 < 65536)
|
||||
if (UnicodeUtility.IsBmpCodePoint((uint)utf32))
|
||||
return char.ToString((char)utf32);
|
||||
|
||||
utf32 -= 65536;
|
||||
return new string(
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
stackalloc
|
||||
#else
|
||||
new
|
||||
#endif
|
||||
char[]
|
||||
{
|
||||
(char)((uint)utf32 / 1024 + 55296),
|
||||
(char)((uint)utf32 % 1024 + 56320)
|
||||
});
|
||||
UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)utf32, out char high, out char low);
|
||||
return new string([high, low]);
|
||||
}
|
||||
|
||||
internal static void DecodeEntity(int utf32, ref ValueStringBuilder sb)
|
||||
{
|
||||
if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343))
|
||||
if (utf32 == 0 || !UnicodeUtility.IsValidUnicodeScalar((uint)utf32))
|
||||
{
|
||||
sb.Append(CharHelper.ReplacementChar);
|
||||
}
|
||||
else if (utf32 < 65536)
|
||||
else if (UnicodeUtility.IsBmpCodePoint((uint)utf32))
|
||||
{
|
||||
sb.Append((char)utf32);
|
||||
}
|
||||
else
|
||||
{
|
||||
utf32 -= 65536;
|
||||
sb.Append((char)((uint)utf32 / 1024 + 55296));
|
||||
sb.Append((char)((uint)utf32 % 1024 + 56320));
|
||||
UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)utf32, out char high, out char low);
|
||||
sb.Append(high);
|
||||
sb.Append(low);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
@@ -278,8 +278,7 @@ internal sealed class FastStringWriter : TextWriter
|
||||
_pos = 0;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _chars.AsSpan(0, _pos).ToString();
|
||||
}
|
||||
public override string ToString() => AsSpan().ToString();
|
||||
|
||||
public ReadOnlySpan<char> AsSpan() => _chars.AsSpan(0, _pos);
|
||||
}
|
||||
|
||||
27
src/Markdig/Helpers/HexConverter.cs
Normal file
27
src/Markdig/Helpers/HexConverter.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Markdig.Helpers;
|
||||
|
||||
// Based on https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/HexConverter.cs
|
||||
internal static class HexConverter
|
||||
{
|
||||
public enum Casing : uint
|
||||
{
|
||||
Upper = 0,
|
||||
Lower = 0x2020U,
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ToCharsBuffer(byte value, Span<char> buffer, int startingIndex = 0, Casing casing = Casing.Upper)
|
||||
{
|
||||
uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U;
|
||||
uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing;
|
||||
|
||||
buffer[startingIndex + 1] = (char)(packedResult & 0xFF);
|
||||
buffer[startingIndex] = (char)(packedResult >> 8);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -12,8 +13,8 @@ namespace Markdig.Helpers;
|
||||
/// </summary>
|
||||
public static class HtmlHelper
|
||||
{
|
||||
private static readonly char[] SearchBackAndAmp = { '\\', '&' };
|
||||
private static readonly char[] SearchAmp = { '&' };
|
||||
private static readonly char[] SearchBackAndAmp = ['\\', '&'];
|
||||
private static readonly char[] SearchAmp = ['&'];
|
||||
private static readonly string[] EscapeUrlsForAscii = new string[128];
|
||||
|
||||
static HtmlHelper()
|
||||
@@ -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)
|
||||
|
||||
@@ -53,7 +53,7 @@ public struct LineReader
|
||||
else
|
||||
{
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
ReadOnlySpan<char> span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition);
|
||||
ReadOnlySpan<char> span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), sourcePosition), end - sourcePosition);
|
||||
#else
|
||||
ReadOnlySpan<char> span = text.AsSpan(sourcePosition);
|
||||
#endif
|
||||
@@ -65,7 +65,7 @@ public struct LineReader
|
||||
newSourcePosition = end + 1;
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r')
|
||||
if (Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), end) == '\r')
|
||||
#else
|
||||
if ((uint)end < (uint)text.Length && text[end] == '\r')
|
||||
#endif
|
||||
|
||||
@@ -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;
|
||||
@@ -19,6 +21,11 @@ public static class LinkHelper
|
||||
}
|
||||
|
||||
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
|
||||
{
|
||||
return Urilize(headingText.AsSpan(), allowOnlyAscii, keepOpeningDigits);
|
||||
}
|
||||
|
||||
public static string Urilize(ReadOnlySpan<char> headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
|
||||
{
|
||||
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]);
|
||||
@@ -95,15 +102,24 @@ public static class LinkHelper
|
||||
}
|
||||
|
||||
public static string UrilizeAsGfm(string headingText)
|
||||
{
|
||||
return UrilizeAsGfm(headingText.AsSpan());
|
||||
}
|
||||
|
||||
public static string UrilizeAsGfm(ReadOnlySpan<char> headingText)
|
||||
{
|
||||
// Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
|
||||
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
for (int i = 0; i < headingText.Length; i++)
|
||||
{
|
||||
var c = headingText[i];
|
||||
if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_')
|
||||
if (char.IsLetterOrDigit(c) || c == '-' || c == '_')
|
||||
{
|
||||
headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c));
|
||||
headingBuffer.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
else if (c == ' ')
|
||||
{
|
||||
headingBuffer.Append('-');
|
||||
}
|
||||
}
|
||||
return headingBuffer.ToString();
|
||||
@@ -152,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;
|
||||
}
|
||||
@@ -272,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +411,7 @@ public static class LinkHelper
|
||||
{
|
||||
// Skip ')'
|
||||
text.SkipChar();
|
||||
title ??= string.Empty;
|
||||
// not to normalize nulls into empty strings, since LinkInline.Title property is nullable.
|
||||
}
|
||||
|
||||
return isValid;
|
||||
@@ -531,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
|
||||
@@ -627,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
|
||||
@@ -744,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;
|
||||
@@ -760,8 +739,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -800,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))
|
||||
@@ -891,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;
|
||||
@@ -907,8 +888,6 @@ public static class LinkHelper
|
||||
break;
|
||||
}
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
buffer.Append(c);
|
||||
|
||||
} while (c != '\0');
|
||||
@@ -947,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))
|
||||
@@ -1022,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 (.).
|
||||
@@ -1035,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;
|
||||
|
||||
@@ -1058,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;
|
||||
@@ -1068,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
|
||||
}
|
||||
@@ -1145,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
|
||||
@@ -1285,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
|
||||
@@ -1585,4 +1565,4 @@ public static class LinkHelper
|
||||
label = buffer.ToString();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public abstract class ObjectCache<T> where T : class
|
||||
/// <returns></returns>
|
||||
public T Get()
|
||||
{
|
||||
if (_builders.TryDequeue(out T instance))
|
||||
if (_builders.TryDequeue(out T? instance))
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -187,9 +187,32 @@ public struct StringLineGroup : IEnumerable
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
public struct Enumerator(StringLineGroup parent) : IEnumerator
|
||||
{
|
||||
return Lines.GetEnumerator();
|
||||
private readonly StringLineGroup _parent = parent;
|
||||
private int _index = -1;
|
||||
|
||||
public object Current => _parent.Lines[_index];
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
return ++_index < _parent.Count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private void IncreaseCapacity()
|
||||
@@ -334,9 +357,12 @@ public struct StringLineGroup : IEnumerable
|
||||
goto Return;
|
||||
|
||||
MoveToNewLine:
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
_currentSlice = _lines.Lines[SliceIndex];
|
||||
if (SliceIndex < _lines.Lines.Length - 1)
|
||||
{
|
||||
SliceIndex++;
|
||||
_offset = -1;
|
||||
_currentSlice = _lines.Lines[SliceIndex];
|
||||
}
|
||||
|
||||
Return:
|
||||
return CurrentChar;
|
||||
@@ -420,25 +446,21 @@ public struct StringLineGroup : IEnumerable
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct LineOffset
|
||||
public readonly struct LineOffset(
|
||||
int linePosition,
|
||||
int column,
|
||||
int offset,
|
||||
int start,
|
||||
int end)
|
||||
{
|
||||
public LineOffset(int linePosition, int column, int offset, int start, int end)
|
||||
{
|
||||
LinePosition = linePosition;
|
||||
Column = column;
|
||||
Offset = offset;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
public readonly int LinePosition = linePosition;
|
||||
|
||||
public readonly int LinePosition;
|
||||
public readonly int Column = column;
|
||||
|
||||
public readonly int Column;
|
||||
public readonly int Offset = offset;
|
||||
|
||||
public readonly int Offset;
|
||||
public readonly int Start = start;
|
||||
|
||||
public readonly int Start;
|
||||
|
||||
public readonly int End;
|
||||
public readonly int End = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -475,7 +475,7 @@ public struct StringSlice : ICharIterator
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length);
|
||||
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start), length);
|
||||
#else
|
||||
return text.AsSpan(start, length);
|
||||
#endif
|
||||
|
||||
@@ -80,7 +80,7 @@ internal static class ThrowHelper
|
||||
if (depth > limit)
|
||||
DepthLimitExceeded();
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
[DoesNotReturn]
|
||||
static void DepthLimitExceeded() => throw new ArgumentException("Markdown elements in the input are too deeply nested - depth limit exceeded. Input is most likely not sensible or is a very large table.");
|
||||
}
|
||||
|
||||
|
||||
30
src/Markdig/Helpers/UnicodeUtility.cs
Normal file
30
src/Markdig/Helpers/UnicodeUtility.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace System.Text;
|
||||
|
||||
// Based on https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs
|
||||
internal static class UnicodeUtility
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsValidUnicodeScalar(uint value)
|
||||
{
|
||||
return ((value - 0x110000u) ^ 0xD800u) >= 0xFFEF0800u;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void GetUtf16SurrogatesFromSupplementaryPlaneScalar(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint)
|
||||
{
|
||||
Debug.Assert(IsValidUnicodeScalar(value) && IsBmpCodePoint(value));
|
||||
|
||||
highSurrogateCodePoint = (char)((value + ((0xD800u - 0x40u) << 10)) >> 10);
|
||||
lowSurrogateCodePoint = (char)((value & 0x3FFu) + 0xDC00u);
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,16 @@
|
||||
<Copyright>Alexandre Mutel</Copyright>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Authors>Alexandre Mutel</Authors>
|
||||
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net6.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>10</LangVersion>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -24,22 +24,18 @@
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="System.Memory" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../../img/markdig.png" Pack="true" PackagePath="" />
|
||||
<None Include="../../readme.md" Pack="true" PackagePath="/"/>
|
||||
<PackageReference Include="MinVer" Version="3.1.0">
|
||||
<PackageReference Include="MinVer" Version="4.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.*" PrivateAssets="All"/>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.*" PrivateAssets="All"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PatchVersion" AfterTargets="MinVer">
|
||||
|
||||
@@ -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;
|
||||
@@ -17,21 +17,15 @@ namespace Markdig;
|
||||
/// <summary>
|
||||
/// Provides methods for parsing a Markdown string to a syntax tree and converting it to other formats.
|
||||
/// </summary>
|
||||
public static partial class Markdown
|
||||
public static class Markdown
|
||||
{
|
||||
public static string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Version == null)
|
||||
_Version = ((AssemblyFileVersionAttribute)typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault())?.Version ?? "Unknown";
|
||||
return _Version;
|
||||
}
|
||||
}
|
||||
private static string? _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)
|
||||
{
|
||||
@@ -96,8 +90,8 @@ public static partial 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();
|
||||
@@ -114,8 +108,8 @@ public static partial 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));
|
||||
@@ -137,8 +131,8 @@ public static partial 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));
|
||||
@@ -171,11 +165,7 @@ public static partial 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;
|
||||
}
|
||||
@@ -212,7 +202,7 @@ public static partial class Markdown
|
||||
{
|
||||
if (markdown is null) ThrowHelper.ArgumentNullException_markdown();
|
||||
|
||||
MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null;
|
||||
MarkdownPipeline? pipeline = trackTrivia ? DefaultTrackTriviaPipeline : null;
|
||||
|
||||
return Parse(markdown, pipeline);
|
||||
}
|
||||
@@ -268,7 +258,7 @@ public static partial class Markdown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string to HTML.
|
||||
/// Converts a Markdown string to Plain text by using a <see cref="StringWriter"/> .
|
||||
/// </summary>
|
||||
/// <param name="markdown">A Markdown text.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Extensions.Abbreviations;
|
||||
using Markdig.Extensions.Alerts;
|
||||
using Markdig.Extensions.AutoIdentifiers;
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.Bootstrap;
|
||||
@@ -34,6 +35,7 @@ using Markdig.Extensions.Yaml;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Parsers.Inlines;
|
||||
using Markdig.Renderers;
|
||||
|
||||
namespace Markdig;
|
||||
|
||||
@@ -74,6 +76,7 @@ public static class MarkdownExtensions
|
||||
public static MarkdownPipelineBuilder UseAdvancedExtensions(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
return pipeline
|
||||
.UseAlertBlocks()
|
||||
.UseAbbreviations()
|
||||
.UseAutoIdentifiers()
|
||||
.UseCitations()
|
||||
@@ -94,6 +97,18 @@ public static class MarkdownExtensions
|
||||
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses this extension to enable alert blocks.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <param name="renderKind">Replace the default renderer for the kind with a custom renderer</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseAlertBlocks(this MarkdownPipelineBuilder pipeline, Action<HtmlRenderer, StringSlice>? renderKind = null)
|
||||
{
|
||||
pipeline.Extensions.ReplaceOrAdd<AlertExtension>(new AlertExtension() { RenderKind = renderKind });
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy`
|
||||
/// </summary>
|
||||
@@ -552,6 +567,9 @@ public static class MarkdownExtensions
|
||||
case "advanced":
|
||||
pipeline.UseAdvancedExtensions();
|
||||
break;
|
||||
case "alerts":
|
||||
pipeline.UseAlertBlocks();
|
||||
break;
|
||||
case "pipetables":
|
||||
pipeline.UsePipeTables();
|
||||
break;
|
||||
|
||||
@@ -95,18 +95,14 @@ public sealed class MarkdownPipeline
|
||||
return new RentedHtmlRenderer(cache, renderer);
|
||||
}
|
||||
|
||||
internal sealed class HtmlRendererCache : ObjectCache<HtmlRenderer>
|
||||
internal sealed class HtmlRendererCache(
|
||||
MarkdownPipeline pipeline,
|
||||
bool customWriter = false) : ObjectCache<HtmlRenderer>
|
||||
{
|
||||
private static readonly TextWriter s_dummyWriter = new FastStringWriter();
|
||||
private static readonly FastStringWriter s_dummyWriter = new();
|
||||
|
||||
private readonly MarkdownPipeline _pipeline;
|
||||
private readonly bool _customWriter;
|
||||
|
||||
public HtmlRendererCache(MarkdownPipeline pipeline, bool customWriter = false)
|
||||
{
|
||||
_pipeline = pipeline;
|
||||
_customWriter = customWriter;
|
||||
}
|
||||
private readonly MarkdownPipeline _pipeline = pipeline;
|
||||
private readonly bool _customWriter = customWriter;
|
||||
|
||||
protected override HtmlRenderer NewInstance()
|
||||
{
|
||||
@@ -131,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.
|
||||
@@ -24,8 +24,8 @@ public class MarkdownPipelineBuilder
|
||||
public MarkdownPipelineBuilder()
|
||||
{
|
||||
// Add all default parsers
|
||||
BlockParsers = new OrderedList<BlockParser>()
|
||||
{
|
||||
BlockParsers =
|
||||
[
|
||||
new ThematicBreakParser(),
|
||||
new HeadingBlockParser(),
|
||||
new QuoteBlockParser(),
|
||||
@@ -35,10 +35,10 @@ public class MarkdownPipelineBuilder
|
||||
new FencedCodeBlockParser(),
|
||||
new IndentedCodeBlockParser(),
|
||||
new ParagraphBlockParser(),
|
||||
};
|
||||
];
|
||||
|
||||
InlineParsers = new OrderedList<InlineParser>()
|
||||
{
|
||||
InlineParsers =
|
||||
[
|
||||
new HtmlEntityParser(),
|
||||
new LinkInlineParser(),
|
||||
new EscapeInlineParser(),
|
||||
@@ -46,7 +46,7 @@ public class MarkdownPipelineBuilder
|
||||
new CodeInlineParser(),
|
||||
new AutolinkInlineParser(),
|
||||
new LineBreakInlineParser(),
|
||||
};
|
||||
];
|
||||
|
||||
Extensions = new OrderedList<IMarkdownExtension>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class BlockProcessor
|
||||
/// <summary>
|
||||
/// Gets the current stack of <see cref="Block"/> being processed.
|
||||
/// </summary>
|
||||
private List<BlockWrapper> OpenedBlocks { get; } = new();
|
||||
private List<BlockWrapper> OpenedBlocks { get; } = [];
|
||||
|
||||
private bool ContinueProcessingLine { get; set; }
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ public abstract class FencedBlockParserBase : BlockParser, IAttributesParseable
|
||||
/// <seealso cref="BlockParser" />
|
||||
public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T : Block, IFencedBlock
|
||||
{
|
||||
private static readonly TransformedStringCache _infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString));
|
||||
private static readonly TransformedStringCache s_infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString));
|
||||
private static readonly TransformedStringCache s_argumentsStringCache = new(static argumentsString => HtmlHelper.Unescape(argumentsString));
|
||||
private TransformedStringCache? _infoPrefixCache;
|
||||
|
||||
/// <summary>
|
||||
@@ -176,7 +177,7 @@ public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T :
|
||||
|
||||
end:
|
||||
fenced.TriviaAfterFencedChar = afterFence;
|
||||
fenced.Info = _infoStringCache.Get(info.AsSpan());
|
||||
fenced.Info = s_infoStringCache.Get(info.AsSpan());
|
||||
fenced.UnescapedInfo = info;
|
||||
fenced.TriviaAfterInfo = afterInfo;
|
||||
fenced.Arguments = HtmlHelper.Unescape(arg.ToString());
|
||||
@@ -197,71 +198,47 @@ public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T :
|
||||
/// <returns><c>true</c> if parsing of the line is successfull; <c>false</c> otherwise</returns>
|
||||
public static bool DefaultInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter)
|
||||
{
|
||||
// An info string cannot contain any backticks (unless it is a tilde block)
|
||||
int firstSpace = -1;
|
||||
if (openingCharacter == '`')
|
||||
ReadOnlySpan<char> lineSpan = line.AsSpan();
|
||||
|
||||
if (!lineSpan.IsEmpty)
|
||||
{
|
||||
for (int i = line.Start; i <= line.End; i++)
|
||||
if (openingCharacter == '`')
|
||||
{
|
||||
char c = line.Text[i];
|
||||
if (c == '`')
|
||||
firstSpace = lineSpan.IndexOfAny(' ', '\t', '`');
|
||||
|
||||
// An info string cannot contain any backticks (unless it is a tilde block)
|
||||
if (firstSpace >= 0 && lineSpan.Slice(firstSpace).Contains('`'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstSpace < 0 && c.IsSpaceOrTab())
|
||||
{
|
||||
firstSpace = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = line.Start; i <= line.End; i++)
|
||||
else
|
||||
{
|
||||
if (line.Text[i].IsSpaceOrTab())
|
||||
{
|
||||
firstSpace = i;
|
||||
break;
|
||||
}
|
||||
firstSpace = lineSpan.IndexOfAny(' ', '\t');
|
||||
}
|
||||
}
|
||||
|
||||
StringSlice infoStringSlice;
|
||||
string? argString = null;
|
||||
|
||||
if (firstSpace > 0)
|
||||
if (firstSpace >= 0)
|
||||
{
|
||||
firstSpace += line.Start;
|
||||
infoStringSlice = new StringSlice(line.Text, line.Start, firstSpace - 1);
|
||||
|
||||
// Skip any spaces after info string
|
||||
firstSpace++;
|
||||
while (firstSpace <= line.End)
|
||||
{
|
||||
char c = line[firstSpace];
|
||||
if (c.IsSpaceOrTab())
|
||||
{
|
||||
firstSpace++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var argStringSlice = new StringSlice(line.Text, firstSpace, line.End);
|
||||
argStringSlice.Trim();
|
||||
argString = argStringSlice.ToString();
|
||||
fenced.Arguments = s_argumentsStringCache.Get(argStringSlice.AsSpan());
|
||||
}
|
||||
else
|
||||
{
|
||||
infoStringSlice = line;
|
||||
fenced.Arguments = string.Empty;
|
||||
}
|
||||
|
||||
infoStringSlice.Trim();
|
||||
|
||||
fenced.Info = _infoStringCache.Get(infoStringSlice.AsSpan());
|
||||
fenced.Arguments = HtmlHelper.Unescape(argString);
|
||||
fenced.Info = s_infoStringCache.Get(infoStringSlice.AsSpan());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -303,17 +280,19 @@ public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T :
|
||||
// Try to parse any attached attributes
|
||||
TryParseAttributes?.Invoke(processor, ref line, fenced);
|
||||
|
||||
// If the info parser was not successfull, early exit
|
||||
// If the info parser was not successful, early exit
|
||||
if (InfoParser != null && !InfoParser(processor, ref line, fenced, matchChar))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
// Add the language as an attribute by default
|
||||
if (!string.IsNullOrEmpty(fenced.Info))
|
||||
string? info = fenced.Info;
|
||||
|
||||
if (!string.IsNullOrEmpty(info))
|
||||
{
|
||||
Debug.Assert(_infoPrefixCache is not null || InfoPrefix is null);
|
||||
string infoWithPrefix = _infoPrefixCache?.Get(fenced.Info!) ?? fenced.Info!;
|
||||
string infoWithPrefix = _infoPrefixCache?.Get(info!) ?? info!;
|
||||
fenced.GetAttributes().AddClass(infoWithPrefix);
|
||||
}
|
||||
|
||||
@@ -329,34 +308,32 @@ public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T :
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var fence = (IFencedBlock)block;
|
||||
var openingCount = fence.OpeningFencedCharCount;
|
||||
|
||||
// Match if we have a closing fence
|
||||
var line = processor.Line;
|
||||
var sourcePosition = processor.Start;
|
||||
var closingCount = line.CountAndSkipChar(fence.FencedChar);
|
||||
var diff = openingCount - closingCount;
|
||||
|
||||
char c = line.CurrentChar;
|
||||
var lastFenceCharPosition = processor.Start + closingCount;
|
||||
|
||||
// If we have a closing fence, close it and discard the current line
|
||||
// The line must contain only fence opening character followed only by whitespaces.
|
||||
var startBeforeTrim = line.Start;
|
||||
var endBeforeTrim = line.End;
|
||||
var trimmed = line.TrimEnd();
|
||||
if (diff <= 0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && trimmed)
|
||||
|
||||
if (fence.OpeningFencedCharCount <= closingCount &&
|
||||
!processor.IsCodeIndent &&
|
||||
c.IsWhiteSpaceOrZero() &&
|
||||
line.TrimEnd())
|
||||
{
|
||||
block.UpdateSpanEnd(startBeforeTrim - 1);
|
||||
|
||||
var fencedBlock = (IFencedBlock)block;
|
||||
fencedBlock.ClosingFencedCharCount = closingCount;
|
||||
fence.ClosingFencedCharCount = closingCount;
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
fencedBlock.NewLine = processor.Line.NewLine;
|
||||
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
|
||||
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
|
||||
fence.NewLine = line.NewLine;
|
||||
fence.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
|
||||
fence.TriviaAfter = new StringSlice(line.Text, processor.Start + closingCount, processor.Line.End);
|
||||
}
|
||||
|
||||
// Don't keep the last line
|
||||
|
||||
@@ -20,7 +20,7 @@ public class FencedCodeBlockParser : FencedBlockParserBase<FencedCodeBlock>
|
||||
/// </summary>
|
||||
public FencedCodeBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'`', '~'};
|
||||
OpeningCharacters = ['`', '~'];
|
||||
InfoPrefix = DefaultInfoPrefix;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public class HeadingBlockParser : BlockParser, IAttributesParseable
|
||||
/// </summary>
|
||||
public HeadingBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'#'};
|
||||
OpeningCharacters = ['#'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ public class HtmlBlockParser : BlockParser
|
||||
/// </summary>
|
||||
public HtmlBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '<' };
|
||||
OpeningCharacters = ['<'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
@@ -26,9 +26,11 @@ public delegate void ProcessInlineDelegate(InlineProcessor processor, Inline? in
|
||||
/// </summary>
|
||||
public class InlineProcessor
|
||||
{
|
||||
private readonly List<StringLineGroup.LineOffset> lineOffsets = new();
|
||||
private readonly List<StringLineGroup.LineOffset> lineOffsets = [];
|
||||
private int previousSliceOffset;
|
||||
private int previousLineIndexForSliceOffset;
|
||||
internal ContainerBlock? PreviousContainerToReplace;
|
||||
internal ContainerBlock? NewContainerToReplace;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InlineProcessor" /> class.
|
||||
@@ -203,6 +205,24 @@ public class InlineProcessor
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace a parent container. This method is experimental and should be used with caution.
|
||||
/// </summary>
|
||||
/// <param name="previousParentContainer">The previous parent container to replace</param>
|
||||
/// <param name="newParentContainer">The new parent container</param>
|
||||
/// <exception cref="InvalidOperationException">If a new parent container has been already setup.</exception>
|
||||
internal void ReplaceParentContainer(ContainerBlock previousParentContainer, ContainerBlock newParentContainer)
|
||||
{
|
||||
// Limitation for now, only one parent container can be replaced.
|
||||
if (PreviousContainerToReplace != null)
|
||||
{
|
||||
throw new InvalidOperationException("A block is already being replaced");
|
||||
}
|
||||
|
||||
PreviousContainerToReplace = previousParentContainer;
|
||||
NewContainerToReplace = newParentContainer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the inline of the specified <see cref="LeafBlock"/>.
|
||||
/// </summary>
|
||||
@@ -211,6 +231,9 @@ public class InlineProcessor
|
||||
{
|
||||
if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock();
|
||||
|
||||
PreviousContainerToReplace = null;
|
||||
NewContainerToReplace = null;
|
||||
|
||||
// clear parser states
|
||||
Array.Clear(ParserStates, 0, ParserStates.Length);
|
||||
|
||||
@@ -225,6 +248,7 @@ public class InlineProcessor
|
||||
previousLineIndexForSliceOffset = 0;
|
||||
lineOffsets.Clear();
|
||||
var text = leafBlock.Lines.ToSlice(lineOffsets);
|
||||
var textEnd = text.End;
|
||||
leafBlock.Lines.Release();
|
||||
int previousStart = -1;
|
||||
|
||||
@@ -319,7 +343,8 @@ public class InlineProcessor
|
||||
var newLine = leafBlock.NewLine;
|
||||
if (newLine != NewLine.None)
|
||||
{
|
||||
leafBlock.Inline.AppendChild(new LineBreakInline { NewLine = newLine });
|
||||
var position = GetSourcePosition(textEnd + 1, out int line, out int column);
|
||||
leafBlock.Inline.AppendChild(new LineBreakInline { NewLine = newLine, Line = line, Column = column, Span = { Start = position, End = position + (newLine == NewLine.CarriageReturnLineFeed ? 1 : 0) } });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +367,12 @@ public class InlineProcessor
|
||||
// DebugLog.WriteLine("** Dump after Emphasis:");
|
||||
// leafBlock.Inline.DumpTo(DebugLog);
|
||||
//}
|
||||
|
||||
if (leafBlock.Inline.LastChild is not null)
|
||||
{
|
||||
leafBlock.Inline.Span.End = leafBlock.Inline.LastChild.Span.End;
|
||||
leafBlock.UpdateSpanEnd(leafBlock.Inline.Span.End);
|
||||
}
|
||||
}
|
||||
|
||||
public void PostProcessInlines(int startingIndex, Inline? root, Inline? lastChild, bool isFinalProcessing)
|
||||
|
||||
@@ -19,7 +19,7 @@ public class AutolinkInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public AutolinkInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'<'};
|
||||
OpeningCharacters = ['<'];
|
||||
EnableHtmlParsing = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,27 +21,21 @@ public class CodeInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public CodeInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '`' };
|
||||
OpeningCharacters = ['`'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
var match = slice.CurrentChar;
|
||||
char match = slice.CurrentChar;
|
||||
if (slice.PeekCharExtra(-1) == match)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startPosition = slice.Start;
|
||||
Debug.Assert(match is not ('\r' or '\n'));
|
||||
|
||||
// Match the opened sticks
|
||||
int openSticks = slice.CountAndSkipChar(match);
|
||||
int contentStart = slice.Start;
|
||||
int closeSticks = 0;
|
||||
|
||||
char c = slice.CurrentChar;
|
||||
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
// A backtick string is a string of one or more backtick characters (`) that is neither preceded nor followed by a backtick.
|
||||
// A code span begins with a backtick string and ends with a backtick string of equal length.
|
||||
@@ -54,91 +48,106 @@ public class CodeInlineParser : InlineParser
|
||||
// This allows you to include code that begins or ends with backtick characters, which must be separated by
|
||||
// whitespace from the opening or closing backtick strings.
|
||||
|
||||
bool allSpace = true;
|
||||
bool containsNewLine = false;
|
||||
var contentEnd = -1;
|
||||
ReadOnlySpan<char> span = slice.AsSpan();
|
||||
bool containsNewLines = false;
|
||||
|
||||
while (c != '\0')
|
||||
while (true)
|
||||
{
|
||||
// Transform '\n' into a single space
|
||||
if (c == '\n')
|
||||
int i = span.IndexOfAny('\r', '\n', match);
|
||||
|
||||
if ((uint)i >= (uint)span.Length)
|
||||
{
|
||||
containsNewLine = true;
|
||||
c = ' ';
|
||||
}
|
||||
else if (c == '\r')
|
||||
{
|
||||
containsNewLine = true;
|
||||
slice.SkipChar();
|
||||
c = slice.CurrentChar;
|
||||
continue;
|
||||
// We got to the end of the input before seeing the match character. CodeInline can't match here.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c == match)
|
||||
int closeSticks = 0;
|
||||
|
||||
while ((uint)i < (uint)span.Length && span[i] == match)
|
||||
{
|
||||
contentEnd = slice.Start;
|
||||
closeSticks = slice.CountAndSkipChar(match);
|
||||
|
||||
if (openSticks == closeSticks)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
allSpace = false;
|
||||
builder.Append(match, closeSticks);
|
||||
c = slice.CurrentChar;
|
||||
closeSticks++;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
|
||||
span = span.Slice(i);
|
||||
|
||||
if (openSticks == closeSticks)
|
||||
{
|
||||
builder.Append(c);
|
||||
if (c != ' ')
|
||||
{
|
||||
allSpace = false;
|
||||
}
|
||||
c = slice.NextChar();
|
||||
break;
|
||||
}
|
||||
else if (closeSticks == 0)
|
||||
{
|
||||
containsNewLines = true;
|
||||
span = span.Slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
bool isMatching = false;
|
||||
if (closeSticks == openSticks)
|
||||
ReadOnlySpan<char> rawContent = slice.AsSpan().Slice(0, slice.Length - span.Length - openSticks);
|
||||
|
||||
var content = containsNewLines
|
||||
? new LazySubstring(ReplaceNewLines(rawContent)) // Should be the rare path.
|
||||
: new LazySubstring(slice.Text, slice.Start, rawContent.Length);
|
||||
|
||||
// Remove one space from front and back if the string is not all spaces
|
||||
if (rawContent.Length > 2 &&
|
||||
rawContent[0] is ' ' or '\n' &&
|
||||
rawContent[rawContent.Length - 1] is ' ' or '\n' &&
|
||||
rawContent.ContainsAnyExcept(' ', '\r', '\n'))
|
||||
{
|
||||
ReadOnlySpan<char> contentSpan = builder.AsSpan();
|
||||
|
||||
var content = containsNewLine
|
||||
? new LazySubstring(contentSpan.ToString())
|
||||
: new LazySubstring(slice.Text, contentStart, contentSpan.Length);
|
||||
|
||||
Debug.Assert(contentSpan.SequenceEqual(content.AsSpan()));
|
||||
|
||||
// Remove one space from front and back if the string is not all spaces
|
||||
if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ')
|
||||
{
|
||||
content.Offset++;
|
||||
content.Length -= 2;
|
||||
}
|
||||
|
||||
int delimiterCount = Math.Min(openSticks, closeSticks);
|
||||
var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column);
|
||||
var spanEnd = processor.GetSourcePosition(slice.Start - 1);
|
||||
var codeInline = new CodeInline(content)
|
||||
{
|
||||
Delimiter = match,
|
||||
Span = new SourceSpan(spanStart, spanEnd),
|
||||
Line = line,
|
||||
Column = column,
|
||||
DelimiterCount = delimiterCount,
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1);
|
||||
}
|
||||
|
||||
processor.Inline = codeInline;
|
||||
isMatching = true;
|
||||
content.Offset++;
|
||||
content.Length -= 2;
|
||||
}
|
||||
|
||||
builder.Dispose();
|
||||
return isMatching;
|
||||
int startPosition = slice.Start;
|
||||
slice.Start = startPosition + rawContent.Length + openSticks;
|
||||
|
||||
// We've already skipped the opening sticks. Account for that here.
|
||||
startPosition -= openSticks;
|
||||
|
||||
var codeInline = new CodeInline(content)
|
||||
{
|
||||
Delimiter = slice.Text[startPosition],
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
DelimiterCount = openSticks,
|
||||
};
|
||||
|
||||
if (processor.TrackTrivia)
|
||||
{
|
||||
// startPosition and slice.Start include the opening/closing sticks.
|
||||
codeInline.ContentWithTrivia = new StringSlice(slice.Text, startPosition + openSticks, slice.Start - openSticks - 1);
|
||||
}
|
||||
|
||||
processor.Inline = codeInline;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ReplaceNewLines(ReadOnlySpan<char> content)
|
||||
{
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
|
||||
while (true)
|
||||
{
|
||||
int i = content.IndexOfAny('\r', '\n');
|
||||
|
||||
if ((uint)i >= (uint)content.Length)
|
||||
{
|
||||
builder.Append(content);
|
||||
break;
|
||||
}
|
||||
|
||||
builder.Append(content.Slice(0, i));
|
||||
|
||||
if (content[i] == '\n')
|
||||
{
|
||||
// Transform '\n' into a single space
|
||||
builder.Append(' ');
|
||||
}
|
||||
|
||||
content = content.Slice(i + 1);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user