Compare commits

...

135 Commits

Author SHA1 Message Date
Alexandre Mutel
5528023158 Merge pull request #844 from snnz/fix-gridtables
Prevent GridTableParser from looking beyond the end of a line.
2025-01-09 18:15:11 +01:00
Alexandre Mutel
f93b9d79d9 Merge branch 'master' into fix-gridtables 2025-01-06 08:43:45 +01:00
Alexandre Mutel
d53fd0e870 Merge pull request #843 from snnz/fix-deflists
Fixes exception in DefinitionListParser.GetCurrentDefinitionList()
2025-01-06 08:42:36 +01:00
Alexandre Mutel
c488aca96c Merge branch 'master' into fix-deflists 2025-01-05 21:12:33 +01:00
Alexandre Mutel
9b3f442765 Merge pull request #842 from snnz/fix-alerts
Check that the alert candidate is not already in an alert block or nested within other elements.
2025-01-05 21:11:11 +01:00
Sergey Nozhenko
7b6d659bbd A test has been added. 2025-01-03 07:03:28 +03:00
Sergey Nozhenko
bc8ba4fecb A test has been added. 2025-01-03 07:02:38 +03:00
Sergey Nozhenko
d87bb7292d A test has been added. 2025-01-03 07:01:29 +03:00
Sergey Nozhenko
118d28f886 Prevent GridTableParser from looking beyond the end of a line. 2025-01-03 04:29:24 +03:00
Sergey Nozhenko
3e0c72f043 Fixes exception in DefinitionListParser.GetCurrentDefinitionList() 2025-01-03 03:30:49 +03:00
Sergey Nozhenko
f2590e7b80 Check that the alert candidate is not already in an alert block or nested within other elements. 2025-01-03 01:27:11 +03:00
Alexandre Mutel
d1233ffe66 Merge pull request #837 from snnz/fix-links
Fix errors in LinkHelper and LinkInlineParser.
2024-12-27 09:49:04 +01:00
Sergey Nozhenko
ab8e85b06e Remove additional condition, since a carriage return constitute a line ending regardless of whether it is followed by a line feed or not. 2024-12-21 06:56:23 +03:00
snnz
90bc15c016 Update src/Markdig.Tests/TestPlayParser.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-12-21 06:14:16 +03:00
snnz
7f604bef30 Update src/Markdig/Parsers/Inlines/LinkInlineParser.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-12-21 06:14:07 +03:00
snnz
54783b8f65 Update src/Markdig/Parsers/Inlines/LinkInlineParser.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-12-21 06:13:56 +03:00
snnz
ad0770a594 Update src/Markdig/Parsers/Inlines/LinkInlineParser.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-12-21 06:13:22 +03:00
snnz
90365bfeee Update src/Markdig/Parsers/Inlines/LinkInlineParser.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-12-21 06:13:09 +03:00
Sergey Nozhenko
c35f7fff17 Fixed errors in LinkHelper and LinkInlineParser. 2024-12-21 03:29:31 +03:00
Alexandre Mutel
fdaef77474 Update ci badge 2024-12-19 05:48:36 +01:00
Alexandre Mutel
733c028311 Merge pull request #836 from snnz/fix-abbreviation
Fix an error in the AbbreviationParser.
2024-12-19 05:46:48 +01:00
Sergey Nozhenko
bc41b0c2a3 Existing test has been extended. 2024-12-19 00:44:29 +03:00
Alexandre Mutel
a8de2087d8 Merge pull request #835 from snnz/fix-pipetable-span
Include opening and closing pipes in the table span
2024-12-18 20:36:31 +01:00
Sergey Nozhenko
2cff6c5194 It's necessary to keep a copy of the original literal.Span.End, because otherwise it is just lost in some cases. 2024-12-18 13:09:00 +03:00
Sergey Nozhenko
5e4a917dbd Fixes an error in the AbbreviationParser. 2024-12-18 12:38:13 +03:00
Sergey Nozhenko
aff8a6823a A test has been added. 2024-12-18 10:21:29 +03:00
Alexandre Mutel
b8a3c270cc Merge pull request #834 from snnz/fix-gridtables
Fix an incorrect offset in GridTableParser.
2024-12-18 06:47:17 +01:00
Sergey Nozhenko
68659f4037 Include opening and closing pipes in the table span 2024-12-18 07:43:22 +03:00
Sergey Nozhenko
e92a8097d0 Fixes an incorrect offset in GridTableParser. 2024-12-18 02:36:56 +03:00
Alexandre Mutel
57fad6fc1a Merge pull request #828 from MihaZupan/net90
Add .NET 9 target, drop .NET 6
2024-12-17 08:43:00 +01:00
Alexandre Mutel
260f4d5acc Merge pull request #832 from snnz/footnote
Set the correct source location in Footnote and FootnoteLinkReferenceDefinition
2024-12-17 08:41:22 +01:00
Alexandre Mutel
102d02a6c1 Merge pull request #831 from snnz/custom-container
Set delimiter char and count in CustomContainerInline instances.
2024-12-17 08:40:55 +01:00
Alexandre Mutel
5ae8ab7a74 Merge pull request #829 from snnz/fix-emphasis-span
Fix incorrect emphasis span calculation.
2024-12-17 08:40:24 +01:00
Sergey Nozhenko
eb28f76588 Set the correct source location to the Footnote and FootnoteLinkReferenceDefinition. 2024-12-15 20:13:35 +03:00
Sergey Nozhenko
d0311b4cea Set delimiter in the CustomContainer instance. 2024-12-15 19:49:56 +03:00
Sergey Nozhenko
a11899a350 Fixes emphasis span calculation. A test is added. 2024-12-09 22:20:32 +03:00
Miha Zupan
40781737c3 Tweak SpecFileGen paths 2024-11-30 03:19:49 +01:00
Miha Zupan
455f8f333d Fix dotnet-versions format 2024-11-29 20:22:40 +01:00
Miha Zupan
98a060f2a3 Add .NET 9 target, drop .NET 6 2024-11-29 19:06:58 +01:00
snnz
49cf59b819 Fix extra line feeds in link title (#826)
* Fix extra line feeds in link title

* Test added.
2024-11-24 11:51:56 +01:00
Alexandre Mutel
310a55c724 Update readme.md 2024-10-30 19:08:35 +01:00
Alexandre Mutel
f734e91568 Merge pull request #823 from xoofx/fix-mermaid
Update DiagramExtension.cs
2024-10-25 22:01:40 +02:00
Alexandre Mutel
090e6d791a Update DiagramExtension.cs
Update tests
2024-10-25 21:58:28 +02:00
Alexandre Mutel
41bdb0f0ab Merge pull request #817 from ehsankalafchi/rename-variable
Rename a variable
2024-10-01 20:53:47 +02:00
Ehsan Kalafchi
b27ef11240 Rename a variable 2024-09-26 13:51:18 +02:00
Alexandre Mutel
dfa2c94b88 Merge pull request #808 from digvijayad/master
Fix mermaid link in readme.md
2024-07-18 06:50:19 +02:00
Digvijay Naruka
89330f3524 Update mermaid link in readme.md
Updated broken old GitHub pages link to the new domain https://mermaid.js.org/
2024-07-17 12:44:26 +05:30
Alexandre Mutel
1a1bbecc46 Merge pull request #786 from MartinZikmund/feature/youtube-short-support
Support for YouTube Shorts embedding
2024-04-09 21:11:30 +02:00
Martin Zikmund
68bd3074b2 Add support for YouTube Shorts embedding 2024-04-01 17:42:46 +02:00
Martin Zikmund
e486903687 Test support for YouTube Shorts embedding 2024-04-01 17:42:12 +02:00
Alexandre Mutel
8e22754db4 Merge pull request #785 from toothache/fix_issues
Fix issues for math span calculation
2024-03-29 21:23:31 +01:00
teethache
93d88ab994 Fix math span calculation. 2024-03-27 07:06:56 +08:00
Alexandre Mutel
000393f46a Fix invalid setext heading (#785) 2024-03-26 21:02:48 +01:00
Alexandre Mutel
a5796890e1 Merge pull request #784 from Abrynos/case-invariant-alerts
Make alert block headers case-invariant
2024-03-25 21:26:19 +01:00
Sebastian Göls
c19ba5b0eb Add fallback value in order to mark unknown alert kinds in some way as well 2024-03-25 12:29:22 +01:00
Sebastian Göls
03390e4f71 Misc. 2024-03-18 10:28:36 +01:00
Sebastian Göls
42bd65caaf Apply feedback 2024-03-18 09:18:39 +01:00
Sebastian Göls
b7ae04bdba Make alert block headers case-invariant 2024-03-18 09:04:47 +01:00
Alexandre Mutel
391f376fa2 Merge pull request #782 from Abrynos/bootstrap-alerts
Add bootstrap alert renderer
2024-03-18 08:11:19 +01:00
Sebastian Göls
f9e96bc9c9 Apply feedback 2024-03-18 07:48:22 +01:00
Alexandre Mutel
c75a11ec32 Update parsing-extensions.md 2024-03-17 15:01:14 +01:00
Alexandre Mutel
fd226d53e9 Update readme.md 2024-03-17 14:59:05 +01:00
Sebastian Göls
7132584996 Add bootstrap alert renderer 2024-03-15 11:28:48 +01:00
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
Alexandre Mutel
f52ecee0b9 Update packages 2023-12-14 06:24:19 +01:00
Alexandre Mutel
a092ec23b3 Merge pull request #760 from zickb/fix_source_span_calculation_for_linebreak_inline
Fix source span calculation for LineBreakInline
2023-12-14 06:18:09 +01:00
Benni
6f1dce6306 Fix last LineBreakInline source span in multi block scenario 2023-12-14 02:53:02 +01:00
Alexandre Mutel
040a778d87 Merge pull request #759 from Akarinnnnn/fix-757
Make StringLineGroup returns a count limited Enumerator
2023-12-12 19:13:52 +01:00
Fa鸽
2ae2cf9263 Add tests for non-boxed enumerator of StringLineGroup. 2023-12-12 16:18:10 +08:00
Fa鸽
ba1e562d2f Fix complication error of StringLineGroup. 2023-12-12 16:15:55 +08:00
Fa鸽
65a02e44ec Add Enumerator GetEnumerator() for StringLineGroup 2023-12-12 15:45:30 +08:00
Alexandre Mutel
e78833ae30 Update src/Markdig/Helpers/StringLineGroup.cs 2023-12-12 08:00:54 +01:00
Fa鸽
2ab716bec1 Make StringLineGroup returns a count limited Enumerator
Fixes #757, before we return the array enumerator directly, enumerate it will get phantom empty lines.
2023-12-11 19:57:51 +08:00
Alexandre Mutel
feeb1867ce Merge pull request #753 from MihaZupan/perf-nov23-3
A few more perf improvements
2023-11-29 10:00:01 +01:00
Miha Zupan
f3aa7e73e3 Avoid Dictionary lookups in RendererBase.Write 2023-11-26 02:27:26 +01:00
Miha Zupan
dce5572356 Create inlining boundaries in MarkdownParser.Parse 2023-11-26 00:59:12 +01:00
Miha Zupan
dbbabd2221 Avoid redundant work in FencedBlockParserBase.TryContinue 2023-11-26 00:59:06 +01:00
Miha Zupan
22145c2fb0 Add UnicodeUtility helper 2023-11-26 00:58:57 +01:00
Miha Zupan
2517003edc Speed up code block arguments string parsing 2023-11-25 21:36:04 +01:00
Miha Zupan
50a3d02c2c Remove NoInlining from throw helper 2023-11-25 20:15:40 +01:00
Alexandre Mutel
40fb2b8249 Merge pull request #751 from MihaZupan/net8
.NET 8.0 and a few other perf improvements
2023-11-25 10:08:32 +01:00
Miha Zupan
5c54968807 Fix polyfill namespace 2023-11-25 03:25:37 +01:00
Miha Zupan
58ea46d58b Also install 6.0, 7.0 SDKs 2023-11-25 02:33:59 +01:00
Miha Zupan
f557e57ab1 Optimize WriteEscapeUrl 2023-11-24 03:34:45 +01:00
Miha Zupan
87aa32e1bd Optimize WriteEscape 2023-11-24 02:57:12 +01:00
Miha Zupan
4f1cb9da08 Avoid allocating strings for known emphasis character fallbacks 2023-11-24 02:44:47 +01:00
Miha Zupan
5cff880c90 Remove temporary string allocations in AutoIdentifierExtension 2023-11-24 02:44:08 +01:00
Miha Zupan
c7aec822b0 Speed up a few character checks 2023-11-24 02:41:08 +01:00
Miha Zupan
b0bde46cc1 Defer position calculations in LiteralInlineParser 2023-11-24 02:39:16 +01:00
Miha Zupan
7803417e5c Rewrite CodeInline matching to make use of vectorization 2023-11-24 02:37:45 +01:00
Miha Zupan
047c4cbcbb Skip _lineBits read on MarkdownObject creation 2023-11-24 02:28:25 +01:00
Miha Zupan
e4f57ca21e Fix build warnings 2023-11-24 02:26:51 +01:00
Miha Zupan
1f1364e69b Add SearchValues polyfill and use it in CharacterMap 2023-11-24 02:23:55 +01:00
Miha Zupan
4eea9db35c Add .NET 8.0 target 2023-11-24 02:17:47 +01:00
Alexandre Mutel
cce7284b84 Merge pull request #749 from michaelvolz/patch-1
Update Visual Studio editor link
2023-11-14 18:36:26 +01:00
Michael A. Volz (Flynn)
8e1e0b9bf3 Update Visual Studio editor link
Markdown Editor v2 (Visual Studio 2022)

This is a complete rewrite of the original Markdown Editor with tons of fixes, tweeks, and performance improvements.
2023-11-14 17:50:51 +01:00
Alexandre Mutel
7d40bc118b Merge pull request #736 from zickb/better_literal_delimiter_content_string_slice
Better literal delimiter content string slice
2023-08-30 07:29:44 +02:00
Benni
dba94a2371 Add documentation for the new constructor. 2023-08-30 00:55:35 +02:00
Benni
6d75eed3bb Don't break external users of the public constructor 2023-08-30 00:53:05 +02:00
Benni
ccb75fd5f0 Merge branch 'master' into better_literal_delimiter_content_string_slice 2023-08-30 00:50:18 +02:00
Benni
06eb6ba774 Better content string slice of delimiter literals:
instead of creating a new StringSlice only containing the delimiter chars, use the provided StringSlice from the match method with an appropriate start and end index
2023-08-30 00:40:10 +02:00
Alexandre Mutel
f15e9f020e Merge pull request #733 from zickb/fix_source_span_calculation
Fix source span calculation
2023-08-26 16:04:01 +02:00
Benni
a70ca6304f Fix source span of paragraphs in table cells and cleanup InlineProcessor 2023-08-25 08:54:30 +02:00
Benni
d26822be05 fix inline source spans calculation 2023-08-22 23:38:37 +02:00
124 changed files with 2291 additions and 1146 deletions

View File

@@ -11,22 +11,11 @@ on:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
fetch-depth: 0
- name: Install .NET 6.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- name: Build, Test, Pack, Publish
shell: bash
run: |
dotnet tool install -g dotnet-releaser
dotnet-releaser run --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" src/dotnet-releaser.toml
uses: xoofx/.github/.github/workflows/dotnet.yml@main
with:
dotnet-version: |
6.0
8.0
9.0
secrets:
NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}

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 @@
# Extensions and Parsers
Markdig was [implemented in such a way](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
Markdig was [implemented in such a way](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
The basic mechanism for extension of Markdig is the `IMarkdownExtension` interface, which allows any implementing class to be registered with the pipeline builder and thus to directly modify the collections of `BlockParser` and `InlineParser` objects which end up in the pipeline.

View File

@@ -1,4 +1,4 @@
# Markdig [![Build Status](https://github.com/lunet-io/markdig/workflows/ci/badge.svg?branch=master)](https://github.com/lunet-io/markdig/actions) [![Coverage Status](https://coveralls.io/repos/github/xoofx/markdig/badge.svg?branch=master)](https://coveralls.io/github/xoofx/markdig?branch=master) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
# Markdig [![ci](https://github.com/xoofx/markdig/actions/workflows/ci.yml/badge.svg)](https://github.com/xoofx/markdig/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/xoofx/markdig/badge.svg?branch=master)](https://coveralls.io/github/xoofx/markdig?branch=master) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
<img align="right" width="160px" height="160px" src="img/markdig.png">
@@ -12,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:
@@ -48,7 +48,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- [**Emoji**](src/Markdig.Tests/Specs/EmojiSpecs.md) support (inspired from [Markdown-it](https://markdown-it.github.io/))
- [**SmartyPants**](src/Markdig.Tests/Specs/SmartyPantsSpecs.md) (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
- [**Bootstrap**](src/Markdig.Tests/Specs/BootstrapSpecs.md) class (to output bootstrap class)
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://knsv.github.io/mermaid/) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://mermaid.js.org) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
- [**YAML Front Matter**](src/Markdig.Tests/Specs/YamlSpecs.md) to parse without evaluating the front matter and to discard it from the HTML output (typically used for previewing without the front matter in MarkdownEditor)
- [**JIRA links**](src/Markdig.Tests/Specs/JiraLinks.md) to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker)
- Starting with Markdig version `0.20.0+`, Markdig is compatible only with `NETStandard 2.0`, `NETStandard 2.1`, `NETCoreApp 2.1` and `NETCoreApp 3.1`.
@@ -70,7 +70,7 @@ If you are looking for support for an old .NET Framework 3.5 or 4.0, you can dow
While there is not yet a dedicated documentation, you can find from the [specs documentation](src/Markdig.Tests/Specs/readme.md) how to use these extensions.
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
## Download
@@ -144,12 +144,12 @@ AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
- Markdig is roughly **x100 times faster than MarkdownSharp**
- **20% faster than the reference cmark C implementation**
## Sponsors
## Donate
Supports this project with a monthly donation and help me continue improving it. \[[Become a sponsor](https://github.com/sponsors/xoofx)\]
If you are using this library and find it useful for your project, please consider a donation for it!
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
[<img src="https://github.com/lilith.png?size=200" width="64px;" style="border-radius: 50%" alt="lilith"/>](https://github.com/lilith) Lilith River, author of [Imageflow Server, an easy on-demand
image editing, optimization, and delivery server](https://github.com/imazen/imageflow-server)
## Credits
@@ -164,4 +164,4 @@ Some decoding part (e.g HTML [EntityHelper.cs](https://github.com/lunet-io/markd
Thanks to the work done by @clarkd on the JIRA Link extension (https://github.com/clarkd/MarkdigJiraLinker), now included with this project!
## Author
Alexandre MUTEL aka [xoofx](http://xoofx.com)
Alexandre MUTEL aka [xoofx](http://xoofx.github.io)

View File

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

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

View File

@@ -1,19 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>13.0</LangVersion>
<StartupObject>Markdig.Tests.Program</StartupObject>
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.dll</SpecExecutable>
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.timestamp</SpecTimestamp>
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\$(TargetFramework)\SpecFileGen.dll</SpecExecutable>
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\$(TargetFramework)\SpecFileGen.timestamp</SpecTimestamp>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,7 +1,8 @@
using System.Text.RegularExpressions;
using Markdig.Extensions.AutoLinks;
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests;
@@ -198,9 +199,9 @@ $$
<div class=""math"">
\begin{align}
\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
& = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
& \approx \frac{73}{12}\left(1 - \frac{1}{2\cdot73^2}\right)
\end{align}
</div>
@@ -291,4 +292,98 @@ $$
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"https://www.foo.bar\">www.foo.bar</a></p>", httpsPipeline);
}
[Test]
public void RootInlineHasCorrectSourceSpan()
{
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
pipeline.TrackTrivia = true;
var document = Markdown.Parse("0123456789\n", pipeline);
var expectedSourceSpan = new SourceSpan(0, 10);
Assert.That(((LeafBlock)document.LastChild).Inline.Span == expectedSourceSpan);
}
[Test]
public void RootInlineInTableCellHasCorrectSourceSpan()
{
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseAdvancedExtensions().Build();
pipeline.TrackTrivia = true;
var document = Markdown.Parse("| a | b |\n| --- | --- |\n| <span id=\"dest\"></span><span id=\"DEST\"></span>*dest*<br/> | \\[in\\] The address of the result of the operation.<br/> |", pipeline);
var paragraph = (ParagraphBlock)((TableCell)((TableRow)((Table)document.LastChild).LastChild).First()).LastChild;
Assert.That(paragraph.Inline.Span.Start == paragraph.Inline.FirstChild.Span.Start);
Assert.That(paragraph.Inline.Span.End == paragraph.Inline.LastChild.Span.End);
}
[Test]
public void TestGridTableShortLine()
{
var input = @"
+--+
| |
+-";
var expected = @"<table>
<col style=""width:100%"" />
<tbody>
<tr>
<td></td>
</tr>
</tbody>
</table>
";
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseGridTables().Build());
}
[Test]
public void TestDefinitionListInListItemWithBlankLine()
{
var input = @"
-
term
: definition
";
var expected = @"<ul>
<li>
<dl>
<dt>term</dt>
<dd>definition</dd>
</dl>
</li>
</ul>
";
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseDefinitionLists().Build());
}
[Test]
public void TestAlertWithinAlertOrNestedBlock()
{
var input = @"
>[!NOTE]
[!NOTE]
The second one is not a note.
>>[!NOTE]
Also not a note.
";
var expected = @"<div class=""markdown-alert markdown-alert-note"">
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
<p>[!NOTE]
The second one is not a note.</p>
</div>
<blockquote>
<blockquote>
<p>[!NOTE]
Also not a note.</p>
</blockquote>
</blockquote>
";
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks().Build());
}
}

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

@@ -17,7 +17,7 @@ namespace Markdig.Tests.Specs.Diagrams
//
// ## Mermaid diagrams
//
// Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
// Using a fenced code block with the `mermaid` language info will output a `<pre class='mermaid'>` block (which is the default for other code block):
[Test]
public void ExtensionsMermaidDiagrams_Example001()
{
@@ -34,14 +34,14 @@ namespace Markdig.Tests.Specs.Diagrams
// ```
//
// Should be rendered as:
// <div class="mermaid">graph TD;
// <pre class="mermaid">graph TD;
// A-->B;
// A-->C;
// B-->D;
// C-->D;
// </div>
// </pre>
TestParser.TestSpec("```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```", "<div class=\"mermaid\">graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n</div>", "diagrams|advanced", context: "Example 1\nSection Extensions / Mermaid diagrams\n");
TestParser.TestSpec("```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```", "<pre class=\"mermaid\">graph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n</pre>", "diagrams|advanced", context: "Example 1\nSection Extensions / Mermaid diagrams\n");
}
}

View File

@@ -4,7 +4,7 @@ Adds support for diagrams extension:
## Mermaid diagrams
Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
Using a fenced code block with the `mermaid` language info will output a `<pre class='mermaid'>` block (which is the default for other code block):
```````````````````````````````` example
```mermaid
@@ -15,12 +15,12 @@ graph TD;
C-->D;
```
.
<div class="mermaid">graph TD;
<pre class="mermaid">graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
</div>
</pre>
````````````````````````````````
## nomnoml diagrams

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

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

View File

@@ -81,6 +81,22 @@ public class TestLinkHelper
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestTitleMultiline()
{
var text = new StringSlice("'this\ris\r\na\ntitle'");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual("this\ris\r\na\ntitle", title);
}
[Test]
public void TestTitleMultilineWithSpaceAndBackslash()
{
var text = new StringSlice("'a\n\\ \\\ntitle'");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual("a\n\\ \\\ntitle", title);
}
[Test]
public void TestUrlAndTitle()
{
@@ -230,6 +246,13 @@ public class TestLinkHelper
}
[Test]
public void TestlLinkReferenceDefinitionInvalid()
{
var text = new StringSlice("[foo]: /url (title) x\n");
Assert.False(LinkHelper.TryParseLinkReferenceDefinition(ref text, out _, out _, out _, out _, out _, out _));
}
[Test]
public void TestAutoLinkUrlSimple()
{

View File

@@ -25,6 +25,7 @@ public class TestMediaLinks
[Test]
[TestCase("![static mp4](https://sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"https://sample.com/video.mp4\"></source></video></p>\n")]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n")]
[TestCase(@"![youtube short](https://www.youtube.com/shorts/6BUptHVuvyI?feature=share)", "<p><iframe src=\"https://www.youtube.com/embed/6BUptHVuvyI\" class=\"youtubeshort\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![yandex.ru](https://music.yandex.ru/album/411845/track/4402274)", "<p><iframe src=\"https://music.yandex.ru/iframe/#track/4402274/411845/\" class=\"yandex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n")]
[TestCase("![vimeo](https://vimeo.com/8607834)", "<p><iframe src=\"https://player.vimeo.com/video/8607834\" class=\"vimeo\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
@@ -33,7 +34,7 @@ public class TestMediaLinks
public void TestBuiltInHosts(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
Assert.AreEqual(expected, html);
}
[TestCase("![static video relative path](./video.mp4)",
@@ -43,7 +44,7 @@ public class TestMediaLinks
public void TestBuiltInHostsWithRelativePaths(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
Assert.AreEqual(expected, html);
}
private class TestHostProvider : IHostProvider

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

@@ -9,6 +9,13 @@ namespace Markdig.Tests;
[TestFixture]
public class TestPlayParser
{
[Test]
public void TestInvalidSetext()
{
TestParser.TestSpec("test\n===n", "<p>test\n===n</p>", "advanced");
}
[Test]
public void TestBugWithEmphasisAndTable()
{
@@ -39,6 +46,14 @@ public class TestPlayParser
Assert.AreEqual("/yoyo", link?.Url);
}
[Test]
public void TestLinkWithMultipleBackslashesInTitle()
{
var doc = Markdown.Parse(@"[link](/uri '\\\\127.0.0.1')");
var link = doc.Descendants<LinkInline>().FirstOrDefault();
Assert.AreEqual(@"\\127.0.0.1", link?.Title);
}
[Test]
public void TestListBug2()
{

View File

@@ -67,6 +67,60 @@ literal ( 2, 0) 12-21
");
}
[Test]
public void TestParagraphWithEndNewLine()
{
Check("0123456789\n", @"
paragraph ( 0, 0) 0-10
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
", trackTrivia: true);
Check("0123456789\r", @"
paragraph ( 0, 0) 0-10
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
", trackTrivia: true);
Check("0123456789\r\n", @"
paragraph ( 0, 0) 0-11
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-11
", trackTrivia: true);
}
[Test]
public void TestMultipleParagraphsWithEndNewLine()
{
Check("0123456789\n\n0123456789\n\n", @"
paragraph ( 0, 0) 0-10
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
paragraph ( 2, 0) 12-22
literal ( 2, 0) 12-21
linebreak ( 2,10) 22-22
", trackTrivia: true);
Check("0123456789\r\r0123456789\r\r", @"
paragraph ( 0, 0) 0-10
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
paragraph ( 2, 0) 12-22
literal ( 2, 0) 12-21
linebreak ( 2,10) 22-22
", trackTrivia: true);
Check("0123456789\r\n\r\n0123456789\r\n\r\n", @"
paragraph ( 0, 0) 0-11
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-11
paragraph ( 2, 0) 14-25
literal ( 2, 0) 14-23
linebreak ( 2,10) 24-25
", trackTrivia: true);
}
[Test]
public void TestEmphasis()
{
@@ -106,6 +160,17 @@ literal ( 0, 8) 8-8
");
}
[Test]
public void TestEmphasis4()
{
Check("**foo*", @"
paragraph ( 0, 0) 0-5
literal ( 0, 0) 0-0
emphasis ( 0, 1) 1-5
literal ( 0, 2) 2-4
");
}
[Test]
public void TestEmphasisFalse()
{
@@ -468,13 +533,17 @@ literal ( 1, 6) 8-9
[Test]
public void TestAbbreviations()
{
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @"
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML\r\n\r\nHTML abbreviation at the beginning of a line", @"
paragraph ( 2, 0) 38-102
container ( 2, 0) 38-102
literal ( 2, 0) 38-66
abbreviation ( 2,29) 67-70
literal ( 2,33) 71-98
abbreviation ( 2,61) 99-102
paragraph ( 4, 0) 107-150
container ( 4, 0) 107-150
abbreviation ( 4, 0) 107-110
literal ( 4, 4) 111-150
", "abbreviations");
}
@@ -644,14 +713,22 @@ literal ( 0, 2) 2-3
[Test]
public void TestMathematicsInline()
{
// 01 23456789AB
Check("0\n012 $abcd$", @"
paragraph ( 0, 0) 0-11
// 01 23456789ABCDEF
Check("0\n012 $abcd$ 321", @"
paragraph ( 0, 0) 0-15
literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
literal ( 1, 0) 2-5
math ( 1, 4) 6-11
attributes ( 0, 0) 0--1
literal ( 1,10) 12-15
", "mathematics");
// 012345678
Check("$ abcd $", @"
paragraph ( 0, 0) 0-7
math ( 0, 0) 0-7
attributes ( 0, 0) 0--1
", "mathematics");
}
@@ -734,6 +811,29 @@ literal ( 4, 2) 13-13
", "pipetables");
}
[Test]
public void TestPipeTable3()
{
// 01234 5678 9ABCD
Check("|a|b\n-|-\n0|1|\n", @"
table ( 0, 0) 0-12
tablerow ( 0, 1) 1-3
tablecell ( 0, 1) 1-1
paragraph ( 0, 1) 1-1
literal ( 0, 1) 1-1
tablecell ( 0, 3) 3-3
paragraph ( 0, 3) 3-3
literal ( 0, 3) 3-3
tablerow ( 2, 0) 9-11
tablecell ( 2, 0) 9-9
paragraph ( 2, 0) 9-9
literal ( 2, 0) 9-9
tablecell ( 2, 2) 11-11
paragraph ( 2, 2) 11-11
literal ( 2, 2) 11-11
", "pipetables");
}
[Test]
public void TestIndentedCode()
{
@@ -825,9 +925,10 @@ literal ( 8, 2) 77-92
");
}
private static void Check(string text, string expectedResult, string extensions = null)
private static void Check(string text, string expectedResult, string extensions = null, bool trackTrivia = false)
{
var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation();
pipelineBuilder.TrackTrivia = trackTrivia;
if (extensions != null)
{
pipelineBuilder.Configure(extensions);

View File

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

View File

@@ -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)
{
@@ -104,7 +107,7 @@ public class TestYamlFrontMatterExtension
}
}
Assert.Pass("No exception parsing and iterating through YAML front matter block lines");
// No exception parsing and iterating through YAML front matter block lines
}
}

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

View File

@@ -20,7 +20,7 @@ public class AbbreviationParser : BlockParser
/// </summary>
public AbbreviationParser()
{
OpeningCharacters = new[] { '*' };
OpeningCharacters = ['*'];
}
public override BlockState TryOpen(BlockProcessor processor)
@@ -89,6 +89,7 @@ public class AbbreviationParser : BlockParser
{
var literal = (LiteralInline)processor.Inline!;
var originalLiteral = literal;
var originalSpanEnd = literal.Span.End;
ContainerInline? container = null;
@@ -171,7 +172,7 @@ public class AbbreviationParser : BlockParser
// Process the remaining literal
literal = new LiteralInline()
{
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
Span = new SourceSpan(abbrInline.Span.End + 1, originalSpanEnd),
Line = line,
Column = column + match.Length,
};

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,87 @@
// 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)
{
if (kind.Length >= 16)
{
return;
}
Span<char> upperKind = stackalloc char[kind.Length];
kind.AsSpan().ToUpperInvariant(upperKind);
string? html = upperKind switch
{
"NOTE" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>",
"TIP" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>",
"IMPORTANT" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Important</p>",
"WARNING" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>",
"CAUTION" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>",
_ => null
};
if (html is not null)
{
renderer.WriteLine(html);
}
}
}

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,128 @@
// 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
|| quoteBlock is AlertBlock || quoteBlock.Parent is not MarkdownDocument)
{
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.IsAlpha())
{
end = slice.Start;
c = slice.NextChar();
}
// We need at least one character
if (c != ']' || start == end)
{
slice = saved;
return false;
}
var alertType = new StringSlice(slice.Text, start, end);
c = slice.NextChar(); // Skip ]
start = slice.Start;
while (true)
{
if (c == '\0' || c == '\n' || c == '\r')
{
end = slice.Start;
if (c == '\r')
{
c = slice.NextChar(); // Skip \r
if (c == '\0' || c == '\n')
{
end = slice.Start;
if (c == '\n')
{
slice.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;
@@ -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();
}
}
}

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

@@ -1,7 +1,9 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Linq;
using Markdig.Extensions.Alerts;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
@@ -24,10 +26,22 @@ public class BootstrapExtension : IMarkdownExtension
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
var alertRenderer = htmlRenderer.ObjectRenderers.OfType<AlertBlockRenderer>().FirstOrDefault();
if (alertRenderer == null)
{
alertRenderer = new AlertBlockRenderer();
renderer.ObjectRenderers.InsertBefore<QuoteBlockRenderer>(new AlertBlockRenderer());
}
alertRenderer.RenderKind = (_, _) => { };
}
}
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
{
Span<char> upperKind = new char[16];
foreach (var node in document.Descendants())
{
if (node.IsInline)
@@ -43,6 +57,28 @@ public class BootstrapExtension : IMarkdownExtension
{
node.GetAttributes().AddClass("table");
}
else if (node is AlertBlock alertBlock) // Needs to be before QuoteBlock
{
var attributes = node.GetAttributes();
attributes.AddClass("alert");
attributes.AddProperty("role", "alert");
if (alertBlock.Kind.Length <= upperKind.Length)
{
alertBlock.Kind.AsSpan().ToUpperInvariant(upperKind);
attributes.AddClass(upperKind.Slice(0, alertBlock.Kind.Length) switch
{
"NOTE" => "alert-primary",
"TIP" => "alert-success",
"IMPORTANT" => "alert-info",
"WARNING" => "alert-warning",
"CAUTION" => "alert-danger",
_ => "alert-dark",
});
}
var lastParagraph = alertBlock.Descendants().OfType<ParagraphBlock>().LastOrDefault();
lastParagraph?.GetAttributes().AddClass("mb-0");
}
else if (node is QuoteBlock)
{
node.GetAttributes().AddClass("blockquote");

View File

@@ -30,7 +30,11 @@ public class CustomContainerExtension : IMarkdownExtension
{
if (delimiterCount == 2 && emphasisChar == ':')
{
return new CustomContainerInline();
return new CustomContainerInline
{
DelimiterChar = ':',
DelimiterCount = 2
};
}
return null;
});

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)
@@ -105,13 +105,20 @@ public class DefinitionListParser : BlockParser
{
var index = previousParent.IndexOf(paragraphBlock) - 1;
if (index < 0) return null;
var lastBlock = previousParent[index];
if (lastBlock is BlankLineBlock)
switch (previousParent[index])
{
lastBlock = previousParent[index - 1];
previousParent.RemoveAt(index);
case DefinitionList definitionList:
return definitionList;
case BlankLineBlock:
if (index > 0 && previousParent[index - 1] is DefinitionList definitionList2)
{
previousParent.RemoveAt(index);
return definitionList2;
}
break;
}
return lastBlock as DefinitionList;
return null;
}
public override BlockState TryContinue(BlockProcessor processor, Block block)

View File

@@ -1,5 +1,5 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
@@ -22,9 +22,8 @@ public class DiagramExtension : IMarkdownExtension
if (renderer is HtmlRenderer htmlRenderer)
{
var codeRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>()!;
// TODO: Add other well known diagram languages
codeRenderer.BlocksAsDiv.Add("mermaid");
codeRenderer.BlockMapping["mermaid"] = "pre";
codeRenderer.BlocksAsDiv.Add("nomnoml");
}
}
}
}

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 ':'
@@ -57,6 +57,8 @@ public class FootnoteParser : BlockParser
{
Label = label,
LabelSpan = labelSpan,
Column = processor.Column,
Span = new SourceSpan(processor.Start, processor.Line.End),
};
// Maintain a list of all footnotes at document level
@@ -74,6 +76,7 @@ public class FootnoteParser : BlockParser
{
CreateLinkInline = CreateLinkToFootnote,
Line = processor.LineIndex,
Column = saved,
Span = new SourceSpan(start, processor.Start - 2), // account for ]:
LabelSpan = labelSpan,
Label = label
@@ -170,10 +173,8 @@ public class FootnoteParser : BlockParser
paragraphBlock = new ParagraphBlock();
footnote.Add(paragraphBlock);
}
if (paragraphBlock.Inline == null)
{
paragraphBlock.Inline = new ContainerInline();
}
paragraphBlock.Inline ??= new ContainerInline();
foreach (var link in footnote.Links)
{

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(slice.Start - 1)),
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)
{
@@ -62,6 +58,7 @@ public class HostProviderBuilder
internal static Dictionary<string, IHostProvider> KnownHosts { get; }
= new Dictionary<string, IHostProvider>(StringComparer.OrdinalIgnoreCase)
{
["YouTubeShort"] = Create("www.youtube.com", YouTubeShort, iframeClass: "youtubeshort"),
["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"),
["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
@@ -71,7 +68,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);
@@ -96,6 +93,19 @@ public class HostProviderBuilder
);
}
private static string? YouTubeShort(Uri uri)
{
string uriPath = uri.AbsolutePath;
bool isYouTubeShort = uriPath.StartsWith("/shorts/", StringComparison.OrdinalIgnoreCase);
if (!isYouTubeShort)
{
return null;
}
var shortId = uriPath.Substring("/shorts/".Length).Split('?').FirstOrDefault(); // the format might be "/shorts/6BUptHVuvyI?feature=share"
return BuildYouTubeIframeUrl(shortId, null);
}
private static string? YouTubeShortened(Uri uri)
{
return BuildYouTubeIframeUrl(

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)
@@ -135,6 +135,7 @@ public class GridTableParser : BlockParser
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
{
var lineStart = line.Start;
var lineEnd = line.End;
isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '=';
hasRowSpan = false;
foreach (var columnSlice in columns)
@@ -142,9 +143,9 @@ public class GridTableParser : BlockParser
if (columnSlice.CurrentCell != null)
{
line.Start = lineStart + columnSlice.Start + 1;
line.End = lineStart + columnSlice.End - 1;
line.End = Math.Min(lineStart + columnSlice.End - 1, lineEnd);
line.Trim();
if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line))
if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
{
hasRowSpan = true;
columnSlice.CurrentCell.RowSpan++;
@@ -158,7 +159,7 @@ public class GridTableParser : BlockParser
}
}
private static bool IsRowSeperator(StringSlice slice)
private static bool IsRowSeparator(StringSlice slice)
{
char c = slice.CurrentChar;
do
@@ -256,14 +257,14 @@ public class GridTableParser : BlockParser
{
sliceForCell.End = line.Start + columnEnd - 1;
}
else if (line.PeekCharExtra(line.End) == '|')
else if (line.PeekCharExtra(line.End - line.Start) == '|')
{
sliceForCell.End = line.End - 1;
}
}
sliceForCell.TrimEnd();
if (!isRowLine || !IsRowSeperator(sliceForCell))
if (!isRowLine || !IsRowSeparator(sliceForCell))
{
if (columnSlice.CurrentCell is null)
{
@@ -273,10 +274,7 @@ public class GridTableParser : BlockParser
ColumnIndex = i
};
if (columnSlice.BlockProcessor is null)
{
columnSlice.BlockProcessor = processor.CreateChild();
}
columnSlice.BlockProcessor ??= processor.CreateChild();
// Ensure that the BlockParser is aware that the TableCell is the top-level container
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);

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();
}
@@ -280,6 +280,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
tableState.EndOfLines.Add(endOfTable);
}
int lastPipePos = 0;
// Cell loop
// Reconstruct the table from the delimiters
TableRow? row = null;
@@ -302,6 +304,12 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
if (pipeSeparator != null && (delimiter.PreviousSibling is null || delimiter.PreviousSibling is LineBreakInline))
{
delimiter.Remove();
if (table.Span.IsEmpty)
{
table.Span = delimiter.Span;
table.Line = delimiter.Line;
table.Column = delimiter.Column;
}
continue;
}
}
@@ -354,6 +362,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
// If the delimiter is a pipe, we need to remove it from the tree
// so that previous loop looking for a parent will not go further on subsequent cells
delimiter.Remove();
lastPipePos = delimiter.Span.End;
}
// We trim whitespace at the beginning and ending of the cell
@@ -421,6 +430,11 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
}
}
if (lastPipePos > table.Span.End)
{
table.UpdateSpanEnd(lastPipePos);
}
// Once we are done with the cells, we can remove all end of lines in the table tree
foreach (var endOfLine in tableState.EndOfLines)
{
@@ -443,6 +457,11 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
{
var paragraph = (ParagraphBlock) cell[0];
state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true);
if (paragraph.Inline?.LastChild is not null)
{
paragraph.Inline.Span.End = paragraph.Inline.LastChild.Span.End;
paragraph.UpdateSpanEnd(paragraph.Inline.LastChild.Span.End);
}
}
// Clear cells when we are done
@@ -520,7 +539,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 +651,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;
@@ -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();
}

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ internal sealed class FastStringWriter : TextWriter
public override string NewLine
{
get => _newLine;
set => _newLine = value ?? Environment.NewLine;
set => base.NewLine = _newLine = value ?? Environment.NewLine;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -278,8 +278,7 @@ internal sealed class FastStringWriter : TextWriter
_pos = 0;
}
public override string ToString()
{
return _chars.AsSpan(0, _pos).ToString();
}
public override string ToString() => AsSpan().ToString();
public ReadOnlySpan<char> AsSpan() => _chars.AsSpan(0, _pos);
}

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

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

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

View File

@@ -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();
@@ -531,87 +545,70 @@ public static class LinkHelper
enclosingCharacter = c;
var closingQuote = c == '(' ? ')' : c;
bool hasEscape = false;
// -1: undefined
// 0: has only spaces
// 1: has other characters
int hasOnlyWhiteSpacesSinceLastLine = -1;
while (true)
bool isLineBlank = false; // the first line is never blank
while ((c = text.NextChar()) != '\0')
{
c = text.NextChar();
if (c == '\r' || c == '\n')
{
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
if (isLineBlank)
{
if (hasOnlyWhiteSpacesSinceLastLine == 1)
{
break;
}
hasOnlyWhiteSpacesSinceLastLine = -1;
break;
}
if (hasEscape)
{
hasEscape = false;
buffer.Append('\\');
}
buffer.Append(c);
if (c == '\r' && text.PeekChar() == '\n')
{
buffer.Append('\n');
text.SkipChar();
}
continue;
}
if (c == '\0')
{
break;
isLineBlank = true;
}
if (c == closingQuote)
else if (hasEscape)
{
if (hasEscape)
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append(closingQuote);
hasEscape = false;
continue;
buffer.Append('\\');
}
buffer.Append(c);
}
else if (c == closingQuote)
{
// Skip last quote
text.SkipChar();
goto ReturnValid;
title = buffer.ToString();
return true;
}
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
isLineBlank = false;
}
hasEscape = false;
if (c.IsSpaceOrTab())
else
{
if (hasOnlyWhiteSpacesSinceLastLine < 0)
if (isLineBlank && !c.IsSpaceOrTab())
{
hasOnlyWhiteSpacesSinceLastLine = 1;
isLineBlank = false;
}
}
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
{
hasOnlyWhiteSpacesSinceLastLine = 0;
}
buffer.Append(c);
buffer.Append(c);
}
}
}
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseTitleTrivia<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
@@ -627,87 +624,70 @@ public static class LinkHelper
enclosingCharacter = c;
var closingQuote = c == '(' ? ')' : c;
bool hasEscape = false;
// -1: undefined
// 0: has only spaces
// 1: has other characters
int hasOnlyWhiteSpacesSinceLastLine = -1;
while (true)
bool isLineBlank = false; // the first line is never blank
while ((c = text.NextChar()) != '\0')
{
c = text.NextChar();
if (c == '\r' || c == '\n')
{
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
if (isLineBlank)
{
if (hasOnlyWhiteSpacesSinceLastLine == 1)
{
break;
}
hasOnlyWhiteSpacesSinceLastLine = -1;
break;
}
if (hasEscape)
{
hasEscape = false;
buffer.Append('\\');
}
buffer.Append(c);
if (c == '\r' && text.PeekChar() == '\n')
{
buffer.Append('\n');
text.SkipChar();
}
continue;
}
if (c == '\0')
{
break;
isLineBlank = true;
}
if (c == closingQuote)
else if (hasEscape)
{
if (hasEscape)
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append(closingQuote);
hasEscape = false;
continue;
buffer.Append('\\');
}
buffer.Append(c);
}
else if (c == closingQuote)
{
// Skip last quote
text.SkipChar();
goto ReturnValid;
title = buffer.ToString();
return true;
}
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
isLineBlank = false;
}
hasEscape = false;
if (c.IsSpaceOrTab())
else
{
if (hasOnlyWhiteSpacesSinceLastLine < 0)
if (isLineBlank && !c.IsSpaceOrTab())
{
hasOnlyWhiteSpacesSinceLastLine = 1;
isLineBlank = false;
}
}
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
{
hasOnlyWhiteSpacesSinceLastLine = 0;
}
buffer.Append(c);
buffer.Append(c);
}
}
}
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseUrl<T>(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator
@@ -744,12 +724,15 @@ public static class LinkHelper
break;
}
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
@@ -760,8 +743,6 @@ public static class LinkHelper
break;
}
hasEscape = false;
buffer.Append(c);
} while (c != '\0');
@@ -800,20 +781,21 @@ public static class LinkHelper
if (!isAutoLink)
{
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
// If we have an escape
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
hasEscape = false;
}
if (IsEndOfUri(c, isAutoLink))
@@ -891,12 +873,15 @@ public static class LinkHelper
break;
}
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
@@ -907,8 +892,6 @@ public static class LinkHelper
break;
}
hasEscape = false;
buffer.Append(c);
} while (c != '\0');
@@ -947,20 +930,21 @@ public static class LinkHelper
if (!isAutoLink)
{
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
// If we have an escape
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
hasEscape = false;
}
if (IsEndOfUri(c, isAutoLink))
@@ -1145,7 +1129,7 @@ public static class LinkHelper
c = text.NextChar();
}
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
if (c != '\0' && c != '\n' && c != '\r')
{
// If we were able to parse the url but the title doesn't end with space,
// we are still returning a valid definition
@@ -1285,7 +1269,7 @@ public static class LinkHelper
c = text.NextChar();
}
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
if (c != '\0' && c != '\n' && c != '\r')
{
// If we were able to parse the url but the title doesn't end with space,
// we are still returning a valid definition

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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;net8.0;net9.0</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
<PackageReleaseNotes>https://github.com/lunet-io/markdig/blob/master/changelog.md</PackageReleaseNotes>
@@ -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">

View File

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

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

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

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

View File

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

View File

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

View File

@@ -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;
@@ -302,13 +302,14 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
var openDelimitercount = openDelimiter.DelimiterCount;
var closeDelimitercount = closeDelimiter.DelimiterCount;
emphasis!.Span.Start = openDelimiter.Span.Start;
emphasis!.Span.Start = openDelimiter.Span.Start + openDelimitercount - delimiterDelta;
emphasis.Line = openDelimiter.Line;
emphasis.Column = openDelimiter.Column;
emphasis.Column = openDelimiter.Column + openDelimitercount - delimiterDelta;
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
openDelimiter.Span.Start += delimiterDelta;
openDelimiter.Column += delimiterDelta;
openDelimiter.Span.End -= delimiterDelta;
openDelimiter.Content.End -= delimiterDelta;
closeDelimiter.Content.Start += delimiterDelta;
closeDelimiter.Span.Start += delimiterDelta;
closeDelimiter.Column += delimiterDelta;
@@ -331,7 +332,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--;
}

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)
@@ -137,6 +137,9 @@ public class LinkInlineParser : InlineParser
if (linkRef.CreateLinkInline != null)
{
link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild);
link.Span = new SourceSpan(parent.Span.Start, endPosition);
link.Line = parent.Line;
link.Column = parent.Column;
}
// Create a default link if the callback was not found
@@ -145,8 +148,8 @@ public class LinkInlineParser : InlineParser
// Inline Link
var linkInline = new LinkInline()
{
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
Url = HtmlHelper.Unescape(linkRef.Url, removeBackSlash: false),
Title = HtmlHelper.Unescape(linkRef.Title, removeBackSlash: false),
Label = label,
LabelSpan = labelSpan,
UrlSpan = linkRef.UrlSpan,
@@ -256,8 +259,8 @@ public class LinkInlineParser : InlineParser
// Inline Link
link = new LinkInline()
{
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
Title = HtmlHelper.Unescape(title, removeBackSlash: false),
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
@@ -322,7 +325,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);
@@ -382,11 +385,11 @@ public class LinkInlineParser : InlineParser
return new LinkInline()
{
TriviaBeforeUrl = wsBeforeLink,
Url = HtmlHelper.Unescape(url),
Url = HtmlHelper.Unescape(url, removeBackSlash: false),
UnescapedUrl = unescapedUrl,
UrlHasPointyBrackets = urlHasPointyBrackets,
TriviaAfterUrl = wsAfterLink,
Title = HtmlHelper.Unescape(title),
Title = HtmlHelper.Unescape(title, removeBackSlash: false),
UnescapedTitle = unescapedTitle,
TitleEnclosingCharacter = titleEnclosingCharacter,
TriviaAfterTitle = wsAfterTitle,

View File

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

View File

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

View File

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

View File

@@ -171,13 +171,9 @@ public class ParagraphBlockParser : BlockParser
{
count = line.CountAndSkipChar(headingChar);
if (line.IsEmpty)
{
return headingChar;
}
while (line.NextChar().IsSpaceOrTab())
while (line.CurrentChar.IsSpaceOrTab())
{
line.NextChar();
}
if (line.IsEmpty)

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

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

View 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

View 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

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

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

View File

@@ -1,5 +1,5 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers;
@@ -13,8 +13,6 @@ namespace Markdig.Renderers.Html;
/// <seealso cref="HtmlObjectRenderer{CodeBlock}" />
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
{
private HashSet<string>? _blocksAsDiv;
/// <summary>
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
/// </summary>
@@ -25,23 +23,32 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
/// <summary>
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
/// </summary>
public HashSet<string> BlocksAsDiv => _blocksAsDiv ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public HashSet<string> BlocksAsDiv { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets a map of custom block mapping to render as custom blocks instead of pre/code blocks.
/// For example defining {"mermaid", "pre"} will render a block with info `mermaid` as a `pre` block but without the code HTML element.
/// </summary>
public Dictionary<string, string> BlockMapping { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
{
renderer.EnsureLine();
if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info))
if ((obj as FencedCodeBlock)?.Info is string info && (BlocksAsDiv.Contains(info) || BlockMapping.ContainsKey(info)))
{
var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ??
FencedCodeBlockParser.DefaultInfoPrefix;
var htmlBlock = BlockMapping.TryGetValue(info, out var blockType) ? blockType : "div";
// We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block
// NOTE that we are allocating a closure here
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<div")
renderer.WriteRaw('<');
renderer.Write(htmlBlock)
.WriteAttributes(obj.TryGetAttributes(),
cls => cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls)
.WriteRaw('>');
@@ -51,7 +58,7 @@ public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
if (renderer.EnableHtmlForBlock)
{
renderer.WriteLine("</div>");
renderer.Write("</").Write(htmlBlock).WriteLine(">");
}
}
else

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

@@ -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
{
'<' => "&lt;",
'>' => "&gt;",
'&' => "&amp;",
_ => "&quot;",
});
}
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("&lt;");
}
previousOffset = i + 1;
break;
case '>':
if (!softEscape)
{
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
WriteRaw("&gt;");
}
previousOffset = i + 1;
}
break;
case '&':
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
WriteRaw("&amp;");
}
previousOffset = i + 1;
break;
case '"':
if (!softEscape)
{
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
WriteRaw("&quot;");
}
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>

View File

@@ -28,15 +28,15 @@ public abstract class MarkdownObjectRenderer<TRenderer, TObject> : IMarkdownObje
public virtual void Write(RendererBase renderer, MarkdownObject obj)
{
var htmlRenderer = (TRenderer)renderer;
var typedRenderer = (TRenderer)renderer;
var typedObj = (TObject)obj;
if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj))
if (_tryWriters is not null && TryWrite(typedRenderer, typedObj))
{
return;
}
Write(htmlRenderer, typedObj);
Write(typedRenderer, typedObj);
}
private bool TryWrite(TRenderer renderer, TObject obj)

Some files were not shown because too many files have changed in this diff Show More