Compare commits

...

32 Commits

Author SHA1 Message Date
Alexandre Mutel
f48331d6c7 Fix missing code in commit for #780 2024-03-14 18:34:44 +01:00
Alexandre Mutel
6549d3b726 Fixes #780 where only the first paragraph of an alert block is processed. 2024-03-14 18:31:11 +01:00
Alexandre Mutel
d434f00355 Merge pull request #779 from toothache/fix_math_span
Fix math source span
2024-03-14 08:14:58 +01:00
Alexandre Mutel
b62a12d32d Add support for GitHub alert blocks (#776)
* Add support for GitHub alert blocks

* Fix alert for "must come first in a quote block"

* Fix comment

* Update src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

* Update src/Markdig/MarkdownExtensions.cs

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

* Fix parsing of alert block with multiple children blocks

* Allow null for BlockParser ctor argument of QuoteBlock

---------

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-03-14 08:09:42 +01:00
teethache
3c7edaa82d Fix math source span. 2024-03-11 20:35:54 +08:00
Alexandre Mutel
fcb56fb037 Merge pull request #773 from hhyyrylainen/patch-1
Fixed a typo in MathSpecs.md
2024-02-29 08:21:10 +01:00
Henri Hyyryläinen
50bc6cadfc Fix typo in MathSpecs.md 2024-02-18 18:58:04 +02:00
Alexandre Mutel
201aa4ef73 Try to use the reusable workflow 2024-02-17 07:11:33 +01:00
Alexandre Mutel
adce9797d6 Use ubuntu for the CI 2024-02-17 06:49:39 +01:00
Alexandre Mutel
fb71dff0ec Fix tests 2024-02-17 06:48:47 +01:00
Alexandre Mutel
399570941a Remove net7.0 from CI 2024-02-17 06:47:31 +01:00
Alexandre Mutel
2f903697e2 Update projects to net8.0 2024-02-17 06:43:07 +01:00
Alexandre Mutel
eb8fe15679 Update ci.yml with nuget config 2024-02-13 09:29:26 +01:00
Alexandre Mutel
8f008e45ca Add nuget.org only config 2024-02-13 09:29:26 +01:00
Alexandre Mutel
0014ec4138 Merge pull request #769 from carbon/cq3
Eliminate various string allocations
2024-02-13 08:25:40 +01:00
Jason Nelson
2ca05ccad7 Eliminate string allocation in CodeInlineRenderer 2024-02-09 13:13:31 -08:00
Jason Nelson
6a15c804bc Add test coverage for headlines with > 6 # characters 2024-02-09 12:54:52 -08:00
Jason Nelson
0446959623 Add TextRendererBase.Write(char c, int count) method, and eliminate various string allocations 2024-02-09 12:54:30 -08:00
Alexandre Mutel
e6afddbaa0 Merge pull request #761 from carbon/collection-expressions
Use C# 12 syntax
2023-12-18 21:11:50 +01:00
Jason Nelson
a377239e91 Use null-coalescing assignment operator 2023-12-14 20:08:53 -08:00
Jason Nelson
35aa304faf Remove unused using statement 2023-12-14 19:56:08 -08:00
Jason Nelson
e4568979ec Fix typo 2023-12-14 19:55:45 -08:00
Jason Nelson
3470ec0d54 Make various members readonly on SourceSpan 2023-12-14 19:55:37 -08:00
Jason Nelson
113ef7f215 Use primary constructors (part 2) 2023-12-14 19:50:09 -08:00
Jason Nelson
4cb4b68883 Use collection expressions (part 5) 2023-12-14 19:43:15 -08:00
Jason Nelson
64ae344b74 Use collection expressions (part 4) 2023-12-14 15:41:07 -08:00
Jason Nelson
b5f3c9fc67 Use collection expressions (part 3) 2023-12-14 12:57:11 -08:00
Jason Nelson
8a88fd0557 Use collection expressions (part 2) 2023-12-14 12:46:40 -08:00
Jason Nelson
cc7623989d Fix typo on private method 2023-12-14 12:39:19 -08:00
Jason Nelson
b6a7acf5fc Use primary constructors 2023-12-14 12:35:22 -08:00
Jason Nelson
804a6f0dbc Use accelerated IndexOfAny in one more case 2023-12-14 12:32:52 -08:00
Jason Nelson
342e264988 Use collection expressions 2023-12-14 12:32:34 -08:00
83 changed files with 900 additions and 283 deletions

View File

@@ -11,25 +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, 7.0, and 8.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
6.0.x
7.0.x
8.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 }}

View 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>

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<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.11" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.11" />
<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="3.1.456101" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.506101" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />

View File

@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<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.8.0" />
<PackageReference Include="NUnit" Version="4.0.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>

View 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 = &quot;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 = &quot;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");
}
}
}

View 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 = &quot;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>
````````````````````````````````

View File

@@ -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()

View File

@@ -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

View File

@@ -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()
{

View File

@@ -699,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");
}

View File

@@ -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>

View File

@@ -20,7 +20,7 @@ public class AbbreviationParser : BlockParser
/// </summary>
public AbbreviationParser()
{
OpeningCharacters = new[] { '*' };
OpeningCharacters = ['*'];
}
public override BlockState TryOpen(BlockProcessor processor)

View 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; }
}

View 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);
}
}
}

View 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
});
}
}
}

View 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;
}
}

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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();
}

View File

@@ -17,7 +17,7 @@ public class CustomContainerParser : FencedBlockParserBase<CustomContainer>
/// </summary>
public CustomContainerParser()
{
OpeningCharacters = new [] {':'};
OpeningCharacters = [':'];
// We don't need a prefix
InfoPrefix = null;

View File

@@ -18,7 +18,7 @@ public class DefinitionListParser : BlockParser
/// </summary>
public DefinitionListParser()
{
OpeningCharacters = new [] {':', '~'};
OpeningCharacters = [':', '~'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -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];
}
}

View File

@@ -18,7 +18,7 @@ public class FigureBlockParser : BlockParser
/// </summary>
public FigureBlockParser()
{
OpeningCharacters = new[] { '^' };
OpeningCharacters = ['^'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -19,7 +19,7 @@ public class FooterBlockParser : BlockParser
/// </summary>
public FooterBlockParser()
{
OpeningCharacters = new[] {'^'};
OpeningCharacters = ['^'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -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)
{

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -17,7 +17,7 @@ public class NoFollowLinksExtension : IMarkdownExtension
public NoFollowLinksExtension()
{
_referralLinksExtension = new ReferralLinksExtension(new[] { "nofollow" });
_referralLinksExtension = new ReferralLinksExtension(["nofollow"]);
}
public void Setup(MarkdownPipelineBuilder pipeline)

View File

@@ -19,7 +19,7 @@ public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
/// </summary>
public SmartyPantsInlineParser()
{
OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'};
OpeningCharacters = ['\'', '"', '<', '>', '.', '-'];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)

View File

@@ -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);

View File

@@ -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; }

View File

@@ -22,7 +22,7 @@ public class PipeTableBlockParser : BlockParser
/// </summary>
public PipeTableBlockParser()
{
OpeningCharacters = new[] {'-'};
OpeningCharacters = ['-'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -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();
}
@@ -637,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; } = [];
}
}

View File

@@ -19,7 +19,7 @@ public class TaskListInlineParser : InlineParser
/// </summary>
public TaskListInlineParser()
{
OpeningCharacters = new[] {'['};
OpeningCharacters = ['['];
ListClass = "contains-task-list";
ListItemClass = "task-list-item";
}

View File

@@ -26,7 +26,7 @@ public class YamlFrontMatterParser : BlockParser
/// </summary>
public YamlFrontMatterParser()
{
this.OpeningCharacters = new[] { '-' };
OpeningCharacters = ['-'];
}
/// <summary>

View File

@@ -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;

View File

@@ -722,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)
{

View File

@@ -35,7 +35,7 @@ public sealed class CharacterMap<T> where T : class
charSet.Add(map.Key);
}
OpeningCharacters = charSet.ToArray();
OpeningCharacters = [.. charSet];
Array.Sort(OpeningCharacters);
_asciiMap = new T[128];

View File

@@ -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()

View File

@@ -187,16 +187,10 @@ public struct StringLineGroup : IEnumerable
}
}
public struct Enumerator : IEnumerator
public struct Enumerator(StringLineGroup parent) : IEnumerator
{
private readonly StringLineGroup _parent;
private int _index;
public Enumerator(StringLineGroup parent)
{
_parent = parent;
_index = -1;
}
private readonly StringLineGroup _parent = parent;
private int _index = -1;
public object Current => _parent.Lines[_index];
@@ -452,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;
}
}

View File

@@ -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;

View File

@@ -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()
{

View File

@@ -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>();
}

View File

@@ -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; }

View File

@@ -20,7 +20,7 @@ public class FencedCodeBlockParser : FencedBlockParserBase<FencedCodeBlock>
/// </summary>
public FencedCodeBlockParser()
{
OpeningCharacters = new[] {'`', '~'};
OpeningCharacters = ['`', '~'];
InfoPrefix = DefaultInfoPrefix;
}

View File

@@ -20,7 +20,7 @@ public class HeadingBlockParser : BlockParser, IAttributesParseable
/// </summary>
public HeadingBlockParser()
{
OpeningCharacters = new[] {'#'};
OpeningCharacters = ['#'];
}
/// <summary>

View File

@@ -18,7 +18,7 @@ public class HtmlBlockParser : BlockParser
/// </summary>
public HtmlBlockParser()
{
OpeningCharacters = new[] { '<' };
OpeningCharacters = ['<'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -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);

View File

@@ -19,7 +19,7 @@ public class AutolinkInlineParser : InlineParser
/// </summary>
public AutolinkInlineParser()
{
OpeningCharacters = new[] {'<'};
OpeningCharacters = ['<'];
EnableHtmlParsing = true;
}

View File

@@ -21,7 +21,7 @@ public class CodeInlineParser : InlineParser
/// </summary>
public CodeInlineParser()
{
OpeningCharacters = new[] { '`' };
OpeningCharacters = ['`'];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)

View File

@@ -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()
{

View File

@@ -15,7 +15,7 @@ public class EscapeInlineParser : InlineParser
{
public EscapeInlineParser()
{
OpeningCharacters = new[] {'\\'};
OpeningCharacters = ['\\'];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)

View File

@@ -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)

View File

@@ -18,7 +18,7 @@ public class LineBreakInlineParser : InlineParser
/// </summary>
public LineBreakInlineParser()
{
OpeningCharacters = new[] { '\n', '\r' };
OpeningCharacters = ['\n', '\r'];
}
/// <summary>

View File

@@ -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);

View File

@@ -170,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);
@@ -210,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;
}
}

View File

@@ -15,7 +15,7 @@ public abstract class OrderedListItemParser : ListItemParser
/// </summary>
protected OrderedListItemParser()
{
OrderedDelimiters = new[] { '.', ')' };
OrderedDelimiters = ['.', ')'];
}
/// <summary>

View File

@@ -18,7 +18,7 @@ public class QuoteBlockParser : BlockParser
/// </summary>
public QuoteBlockParser()
{
OpeningCharacters = new[] {'>'};
OpeningCharacters = ['>'];
}
public override BlockState TryOpen(BlockProcessor processor)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -144,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>
@@ -159,7 +161,7 @@ public class HtmlRenderer : TextRendererBase<HtmlRenderer>
{
int indexOfCharToEscape = softEscape
? content.IndexOfAny('<', '&')
: content.IndexOfAny("<>&\"");
: content.IndexOfAny(s_escapedChars);
if ((uint)indexOfCharToEscape >= (uint)content.Length)
{

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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)
{

View File

@@ -155,16 +155,10 @@ public abstract class MarkdownObject : IMarkdownObject
private class DataEntriesAndTrivia
{
private struct DataEntry
private struct DataEntry(object key, object value)
{
public readonly object Key;
public object Value;
public DataEntry(object key, object value)
{
Key = key;
Value = value;
}
public readonly object Key = key;
public object Value = value;
}
private DataEntry[]? _entries;

View File

@@ -19,7 +19,7 @@ public class QuoteBlock : ContainerBlock
/// Initializes a new instance of the <see cref="QuoteBlock"/> class.
/// </summary>
/// <param name="parser">The parser used to create this block.</param>
public QuoteBlock(BlockParser parser) : base(parser)
public QuoteBlock(BlockParser? parser) : base(parser)
{
}
@@ -42,7 +42,7 @@ public class QuoteBlock : ContainerBlock
public class QuoteBlockLine
{
/// <summary>
/// Gets or sets trivia occuring before the first quote character.
/// Gets or sets trivia occurring before the first quote character.
/// </summary>
public StringSlice TriviaBefore { get; set; }

View File

@@ -37,26 +37,26 @@ public struct SourceSpan : IEquatable<SourceSpan>
/// <summary>
/// Gets the character length of this element within the original source code.
/// </summary>
public int Length => End - Start + 1;
public readonly int Length => End - Start + 1;
public bool IsEmpty => Start > End;
public readonly bool IsEmpty => Start > End;
public SourceSpan MoveForward(int count)
{
return new SourceSpan(Start + count, End + count);
}
public bool Equals(SourceSpan other)
public readonly bool Equals(SourceSpan other)
{
return Start == other.Start && End == other.End;
}
public override bool Equals(object? obj)
public override readonly bool Equals(object? obj)
{
return obj is SourceSpan sourceSpan && Equals(sourceSpan);
}
public override int GetHashCode()
public override readonly int GetHashCode()
{
unchecked
{
@@ -74,7 +74,7 @@ public struct SourceSpan : IEquatable<SourceSpan>
return !left.Equals(right);
}
public override string ToString()
public override readonly string ToString()
{
return $"{Start}-{End}";
}

View File

@@ -61,6 +61,7 @@ class Program
static readonly Spec[] Specs = new[]
{
new Spec("CommonMarkSpecs", "CommonMark.md", ""),
new Spec("Alert Blocks", "AlertBlockSpecs.md", "advanced"),
new Spec("Pipe Tables", "PipeTableSpecs.md", "pipetables|advanced"),
new Spec("GFM Pipe Tables", "PipeTableGfmSpecs.md", "gfm-pipetables"),
new Spec("Footnotes", "FootnotesSpecs.md", "footnotes|advanced"),

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>