mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 05:44:50 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -11,22 +11,8 @@ 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'
|
||||
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>
|
||||
@@ -12,7 +12,7 @@ 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/)
|
||||
- Includes all the core elements of CommonMark:
|
||||
|
||||
@@ -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.13.12" />
|
||||
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.12" />
|
||||
<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.506101" />
|
||||
</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,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<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)\net8.0\SpecFileGen.dll</SpecExecutable>
|
||||
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net8.0\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.9.0" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.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,29 @@ $$
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -645,13 +699,14 @@ literal ( 0, 2) 2-3
|
||||
public void TestMathematicsInline()
|
||||
{
|
||||
// 01 23456789AB
|
||||
Check("0\n012 $abcd$", @"
|
||||
paragraph ( 0, 0) 0-11
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -825,9 +880,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;
|
||||
@@ -215,4 +216,27 @@ public class TestStringSliceList
|
||||
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,8 +75,11 @@ 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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,7 +20,7 @@ public class AbbreviationParser : BlockParser
|
||||
/// </summary>
|
||||
public AbbreviationParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '*' };
|
||||
OpeningCharacters = ['*'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
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; }
|
||||
}
|
||||
79
src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs
Normal file
79
src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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;
|
||||
using Markdig.Syntax;
|
||||
|
||||
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)
|
||||
{
|
||||
string? html = kind.AsSpan() 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/Markdig/Extensions/Alerts/AlertInlineParser.cs
Normal file
127
src/Markdig/Extensions/Alerts/AlertInlineParser.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.Alerts;
|
||||
|
||||
/// <summary>
|
||||
/// An inline parser for an alert inline (e.g. `[!NOTE]`).
|
||||
/// </summary>
|
||||
/// <seealso cref="InlineParser" />
|
||||
public class AlertInlineParser : InlineParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AlertInlineParser"/> class.
|
||||
/// </summary>
|
||||
public AlertInlineParser()
|
||||
{
|
||||
OpeningCharacters = ['['];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var saved = slice;
|
||||
var c = slice.NextChar();
|
||||
if (c != '!')
|
||||
{
|
||||
slice = saved;
|
||||
return false;
|
||||
}
|
||||
|
||||
c = slice.NextChar(); // Skip !
|
||||
|
||||
var start = slice.Start;
|
||||
var end = start;
|
||||
while (c.IsAlphaUpper())
|
||||
{
|
||||
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.NextChar(); // Skip \n
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == '\n')
|
||||
{
|
||||
slice.NextChar(); // 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,
|
||||
};
|
||||
|
||||
alertBlock.GetAttributes().AddClass("markdown-alert");
|
||||
alertBlock.GetAttributes().AddClass($"markdown-alert-{alertType.ToString().ToLowerInvariant()}");
|
||||
|
||||
// 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,10 @@ 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
|
||||
@@ -29,7 +29,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
|
||||
/// <param name="options">The options.</param>
|
||||
public AutoIdentifierExtension(AutoIdentifierOptions options)
|
||||
{
|
||||
this.options = options;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
@@ -68,7 +68,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];
|
||||
|
||||
@@ -157,16 +157,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 +198,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 +210,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,14 +22,14 @@ 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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ':'
|
||||
|
||||
@@ -170,10 +170,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)
|
||||
@@ -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
|
||||
|
||||
@@ -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(end)),
|
||||
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)
|
||||
{
|
||||
@@ -71,7 +67,7 @@ public class HostProviderBuilder
|
||||
|
||||
#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);
|
||||
|
||||
@@ -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)
|
||||
@@ -144,7 +144,7 @@ public class GridTableParser : BlockParser
|
||||
line.Start = lineStart + columnSlice.Start + 1;
|
||||
line.End = lineStart + columnSlice.End - 1;
|
||||
line.Trim();
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line))
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
|
||||
{
|
||||
hasRowSpan = true;
|
||||
columnSlice.CurrentCell.RowSpan++;
|
||||
@@ -158,7 +158,7 @@ public class GridTableParser : BlockParser
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRowSeperator(StringSlice slice)
|
||||
private static bool IsRowSeparator(StringSlice slice)
|
||||
{
|
||||
char c = slice.CurrentChar;
|
||||
do
|
||||
@@ -263,7 +263,7 @@ public class GridTableParser : BlockParser
|
||||
}
|
||||
sliceForCell.TrimEnd();
|
||||
|
||||
if (!isRowLine || !IsRowSeperator(sliceForCell))
|
||||
if (!isRowLine || !IsRowSeparator(sliceForCell))
|
||||
{
|
||||
if (columnSlice.CurrentCell is null)
|
||||
{
|
||||
@@ -273,10 +273,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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -443,6 +443,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
|
||||
@@ -520,7 +525,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
// Create aligns until we may have a header row
|
||||
|
||||
aligns ??= new List<TableColumnDefinition>();
|
||||
|
||||
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
|
||||
// If this is the last delimiter, we need to check the right side of the `|` delimiter
|
||||
@@ -632,10 +637,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; } = [];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -30,15 +30,17 @@ public static class CharHelper
|
||||
{ 'I', 1 }, { 'V', 5 }, { 'X', 10 }
|
||||
};
|
||||
|
||||
private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' };
|
||||
[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
|
||||
@@ -126,19 +128,6 @@ 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)
|
||||
{
|
||||
@@ -178,7 +167,7 @@ public static class CharHelper
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsControl(this char c)
|
||||
{
|
||||
return c < ' ' || char.IsControl(c);
|
||||
return char.IsControl(c);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -211,15 +200,17 @@ public static class CharHelper
|
||||
{
|
||||
// A Unicode punctuation character is an ASCII punctuation character
|
||||
// or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps.
|
||||
const int PunctuationCategoryMask =
|
||||
1 << (int)UnicodeCategory.ConnectorPunctuation |
|
||||
1 << (int)UnicodeCategory.DashPunctuation |
|
||||
1 << (int)UnicodeCategory.OpenPunctuation |
|
||||
1 << (int)UnicodeCategory.ClosePunctuation |
|
||||
1 << (int)UnicodeCategory.InitialQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.FinalQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.OtherPunctuation;
|
||||
|
||||
space = false;
|
||||
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 = (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,14 +227,16 @@ public static class CharHelper
|
||||
}
|
||||
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;
|
||||
const int PunctuationCategoryMask =
|
||||
1 << (int)UnicodeCategory.ConnectorPunctuation |
|
||||
1 << (int)UnicodeCategory.DashPunctuation |
|
||||
1 << (int)UnicodeCategory.OpenPunctuation |
|
||||
1 << (int)UnicodeCategory.ClosePunctuation |
|
||||
1 << (int)UnicodeCategory.InitialQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.FinalQuotePunctuation |
|
||||
1 << (int)UnicodeCategory.OtherPunctuation;
|
||||
|
||||
return (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,11 +722,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)
|
||||
{
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
// 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 +15,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 Dictionary<uint, T>? _nonAsciiMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterMap{T}"/> class.
|
||||
@@ -35,64 +27,38 @@ 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];
|
||||
|
||||
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 ??= new Dictionary<uint, T>();
|
||||
|
||||
if (!_nonAsciiMap.ContainsKey(openingChar))
|
||||
{
|
||||
_nonAsciiMap[openingChar] = state.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if NETCOREAPP3_1_OR_GREATER
|
||||
if (nonAsciiMap is 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();
|
||||
}
|
||||
#endif
|
||||
_values = SearchValues.Create(OpeningCharacters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,7 +76,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 +84,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 +97,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,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 +100,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();
|
||||
|
||||
@@ -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()
|
||||
@@ -423,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +5,7 @@
|
||||
<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;net6.0;net8.0</TargetFrameworks>
|
||||
<CheckEolTargetFramework>false</CheckEolTargetFramework>
|
||||
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
|
||||
<PackageReleaseNotes>https://github.com/lunet-io/markdig/blob/master/changelog.md</PackageReleaseNotes>
|
||||
@@ -14,7 +14,7 @@
|
||||
<PackageIcon>markdig.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/lunet-io/markdig</PackageProjectUrl>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>10</LangVersion>
|
||||
<LangVersion>12</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.5.5" />
|
||||
</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">
|
||||
|
||||
@@ -19,16 +19,10 @@ namespace Markdig;
|
||||
/// </summary>
|
||||
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;
|
||||
public static string Version =>
|
||||
s_version ??= typeof(Markdown).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
|
||||
|
||||
private static string? s_version;
|
||||
|
||||
internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build();
|
||||
private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build();
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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 == '\0' || c.IsWhitespace()) &&
|
||||
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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
@@ -20,7 +20,7 @@ namespace Markdig.Parsers.Inlines;
|
||||
public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
private CharacterMap<EmphasisDescriptor>? emphasisMap;
|
||||
private readonly DelimitersObjectCache inlinesCache = new DelimitersObjectCache();
|
||||
private readonly DelimitersObjectCache inlinesCache = new();
|
||||
|
||||
[Obsolete("Use TryCreateEmphasisInlineDelegate instead", error: false)]
|
||||
public delegate EmphasisInline CreateEmphasisInlineDelegate(char emphasisChar, bool isStrong);
|
||||
@@ -31,11 +31,11 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
/// </summary>
|
||||
public EmphasisInlineParser()
|
||||
{
|
||||
EmphasisDescriptors = new List<EmphasisDescriptor>()
|
||||
{
|
||||
EmphasisDescriptors =
|
||||
[
|
||||
new EmphasisDescriptor('*', 1, 2, true),
|
||||
new EmphasisDescriptor('_', 1, 2, false)
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -65,7 +65,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
/// </summary>
|
||||
[Obsolete("Use TryCreateEmphasisInlineList instead", error: false)]
|
||||
public CreateEmphasisInlineDelegate? CreateEmphasisInline { get; set; }
|
||||
public readonly List<TryCreateEmphasisInlineDelegate> TryCreateEmphasisInlineList = new List<TryCreateEmphasisInlineDelegate>();
|
||||
public readonly List<TryCreateEmphasisInlineDelegate> TryCreateEmphasisInlineList = [];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -109,7 +109,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
var child = container.FirstChild;
|
||||
while (child != null)
|
||||
{
|
||||
// Stop the search on the delimitation child
|
||||
// Stop the search on the delimitation child
|
||||
if (child == lastChild)
|
||||
{
|
||||
break;
|
||||
@@ -197,7 +197,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
if (canOpen) delimiterType |= DelimiterType.Open;
|
||||
if (canClose) delimiterType |= DelimiterType.Close;
|
||||
|
||||
var delimiter = new EmphasisDelimiterInline(this, emphasisDesc)
|
||||
var delimiter = new EmphasisDelimiterInline(this, emphasisDesc, new StringSlice(slice.Text, startPosition, slice.Start - 1))
|
||||
{
|
||||
DelimiterCount = delimiterCount,
|
||||
Type = delimiterType,
|
||||
@@ -221,7 +221,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
|
||||
// TODO: Benchmark difference between using List and LinkedList here since there could be a few Remove calls
|
||||
|
||||
// Move current_position forward in the delimiter stack (if needed) until
|
||||
// Move current_position forward in the delimiter stack (if needed) until
|
||||
// we find the first potential closer with delimiter * or _. (This will be the potential closer closest to the beginning of the input – the first one in parse order.)
|
||||
for (int i = 0; i < delimiters.Count; i++)
|
||||
{
|
||||
@@ -237,7 +237,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type)
|
||||
// Now, look back in the stack (staying above stack_bottom and the openers_bottom for this delimiter type)
|
||||
// for the first matching potential opener (“matching” means same delimiter).
|
||||
EmphasisDelimiterInline? openDelimiter = null;
|
||||
int openDelimiterIndex = -1;
|
||||
@@ -307,8 +307,10 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
emphasis.Column = openDelimiter.Column;
|
||||
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
|
||||
|
||||
openDelimiter.Content.Start += delimiterDelta;
|
||||
openDelimiter.Span.Start += delimiterDelta;
|
||||
openDelimiter.Column += delimiterDelta;
|
||||
closeDelimiter.Content.Start += delimiterDelta;
|
||||
closeDelimiter.Span.Start += delimiterDelta;
|
||||
closeDelimiter.Column += delimiterDelta;
|
||||
|
||||
@@ -331,7 +333,7 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
for (int k = i - 1; k >= openDelimiterIndex + 1; k--)
|
||||
{
|
||||
var literalDelimiter = delimiters[k];
|
||||
literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline());
|
||||
literalDelimiter.ReplaceBy(literalDelimiter.AsLiteralInline());
|
||||
delimiters.RemoveAt(k);
|
||||
i--;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class EscapeInlineParser : InlineParser
|
||||
{
|
||||
public EscapeInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'\\'};
|
||||
OpeningCharacters = ['\\'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
|
||||
@@ -21,7 +21,7 @@ public class HtmlEntityParser : InlineParser
|
||||
/// </summary>
|
||||
public HtmlEntityParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'&'};
|
||||
OpeningCharacters = ['&'];
|
||||
}
|
||||
|
||||
public static bool TryParse(ref StringSlice slice, [NotNullWhen(true)] out string? literal, out int match)
|
||||
|
||||
@@ -18,7 +18,7 @@ public class LineBreakInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public LineBreakInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '\n', '\r' };
|
||||
OpeningCharacters = ['\n', '\r'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -19,7 +19,7 @@ public class LinkInlineParser : InlineParser
|
||||
/// </summary>
|
||||
public LinkInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'[', ']', '!'};
|
||||
OpeningCharacters = ['[', ']', '!'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
@@ -322,7 +322,7 @@ public class LinkInlineParser : InlineParser
|
||||
|
||||
if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan))
|
||||
{
|
||||
SourceSpan labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
|
||||
var labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
|
||||
if (isLabelSpanLocal)
|
||||
{
|
||||
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
|
||||
|
||||
@@ -32,16 +32,12 @@ public sealed class LiteralInlineParser : InlineParser
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
var text = slice.Text;
|
||||
string text = slice.Text;
|
||||
|
||||
var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column);
|
||||
|
||||
// Slightly faster to perform our own search for opening characters
|
||||
var nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End);
|
||||
//var nextStart = str.IndexOfAny(processor.SpecialCharacters, slice.Start + 1, slice.Length - 1);
|
||||
int nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End);
|
||||
int length;
|
||||
|
||||
if (nextStart < 0)
|
||||
if ((uint)nextStart >= (uint)text.Length)
|
||||
{
|
||||
nextStart = slice.End + 1;
|
||||
length = nextStart - slice.Start;
|
||||
@@ -50,10 +46,10 @@ public sealed class LiteralInlineParser : InlineParser
|
||||
{
|
||||
// Remove line endings if the next char is a new line
|
||||
length = nextStart - slice.Start;
|
||||
|
||||
if (!processor.TrackTrivia)
|
||||
{
|
||||
var nextText = text[nextStart];
|
||||
if (nextText == '\n' || nextText == '\r')
|
||||
if (text[nextStart] is '\n' or '\r')
|
||||
{
|
||||
int end = nextStart - 1;
|
||||
while (length > 0 && text[end].IsSpace())
|
||||
@@ -86,7 +82,7 @@ public sealed class LiteralInlineParser : InlineParser
|
||||
processor.Inline = new LiteralInline
|
||||
{
|
||||
Content = length > 0 ? newSlice : StringSlice.Empty,
|
||||
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
|
||||
Span = new SourceSpan(processor.GetSourcePosition(slice.Start, out int line, out int column), processor.GetSourcePosition(endPosition)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
};
|
||||
|
||||
@@ -53,34 +53,11 @@ public static class MarkdownParser
|
||||
{
|
||||
blockProcessor.Open(document);
|
||||
|
||||
ProcessBlocks(blockProcessor, new LineReader(text));
|
||||
ProcessBlocks(blockProcessor, text);
|
||||
|
||||
if (pipeline.TrackTrivia)
|
||||
{
|
||||
Block? lastBlock = blockProcessor.LastBlock;
|
||||
if (lastBlock is null && document.Count == 0)
|
||||
{
|
||||
// this means we have unassigned characters
|
||||
var noBlocksFoundBlock = new EmptyBlock(null);
|
||||
List<StringSlice> linesBefore = blockProcessor.UseLinesBefore();
|
||||
noBlocksFoundBlock.LinesAfter = new List<StringSlice>();
|
||||
if (linesBefore != null)
|
||||
{
|
||||
noBlocksFoundBlock.LinesAfter.AddRange(linesBefore);
|
||||
}
|
||||
|
||||
document.Add(noBlocksFoundBlock);
|
||||
}
|
||||
else if (lastBlock != null && blockProcessor.LinesBefore != null)
|
||||
{
|
||||
// this means we're out of lines, but still have unassigned empty lines.
|
||||
// thus, we'll assign the empty unsassigned lines to the last block
|
||||
// of the document.
|
||||
var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock);
|
||||
rootMostContainerBlock.LinesAfter ??= new List<StringSlice>();
|
||||
var linesBefore = blockProcessor.UseLinesBefore();
|
||||
rootMostContainerBlock.LinesAfter.AddRange(linesBefore);
|
||||
}
|
||||
ProcessBlocksTrivia(blockProcessor, document);
|
||||
}
|
||||
|
||||
// At this point the LineIndex is the same as the number of lines in the document
|
||||
@@ -117,12 +94,15 @@ public static class MarkdownParser
|
||||
return text.Replace('\0', CharHelper.ReplacementChar);
|
||||
}
|
||||
|
||||
private static void ProcessBlocks(BlockProcessor blockProcessor, LineReader lineReader)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ProcessBlocks(BlockProcessor blockProcessor, string text)
|
||||
{
|
||||
var lineReader = new LineReader(text);
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Get the precise position of the begining of the line
|
||||
var lineText = lineReader.ReadLine();
|
||||
// Get the precise position of the beginning of the line
|
||||
StringSlice lineText = lineReader.ReadLine();
|
||||
|
||||
// If this is the end of file and the last line is empty
|
||||
if (lineText.Text is null)
|
||||
@@ -132,9 +112,39 @@ public static class MarkdownParser
|
||||
|
||||
blockProcessor.ProcessLine(lineText);
|
||||
}
|
||||
|
||||
blockProcessor.CloseAll(true);
|
||||
}
|
||||
|
||||
private static void ProcessBlocksTrivia(BlockProcessor blockProcessor, MarkdownDocument document)
|
||||
{
|
||||
Block? lastBlock = blockProcessor.LastBlock;
|
||||
if (lastBlock is null && document.Count == 0)
|
||||
{
|
||||
// this means we have unassigned characters
|
||||
var noBlocksFoundBlock = new EmptyBlock(null);
|
||||
List<StringSlice> linesBefore = blockProcessor.UseLinesBefore();
|
||||
noBlocksFoundBlock.LinesAfter = [];
|
||||
if (linesBefore != null)
|
||||
{
|
||||
noBlocksFoundBlock.LinesAfter.AddRange(linesBefore);
|
||||
}
|
||||
|
||||
document.Add(noBlocksFoundBlock);
|
||||
}
|
||||
else if (lastBlock != null && blockProcessor.LinesBefore != null)
|
||||
{
|
||||
// this means we're out of lines, but still have unassigned empty lines.
|
||||
// thus, we'll assign the empty unsassigned lines to the last block
|
||||
// of the document.
|
||||
var rootMostContainerBlock = Block.FindRootMostContainerParent(lastBlock);
|
||||
rootMostContainerBlock.LinesAfter ??= [];
|
||||
var linesBefore = blockProcessor.UseLinesBefore();
|
||||
rootMostContainerBlock.LinesAfter.AddRange(linesBefore);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocument document)
|
||||
{
|
||||
// "stackless" processor
|
||||
@@ -160,6 +170,40 @@ public static class MarkdownParser
|
||||
if (leafBlock.ProcessInlines)
|
||||
{
|
||||
inlineProcessor.ProcessInlineLeaf(leafBlock);
|
||||
|
||||
// Experimental code to handle a replacement of a parent container
|
||||
// Not satisfied with this code, so we are keeping it internal for now
|
||||
if (inlineProcessor.PreviousContainerToReplace != null)
|
||||
{
|
||||
if (container == inlineProcessor.PreviousContainerToReplace)
|
||||
{
|
||||
item = new ContainerItem(inlineProcessor.NewContainerToReplace!) { Index = item.Index };
|
||||
container = item.Container;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool parentBlockFound = false;
|
||||
for (int i = blockCount - 2; i >= 0; i--)
|
||||
{
|
||||
ref var parentBlock = ref blocks[i];
|
||||
if (parentBlock.Container == inlineProcessor.PreviousContainerToReplace)
|
||||
{
|
||||
parentBlock = new ContainerItem(inlineProcessor.NewContainerToReplace!) { Index = parentBlock.Index };
|
||||
parentBlockFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentBlockFound)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot find the parent block to replace");
|
||||
}
|
||||
}
|
||||
|
||||
inlineProcessor.PreviousContainerToReplace = null;
|
||||
inlineProcessor.NewContainerToReplace = null;
|
||||
}
|
||||
|
||||
if (leafBlock.RemoveAfterProcessInlines)
|
||||
{
|
||||
container.RemoveAt(item.Index);
|
||||
@@ -200,16 +244,10 @@ public static class MarkdownParser
|
||||
}
|
||||
}
|
||||
|
||||
private struct ContainerItem
|
||||
private struct ContainerItem(ContainerBlock container)
|
||||
{
|
||||
public ContainerItem(ContainerBlock container)
|
||||
{
|
||||
Container = container;
|
||||
Index = 0;
|
||||
}
|
||||
public readonly ContainerBlock Container = container;
|
||||
|
||||
public readonly ContainerBlock Container;
|
||||
|
||||
public int Index;
|
||||
public int Index = 0;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public abstract class OrderedListItemParser : ListItemParser
|
||||
/// </summary>
|
||||
protected OrderedListItemParser()
|
||||
{
|
||||
OrderedDelimiters = new[] { '.', ')' };
|
||||
OrderedDelimiters = ['.', ')'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,7 +18,7 @@ public class QuoteBlockParser : BlockParser
|
||||
/// </summary>
|
||||
public QuoteBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'>'};
|
||||
OpeningCharacters = ['>'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
@@ -16,14 +16,14 @@ public class ThematicBreakParser : BlockParser
|
||||
/// <summary>
|
||||
/// A singleton instance used by other parsers.
|
||||
/// </summary>
|
||||
public static readonly ThematicBreakParser Default = new ThematicBreakParser();
|
||||
public static readonly ThematicBreakParser Default = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ThematicBreakParser"/> class.
|
||||
/// </summary>
|
||||
public ThematicBreakParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'-', '_', '*'};
|
||||
OpeningCharacters = ['-', '_', '*'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class UnorderedListItemParser : ListItemParser
|
||||
/// </summary>
|
||||
public UnorderedListItemParser()
|
||||
{
|
||||
OpeningCharacters = new [] {'-', '+', '*'};
|
||||
OpeningCharacters = ['-', '+', '*'];
|
||||
}
|
||||
|
||||
public override bool TryParse(BlockProcessor state, char pendingBulletType, out ListInfo result)
|
||||
|
||||
30
src/Markdig/Polyfills/Ascii.cs
Normal file
30
src/Markdig/Polyfills/Ascii.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.
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
namespace System.Text;
|
||||
|
||||
internal static class Ascii
|
||||
{
|
||||
public static bool IsValid(this string value)
|
||||
{
|
||||
return IsValid(value.AsSpan());
|
||||
}
|
||||
|
||||
public static bool IsValid(this ReadOnlySpan<char> value)
|
||||
{
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] > 127)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
24
src/Markdig/Polyfills/EncodingExtensions.cs
Normal file
24
src/Markdig/Polyfills/EncodingExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if !NETSTANDARD2_1_OR_GREATER
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace System.Text;
|
||||
|
||||
internal static class EncodingExtensions
|
||||
{
|
||||
public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan<char> chars, Span<byte> bytes)
|
||||
{
|
||||
fixed (char* charsPtr = &MemoryMarshal.GetReference(chars))
|
||||
{
|
||||
fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes))
|
||||
{
|
||||
return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
53
src/Markdig/Polyfills/IndexOfHelpers.cs
Normal file
53
src/Markdig/Polyfills/IndexOfHelpers.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
namespace System;
|
||||
|
||||
internal static class IndexOfHelpers
|
||||
{
|
||||
public static bool ContainsAnyExcept(this ReadOnlySpan<char> span, char value0, char value1, char value2)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
if (c != value0 && c != value1 && c != value2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !NETSTANDARD2_1_OR_GREATER
|
||||
public static int IndexOfAny(this ReadOnlySpan<char> span, string values)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
|
||||
foreach (char v in values)
|
||||
{
|
||||
if (c == v)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !NET6_0_OR_GREATER
|
||||
public static bool Contains<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T>
|
||||
{
|
||||
return span.IndexOf(value) >= 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ internal sealed class AllowNullAttribute : Attribute { }
|
||||
#if !NET5_0_OR_GREATER
|
||||
internal sealed class MemberNotNullAttribute : Attribute
|
||||
{
|
||||
public MemberNotNullAttribute(string member) => Members = new[] { member };
|
||||
public MemberNotNullAttribute(string member) => Members = [member];
|
||||
|
||||
public MemberNotNullAttribute(params string[] members) => Members = members;
|
||||
|
||||
|
||||
137
src/Markdig/Polyfills/SearchValues.cs
Normal file
137
src/Markdig/Polyfills/SearchValues.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace System.Buffers;
|
||||
|
||||
internal static class SearchValues
|
||||
{
|
||||
public static SearchValues<char> Create(string values) =>
|
||||
Create(values.AsSpan());
|
||||
|
||||
public static SearchValues<char> Create(ReadOnlySpan<char> values) =>
|
||||
new PreNet8CompatSearchValues(values);
|
||||
|
||||
public static int IndexOfAny(this ReadOnlySpan<char> span, SearchValues<char> values) =>
|
||||
values.IndexOfAny(span);
|
||||
|
||||
public static int IndexOfAnyExcept(this ReadOnlySpan<char> span, SearchValues<char> values) =>
|
||||
values.IndexOfAnyExcept(span);
|
||||
}
|
||||
|
||||
internal abstract class SearchValues<T>
|
||||
{
|
||||
public abstract int IndexOfAny(ReadOnlySpan<char> span);
|
||||
|
||||
public abstract int IndexOfAnyExcept(ReadOnlySpan<char> span);
|
||||
}
|
||||
|
||||
internal sealed class PreNet8CompatSearchValues : SearchValues<char>
|
||||
{
|
||||
private readonly BoolVector128 _ascii;
|
||||
private readonly HashSet<char>? _nonAscii;
|
||||
|
||||
public PreNet8CompatSearchValues(ReadOnlySpan<char> values)
|
||||
{
|
||||
foreach (char c in values)
|
||||
{
|
||||
if (c < 128)
|
||||
{
|
||||
_ascii.Set(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
_nonAscii ??= new HashSet<char>();
|
||||
_nonAscii.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int IndexOfAny(ReadOnlySpan<char> span)
|
||||
{
|
||||
if (_nonAscii is null)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
|
||||
if (c < 128 && _ascii[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
|
||||
if (c < 128 ? _ascii[c] : _nonAscii.Contains(c))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public override int IndexOfAnyExcept(ReadOnlySpan<char> span)
|
||||
{
|
||||
if (_nonAscii is null)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
|
||||
if (c >= 128 || !_ascii[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
char c = span[i];
|
||||
|
||||
if (c < 128 ? !_ascii[c] : !_nonAscii.Contains(c))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,14 +12,14 @@ namespace Markdig.Renderers.Html;
|
||||
/// <seealso cref="HtmlObjectRenderer{HeadingBlock}" />
|
||||
public class HeadingRenderer : HtmlObjectRenderer<HeadingBlock>
|
||||
{
|
||||
private static readonly string[] HeadingTexts = {
|
||||
private static readonly string[] HeadingTexts = [
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
};
|
||||
];
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, HeadingBlock obj)
|
||||
{
|
||||
|
||||
@@ -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.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -20,7 +21,10 @@ namespace Markdig.Renderers;
|
||||
/// <seealso cref="TextRendererBase{HtmlRenderer}" />
|
||||
public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
{
|
||||
private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' };
|
||||
private static readonly IdnMapping s_idnMapping = new();
|
||||
|
||||
private static readonly SearchValues<char> s_asciiNonEscapeChars =
|
||||
SearchValues.Create("!#$%()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz");
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HtmlRenderer"/> class.
|
||||
@@ -140,6 +144,8 @@ public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
return this;
|
||||
}
|
||||
|
||||
private static readonly SearchValues<char> s_escapedChars = SearchValues.Create("<>&\"");
|
||||
|
||||
/// <summary>
|
||||
/// Writes the content escaped for HTML.
|
||||
/// </summary>
|
||||
@@ -149,73 +155,38 @@ public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
{
|
||||
if (!content.IsEmpty)
|
||||
{
|
||||
int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars);
|
||||
if (nextIndex == -1)
|
||||
WriteIndent();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Write(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteEscapeSlow(content, softEscape);
|
||||
int indexOfCharToEscape = softEscape
|
||||
? content.IndexOfAny('<', '&')
|
||||
: content.IndexOfAny(s_escapedChars);
|
||||
|
||||
if ((uint)indexOfCharToEscape >= (uint)content.Length)
|
||||
{
|
||||
WriteRaw(content);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteRaw(content.Slice(0, indexOfCharToEscape));
|
||||
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
WriteRaw(content[indexOfCharToEscape] switch
|
||||
{
|
||||
'<' => "<",
|
||||
'>' => ">",
|
||||
'&' => "&",
|
||||
_ => """,
|
||||
});
|
||||
}
|
||||
|
||||
content = content.Slice(indexOfCharToEscape + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEscapeSlow(ReadOnlySpan<char> content, bool softEscape = false)
|
||||
{
|
||||
WriteIndent();
|
||||
|
||||
int previousOffset = 0;
|
||||
for (int i = 0; i < content.Length; i++)
|
||||
{
|
||||
switch (content[i])
|
||||
{
|
||||
case '<':
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
WriteRaw("<");
|
||||
}
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '>':
|
||||
if (!softEscape)
|
||||
{
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
WriteRaw(">");
|
||||
}
|
||||
previousOffset = i + 1;
|
||||
}
|
||||
break;
|
||||
case '&':
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
WriteRaw("&");
|
||||
}
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '"':
|
||||
if (!softEscape)
|
||||
{
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
WriteRaw(""");
|
||||
}
|
||||
previousOffset = i + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WriteRaw(content.Slice(previousOffset));
|
||||
}
|
||||
|
||||
private static readonly IdnMapping IdnMapping = new IdnMapping();
|
||||
|
||||
/// <summary>
|
||||
/// Writes the URL escaped for HTML.
|
||||
/// </summary>
|
||||
@@ -239,120 +210,107 @@ public class HtmlRenderer : TextRendererBase<HtmlRenderer>
|
||||
content = LinkRewriter(content);
|
||||
}
|
||||
|
||||
// a://c.d = 7 chars
|
||||
int schemeOffset = content.Length < 7 ? -1 : content.IndexOf("://", StringComparison.Ordinal);
|
||||
if (schemeOffset != -1) // This is an absolute URL
|
||||
if (!Ascii.IsValid(content))
|
||||
{
|
||||
schemeOffset += 3; // skip ://
|
||||
WriteEscapeUrl(content, 0, schemeOffset);
|
||||
|
||||
bool idnaEncodeDomain = false;
|
||||
int endOfDomain = schemeOffset;
|
||||
for (; endOfDomain < content.Length; endOfDomain++)
|
||||
int schemeOffset = content.IndexOf("://", StringComparison.Ordinal);
|
||||
if (schemeOffset > 0) // This is an absolute URL
|
||||
{
|
||||
char c = content[endOfDomain];
|
||||
if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain part
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (c > 127)
|
||||
{
|
||||
idnaEncodeDomain = true;
|
||||
}
|
||||
}
|
||||
schemeOffset += 3; // skip ://
|
||||
|
||||
if (idnaEncodeDomain)
|
||||
{
|
||||
string domainName;
|
||||
int domainLength = content.AsSpan(schemeOffset).IndexOfAny("/?#:");
|
||||
if (domainLength < 0)
|
||||
{
|
||||
domainLength = content.Length - schemeOffset;
|
||||
}
|
||||
|
||||
string? domainName = null;
|
||||
|
||||
try
|
||||
{
|
||||
domainName = IdnMapping.GetAscii(content, schemeOffset, endOfDomain - schemeOffset);
|
||||
domainName = s_idnMapping.GetAscii(content, schemeOffset, domainLength);
|
||||
}
|
||||
catch
|
||||
catch { }
|
||||
|
||||
if (domainName is not null)
|
||||
{
|
||||
// Not a valid IDN, fallback to non-punycode encoding
|
||||
WriteEscapeUrl(content, schemeOffset, content.Length);
|
||||
WriteEscapeUrlCore(content.AsSpan(0, schemeOffset));
|
||||
WriteEscapeUrlCore(domainName.AsSpan());
|
||||
WriteEscapeUrlCore(content.AsSpan(schemeOffset + domainLength));
|
||||
return this;
|
||||
}
|
||||
|
||||
// Escape the characters (see Commonmark example 327 and think of it with a non-ascii symbol)
|
||||
int previousPosition = 0;
|
||||
for (int i = 0; i < domainName.Length; i++)
|
||||
{
|
||||
var escape = HtmlHelper.EscapeUrlCharacter(domainName[i]);
|
||||
if (escape != null)
|
||||
{
|
||||
Write(domainName, previousPosition, i - previousPosition);
|
||||
previousPosition = i + 1;
|
||||
Write(escape);
|
||||
}
|
||||
}
|
||||
Write(domainName, previousPosition, domainName.Length - previousPosition);
|
||||
WriteEscapeUrl(content, endOfDomain, content.Length);
|
||||
// Not a valid IDN, fallback to non-punycode encoding
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteEscapeUrl(content, schemeOffset, content.Length);
|
||||
}
|
||||
}
|
||||
else // This is a relative URL
|
||||
{
|
||||
WriteEscapeUrl(content, 0, content.Length);
|
||||
}
|
||||
|
||||
WriteEscapeUrlCore(content.AsSpan());
|
||||
return this;
|
||||
}
|
||||
|
||||
private void WriteEscapeUrl(string content, int start, int length)
|
||||
private void WriteEscapeUrlCore(ReadOnlySpan<char> content)
|
||||
{
|
||||
int previousPosition = start;
|
||||
for (var i = previousPosition; i < length; i++)
|
||||
WriteIndent();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var c = content[i];
|
||||
int i = content.IndexOfAnyExcept(s_asciiNonEscapeChars);
|
||||
|
||||
if ((uint)i >= (uint)content.Length)
|
||||
{
|
||||
WriteRaw(content);
|
||||
break;
|
||||
}
|
||||
|
||||
WriteRaw(content.Slice(0, i));
|
||||
|
||||
char c = content[i];
|
||||
|
||||
if (c < 128)
|
||||
{
|
||||
var escape = HtmlHelper.EscapeUrlCharacter(c);
|
||||
if (escape != null)
|
||||
{
|
||||
Write(content, previousPosition, i - previousPosition);
|
||||
previousPosition = i + 1;
|
||||
Write(escape);
|
||||
}
|
||||
WriteRaw(HtmlHelper.EscapeUrlCharacter(c));
|
||||
}
|
||||
else if (UseNonAsciiNoEscape)
|
||||
{
|
||||
// Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working
|
||||
WriteRaw(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(content, previousPosition, i - previousPosition);
|
||||
previousPosition = i + 1;
|
||||
|
||||
// Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working
|
||||
if (UseNonAsciiNoEscape)
|
||||
{
|
||||
Write(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] bytes;
|
||||
if (c >= '\ud800' && c <= '\udfff' && previousPosition < length)
|
||||
{
|
||||
bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] });
|
||||
// Skip next char as it is decoded above
|
||||
i++;
|
||||
previousPosition = i + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
bytes = Encoding.UTF8.GetBytes(new[] { c });
|
||||
}
|
||||
for (var j = 0; j < bytes.Length; j++)
|
||||
{
|
||||
Write($"%{bytes[j]:X2}");
|
||||
}
|
||||
}
|
||||
i = WriteEscapedUtf8Bytes(this, content, c, i);
|
||||
}
|
||||
|
||||
content = content.Slice(i + 1);
|
||||
}
|
||||
|
||||
static int WriteEscapedUtf8Bytes(HtmlRenderer renderer, ReadOnlySpan<char> content, char c, int i)
|
||||
{
|
||||
scoped ReadOnlySpan<char> chars;
|
||||
|
||||
if (CharHelper.IsHighSurrogate(c) && (uint)(i + 1) < (uint)content.Length)
|
||||
{
|
||||
chars = stackalloc char[] { c, content[i + 1] };
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
chars = stackalloc char[] { c };
|
||||
}
|
||||
|
||||
Span<byte> utf8Buffer = stackalloc byte[4];
|
||||
int utf8Length = Encoding.UTF8.GetBytes(chars, utf8Buffer);
|
||||
utf8Buffer = utf8Buffer.Slice(0, utf8Length);
|
||||
|
||||
Span<char> escapedBuffer = stackalloc char[3];
|
||||
escapedBuffer[0] = '%';
|
||||
|
||||
foreach (byte b in utf8Buffer)
|
||||
{
|
||||
HexConverter.ToCharsBuffer(b, escapedBuffer, startingIndex: 1);
|
||||
renderer.WriteRaw(escapedBuffer);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
Write(content, previousPosition, length - previousPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,9 +18,9 @@ public class CodeBlockRenderer : NormalizeObjectRenderer<CodeBlock>
|
||||
{
|
||||
if (obj is FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
var fencedCharCount = Math.Min(fencedCodeBlock.OpeningFencedCharCount, fencedCodeBlock.ClosingFencedCharCount);
|
||||
var opening = new string(fencedCodeBlock.FencedChar, fencedCharCount);
|
||||
renderer.Write(opening);
|
||||
int fencedCharCount = Math.Min(fencedCodeBlock.OpeningFencedCharCount, fencedCodeBlock.ClosingFencedCharCount);
|
||||
|
||||
renderer.Write(fencedCodeBlock.FencedChar, fencedCharCount);
|
||||
if (fencedCodeBlock.Info != null)
|
||||
{
|
||||
renderer.Write(fencedCodeBlock.Info);
|
||||
@@ -41,7 +41,7 @@ public class CodeBlockRenderer : NormalizeObjectRenderer<CodeBlock>
|
||||
renderer.WriteLine();
|
||||
|
||||
renderer.WriteLeafRawLines(obj, true);
|
||||
renderer.Write(opening);
|
||||
renderer.Write(fencedCodeBlock.FencedChar, fencedCharCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -12,22 +12,27 @@ namespace Markdig.Renderers.Normalize;
|
||||
/// <seealso cref="NormalizeObjectRenderer{HeadingBlock}" />
|
||||
public class HeadingRenderer : NormalizeObjectRenderer<HeadingBlock>
|
||||
{
|
||||
private static readonly string[] HeadingTexts = {
|
||||
private static readonly string[] HeadingTexts = [
|
||||
"#",
|
||||
"##",
|
||||
"###",
|
||||
"####",
|
||||
"#####",
|
||||
"######",
|
||||
};
|
||||
];
|
||||
|
||||
protected override void Write(NormalizeRenderer renderer, HeadingBlock obj)
|
||||
{
|
||||
var headingText = obj.Level > 0 && obj.Level <= 6
|
||||
? HeadingTexts[obj.Level - 1]
|
||||
: new string('#', obj.Level);
|
||||
{
|
||||
if (obj.Level is > 0 and <= 6)
|
||||
{
|
||||
renderer.Write(HeadingTexts[obj.Level - 1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Write('#', obj.Level);
|
||||
}
|
||||
|
||||
renderer.Write(headingText).Write(' ');
|
||||
renderer.Write(' ');
|
||||
renderer.WriteLeafInline(obj);
|
||||
|
||||
renderer.FinishBlock(renderer.Options.EmptyLineAfterHeading);
|
||||
|
||||
@@ -31,8 +31,8 @@ public class CodeInlineRenderer : NormalizeObjectRenderer<CodeInline>
|
||||
if (delimiterCount < count)
|
||||
delimiterCount = count;
|
||||
}
|
||||
var delimiterRun = new string(obj.Delimiter, delimiterCount + 1);
|
||||
renderer.Write(delimiterRun);
|
||||
|
||||
renderer.Write(obj.Delimiter, delimiterCount + 1);
|
||||
if (content.Length != 0)
|
||||
{
|
||||
if (content[0] == obj.Delimiter)
|
||||
@@ -49,6 +49,6 @@ public class CodeInlineRenderer : NormalizeObjectRenderer<CodeInline>
|
||||
{
|
||||
renderer.Write(' ');
|
||||
}
|
||||
renderer.Write(delimiterRun);
|
||||
renderer.Write(obj.Delimiter, delimiterCount + 1);
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,8 @@ public class EmphasisInlineRenderer : NormalizeObjectRenderer<EmphasisInline>
|
||||
{
|
||||
protected override void Write(NormalizeRenderer renderer, EmphasisInline obj)
|
||||
{
|
||||
var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount);
|
||||
renderer.Write(emphasisText);
|
||||
renderer.Write(obj.DelimiterChar, obj.DelimiterCount);
|
||||
renderer.WriteChildren(obj);
|
||||
renderer.Write(emphasisText);
|
||||
renderer.Write(obj.DelimiterChar, obj.DelimiterCount);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
@@ -16,31 +16,64 @@ namespace Markdig.Renderers;
|
||||
/// <seealso cref="IMarkdownRenderer" />
|
||||
public abstract class RendererBase : IMarkdownRenderer
|
||||
{
|
||||
private readonly Dictionary<KeyWrapper, IMarkdownObjectRenderer?> _renderersPerType = new();
|
||||
private const int SubTableCount = 32;
|
||||
|
||||
private readonly struct RendererEntry
|
||||
{
|
||||
public readonly IntPtr Key;
|
||||
public readonly IMarkdownObjectRenderer? Renderer;
|
||||
|
||||
public RendererEntry(IntPtr key, IMarkdownObjectRenderer? renderer)
|
||||
{
|
||||
Key = key;
|
||||
Renderer = renderer;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly RendererEntry[][] _renderersPerType;
|
||||
|
||||
internal int _childrenDepth = 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static IntPtr GetKeyForType(MarkdownObject obj) => Type.GetTypeHandle(obj).Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int SubTableIndex(IntPtr key) => (int)((((ulong)key) / 64) & (SubTableCount - 1));
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RendererBase"/> class.
|
||||
/// </summary>
|
||||
protected RendererBase() { }
|
||||
protected RendererBase()
|
||||
{
|
||||
var entries = _renderersPerType = new RendererEntry[SubTableCount][];
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
entries[i] ??= [];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj)
|
||||
{
|
||||
KeyWrapper key = GetKeyForType(obj);
|
||||
Type objectType = obj.GetType();
|
||||
IMarkdownObjectRenderer? renderer = null;
|
||||
|
||||
for (int i = 0; i < ObjectRenderers.Count; i++)
|
||||
foreach (var potentialRenderer in ObjectRenderers)
|
||||
{
|
||||
var renderer = ObjectRenderers[i];
|
||||
if (renderer.Accept(this, objectType))
|
||||
if (potentialRenderer.Accept(this, objectType))
|
||||
{
|
||||
_renderersPerType[key] = renderer;
|
||||
return renderer;
|
||||
renderer = potentialRenderer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_renderersPerType[key] = null;
|
||||
return null;
|
||||
IntPtr key = GetKeyForType(obj);
|
||||
|
||||
ref RendererEntry[] entries = ref _renderersPerType[SubTableIndex(key)];
|
||||
Array.Resize(ref entries, entries.Length + 1);
|
||||
entries[entries.Length - 1] = new RendererEntry(key, renderer);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public ObjectRendererCollection ObjectRenderers { get; } = new();
|
||||
@@ -77,12 +110,11 @@ public abstract class RendererBase : IMarkdownRenderer
|
||||
bool saveIsFirstInContainer = IsFirstInContainer;
|
||||
bool saveIsLastInContainer = IsLastInContainer;
|
||||
|
||||
var children = containerBlock;
|
||||
for (int i = 0; i < children.Count; i++)
|
||||
for (int i = 0; i < containerBlock.Count; i++)
|
||||
{
|
||||
IsFirstInContainer = i == 0;
|
||||
IsLastInContainer = i + 1 == children.Count;
|
||||
Write(children[i]);
|
||||
IsLastInContainer = i + 1 == containerBlock.Count;
|
||||
Write(containerBlock[i]);
|
||||
}
|
||||
|
||||
IsFirstInContainer = saveIsFirstInContainer;
|
||||
@@ -140,11 +172,27 @@ public abstract class RendererBase : IMarkdownRenderer
|
||||
// Calls before writing an object
|
||||
ObjectWriteBefore?.Invoke(this, obj);
|
||||
|
||||
if (!_renderersPerType.TryGetValue(GetKeyForType(obj), out IMarkdownObjectRenderer? renderer))
|
||||
IMarkdownObjectRenderer? renderer = null;
|
||||
IntPtr key = GetKeyForType(obj);
|
||||
|
||||
#if NETFRAMEWORK || NETSTANDARD
|
||||
RendererEntry[] renderers = _renderersPerType[SubTableIndex(key)];
|
||||
#else
|
||||
RendererEntry[] renderers = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_renderersPerType), SubTableIndex(key));
|
||||
#endif
|
||||
|
||||
foreach (RendererEntry entry in renderers)
|
||||
{
|
||||
renderer = GetRendererInstance(obj);
|
||||
if (key == entry.Key)
|
||||
{
|
||||
renderer = entry.Renderer;
|
||||
goto Render;
|
||||
}
|
||||
}
|
||||
|
||||
renderer = GetRendererInstance(obj);
|
||||
|
||||
Render:
|
||||
if (renderer is not null)
|
||||
{
|
||||
renderer.Write(this, obj);
|
||||
@@ -161,24 +209,4 @@ public abstract class RendererBase : IMarkdownRenderer
|
||||
// Calls after writing an object
|
||||
ObjectWriteAfter?.Invoke(this, obj);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static KeyWrapper GetKeyForType(MarkdownObject obj)
|
||||
{
|
||||
IntPtr typeHandle = Type.GetTypeHandle(obj).Value;
|
||||
return new KeyWrapper(typeHandle);
|
||||
}
|
||||
|
||||
private readonly struct KeyWrapper : IEquatable<KeyWrapper>
|
||||
{
|
||||
public readonly IntPtr Key;
|
||||
|
||||
public KeyWrapper(IntPtr key) => Key = key;
|
||||
|
||||
public bool Equals(KeyWrapper other) => Key == other.Key;
|
||||
|
||||
public override int GetHashCode() => Key.GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,7 @@ public class CodeBlockRenderer : RoundtripObjectRenderer<CodeBlock>
|
||||
if (obj is FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
renderer.Write(obj.TriviaBefore);
|
||||
var opening = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount);
|
||||
renderer.Write(opening);
|
||||
renderer.Write(fencedCodeBlock.FencedChar, fencedCodeBlock.OpeningFencedCharCount);
|
||||
|
||||
if (!fencedCodeBlock.TriviaAfterFencedChar.IsEmpty)
|
||||
{
|
||||
@@ -56,9 +55,8 @@ public class CodeBlockRenderer : RoundtripObjectRenderer<CodeBlock>
|
||||
renderer.WriteLeafRawLines(obj);
|
||||
|
||||
renderer.Write(fencedCodeBlock.TriviaBeforeClosingFence);
|
||||
var closing = new string(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount);
|
||||
renderer.Write(closing);
|
||||
if (!string.IsNullOrEmpty(closing))
|
||||
renderer.Write(fencedCodeBlock.FencedChar, fencedCodeBlock.ClosingFencedCharCount);
|
||||
if (fencedCodeBlock.ClosingFencedCharCount > 0)
|
||||
{
|
||||
// See example 207: "> ```\nfoo\n```"
|
||||
renderer.WriteLine(obj.NewLine);
|
||||
|
||||
@@ -12,14 +12,14 @@ namespace Markdig.Renderers.Roundtrip;
|
||||
/// <seealso cref="RoundtripObjectRenderer{HeadingBlock}" />
|
||||
public class HeadingRenderer : RoundtripObjectRenderer<HeadingBlock>
|
||||
{
|
||||
private static readonly string[] HeadingTexts = {
|
||||
private static readonly string[] HeadingTexts = [
|
||||
"#",
|
||||
"##",
|
||||
"###",
|
||||
"####",
|
||||
"#####",
|
||||
"######",
|
||||
};
|
||||
];
|
||||
|
||||
protected override void Write(RoundtripRenderer renderer, HeadingBlock obj)
|
||||
{
|
||||
@@ -28,12 +28,11 @@ public class HeadingRenderer : RoundtripObjectRenderer<HeadingBlock>
|
||||
renderer.RenderLinesBefore(obj);
|
||||
|
||||
var headingChar = obj.Level == 1 ? '=' : '-';
|
||||
var line = new string(headingChar, obj.HeaderCharCount);
|
||||
|
||||
renderer.WriteLeafInline(obj);
|
||||
renderer.WriteLine(obj.SetextNewline);
|
||||
renderer.Write(obj.TriviaBefore);
|
||||
renderer.Write(line);
|
||||
renderer.Write(headingChar, obj.HeaderCharCount);
|
||||
renderer.WriteLine(obj.NewLine);
|
||||
renderer.Write(obj.TriviaAfter);
|
||||
|
||||
@@ -43,12 +42,17 @@ public class HeadingRenderer : RoundtripObjectRenderer<HeadingBlock>
|
||||
{
|
||||
renderer.RenderLinesBefore(obj);
|
||||
|
||||
var headingText = obj.Level > 0 && obj.Level <= 6
|
||||
? HeadingTexts[obj.Level - 1]
|
||||
: new string('#', obj.Level);
|
||||
|
||||
renderer.Write(obj.TriviaBefore);
|
||||
renderer.Write(headingText);
|
||||
|
||||
if (obj.Level is > 0 and <= 6)
|
||||
{
|
||||
renderer.Write(HeadingTexts[obj.Level - 1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Write('#', obj.Level);
|
||||
}
|
||||
|
||||
renderer.Write(obj.TriviaAfterAtxHeaderChar);
|
||||
renderer.WriteLeafInline(obj);
|
||||
renderer.Write(obj.TriviaAfter);
|
||||
|
||||
@@ -14,12 +14,11 @@ public class CodeInlineRenderer : RoundtripObjectRenderer<CodeInline>
|
||||
{
|
||||
protected override void Write(RoundtripRenderer renderer, CodeInline obj)
|
||||
{
|
||||
var delimiterRun = new string(obj.Delimiter, obj.DelimiterCount);
|
||||
renderer.Write(delimiterRun);
|
||||
renderer.Write(obj.Delimiter, obj.DelimiterCount);
|
||||
if (!obj.ContentSpan.IsEmpty)
|
||||
{
|
||||
renderer.Write(obj.ContentWithTrivia);
|
||||
}
|
||||
renderer.Write(delimiterRun);
|
||||
renderer.Write(obj.Delimiter, obj.DelimiterCount);
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,8 @@ public class EmphasisInlineRenderer : RoundtripObjectRenderer<EmphasisInline>
|
||||
{
|
||||
protected override void Write(RoundtripRenderer renderer, EmphasisInline obj)
|
||||
{
|
||||
var emphasisText = new string(obj.DelimiterChar, obj.DelimiterCount);
|
||||
renderer.Write(emphasisText);
|
||||
renderer.Write(obj.DelimiterChar, obj.DelimiterCount);
|
||||
renderer.WriteChildren(obj);
|
||||
renderer.Write(emphasisText);
|
||||
renderer.Write(obj.DelimiterChar, obj.DelimiterCount);
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,25 @@ public abstract class TextRendererBase<T> : TextRendererBase where T : TextRende
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified char repeated a specified number of times.
|
||||
/// </summary>
|
||||
/// <param name="c">The char to write.</param>
|
||||
/// <param name="count">The number of times to write the char.</param>
|
||||
/// <returns>This instance</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal T Write(char c, int count)
|
||||
{
|
||||
WriteIndent();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Writer.Write(c);
|
||||
}
|
||||
|
||||
return (T)this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified slice.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CodeBlock : LeafBlock
|
||||
}
|
||||
|
||||
private List<CodeBlockLine>? _codeBlockLines;
|
||||
public List<CodeBlockLine> CodeBlockLines => _codeBlockLines ??= new();
|
||||
public List<CodeBlockLine> CodeBlockLines => _codeBlockLines ??= [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeBlock"/> class.
|
||||
|
||||
@@ -27,7 +27,7 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
/// <param name="parser">The parser used to create this block.</param>
|
||||
protected ContainerBlock(BlockParser? parser) : base(parser)
|
||||
{
|
||||
_children = Array.Empty<BlockWrapper>();
|
||||
_children = [];
|
||||
SetTypeKind(isInline: false, isContainer: true);
|
||||
}
|
||||
|
||||
@@ -301,14 +301,9 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class BlockComparisonWrapper : IComparer<BlockWrapper>
|
||||
private sealed class BlockComparisonWrapper(Comparison<Block> comparison) : IComparer<BlockWrapper>
|
||||
{
|
||||
private readonly Comparison<Block> _comparison;
|
||||
|
||||
public BlockComparisonWrapper(Comparison<Block> comparison)
|
||||
{
|
||||
_comparison = comparison;
|
||||
}
|
||||
private readonly Comparison<Block> _comparison = comparison;
|
||||
|
||||
public int Compare(BlockWrapper x, BlockWrapper y)
|
||||
{
|
||||
@@ -316,14 +311,9 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockComparerWrapper : IComparer<BlockWrapper>
|
||||
private sealed class BlockComparerWrapper(IComparer<Block> comparer) : IComparer<BlockWrapper>
|
||||
{
|
||||
private readonly IComparer<Block> _comparer;
|
||||
|
||||
public BlockComparerWrapper(IComparer<Block> comparer)
|
||||
{
|
||||
_comparer = comparer;
|
||||
}
|
||||
private readonly IComparer<Block> _comparer = comparer;
|
||||
|
||||
public int Compare(BlockWrapper x, BlockWrapper y)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user