Compare commits

...

195 Commits

Author SHA1 Message Date
Alexandre Mutel
682c727288 Merge pull request #876 from Akarinnnnn/fix-872
Fix #872 by reserve null title string.
2025-06-05 07:57:29 +02:00
Fa鸽
ec2eef25b2 Remove HtmlHelper.UnescapeNullable 2025-06-04 19:23:18 +08:00
Fa鸽
6261660d37 Explain why not to normalize link title into empty strings 2025-05-31 22:26:33 +08:00
Fa鸽
6d1fa96389 Changed link parsing tests for #872 2025-05-31 16:33:29 +08:00
Fa鸽
47c4e9b1e2 Fix #872 by reserve null title string. 2025-05-31 16:01:42 +08:00
Alexandre Mutel
3535701d70 Merge pull request #869 from prozolic/pullreq
Fix bug in `Markdown.ToPlainText` with code blocks
2025-04-27 18:52:57 +02:00
prozolic
c41b389053 Fix CodeBlockRenderer.Write 2025-04-27 16:49:05 +09:00
Alexandre Mutel
09a4b81a6e Update tests 2025-04-15 11:35:54 +02:00
Alexandre Mutel
7b14e2e091 Merge pull request #867 from MihaZupan/commonmark-0.31.2
Update to CommonMark 0.31.2
2025-04-15 10:59:22 +02:00
Alexandre Mutel
1e17dcdd08 Merge pull request #866 from MihaZupan/alert-perf
Improve Alert parsing perf
2025-04-15 10:58:40 +02:00
Alexandre Mutel
40e5ab1514 Merge pull request #863 from Amberg/master
Infer pipe table column widths from separator row
2025-04-15 10:57:47 +02:00
Alexandre Mutel
2953b026fc Merge pull request #865 from RamType0/patch-1
Fix `MathInline` is called "math block"
2025-04-15 10:56:27 +02:00
Miha Zupan
42ab98968d Update readme 2025-04-15 04:32:52 +02:00
Miha Zupan
b15cf582a5 Add 'search' HTML tag support 2025-04-15 04:31:13 +02:00
Miha Zupan
61e9be290b Allow empty HTML comments, double hyphens in text 2025-04-15 04:02:22 +02:00
Miha Zupan
a9ce0eb438 Update definition of punctuation to include symbols 2025-04-15 03:09:59 +02:00
Miha Zupan
023d93c091 Update CommonMark spec to 0.31.2 2025-04-14 23:32:22 +02:00
Miha Zupan
bbefce3b1f Sealed + ref struct 2025-04-14 22:11:53 +02:00
Miha Zupan
0d6343b421 Make AlertBlock parsing a bit cheaper 2025-04-14 22:02:21 +02:00
Ram.Type-0
f4effc25c0 Fix MathInline is called "math block" 2025-04-15 00:57:16 +09:00
Alexandre Mutel
7a83a1fd3d Merge pull request #864 from MihaZupan/net9-perf4
A couple perf improvements
2025-04-14 11:10:48 +02:00
Miha Zupan
8269ff1af5 Improve AutoLinkParser overhead for false-positive opening chars 2025-04-13 17:45:52 +02:00
Miha Zupan
0e6d0f4cb2 Fix style 2025-04-13 17:23:40 +02:00
Miha Zupan
8484420b72 Remove some branches from IsWhiteSpace and IsWhiteSpaceOrZero 2025-04-13 17:23:27 +02:00
Miha Zupan
c82a36884d Use the field keyword in a few places 2025-04-13 17:22:51 +02:00
Miha Zupan
da3d7f4f3a Improve some descriptions 2025-04-13 17:22:24 +02:00
Miha Zupan
eceb70c16a Avoid delegate allocations in AutoIdentifierExtension 2025-04-13 17:22:04 +02:00
Miha Zupan
7a9c192d7d Speed up FencedCodeBlock rendering 2025-04-13 17:21:43 +02:00
Miha Zupan
8cfa0cf0ae Improve more character tests with SearchValues 2025-04-13 16:59:55 +02:00
Miha Zupan
a82c3bd705 Improve some character tests 2025-04-13 16:59:29 +02:00
Miha Zupan
ecfda373b9 Avoid warnings in Markdig.WebApp 2025-04-13 16:11:30 +02:00
Miha Zupan
d8f69218db Commit FrozenDictionary polyfill 2025-04-13 16:11:02 +02:00
Miha Zupan
adfcf42529 Use FrozenDictionary in a couple places 2025-04-13 16:09:37 +02:00
Miha Zupan
dab1ca5483 Avoid unnecessary null check when reading trivia info 2025-04-13 16:09:24 +02:00
Manuel Amstutz
55f770cc07 feat: infer pipe table column widths from separator row
Adds support for calculating column widths in pipe tables based on the number of dashes in the header separator row.
Enabled via the InferColumnWidthsFromSeparator option in PipeTableOptions.
2025-04-09 20:55:54 +02:00
Alexandre Mutel
8b84542527 Merge pull request #861 from Meir017/patch-1
chore: update repository's github path
2025-03-20 16:38:19 +01:00
Meir Blachman
086440bcd3 update repository's github path 2025-03-20 16:59:50 +02:00
Alexandre Mutel
97470bd61f Merge pull request #859 from JamesNK/jamesnk/autolinks-domain-no-period
Add AutoLinkOptions.AllowDomainWithoutPeriod
2025-03-18 10:00:13 +01:00
James Newton-King
90c73b7754 Update src/Markdig/Helpers/LinkHelper.cs
Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2025-03-18 14:54:20 +08:00
James Newton-King
ee403ce28f Port tests 2025-03-17 08:26:51 +08:00
James Newton-King
8b403918b9 Update XML doc 2025-03-17 07:47:40 +08:00
James Newton-King
39b07d6bc5 Add AutoLinkOptions.AllowDomainWithoutPeriod 2025-03-17 07:46:23 +08:00
Alexandre Mutel
fb3fe8b261 Merge pull request #838 from Melodi17/master
Implemented better indent control in TextRendererBase
2025-02-28 09:23:27 +01:00
Alexandre Mutel
abb19ecf37 Merge pull request #851 from Akarinnnnn/encoding-ployfill
Replace encoding polyfill with NET5+ one.
2025-02-28 09:22:48 +01:00
Fa鸽
9dac60df73 Replace encoding polyfill with NET5+ one.
netstandard2.1 is a special TFM that .NET5+ doesn't mark themselves compitable, even if they mostly are.
2025-02-24 10:49:59 +08:00
Melodi
148278417f Added error throwing when stack is empty and PopIndent() is called 2025-01-14 14:25:20 +10:00
Alexandre Mutel
5b32391348 Update dependencies NuGet 2025-01-10 08:56:38 +01:00
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
Melodi
88c5b5cb41 Added method for clearing indents in TextRendererBase as well as added case handling to PopIndent() 2024-12-31 22:57:02 +10: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
Alexandre Mutel
5e3416f8b7 Merge pull request #726 from mkapahnke/make_allow_null_internal
Make AllowNull internal to prevent conflicts
2023-08-04 06:40:22 +02:00
Alexandre Mutel
012a57d361 Merge pull request #724 from DeveloPoel/markdown.cs_findings
Made the markdown class not partial and fixed the ToPlainText summary.
2023-08-04 06:39:58 +02:00
Alexandre Mutel
053a18c684 Merge pull request #723 from RickStrahl/MediaExtensions-RelativePathSupport
Add relative path support for Audio and Video Urls to  MediaLinks Extension
2023-08-04 06:39:38 +02:00
Maximilian Kapahnke
13265453ac make internal 2023-07-25 18:23:23 +02:00
developoel
8ea0783834 Made the markdown class not partial and fixed the ToPlainText summary. 2023-07-18 14:09:19 +02:00
Rick Strahl
3d29430337 Update src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-07-13 12:04:52 -07:00
Rick Strahl
81bc58c6c9 Add support for relative Urls for Video and Audio links. 2023-07-11 14:58:43 -07:00
Rick Strahl
bfe3800130 Allow for use of .NET 7.0 SDK (Major Version Roll Forward) 2023-07-11 12:13:05 -07:00
Alexandre Mutel
b7cb169fd3 Merge pull request #710 from valterc/fix-line-group-oob
Add line count check to avoid out of range
2023-04-22 13:23:04 +01:00
Valter Costa
512b28256a Improved tests 2023-04-18 17:44:41 +01:00
Valter Costa
cd5d11eeff Fix index check 2023-04-18 17:44:23 +01:00
Valter Costa
a9118774a8 Added line count check to avoid out of bounds 2023-04-18 12:46:48 +01:00
150 changed files with 4578 additions and 2861 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,9 +12,9 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
- Checkout [Markdown Editor v2 for Visual Studio 2022](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2) powered by Markdig!
- Converter to **HTML**
- Passing more than **600+ tests** from the latest [CommonMark specs (0.30)](http://spec.commonmark.org/)
- Passing more than **600+ tests** from the latest [CommonMark specs (0.31.2)](http://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
- including **GFM fenced code blocks**.
- **Extensible** architecture
@@ -48,7 +48,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- [**Emoji**](src/Markdig.Tests/Specs/EmojiSpecs.md) support (inspired from [Markdown-it](https://markdown-it.github.io/))
- [**SmartyPants**](src/Markdig.Tests/Specs/SmartyPantsSpecs.md) (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
- [**Bootstrap**](src/Markdig.Tests/Specs/BootstrapSpecs.md) class (to output bootstrap class)
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://knsv.github.io/mermaid/) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://mermaid.js.org) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams)
- [**YAML Front Matter**](src/Markdig.Tests/Specs/YamlSpecs.md) to parse without evaluating the front matter and to discard it from the HTML output (typically used for previewing without the front matter in MarkdownEditor)
- [**JIRA links**](src/Markdig.Tests/Specs/JiraLinks.md) to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker)
- Starting with Markdig version `0.20.0+`, Markdig is compatible only with `NETStandard 2.0`, `NETStandard 2.1`, `NETCoreApp 2.1` and `NETCoreApp 3.1`.
@@ -70,7 +70,7 @@ If you are looking for support for an old .NET Framework 3.5 or 4.0, you can dow
While there is not yet a dedicated documentation, you can find from the [specs documentation](src/Markdig.Tests/Specs/readme.md) how to use these extensions.
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
## Download
@@ -144,12 +144,12 @@ AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
- Markdig is roughly **x100 times faster than MarkdownSharp**
- **20% faster than the reference cmark C implementation**
## Sponsors
## Donate
Supports this project with a monthly donation and help me continue improving it. \[[Become a sponsor](https://github.com/sponsors/xoofx)\]
If you are using this library and find it useful for your project, please consider a donation for it!
[![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.14.0" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.512801" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />

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.12.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.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

@@ -533,5 +533,28 @@ namespace Markdig.Tests.Specs.AutoLinks
TestParser.TestSpec("<http://foö.bar.`baz>`", "<p><a href=\"http://xn--fo-gka.bar.%60baz\">http://foö.bar.`baz</a>`</p>", "autolinks|advanced", context: "Example 25\nSection Extensions / AutoLinks / Unicode support\n");
}
// Unicode punctuation characters are not allowed, but symbols are.
// Note that this does _not_ exactly match CommonMark's "Unicode punctuation character" definition.
[Test]
public void ExtensionsAutoLinksUnicodeSupport_Example026()
{
// Example 26
// Section: Extensions / AutoLinks / Unicode support
//
// The following Markdown:
// http://☃.net?☃ // OtherSymbol
//
// http://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol
//
// http://‰.net?‰ // OtherPunctuation
//
// Should be rendered as:
// <p><a href="http://xn--n3h.net?%E2%98%83">http://☃.net?☃</a> // OtherSymbol</p>
// <p><a href="http://xn--ji8h.net?%F0%9F%8D%89">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>
// <p>http://‰.net?‰ // OtherPunctuation</p>
TestParser.TestSpec("http://☃.net?☃ // OtherSymbol\n\nhttp://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol\n\nhttp://‰.net?‰ // OtherPunctuation", "<p><a href=\"http://xn--n3h.net?%E2%98%83\">http://☃.net?☃</a> // OtherSymbol</p>\n<p><a href=\"http://xn--ji8h.net?%F0%9F%8D%89\">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>\n<p>http://‰.net?‰ // OtherPunctuation</p>", "autolinks|advanced", context: "Example 26\nSection Extensions / AutoLinks / Unicode support\n");
}
}
}

View File

@@ -303,4 +303,19 @@ This will therefore be seen as an autolink and not as code inline.
<http://foö.bar.`baz>`
.
<p><a href="http://xn--fo-gka.bar.%60baz">http://foö.bar.`baz</a>`</p>
````````````````````````````````
Unicode punctuation characters are not allowed, but symbols are.
Note that this does _not_ exactly match CommonMark's "Unicode punctuation character" definition.
```````````````````````````````` example
http://☃.net?☃ // OtherSymbol
http://🍉.net?🍉 // A UTF-16 surrogate pair, but code point is OtherSymbol
http://‰.net?‰ // OtherPunctuation
.
<p><a href="http://xn--n3h.net?%E2%98%83">http://☃.net?☃</a> // OtherSymbol</p>
<p><a href="http://xn--ji8h.net?%F0%9F%8D%89">http://🍉.net?🍉</a> // A UTF-16 surrogate pair, but code point is OtherSymbol</p>
<p>http://‰.net?‰ // OtherPunctuation</p>
````````````````````````````````

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
---
title: CommonMark Spec
author: John MacFarlane
version: '0.30'
date: '2021-06-19'
license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)'
version: '0.31.2'
date: '2024-01-28'
license: '[CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)'
...
# Introduction
@@ -14,7 +14,7 @@ Markdown is a plain text format for writing structured documents,
based on conventions for indicating formatting in email
and usenet posts. It was developed by John Gruber (with
help from Aaron Swartz) and released in 2004 in the form of a
[syntax description](http://daringfireball.net/projects/markdown/syntax)
[syntax description](https://daringfireball.net/projects/markdown/syntax)
and a Perl script (`Markdown.pl`) for converting Markdown to
HTML. In the next decade, dozens of implementations were
developed in many languages. Some extended the original
@@ -34,10 +34,10 @@ As Gruber writes:
> Markdown-formatted document should be publishable as-is, as
> plain text, without looking like it's been marked up with tags
> or formatting instructions.
> (<http://daringfireball.net/projects/markdown/>)
> (<https://daringfireball.net/projects/markdown/>)
The point can be illustrated by comparing a sample of
[AsciiDoc](http://www.methods.co.nz/asciidoc/) with
[AsciiDoc](https://asciidoc.org/) with
an equivalent sample of Markdown. Here is a sample of
AsciiDoc from the AsciiDoc manual:
@@ -103,7 +103,7 @@ source, not just in the processed document.
## Why is a spec needed?
John Gruber's [canonical description of Markdown's
syntax](http://daringfireball.net/projects/markdown/syntax)
syntax](https://daringfireball.net/projects/markdown/syntax)
does not specify the syntax unambiguously. Here are some examples of
questions it does not answer:
@@ -316,9 +316,9 @@ A line containing no characters, or a line containing only spaces
The following definitions of character classes will be used in this spec:
A [Unicode whitespace character](@) is
any code point in the Unicode `Zs` general category, or a tab (`U+0009`),
line feed (`U+000A`), form feed (`U+000C`), or carriage return (`U+000D`).
A [Unicode whitespace character](@) is a character in the Unicode `Zs` general
category, or a tab (`U+0009`), line feed (`U+000A`), form feed (`U+000C`), or
carriage return (`U+000D`).
[Unicode whitespace](@) is a sequence of one or more
[Unicode whitespace characters].
@@ -337,9 +337,8 @@ is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`,
`[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B0060),
`{`, `|`, `}`, or `~` (U+007B007E).
A [Unicode punctuation character](@) is an [ASCII
punctuation character] or anything in
the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`.
A [Unicode punctuation character](@) is a character in the Unicode `P`
(puncuation) or `S` (symbol) general categories.
## Tabs
@@ -579,9 +578,9 @@ raw HTML:
```````````````````````````````` example
<http://example.com?find=\*>
<https://example.com?find=\*>
.
<p><a href="http://example.com?find=%5C*">http://example.com?find=\*</a></p>
<p><a href="https://example.com?find=%5C*">https://example.com?find=\*</a></p>
````````````````````````````````
@@ -1964,7 +1963,7 @@ has been found, the code block contains all of the lines after the
opening code fence until the end of the containing block (or
document). (An alternative spec would require backtracking in the
event that a closing code fence is not found. But this makes parsing
much less efficient, and there seems to be no real down side to the
much less efficient, and there seems to be no real downside to the
behavior described here.)
A fenced code block may interrupt a paragraph, and does not require
@@ -2403,7 +2402,7 @@ followed by one of the strings (case-insensitive) `address`,
`h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `head`, `header`, `hr`,
`html`, `iframe`, `legend`, `li`, `link`, `main`, `menu`, `menuitem`,
`nav`, `noframes`, `ol`, `optgroup`, `option`, `p`, `param`,
`section`, `source`, `summary`, `table`, `tbody`, `td`,
`search`, `section`, `summary`, `table`, `tbody`, `td`,
`tfoot`, `th`, `thead`, `title`, `tr`, `track`, `ul`, followed
by a space, a tab, the end of the line, the string `>`, or
the string `/>`.\
@@ -4115,7 +4114,7 @@ The following rules define [list items]:
blocks *Bs* starting with a character other than a space or tab, and *M* is
a list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces of indentation,
then the result of prepending *M* and the following spaces to the first line
of Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a
of *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a
list item with *Bs* as its contents. The type of the list item
(bullet or ordered) is determined by the type of its list marker.
If the list item is ordered, then it is also assigned a start
@@ -5350,11 +5349,11 @@ by itself should be a paragraph followed by a nested sublist.
Since it is well established Markdown practice to allow lists to
interrupt paragraphs inside list items, the [principle of
uniformity] requires us to allow this outside list items as
well. ([reStructuredText](http://docutils.sourceforge.net/rst.html)
well. ([reStructuredText](https://docutils.sourceforge.net/rst.html)
takes a different approach, requiring blank lines before lists
even inside other list items.)
In order to solve of unwanted lists in paragraphs with
In order to solve the problem of unwanted lists in paragraphs with
hard-wrapped numerals, we allow only lists starting with `1` to
interrupt paragraphs. Thus,
@@ -6055,18 +6054,18 @@ But this is an HTML tag:
And this is code:
```````````````````````````````` example
`<http://foo.bar.`baz>`
`<https://foo.bar.`baz>`
.
<p><code>&lt;http://foo.bar.</code>baz&gt;`</p>
<p><code>&lt;https://foo.bar.</code>baz&gt;`</p>
````````````````````````````````
But this is an autolink:
```````````````````````````````` example
<http://foo.bar.`baz>`
<https://foo.bar.`baz>`
.
<p><a href="http://foo.bar.%60baz">http://foo.bar.`baz</a>`</p>
<p><a href="https://foo.bar.%60baz">https://foo.bar.`baz</a>`</p>
````````````````````````````````
@@ -6099,7 +6098,7 @@ closing backtick strings to be equal in length:
## Emphasis and strong emphasis
John Gruber's original [Markdown syntax
description](http://daringfireball.net/projects/markdown/syntax#em) says:
description](https://daringfireball.net/projects/markdown/syntax#em) says:
> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML
@@ -6201,7 +6200,7 @@ Here are some examples of delimiter runs.
(The idea of distinguishing left-flanking and right-flanking
delimiter runs based on the character before and the character
after comes from Roopesh Chander's
[vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags).
[vfmd](https://web.archive.org/web/20220608143320/http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags).
vfmd uses the terminology "emphasis indicator string" instead of "delimiter
run," and its rules for distinguishing left- and right-flanking runs
are a bit more complex than the ones given here.)
@@ -6343,6 +6342,21 @@ Unicode nonbreaking spaces count as whitespace, too:
````````````````````````````````
Unicode symbols count as punctuation, too:
```````````````````````````````` example
*$*alpha.
*£*bravo.
*€*charlie.
.
<p>*$*alpha.</p>
<p>*£*bravo.</p>
<p>*€*charlie.</p>
````````````````````````````````
Intraword emphasis with `*` is permitted:
```````````````````````````````` example
@@ -7428,16 +7442,16 @@ _a `_`_
```````````````````````````````` example
**a<http://foo.bar/?q=**>
**a<https://foo.bar/?q=**>
.
<p>**a<a href="http://foo.bar/?q=**">http://foo.bar/?q=**</a></p>
<p>**a<a href="https://foo.bar/?q=**">https://foo.bar/?q=**</a></p>
````````````````````````````````
```````````````````````````````` example
__a<http://foo.bar/?q=__>
__a<https://foo.bar/?q=__>
.
<p>__a<a href="http://foo.bar/?q=__">http://foo.bar/?q=__</a></p>
<p>__a<a href="https://foo.bar/?q=__">https://foo.bar/?q=__</a></p>
````````````````````````````````
@@ -7685,13 +7699,13 @@ A link can contain fragment identifiers and queries:
```````````````````````````````` example
[link](#fragment)
[link](http://example.com#fragment)
[link](https://example.com#fragment)
[link](http://example.com?foo=3#frag)
[link](https://example.com?foo=3#frag)
.
<p><a href="#fragment">link</a></p>
<p><a href="http://example.com#fragment">link</a></p>
<p><a href="http://example.com?foo=3#frag">link</a></p>
<p><a href="https://example.com#fragment">link</a></p>
<p><a href="https://example.com?foo=3#frag">link</a></p>
````````````````````````````````
@@ -7935,9 +7949,9 @@ and autolinks over link grouping:
```````````````````````````````` example
[foo<http://example.com/?search=](uri)>
[foo<https://example.com/?search=](uri)>
.
<p>[foo<a href="http://example.com/?search=%5D(uri)">http://example.com/?search=](uri)</a></p>
<p>[foo<a href="https://example.com/?search=%5D(uri)">https://example.com/?search=](uri)</a></p>
````````````````````````````````
@@ -8091,11 +8105,11 @@ and autolinks over link grouping:
```````````````````````````````` example
[foo<http://example.com/?search=][ref]>
[foo<https://example.com/?search=][ref]>
[ref]: /uri
.
<p>[foo<a href="http://example.com/?search=%5D%5Bref%5D">http://example.com/?search=][ref]</a></p>
<p>[foo<a href="https://example.com/?search=%5D%5Bref%5D">https://example.com/?search=][ref]</a></p>
````````````````````````````````
@@ -8295,7 +8309,7 @@ A [collapsed reference link](@)
consists of a [link label] that [matches] a
[link reference definition] elsewhere in the
document, followed by the string `[]`.
The contents of the first link label are parsed as inlines,
The contents of the link label are parsed as inlines,
which are used as the link's text. The link's URI and title are
provided by the matching reference link definition. Thus,
`[foo][]` is equivalent to `[foo][foo]`.
@@ -8348,7 +8362,7 @@ A [shortcut reference link](@)
consists of a [link label] that [matches] a
[link reference definition] elsewhere in the
document and is not followed by `[]` or a link label.
The contents of the first link label are parsed as inlines,
The contents of the link label are parsed as inlines,
which are used as the link's text. The link's URI and title
are provided by the matching link reference definition.
Thus, `[foo]` is equivalent to `[foo][]`.
@@ -8435,7 +8449,7 @@ following closing bracket:
````````````````````````````````
Full and compact references take precedence over shortcut
Full and collapsed references take precedence over shortcut
references:
```````````````````````````````` example
@@ -8771,9 +8785,9 @@ Here are some valid autolinks:
```````````````````````````````` example
<http://foo.bar.baz/test?q=hello&id=22&boolean>
<https://foo.bar.baz/test?q=hello&id=22&boolean>
.
<p><a href="http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean">http://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
<p><a href="https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean">https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</a></p>
````````````````````````````````
@@ -8813,9 +8827,9 @@ with their syntax:
```````````````````````````````` example
<http://../>
<https://../>
.
<p><a href="http://../">http://../</a></p>
<p><a href="https://../">https://../</a></p>
````````````````````````````````
@@ -8829,18 +8843,18 @@ with their syntax:
Spaces are not allowed in autolinks:
```````````````````````````````` example
<http://foo.bar/baz bim>
<https://foo.bar/baz bim>
.
<p>&lt;http://foo.bar/baz bim&gt;</p>
<p>&lt;https://foo.bar/baz bim&gt;</p>
````````````````````````````````
Backslash-escapes do not work inside autolinks:
```````````````````````````````` example
<http://example.com/\[\>
<https://example.com/\[\>
.
<p><a href="http://example.com/%5C%5B%5C">http://example.com/\[\</a></p>
<p><a href="https://example.com/%5C%5B%5C">https://example.com/\[\</a></p>
````````````````````````````````
@@ -8892,9 +8906,9 @@ These are not autolinks:
```````````````````````````````` example
< http://foo.bar >
< https://foo.bar >
.
<p>&lt; http://foo.bar &gt;</p>
<p>&lt; https://foo.bar &gt;</p>
````````````````````````````````
@@ -8913,9 +8927,9 @@ These are not autolinks:
```````````````````````````````` example
http://example.com
https://example.com
.
<p>http://example.com</p>
<p>https://example.com</p>
````````````````````````````````
@@ -8977,10 +8991,9 @@ A [closing tag](@) consists of the string `</`, a
[tag name], optional spaces, tabs, and up to one line ending, and the character
`>`.
An [HTML comment](@) consists of `<!--` + *text* + `-->`,
where *text* does not start with `>` or `->`, does not end with `-`,
and does not contain `--`. (See the
[HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).)
An [HTML comment](@) consists of `<!-->`, `<!--->`, or `<!--`, a string of
characters not including the string `-->`, and `-->` (see the
[HTML spec](https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state)).
A [processing instruction](@)
consists of the string `<?`, a string
@@ -9119,30 +9132,20 @@ Illegal attributes in closing tag:
Comments:
```````````````````````````````` example
foo <!-- this is a
comment - with hyphen -->
foo <!-- this is a --
comment - with hyphens -->
.
<p>foo <!-- this is a
comment - with hyphen --></p>
<p>foo <!-- this is a --
comment - with hyphens --></p>
````````````````````````````````
```````````````````````````````` example
foo <!-- not a comment -- two hyphens -->
.
<p>foo &lt;!-- not a comment -- two hyphens --&gt;</p>
````````````````````````````````
Not comments:
```````````````````````````````` example
foo <!--> foo -->
foo <!-- foo--->
foo <!---> foo -->
.
<p>foo &lt;!--&gt; foo --&gt;</p>
<p>foo &lt;!-- foo---&gt;</p>
<p>foo <!--> foo --&gt;</p>
<p>foo <!---> foo --&gt;</p>
````````````````````````````````
@@ -9671,7 +9674,7 @@ through the stack for an opening `[` or `![` delimiter.
delimiter from the stack, and return a literal text node `]`.
- If we find one and it's active, then we parse ahead to see if
we have an inline link/image, reference link/image, compact reference
we have an inline link/image, reference link/image, collapsed reference
link/image, or shortcut reference link/image.
+ If we don't, then we remove the opening delimiter from the

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

@@ -123,6 +123,8 @@ namespace Markdig.Tests.Specs.EmphasisExtra
public class TestExtensionsEmphasisOnHtmlEntities
{
// ## Emphasis on Html Entities
//
// Note that Unicode symbols are treated as punctuation, which are not allowed to open the emphasis unless they are preceded by a space.
[Test]
public void ExtensionsEmphasisOnHtmlEntities_Example006()
{
@@ -132,14 +134,14 @@ namespace Markdig.Tests.Specs.EmphasisExtra
// The following Markdown:
// This is text MyBrand ^&reg;^ and MyTrademark ^&trade;^
// This is text MyBrand^&reg;^ and MyTrademark^&trade;^
// This is text MyBrand~&reg;~ and MyCopyright^&copy;^
// This is text MyBrand ~&reg;~ and MyCopyright ^&copy;^
//
// Should be rendered as:
// <p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>
// This is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>
// This is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>
// This is text MyBrand^®^ and MyTrademark^TM^
// This is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>
TestParser.TestSpec("This is text MyBrand ^&reg;^ and MyTrademark ^&trade;^\nThis is text MyBrand^&reg;^ and MyTrademark^&trade;^\nThis is text MyBrand~&reg;~ and MyCopyright^&copy;^", "<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>\nThis is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>\nThis is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>", "emphasisextras|advanced", context: "Example 6\nSection Extensions / Emphasis on Html Entities\n");
TestParser.TestSpec("This is text MyBrand ^&reg;^ and MyTrademark ^&trade;^\nThis is text MyBrand^&reg;^ and MyTrademark^&trade;^\nThis is text MyBrand ~&reg;~ and MyCopyright ^&copy;^", "<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>\nThis is text MyBrand^®^ and MyTrademark^TM^\nThis is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>", "emphasisextras|advanced", context: "Example 6\nSection Extensions / Emphasis on Html Entities\n");
}
}
}

View File

@@ -52,16 +52,17 @@ Marked text can be used to specify that a text has been marked in a document. T
.
<p><mark>Marked text</mark></p>
````````````````````````````````
## Emphasis on Html Entities
Note that Unicode symbols are treated as punctuation, which are not allowed to open the emphasis unless they are preceded by a space.
```````````````````````````````` example
This is text MyBrand ^&reg;^ and MyTrademark ^&trade;^
This is text MyBrand^&reg;^ and MyTrademark^&trade;^
This is text MyBrand~&reg;~ and MyCopyright^&copy;^
This is text MyBrand ~&reg;~ and MyCopyright ^&copy;^
.
<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>
This is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>
This is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>
This is text MyBrand^®^ and MyTrademark^TM^
This is text MyBrand <sub>®</sub> and MyCopyright <sup>©</sup></p>
````````````````````````````````

View File

@@ -17,7 +17,7 @@ namespace Markdig.Tests.Specs.Math
//
// ## Math Inline
//
// Allows to define a mathematic block embraced by `$...$`
// Allows to define a mathematic inline block embraced by `$...$`
[Test]
public void ExtensionsMathInline_Example001()
{
@@ -25,12 +25,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $math block$
// This is a $math inline$
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span></p>
// <p>This is a <span class="math">\(math inline\)</span></p>
TestParser.TestSpec("This is a $math block$", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 1\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $math inline$", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 1\nSection Extensions / Math Inline\n");
}
// Or by `$$...$$` embracing it by:
@@ -41,12 +41,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $$math block$$
// This is a $$math inline$$
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span></p>
// <p>This is a <span class="math">\(math inline\)</span></p>
TestParser.TestSpec("This is a $$math block$$", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 2\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $$math inline$$", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 2\nSection Extensions / Math Inline\n");
}
// Newlines inside an inline math are not allowed:
@@ -58,13 +58,13 @@ namespace Markdig.Tests.Specs.Math
//
// The following Markdown:
// This is not a $$math
// block$$ and? this is a $$math block$$
// inline$$ and? this is a $$math inline$$
//
// Should be rendered as:
// <p>This is not a $$math
// block$$ and? this is a <span class="math">\(math block\)</span></p>
// inline$$ and? this is a <span class="math">\(math inline\)</span></p>
TestParser.TestSpec("This is not a $$math \nblock$$ and? this is a $$math block$$", "<p>This is not a $$math\nblock$$ and? this is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 3\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is not a $$math \ninline$$ and? this is a $$math inline$$", "<p>This is not a $$math\ninline$$ and? this is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 3\nSection Extensions / Math Inline\n");
}
[Test]
@@ -75,13 +75,13 @@ namespace Markdig.Tests.Specs.Math
//
// The following Markdown:
// This is not a $math
// block$ and? this is a $math block$
// inline$ and? this is a $math inline$
//
// Should be rendered as:
// <p>This is not a $math
// block$ and? this is a <span class="math">\(math block\)</span></p>
// inline$ and? this is a <span class="math">\(math inline\)</span></p>
TestParser.TestSpec("This is not a $math \nblock$ and? this is a $math block$", "<p>This is not a $math\nblock$ and? this is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 4\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is not a $math \ninline$ and? this is a $math inline$", "<p>This is not a $math\ninline$ and? this is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 4\nSection Extensions / Math Inline\n");
}
// An opening `$` can be followed by a space if the closing is also preceded by a space `$`:
@@ -92,12 +92,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $ math block $
// This is a $ math inline $
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span></p>
// <p>This is a <span class="math">\(math inline\)</span></p>
TestParser.TestSpec("This is a $ math block $", "<p>This is a <span class=\"math\">\\(math block\\)</span></p>", "mathematics|advanced", context: "Example 5\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $ math inline $", "<p>This is a <span class=\"math\">\\(math inline\\)</span></p>", "mathematics|advanced", context: "Example 5\nSection Extensions / Math Inline\n");
}
[Test]
@@ -107,12 +107,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $ math block $ after
// This is a $ math inline $ after
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span> after</p>
// <p>This is a <span class="math">\(math inline\)</span> after</p>
TestParser.TestSpec("This is a $ math block $ after", "<p>This is a <span class=\"math\">\\(math block\\)</span> after</p>", "mathematics|advanced", context: "Example 6\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $ math inline $ after", "<p>This is a <span class=\"math\">\\(math inline\\)</span> after</p>", "mathematics|advanced", context: "Example 6\nSection Extensions / Math Inline\n");
}
[Test]
@@ -122,12 +122,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $$ math block $$ after
// This is a $$ math inline $$ after
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span> after</p>
// <p>This is a <span class="math">\(math inline\)</span> after</p>
TestParser.TestSpec("This is a $$ math block $$ after", "<p>This is a <span class=\"math\">\\(math block\\)</span> after</p>", "mathematics|advanced", context: "Example 7\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $$ math inline $$ after", "<p>This is a <span class=\"math\">\\(math inline\\)</span> after</p>", "mathematics|advanced", context: "Example 7\nSection Extensions / Math Inline\n");
}
[Test]
@@ -137,12 +137,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a not $ math block$ because there is not a whitespace before the closing
// This is a not $ math inline$ because there is not a whitespace before the closing
//
// Should be rendered as:
// <p>This is a not $ math block$ because there is not a whitespace before the closing</p>
// <p>This is a not $ math inline$ because there is not a whitespace before the closing</p>
TestParser.TestSpec("This is a not $ math block$ because there is not a whitespace before the closing", "<p>This is a not $ math block$ because there is not a whitespace before the closing</p>", "mathematics|advanced", context: "Example 8\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a not $ math inline$ because there is not a whitespace before the closing", "<p>This is a not $ math inline$ because there is not a whitespace before the closing</p>", "mathematics|advanced", context: "Example 8\nSection Extensions / Math Inline\n");
}
// For the opening `$` it requires a space or a punctuation before (but cannot be used within a word):
@@ -153,12 +153,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is not a m$ath block$
// This is not a m$ath inline$
//
// Should be rendered as:
// <p>This is not a m$ath block$</p>
// <p>This is not a m$ath inline$</p>
TestParser.TestSpec("This is not a m$ath block$", "<p>This is not a m$ath block$</p>", "mathematics|advanced", context: "Example 9\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is not a m$ath inline$", "<p>This is not a m$ath inline$</p>", "mathematics|advanced", context: "Example 9\nSection Extensions / Math Inline\n");
}
// For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
@@ -169,12 +169,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is not a $math bloc$k
// This is not a $math inlin$e
//
// Should be rendered as:
// <p>This is not a $math bloc$k</p>
// <p>This is not a $math inlin$e</p>
TestParser.TestSpec("This is not a $math bloc$k", "<p>This is not a $math bloc$k</p>", "mathematics|advanced", context: "Example 10\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is not a $math inlin$e", "<p>This is not a $math inlin$e</p>", "mathematics|advanced", context: "Example 10\nSection Extensions / Math Inline\n");
}
// For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
@@ -201,12 +201,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $math \$ block$
// This is a $math \$ inline$
//
// Should be rendered as:
// <p>This is a <span class="math">\(math \$ block\)</span></p>
// <p>This is a <span class="math">\(math \$ inline\)</span></p>
TestParser.TestSpec("This is a $math \\$ block$", "<p>This is a <span class=\"math\">\\(math \\$ block\\)</span></p>", "mathematics|advanced", context: "Example 12\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $math \\$ inline$", "<p>This is a <span class=\"math\">\\(math \\$ inline\\)</span></p>", "mathematics|advanced", context: "Example 12\nSection Extensions / Math Inline\n");
}
// At most, two `$` will be matched for the opening and closing:
@@ -217,12 +217,12 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $$$math block$$$
// This is a $$$math inline$$$
//
// Should be rendered as:
// <p>This is a <span class="math">\($math block$\)</span></p>
// <p>This is a <span class="math">\($math inline$\)</span></p>
TestParser.TestSpec("This is a $$$math block$$$", "<p>This is a <span class=\"math\">\\($math block$\\)</span></p>", "mathematics|advanced", context: "Example 13\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $$$math inline$$$", "<p>This is a <span class=\"math\">\\($math inline$\\)</span></p>", "mathematics|advanced", context: "Example 13\nSection Extensions / Math Inline\n");
}
// Regular text can come both before and after the math inline
@@ -233,15 +233,15 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is a $math block$ with text on both sides.
// This is a $math inline$ with text on both sides.
//
// Should be rendered as:
// <p>This is a <span class="math">\(math block\)</span> with text on both sides.</p>
// <p>This is a <span class="math">\(math inline\)</span> with text on both sides.</p>
TestParser.TestSpec("This is a $math block$ with text on both sides.", "<p>This is a <span class=\"math\">\\(math block\\)</span> with text on both sides.</p>", "mathematics|advanced", context: "Example 14\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is a $math inline$ with text on both sides.", "<p>This is a <span class=\"math\">\\(math inline\\)</span> with text on both sides.</p>", "mathematics|advanced", context: "Example 14\nSection Extensions / Math Inline\n");
}
// A mathematic block takes precedence over standard emphasis `*` `_`:
// A mathematic inline block takes precedence over standard emphasis `*` `_`:
[Test]
public void ExtensionsMathInline_Example015()
{
@@ -249,15 +249,15 @@ namespace Markdig.Tests.Specs.Math
// Section: Extensions / Math Inline
//
// The following Markdown:
// This is *a $math* block$
// This is *a $math* inline$
//
// Should be rendered as:
// <p>This is *a <span class="math">\(math* block\)</span></p>
// <p>This is *a <span class="math">\(math* inline\)</span></p>
TestParser.TestSpec("This is *a $math* block$", "<p>This is *a <span class=\"math\">\\(math* block\\)</span></p>", "mathematics|advanced", context: "Example 15\nSection Extensions / Math Inline\n");
TestParser.TestSpec("This is *a $math* inline$", "<p>This is *a <span class=\"math\">\\(math* inline\\)</span></p>", "mathematics|advanced", context: "Example 15\nSection Extensions / Math Inline\n");
}
// An opening $$ at the beginning of a line should not be interpreted as a Math block:
// An opening $$ at the beginning of a line should not be interpreted as a Math inline:
[Test]
public void ExtensionsMathInline_Example016()
{
@@ -279,7 +279,7 @@ namespace Markdig.Tests.Specs.Math
{
// ## Math Block
//
// The match block can spawn on multiple lines by having a $$ starting on a line.
// The math block can spawn on multiple lines by having a $$ starting on a line.
// It is working as a fenced code block.
[Test]
public void ExtensionsMathBlock_Example017()

View File

@@ -4,79 +4,79 @@ Adds support for mathematics spans:
## Math Inline
Allows to define a mathematic block embraced by `$...$`
Allows to define a mathematic inline block embraced by `$...$`
```````````````````````````````` example
This is a $math block$
This is a $math inline$
.
<p>This is a <span class="math">\(math block\)</span></p>
<p>This is a <span class="math">\(math inline\)</span></p>
````````````````````````````````
Or by `$$...$$` embracing it by:
```````````````````````````````` example
This is a $$math block$$
This is a $$math inline$$
.
<p>This is a <span class="math">\(math block\)</span></p>
<p>This is a <span class="math">\(math inline\)</span></p>
````````````````````````````````
Newlines inside an inline math are not allowed:
```````````````````````````````` example
This is not a $$math
block$$ and? this is a $$math block$$
inline$$ and? this is a $$math inline$$
.
<p>This is not a $$math
block$$ and? this is a <span class="math">\(math block\)</span></p>
inline$$ and? this is a <span class="math">\(math inline\)</span></p>
````````````````````````````````
```````````````````````````````` example
This is not a $math
block$ and? this is a $math block$
inline$ and? this is a $math inline$
.
<p>This is not a $math
block$ and? this is a <span class="math">\(math block\)</span></p>
inline$ and? this is a <span class="math">\(math inline\)</span></p>
````````````````````````````````
An opening `$` can be followed by a space if the closing is also preceded by a space `$`:
```````````````````````````````` example
This is a $ math block $
This is a $ math inline $
.
<p>This is a <span class="math">\(math block\)</span></p>
<p>This is a <span class="math">\(math inline\)</span></p>
````````````````````````````````
```````````````````````````````` example
This is a $ math block $ after
This is a $ math inline $ after
.
<p>This is a <span class="math">\(math block\)</span> after</p>
<p>This is a <span class="math">\(math inline\)</span> after</p>
````````````````````````````````
```````````````````````````````` example
This is a $$ math block $$ after
This is a $$ math inline $$ after
.
<p>This is a <span class="math">\(math block\)</span> after</p>
<p>This is a <span class="math">\(math inline\)</span> after</p>
````````````````````````````````
```````````````````````````````` example
This is a not $ math block$ because there is not a whitespace before the closing
This is a not $ math inline$ because there is not a whitespace before the closing
.
<p>This is a not $ math block$ because there is not a whitespace before the closing</p>
<p>This is a not $ math inline$ because there is not a whitespace before the closing</p>
````````````````````````````````
For the opening `$` it requires a space or a punctuation before (but cannot be used within a word):
```````````````````````````````` example
This is not a m$ath block$
This is not a m$ath inline$
.
<p>This is not a m$ath block$</p>
<p>This is not a m$ath inline$</p>
````````````````````````````````
For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
```````````````````````````````` example
This is not a $math bloc$k
This is not a $math inlin$e
.
<p>This is not a $math bloc$k</p>
<p>This is not a $math inlin$e</p>
````````````````````````````````
For the closing `$` it requires a space after or a punctuation (but cannot be preceded by a space and cannot be used within a word):
@@ -90,34 +90,34 @@ This is should not match a 16$ or a $15
A `$` can be escaped between a math inline block by using the escape `\\`
```````````````````````````````` example
This is a $math \$ block$
This is a $math \$ inline$
.
<p>This is a <span class="math">\(math \$ block\)</span></p>
<p>This is a <span class="math">\(math \$ inline\)</span></p>
````````````````````````````````
At most, two `$` will be matched for the opening and closing:
```````````````````````````````` example
This is a $$$math block$$$
This is a $$$math inline$$$
.
<p>This is a <span class="math">\($math block$\)</span></p>
<p>This is a <span class="math">\($math inline$\)</span></p>
````````````````````````````````
Regular text can come both before and after the math inline
```````````````````````````````` example
This is a $math block$ with text on both sides.
This is a $math inline$ with text on both sides.
.
<p>This is a <span class="math">\(math block\)</span> with text on both sides.</p>
<p>This is a <span class="math">\(math inline\)</span> with text on both sides.</p>
````````````````````````````````
A mathematic block takes precedence over standard emphasis `*` `_`:
A mathematic inline block takes precedence over standard emphasis `*` `_`:
```````````````````````````````` example
This is *a $math* block$
This is *a $math* inline$
.
<p>This is *a <span class="math">\(math* block\)</span></p>
<p>This is *a <span class="math">\(math* inline\)</span></p>
````````````````````````````````
An opening $$ at the beginning of a line should not be interpreted as a Math block:
An opening $$ at the beginning of a line should not be interpreted as a Math inline:
```````````````````````````````` example
$$ math $$ starting at a line
@@ -127,7 +127,7 @@ $$ math $$ starting at a line
## Math Block
The match block can spawn on multiple lines by having a $$ starting on a line.
The math block can spawn on multiple lines by having a $$ starting on a line.
It is working as a fenced code block.
```````````````````````````````` example

View File

@@ -0,0 +1,24 @@
using Markdig.Extensions.AutoLinks;
namespace Markdig.Tests;
[TestFixture]
public class TestAutoLinks
{
[Test]
[TestCase("https://localhost", "<p><a href=\"https://localhost\">https://localhost</a></p>")]
[TestCase("http://localhost", "<p><a href=\"http://localhost\">http://localhost</a></p>")]
[TestCase("https://l", "<p><a href=\"https://l\">https://l</a></p>")]
[TestCase("www.l", "<p><a href=\"http://www.l\">www.l</a></p>")]
[TestCase("https://localhost:5000", "<p><a href=\"https://localhost:5000\">https://localhost:5000</a></p>")]
[TestCase("www.l:5000", "<p><a href=\"http://www.l:5000\">www.l:5000</a></p>")]
public void TestLinksWithAllowDomainWithoutPeriod(string markdown, string expected)
{
var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks(new AutoLinkOptions { AllowDomainWithoutPeriod = true })
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
}
}

View File

@@ -19,18 +19,32 @@ public class TestCharHelper
'{', '|', '}', '~'
};
// A Unicode punctuation character is an ASCII punctuation character or anything in the general Unicode categories
// Pc, Pd, Pe, Pf, Pi, Po, or Ps.
private static readonly HashSet<UnicodeCategory> s_punctuationCategories = new()
{
// A Unicode punctuation character is a character in the Unicode P (punctuation) or S (symbol) general categories.
private static readonly HashSet<UnicodeCategory> s_punctuationCategories =
[
UnicodeCategory.ConnectorPunctuation,
UnicodeCategory.DashPunctuation,
UnicodeCategory.OpenPunctuation,
UnicodeCategory.ClosePunctuation,
UnicodeCategory.FinalQuotePunctuation,
UnicodeCategory.InitialQuotePunctuation,
UnicodeCategory.FinalQuotePunctuation,
UnicodeCategory.OtherPunctuation,
UnicodeCategory.OpenPunctuation
};
UnicodeCategory.MathSymbol,
UnicodeCategory.CurrencySymbol,
UnicodeCategory.ModifierSymbol,
UnicodeCategory.OtherSymbol,
];
private static readonly HashSet<UnicodeCategory> s_punctuationWithoutSymbolsCategories =
[
UnicodeCategory.ConnectorPunctuation,
UnicodeCategory.DashPunctuation,
UnicodeCategory.OpenPunctuation,
UnicodeCategory.ClosePunctuation,
UnicodeCategory.InitialQuotePunctuation,
UnicodeCategory.FinalQuotePunctuation,
UnicodeCategory.OtherPunctuation,
];
private static bool ExpectedIsPunctuation(char c)
{
@@ -39,23 +53,98 @@ public class TestCharHelper
: s_punctuationCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c));
}
private static bool ExpectedIsPunctuationWithoutSymbols(char c)
{
return c <= 127
? s_asciiPunctuation.Contains(c)
: s_punctuationWithoutSymbolsCategories.Contains(CharUnicodeInfo.GetUnicodeCategory(c));
}
private static bool ExpectedIsWhitespace(char c)
{
// A Unicode whitespace character is any code point in the Unicode Zs general category,
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
return c == '\t' || c == '\n' || c == '\u000C' || c == '\r' ||
return c == '\t' || c == '\n' || c == '\f' || c == '\r' ||
CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
}
[Test]
public void IsAcrossTab()
{
Assert.False(CharHelper.IsAcrossTab(0));
Assert.True(CharHelper.IsAcrossTab(1));
Assert.True(CharHelper.IsAcrossTab(2));
Assert.True(CharHelper.IsAcrossTab(3));
Assert.False(CharHelper.IsAcrossTab(4));
}
[Test]
public void AddTab()
{
Assert.AreEqual(4, CharHelper.AddTab(0));
Assert.AreEqual(4, CharHelper.AddTab(1));
Assert.AreEqual(4, CharHelper.AddTab(2));
Assert.AreEqual(4, CharHelper.AddTab(3));
Assert.AreEqual(8, CharHelper.AddTab(4));
Assert.AreEqual(8, CharHelper.AddTab(5));
}
[Test]
public void IsWhitespace()
{
for (int i = char.MinValue; i <= char.MaxValue; i++)
{
char c = (char)i;
Test(
ExpectedIsWhitespace,
CharHelper.IsWhitespace);
Assert.AreEqual(ExpectedIsWhitespace(c), CharHelper.IsWhitespace(c));
}
Test(
ExpectedIsWhitespace,
CharHelper.WhitespaceChars.Contains);
}
[Test]
public void IsWhiteSpaceOrZero()
{
Test(
c => ExpectedIsWhitespace(c) || c == 0,
CharHelper.IsWhiteSpaceOrZero);
}
[Test]
public void IsAsciiPunctuation()
{
Test(
c => char.IsAscii(c) && ExpectedIsPunctuation(c),
CharHelper.IsAsciiPunctuation);
}
[Test]
public void IsAsciiPunctuationOrZero()
{
Test(
c => char.IsAscii(c) && (ExpectedIsPunctuation(c) || c == 0),
CharHelper.IsAsciiPunctuationOrZero);
}
[Test]
public void IsSpaceOrPunctuationForGFMAutoLink()
{
Test(
c => c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuationWithoutSymbols(c),
CharHelper.IsSpaceOrPunctuationForGFMAutoLink);
}
[Test]
public void InvalidAutoLinkCharacters()
{
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
// zero or more characters other than ASCII control characters, space, <, and >.
//
// 2.1 Characters and lines
// An ASCII control character is a character between U+00001F (both including) or U+007F.
Test(
c => c != 0 && c is < (char)0x20 or ' ' or '<' or '>' or '\u007F',
CharHelper.InvalidAutoLinkCharacters.Contains);
}
[Test]
@@ -76,15 +165,98 @@ public class TestCharHelper
}
[Test]
public void IsSpaceOrPunctuation()
public void IsControl()
{
Test(
char.IsControl,
CharHelper.IsControl);
}
[Test]
public void IsAlpha()
{
Test(
c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'),
CharHelper.IsAlpha);
}
[Test]
public void IsAlphaUpper()
{
Test(
c => c >= 'A' && c <= 'Z',
CharHelper.IsAlphaUpper);
}
[Test]
public void IsAlphaNumeric()
{
Test(
c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'),
CharHelper.IsAlphaNumeric);
}
[Test]
public void IsDigit()
{
Test(
c => c >= '0' && c <= '9',
CharHelper.IsDigit);
}
[Test]
public void IsNewLineOrLineFeed()
{
Test(
c => c is '\r' or '\n',
CharHelper.IsNewLineOrLineFeed);
}
[Test]
public void IsSpaceOrTab()
{
Test(
c => c is ' ' or '\t',
CharHelper.IsSpaceOrTab);
}
[Test]
public void IsEscapableSymbol()
{
Test(
"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~•".Contains,
CharHelper.IsEscapableSymbol);
}
[Test]
public void IsEmailUsernameSpecialChar()
{
Test(
".!#$%&'*+/=?^_`{|}~-+.~".Contains,
CharHelper.IsEmailUsernameSpecialChar);
}
[Test]
public void IsEmailUsernameSpecialCharOrDigit()
{
Test(
c => CharHelper.IsDigit(c) || ".!#$%&'*+/=?^_`{|}~-+.~".Contains(c),
CharHelper.IsEmailUsernameSpecialCharOrDigit);
}
private static void Test(Func<char, bool> expected, Func<char, bool> actual)
{
for (int i = char.MinValue; i <= char.MaxValue; i++)
{
char c = (char)i;
bool expected = c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuation(c);
bool expectedResult = expected(c);
bool actualResult = actual(c);
Assert.AreEqual(expected, CharHelper.IsSpaceOrPunctuation(c));
if (expectedResult != actualResult)
{
Assert.AreEqual(expectedResult, actualResult, $"Char: '{c}' ({i})");
}
}
}
}

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

@@ -0,0 +1,35 @@
using Markdig.Syntax;
namespace Markdig.Tests;
public class TestHtmlCodeBlocks
{
// Start condition: line begins with the string < or </ followed by one of the strings (case-insensitive)
// {list of all tags}, followed by a space, a tab, the end of the line, the string >, or the string />.
public static string[] KnownSimpleHtmlTags =>
[
"address", "article", "aside", "base", "basefont", "blockquote", "body", "caption", "center", "col", "colgroup", "dd", "details",
"dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset",
"h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "iframe", "legend", "li", "link",
"main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param",
"search", "section", "summary", "table", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul",
];
[Theory]
[TestCaseSource(nameof(KnownSimpleHtmlTags))]
public void TestKnownTags(string tag)
{
MarkdownDocument document = Markdown.Parse(
$"""
Hello
<{tag} />
World
""".ReplaceLineEndings("\n"));
HtmlBlock[] htmlBlocks = document.Descendants<HtmlBlock>().ToArray();
Assert.AreEqual(1, htmlBlocks.Length);
Assert.AreEqual(7, htmlBlocks[0].Span.Start);
Assert.AreEqual(10 + tag.Length, htmlBlocks[0].Span.Length);
}
}

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()
{
@@ -96,26 +112,26 @@ public class TestLinkHelper
}
[Test]
public void TestUrlAndTitleEmpty()
public void TestUrlEmptyAndTitleNull()
{
// 01234
var text = new StringSlice(@"(<>)A");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(null, title);
Assert.AreEqual(new SourceSpan(1, 2), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty2()
public void TestUrlEmptyAndTitleNull2()
{
// 012345
var text = new StringSlice(@"( <> )A");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(null, title);
Assert.AreEqual(new SourceSpan(2, 3), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
@@ -142,7 +158,7 @@ public class TestLinkHelper
var text = new StringSlice(@"()A");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(null, title);
Assert.AreEqual(SourceSpan.Empty, linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
@@ -230,6 +246,13 @@ public class TestLinkHelper
}
[Test]
public void TestlLinkReferenceDefinitionInvalid()
{
var text = new StringSlice("[foo]: /url (title) x\n");
Assert.False(LinkHelper.TryParseLinkReferenceDefinition(ref text, out _, out _, out _, out _, out _, out _));
}
[Test]
public void TestAutoLinkUrlSimple()
{

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,9 +34,19 @@ public class TestMediaLinks
public void TestBuiltInHosts(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
Assert.AreEqual(expected, html);
}
[TestCase("![static video relative path](./video.mp4)",
"<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"./video.mp4\"></source></video></p>\n")]
[TestCase("![static audio relative path](./audio.mp3)",
"<p><audio width=\"500\" controls=\"\"><source type=\"audio/mpeg\" src=\"./audio.mp3\"></source></audio></p>\n")]
public void TestBuiltInHostsWithRelativePaths(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(expected, html);
}
private class TestHostProvider : IHostProvider
{
public string Class { get; } = "regex";

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

@@ -12,10 +12,47 @@ public sealed class TestPipeTable
[TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)]
public void TestTableBug(string markdown, int tableCount = 1)
{
MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
MarkdownDocument document =
Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
Table[] tables = document.Descendants().OfType<Table>().ToArray();
Assert.AreEqual(tableCount, tables.Length);
}
[TestCase("A | B\r\n---|---", new[] {50.0f, 50.0f})]
[TestCase("A | B\r\n-|---", new[] {25.0f, 75.0f})]
[TestCase("A | B\r\n-|---\r\nA | B\r\n---|---", new[] {25.0f, 75.0f})]
[TestCase("A | B\r\n---|---|---", new[] {33.33f, 33.33f, 33.33f})]
[TestCase("A | B\r\n---|---|---|", new[] {33.33f, 33.33f, 33.33f})]
public void TestColumnWidthByHeaderLines(string markdown, float[] expectedWidth)
{
var pipeline = new MarkdownPipelineBuilder()
.UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = true})
.Build();
var document = Markdown.Parse(markdown, pipeline);
var table = document.Descendants().OfType<Table>().FirstOrDefault();
Assert.IsNotNull(table);
var actualWidths = table.ColumnDefinitions.Select(x => x.Width).ToList();
Assert.AreEqual(actualWidths.Count, expectedWidth.Length);
for (int i = 0; i < expectedWidth.Length; i++)
{
Assert.AreEqual(actualWidths[i], expectedWidth[i], 0.01);
}
}
[Test]
public void TestColumnWidthIsNotSetWithoutConfigurationFlag()
{
var pipeline = new MarkdownPipelineBuilder()
.UsePipeTables(new PipeTableOptions() {InferColumnWidthsFromSeparator = false})
.Build();
var document = Markdown.Parse("| A | B | C |\r\n|---|---|---|", pipeline);
var table = document.Descendants().OfType<Table>().FirstOrDefault();
Assert.IsNotNull(table);
foreach (var column in table.ColumnDefinitions)
{
Assert.AreEqual(0, column.Width);
}
}
}

View File

@@ -26,6 +26,14 @@ public class TestPlainText
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(/* markdownText: */ "```\nConsole.WriteLine(\"Hello, World!\");\n```", /* expected: */ "Console.WriteLine(\"Hello, World!\");\n")]
public void TestPlainCodeBlock(string markdownText, string expected)
{
var actual = Markdown.ToPlainText(markdownText);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")]
[TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")]

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;
@@ -180,4 +181,62 @@ public class TestStringSliceList
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
}
}
}
[Test]
public void TestStringLineGroupCharIteratorAtCapacity()
{
string str = "ABCDEFGHI";
var text = new StringLineGroup(1)
{
// Will store the following line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
};
var iterator = text.ToCharIterator();
var chars = ToString(iterator);
TextAssert.AreEqual("ABC\r\n", chars.ToString());
TextAssert.AreEqual("ABC", text.ToString());
}
[Test]
public void TestStringLineGroupCharIteratorForcingIncreaseCapacity()
{
string str = "ABCDEFGHI";
var text = new StringLineGroup(1)
{
// Will store the following line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
// Will force increase capacity to 2 and store the line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 3, End = 3 },
};
var iterator = text.ToCharIterator();
var chars = ToString(iterator);
TextAssert.AreEqual("ABC\r\nD\r\n", chars.ToString());
TextAssert.AreEqual("ABC\r\nD", text.ToString());
}
[Test]
public void TestStringLineGroup_EnumeratorReturnsRealLines()
{
string str = "A\r\n";
var text = new StringLineGroup(4)
{
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 0 }
};
var enumerator = ((IEnumerable)text).GetEnumerator();
Assert.True(enumerator.MoveNext());
StringLine currentLine = (StringLine)enumerator.Current;
TextAssert.AreEqual("A", currentLine.ToString());
Assert.False(enumerator.MoveNext());
var nonBoxedEnumerator = text.GetEnumerator();
Assert.True(nonBoxedEnumerator.MoveNext());
currentLine = (StringLine)nonBoxedEnumerator.Current;
TextAssert.AreEqual("A", currentLine.ToString());
Assert.False(nonBoxedEnumerator.MoveNext());
}
}

View File

@@ -75,12 +75,39 @@ public class TestYamlFrontMatterExtension
ObjectRenderers = new ObjectRendererCollection();
}
#pragma warning disable CS0067 // ObjectWriteBefore/ObjectWriteAfter is never used
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
#pragma warning restore CS0067
public ObjectRendererCollection ObjectRenderers { get; }
public object Render(MarkdownObject markdownObject)
{
return null;
}
}
[TestCase("---\nkey1: value1\nkey2: value2\n---\n\n# Content\n")]
[TestCase("---\nkey1: value1\nkey2: value2\nkey3: value3\nkey4: value4\nkey5: value5\nkey6: value6\nkey7: value7\nkey8: value8\n---\n\n# Content\n")]
public void FrontMatterBlockLinesCharIterator(string value)
{
var builder = new MarkdownPipelineBuilder();
builder.Extensions.Add(new YamlFrontMatterExtension());
var markdownDocument = Markdown.Parse(value, builder.Build());
var yamlBlocks = markdownDocument.Descendants<YamlFrontMatterBlock>();
Assert.True(yamlBlocks.Any());
foreach (var yamlBlock in yamlBlocks)
{
var iterator = yamlBlock.Lines.ToCharIterator();
while(iterator.CurrentChar != '\0')
{
iterator.NextChar();
}
}
// No exception parsing and iterating through YAML front matter block lines
}
}

View File

@@ -8,7 +8,7 @@ public class ApiController : Controller
{
[HttpGet()]
[Route("")]
public string Empty()
public new string Empty()
{
return string.Empty;
}

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

@@ -12,7 +12,7 @@ public class Startup
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
builder.AddApplicationInsightsSettings(connectionString: null, developerMode: true);
}
builder.AddEnvironmentVariables();

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,
};
@@ -202,20 +203,17 @@ public class AbbreviationParser : BlockParser
while (index <= contentNew.End)
{
var c = contentNew.PeekCharAbsolute(index);
if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation()))
{
return false;
}
if (c.IsAlphaNumeric())
{
return false;
}
if (c.IsWhitespace())
{
break;
}
if (!c.IsAsciiPunctuationOrZero())
{
return false;
}
index++;
}
return true;

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,86 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Alerts;
/// <summary>
/// A HTML renderer for a <see cref="AlertBlock"/>.
/// </summary>
/// <seealso cref="HtmlObjectRenderer{AlertBlock}" />
public class AlertBlockRenderer : HtmlObjectRenderer<AlertBlock>
{
/// <summary>
/// Creates a new instance of this renderer.
/// </summary>
public AlertBlockRenderer()
{
RenderKind = DefaultRenderKind;
}
/// <summary>
/// Gets of sets a delegate to render the kind of the alert.
/// </summary>
public Action<HtmlRenderer, StringSlice> RenderKind { get; set; }
/// <inheritdoc />
protected override void Write(HtmlRenderer renderer, AlertBlock obj)
{
renderer.EnsureLine();
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<div");
renderer.WriteAttributes(obj);
renderer.WriteLine('>');
}
RenderKind(renderer, obj.Kind);
var savedImplicitParagraph = renderer.ImplicitParagraph;
renderer.ImplicitParagraph = false;
renderer.WriteChildren(obj);
renderer.ImplicitParagraph = savedImplicitParagraph;
if (renderer.EnableHtmlForBlock)
{
renderer.WriteLine("</div>");
}
renderer.EnsureLine();
}
/// <summary>
/// Renders the kind of the alert.
/// </summary>
/// <param name="renderer">The HTML renderer.</param>
/// <param name="kind">The kind of the alert to render</param>
public static void DefaultRenderKind(HtmlRenderer renderer, StringSlice kind)
{
if (kind.Length >= 16)
{
return;
}
Span<char> upperKind = stackalloc char[kind.Length];
kind.AsSpan().ToUpperInvariant(upperKind);
string? html = upperKind switch
{
"NOTE" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Note</p>",
"TIP" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z\"></path></svg>Tip</p>",
"IMPORTANT" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Important</p>",
"WARNING" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"></path></svg>Warning</p>",
"CAUTION" => "<p class=\"markdown-alert-title\"><svg viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z\"></path></svg>Caution</p>",
_ => null
};
if (html is not null)
{
renderer.WriteLine(html);
}
}
}

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,134 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace Markdig.Extensions.Alerts;
/// <summary>
/// An inline parser for an alert inline (e.g. `[!NOTE]`).
/// </summary>
/// <seealso cref="InlineParser" />
public class AlertInlineParser : InlineParser
{
private static readonly TransformedStringCache s_alertTypeClassCache = new(
type => $"markdown-alert-{type.ToLowerInvariant()}");
/// <summary>
/// Initializes a new instance of the <see cref="AlertInlineParser"/> class.
/// </summary>
public AlertInlineParser()
{
OpeningCharacters = ['['];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
if (slice.PeekChar() != '!')
{
return false;
}
// We expect the alert to be the first child of a quote block. Example:
// > [!NOTE]
// > This is a note
if (processor.Block is not ParagraphBlock paragraphBlock ||
paragraphBlock.Parent is not QuoteBlock quoteBlock ||
paragraphBlock.Inline?.FirstChild != null ||
quoteBlock is AlertBlock ||
quoteBlock.Parent is not MarkdownDocument)
{
return false;
}
StringSlice saved = slice;
slice.SkipChar(); // Skip [
char c = slice.NextChar(); // Skip !
int start = slice.Start;
int end = start;
while (c.IsAlpha())
{
end = slice.Start;
c = slice.NextChar();
}
// We need at least one character
if (c != ']' || start == end)
{
slice = saved;
return false;
}
var alertType = new StringSlice(slice.Text, start, end);
c = slice.NextChar(); // Skip ]
start = slice.Start;
while (true)
{
if (c == '\0' || c == '\n' || c == '\r')
{
end = slice.Start;
if (c == '\r')
{
c = slice.NextChar(); // Skip \r
if (c == '\0' || c == '\n')
{
end = slice.Start;
if (c == '\n')
{
slice.SkipChar(); // Skip \n
}
}
}
else if (c == '\n')
{
slice.SkipChar(); // Skip \n
}
break;
}
else if (!c.IsSpaceOrTab())
{
slice = saved;
return false;
}
c = slice.NextChar();
}
var alertBlock = new AlertBlock(alertType)
{
Span = quoteBlock.Span,
TriviaSpaceAfterKind = new StringSlice(slice.Text, start, end),
Line = quoteBlock.Line,
Column = quoteBlock.Column,
};
HtmlAttributes attributes = alertBlock.GetAttributes();
attributes.AddClass("markdown-alert");
attributes.AddClass(s_alertTypeClassCache.Get(alertType.AsSpan()));
// Replace the quote block with the alert block
var parentQuoteBlock = quoteBlock.Parent!;
var indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock);
parentQuoteBlock[indexOfQuoteBlock] = alertBlock;
while (quoteBlock.Count > 0)
{
var block = quoteBlock[0];
quoteBlock.RemoveAt(0);
alertBlock.Add(block);
}
// Workaround to replace the parent container
// Experimental API, so we are keeping it internal for now until we are sure it's the way we want to go
processor.ReplaceParentContainer(quoteBlock, alertBlock);
return true;
}
}

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,12 @@ namespace Markdig.Extensions.AutoIdentifiers;
public class AutoIdentifierExtension : IMarkdownExtension
{
private const string AutoIdentifierKey = "AutoIdentifier";
private readonly AutoIdentifierOptions options;
private readonly StripRendererCache rendererCache = new StripRendererCache();
private static readonly StripRendererCache _rendererCache = new();
private readonly AutoIdentifierOptions _options;
private readonly ProcessInlineDelegate _processInlinesBegin;
private readonly ProcessInlineDelegate _processInlinesEnd;
/// <summary>
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
@@ -29,7 +31,9 @@ public class AutoIdentifierExtension : IMarkdownExtension
/// <param name="options">The options.</param>
public AutoIdentifierExtension(AutoIdentifierOptions options)
{
this.options = options;
_options = options;
_processInlinesBegin = DocumentOnProcessInlinesBegin;
_processInlinesEnd = HeadingBlock_ProcessInlinesEnd;
}
public void Setup(MarkdownPipelineBuilder pipeline)
@@ -68,7 +72,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
}
// If the AutoLink options is set, we register a LinkReferenceDefinition at the document level
if ((options & AutoIdentifierOptions.AutoLink) != 0)
if ((_options & AutoIdentifierOptions.AutoLink) != 0)
{
var headingLine = headingBlock.Lines.Lines[0];
@@ -85,19 +89,19 @@ public class AutoIdentifierExtension : IMarkdownExtension
{
dictionary = new Dictionary<string, HeadingLinkReferenceDefinition>();
doc.SetData(this, dictionary);
doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
doc.ProcessInlinesBegin += _processInlinesBegin;
}
dictionary[text] = linkRef;
}
// Then we register after inline have been processed to actually generate the proper #id
headingBlock.ProcessInlinesEnd += HeadingBlock_ProcessInlinesEnd;
headingBlock.ProcessInlinesEnd += _processInlinesEnd;
}
private void DocumentOnProcessInlinesBegin(InlineProcessor processor, Inline? inline)
{
var doc = processor.Document;
doc.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin;
doc.ProcessInlinesBegin -= _processInlinesBegin;
var dictionary = (Dictionary<string, HeadingLinkReferenceDefinition>)doc.GetData(this)!;
foreach (var keyPair in dictionary)
{
@@ -117,7 +121,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
/// Callback when there is a reference to found to a heading.
/// Note that reference are only working if they are declared after.
/// </summary>
private Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child)
private static Inline CreateLinkInlineForHeading(InlineProcessor inlineState, LinkReferenceDefinition linkRef, Inline? child)
{
var headingRef = (HeadingLinkReferenceDefinition) linkRef;
return new LinkInline()
@@ -157,16 +161,17 @@ public class AutoIdentifierExtension : IMarkdownExtension
}
// Use internally a HtmlRenderer to strip links from a heading
var stripRenderer = rendererCache.Get();
var stripRenderer = _rendererCache.Get();
stripRenderer.Render(headingBlock.Inline);
var headingText = stripRenderer.Writer.ToString()!;
rendererCache.Release(stripRenderer);
ReadOnlySpan<char> rawHeadingText = ((FastStringWriter)stripRenderer.Writer).AsSpan();
// Urilize the link
headingText = (options & AutoIdentifierOptions.GitHub) != 0
? LinkHelper.UrilizeAsGfm(headingText)
: LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
string headingText = (_options & AutoIdentifierOptions.GitHub) != 0
? LinkHelper.UrilizeAsGfm(rawHeadingText)
: LinkHelper.Urilize(rawHeadingText, (_options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
_rendererCache.Release(stripRenderer);
// If the heading is empty, use the word "section" instead
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
@@ -197,7 +202,7 @@ public class AutoIdentifierExtension : IMarkdownExtension
{
protected override HtmlRenderer NewInstance()
{
var headingWriter = new StringWriter();
var headingWriter = new FastStringWriter();
var stripRenderer = new HtmlRenderer(headingWriter)
{
// Set to false both to avoid having any HTML tags in the output
@@ -209,7 +214,9 @@ public class AutoIdentifierExtension : IMarkdownExtension
protected override void Reset(HtmlRenderer instance)
{
instance.Reset();
instance.ResetInternal();
((FastStringWriter)instance.Writer).Reset();
}
}
}

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,4 +22,9 @@ public class AutoLinkOptions
/// Should a www link be prefixed with https:// instead of http:// (false by default)
/// </summary>
public bool UseHttpsForWWWLinks { get; set; }
/// <summary>
/// Should auto-linking allow a domain with no period, e.g. https://localhost (false by default)
/// </summary>
public bool AllowDomainWithoutPeriod { get; set; }
}

View File

@@ -6,6 +6,8 @@ using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax.Inlines;
using System.Buffers;
using System.Diagnostics;
namespace Markdig.Extensions.AutoLinks;
@@ -22,195 +24,180 @@ public class AutoLinkParser : InlineParser
{
Options = options ?? throw new ArgumentNullException(nameof(options));
OpeningCharacters = new char[]
{
OpeningCharacters =
[
'h', // for http:// and https://
'f', // for ftp://
'm', // for mailto:
't', // for tel:
'w', // for www.
};
];
_listOfCharCache = new ListOfCharCache();
_validPreviousCharacters = SearchValues.Create(options.ValidPreviousCharacters);
}
public readonly AutoLinkOptions Options;
private readonly ListOfCharCache _listOfCharCache;
private readonly SearchValues<char> _validPreviousCharacters;
// This is a particularly expensive parser as it gets called for many common letters.
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// Previous char must be a whitespace or a punctuation
var previousChar = slice.PeekCharExtra(-1);
if (!previousChar.IsWhiteSpaceOrZero() && Options.ValidPreviousCharacters.IndexOf(previousChar) == -1)
if (!previousChar.IsWhiteSpaceOrZero() && !_validPreviousCharacters.Contains(previousChar))
{
return false;
}
ReadOnlySpan<char> span = slice.AsSpan();
Debug.Assert(span[0] is 'h' or 'f' or 'm' or 't' or 'w');
// Precheck URL
bool mayBeValid = span.Length >= 4 && span[0] switch
{
'h' => span.StartsWith("https://", StringComparison.Ordinal) || span.StartsWith("http://", StringComparison.Ordinal),
'w' => span.StartsWith("www.", StringComparison.Ordinal), // We won't match http:/www. or /www.xxx
'f' => span.StartsWith("ftp://", StringComparison.Ordinal),
'm' => span.StartsWith("mailto:", StringComparison.Ordinal),
_ => span.StartsWith("tel:", StringComparison.Ordinal),
};
return mayBeValid && MatchCore(processor, ref slice);
}
private bool MatchCore(InlineProcessor processor, ref StringSlice slice)
{
char c = slice.CurrentChar;
var startPosition = slice.Start;
// We don't bother disposing the builder as it'll realistically never grow beyond the initial stack size.
var pendingEmphasis = new ValueStringBuilder(stackalloc char[32]);
// Check that an autolink is possible in the current context
if (!IsAutoLinkValidInCurrentContext(processor, ref pendingEmphasis))
{
return false;
}
// Parse URL
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
{
return false;
}
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
if (pendingEmphasis.Length > 0)
{
for (int i = link.Length - 1; i >= 0; i--)
{
if (pendingEmphasis.AsSpan().Contains(link[i]))
{
slice.Start--;
}
else
{
if (i < link.Length - 1)
{
link = link.Substring(0, i + 1);
}
break;
}
}
}
int domainOffset = 0;
var c = slice.CurrentChar;
// Precheck URL
// Post-check URL
switch (c)
{
case 'h':
if (slice.MatchLowercase("ttp://", 1))
if (string.Equals(link, "http://", StringComparison.Ordinal) ||
string.Equals(link, "https://", StringComparison.Ordinal))
{
domainOffset = 7; // http://
return false;
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
domainOffset = link[4] == 's' ? 8 : 7; // https:// or http://
break;
case 'w':
domainOffset = 4; // www.
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
if (string.Equals(link, "ftp://", StringComparison.Ordinal))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 't':
if (!slice.MatchLowercase("el:", 1))
if (string.Equals(link, "tel", StringComparison.Ordinal))
{
return false;
}
domainOffset = 4;
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
case 'm':
int atIndex = link.IndexOf('@');
if (atIndex == -1 ||
atIndex == 7) // mailto:@ - no email part
{
return false;
}
domainOffset = 4; // www.
domainOffset = atIndex + 1;
break;
}
List<char> pendingEmphasis = _listOfCharCache.Get();
try
// Do not need to check if a telephone number is a valid domain
if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset, Options.AllowDomainWithoutPeriod))
{
// Check that an autolink is possible in the current context
if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis))
{
return false;
}
// Parse URL
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
{
return false;
}
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
if (pendingEmphasis.Count > 0)
{
for (int i = link.Length - 1; i >= 0; i--)
{
if (pendingEmphasis.Contains(link[i]))
{
slice.Start--;
}
else
{
if (i < link.Length - 1)
{
link = link.Substring(0, i + 1);
}
break;
}
}
}
// Post-check URL
switch (c)
{
case 'h':
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'f':
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 't':
if (string.Equals(link, "tel", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'm':
int atIndex = link.IndexOf('@');
if (atIndex == -1 ||
atIndex == 7) // mailto:@ - no email part
{
return false;
}
domainOffset = atIndex + 1;
break;
}
// Do not need to check if a telephone number is a valid domain
if (c != 't' && !LinkHelper.IsValidDomain(link, domainOffset))
{
return false;
}
var inline = new LinkInline()
{
Span =
{
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
},
Line = line,
Column = column,
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
IsClosed = true,
IsAutoLink = true,
};
var skipFromBeginning = c == 'm' ? 7 : 0; // For mailto: skip "mailto:" for content
skipFromBeginning = c == 't' ? 4 : skipFromBeginning; // See above but for tel:
inline.Span.End = inline.Span.Start + link.Length - 1;
inline.UrlSpan = inline.Span;
inline.AppendChild(new LiteralInline()
{
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;
if (Options.OpenInNewWindow)
{
inline.GetAttributes().AddPropertyIfNotExist("target", "_blank");
}
return true;
return false;
}
finally
var inline = new LinkInline()
{
_listOfCharCache.Release(pendingEmphasis);
Span =
{
Start = processor.GetSourcePosition(startPosition, out int line, out int column),
},
Line = line,
Column = column,
Url = c == 'w' ? ((Options.UseHttpsForWWWLinks ? "https://" : "http://") + link) : link,
IsClosed = true,
IsAutoLink = true,
};
int skipFromBeginning = c switch
{
'm' => 7, // For mailto: skip "mailto:" for content
't' => 4, // Same but for tel:
_ => 0
};
inline.Span.End = inline.Span.Start + link.Length - 1;
inline.UrlSpan = inline.Span;
inline.AppendChild(new LiteralInline()
{
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition + skipFromBeginning, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;
if (Options.OpenInNewWindow)
{
inline.GetAttributes().AddPropertyIfNotExist("target", "_blank");
}
return true;
}
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List<char> pendingEmphasis)
private static bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, ref ValueStringBuilder pendingEmphasis)
{
// Case where there is a pending HtmlInline <a>
var currentInline = processor.Inline;
@@ -257,9 +244,9 @@ public class AutoLinkParser : InlineParser
// Record all pending characters for emphasis
if (currentInline is EmphasisDelimiterInline emphasisDelimiter)
{
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
if (!pendingEmphasis.AsSpan().Contains(emphasisDelimiter.DelimiterChar))
{
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
pendingEmphasis.Append(emphasisDelimiter.DelimiterChar);
}
}
}
@@ -268,12 +255,4 @@ public class AutoLinkParser : InlineParser
return countBrackets <= 0;
}
private sealed class ListOfCharCache : DefaultObjectCache<List<char>>
{
protected override void Reset(List<char> instance)
{
instance.Clear();
}
}
}

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)
@@ -124,7 +124,7 @@ public class GenericAttributesParser : InlineParser
var start = line.Start;
// Get all non-whitespace characters following a #
// But stop if we found a } or \0
while (c != '}' && c != '\0' && !c.IsWhitespace())
while (c != '}' && !c.IsWhiteSpaceOrZero())
{
c = line.NextChar();
}
@@ -136,10 +136,7 @@ public class GenericAttributesParser : InlineParser
var text = slice.Text.Substring(start, end - start + 1);
if (isClass)
{
if (classes is null)
{
classes = new List<string>();
}
classes ??= new List<string>();
classes.Add(text);
}
else

View File

@@ -105,7 +105,7 @@ public class GlobalizationExtension : IMarkdownExtension
}
int rune = c;
if (CharHelper.IsHighSurrogate(c) && i < slice.End && CharHelper.IsLowSurrogate(slice[i + 1]))
if (char.IsHighSurrogate(c) && i < slice.End && char.IsLowSurrogate(slice[i + 1]))
{
Debug.Assert(char.IsSurrogatePair(c, slice[i + 1]));
rune = char.ConvertToUtf32(c, slice[i + 1]);

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)
{
@@ -59,19 +55,19 @@ public class HostProviderBuilder
return new DelegateProvider(hostPrefix, handler, allowFullScreen, iframeClass);
}
internal static Dictionary<string, IHostProvider> KnownHosts { get; }
= new Dictionary<string, IHostProvider>(StringComparer.OrdinalIgnoreCase)
{
["YouTube"] = Create("www.youtube.com", YouTube, iframeClass: "youtube"),
["YouTubeShortened"] = Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
["Vimeo"] = Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
["Yandex"] = Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"),
["Odnoklassniki"] = Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"),
};
internal static readonly IHostProvider[] KnownHosts =
[
Create("www.youtube.com", YouTubeShort, iframeClass: "youtubeshort"),
Create("www.youtube.com", YouTube, iframeClass: "youtube"),
Create("youtu.be", YouTubeShortened, iframeClass: "youtube"),
Create("vimeo.com", Vimeo, iframeClass: "vimeo"),
Create("music.yandex.ru", Yandex, allowFullScreen: false, iframeClass: "yandex"),
Create("ok.ru", Odnoklassniki, iframeClass: "odnoklassniki"),
];
#region Known providers
private static readonly string[] SplitAnd = { "&" };
private static readonly string[] SplitAnd = ["&"];
private static string[] SplitQuery(Uri uri)
{
var query = uri.Query.Substring(uri.Query.IndexOf('?') + 1);
@@ -96,6 +92,19 @@ public class HostProviderBuilder
);
}
private static string? YouTubeShort(Uri uri)
{
string uriPath = uri.AbsolutePath;
bool isYouTubeShort = uriPath.StartsWith("/shorts/", StringComparison.OrdinalIgnoreCase);
if (!isYouTubeShort)
{
return null;
}
var shortId = uriPath.Substring("/shorts/".Length).Split('?').FirstOrDefault(); // the format might be "/shorts/6BUptHVuvyI?feature=share"
return BuildYouTubeIframeUrl(shortId, null);
}
private static string? YouTubeShortened(Uri uri)
{
return BuildYouTubeIframeUrl(

View File

@@ -50,27 +50,32 @@ public class MediaLinkExtension : IMarkdownExtension
return false;
}
var url = linkInline.Url;
bool isSchemaRelative = false;
// Only process absolute Uri
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out Uri? uri) || !uri.IsAbsoluteUri)
// force // schema to an absolute url
if (url.StartsWith("//", StringComparison.Ordinal))
{
// see https://tools.ietf.org/html/rfc3986#section-4.2
// since relative uri doesn't support many properties, "http" is used as a placeholder here.
if (linkInline.Url.StartsWith("//", StringComparison.Ordinal) && Uri.TryCreate("http:" + linkInline.Url, UriKind.Absolute, out uri))
url = "https:" + url;
isSchemaRelative = true;
}
// Make sure we have a valid absolute/relative url
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out Uri? uri)) // || !uri.IsAbsoluteUri)
{
return false;
}
// iFrame has to be absolute path
if (uri.IsAbsoluteUri)
{
if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline))
{
isSchemaRelative = true;
}
else
{
return false;
return true;
}
}
if (TryRenderIframeFromKnownProviders(uri, isSchemaRelative, renderer, linkInline))
{
return true;
}
// audio/video has can have relative path
if (TryGuessAudioVideoFile(uri, isSchemaRelative, renderer, linkInline))
{
return true;
@@ -93,7 +98,10 @@ public class MediaLinkExtension : IMarkdownExtension
private bool TryGuessAudioVideoFile(Uri uri, bool isSchemaRelative, HtmlRenderer renderer, LinkInline linkInline)
{
var path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped);
string path = uri.IsAbsoluteUri
? uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)
: uri.ToString();
// Otherwise try to detect if we have an audio/video from the file extension
var lastDot = path.LastIndexOf('.');
if (lastDot >= 0 &&

View File

@@ -81,7 +81,7 @@ public class MediaOptions
{".au", "audio/basic"},
{".wav", "audio/x-wav"},
};
Hosts = new List<IHostProvider>(HostProviderBuilder.KnownHosts.Values);
Hosts = new List<IHostProvider>(HostProviderBuilder.KnownHosts);
}
public string Width { get; set; }

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

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

@@ -33,4 +33,11 @@ public class PipeTableOptions
/// in all other rows (default behavior).
/// </summary>
public bool UseHeaderForColumnCount { get; set; }
/// <summary>
/// Gets or sets a value indicating whether column widths should be inferred based on the number of dashes
/// in the header separator row. Each column's width will be proportional to the dash count in its respective column.
/// </summary>
public bool InferColumnWidthsFromSeparator { get; set; }
}

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
@@ -462,9 +481,10 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
return false;
}
private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align)
private static bool ParseHeaderString(Inline? inline, out TableColumnAlign? align, out int delimiterCount)
{
align = 0;
delimiterCount = 0;
var literal = inline as LiteralInline;
if (literal is null)
{
@@ -473,7 +493,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
// Work on a copy of the slice
var line = literal.Content;
if (TableHelper.ParseColumnHeader(ref line, '-', out align))
if (TableHelper.ParseColumnHeader(ref line, '-', out align, out delimiterCount))
{
if (line.CurrentChar != '\0')
{
@@ -488,7 +508,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
private List<TableColumnDefinition>? FindHeaderRow(List<Inline> delimiters)
{
bool isValidRow = false;
List<TableColumnDefinition>? aligns = null;
int totalDelimiterCount = 0;
List<TableColumnDefinition>? columnDefinitions = null;
for (int i = 0; i < delimiters.Count; i++)
{
if (!IsLine(delimiters[i]))
@@ -510,18 +531,19 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
// Check the left side of a `|` delimiter
TableColumnAlign? align = null;
int delimiterCount = 0;
if (delimiter.PreviousSibling != null &&
!(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) && // ignore parsed whitespace
!ParseHeaderString(delimiter.PreviousSibling, out align))
!ParseHeaderString(delimiter.PreviousSibling, out align, out delimiterCount))
{
break;
}
// Create aligns until we may have a header row
aligns ??= new List<TableColumnDefinition>();
aligns.Add(new TableColumnDefinition() { Alignment = align });
columnDefinitions ??= new List<TableColumnDefinition>();
totalDelimiterCount += delimiterCount;
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
// If this is the last delimiter, we need to check the right side of the `|` delimiter
if (nextDelimiter is null)
@@ -537,13 +559,13 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
break;
}
if (!ParseHeaderString(nextSibling, out align))
if (!ParseHeaderString(nextSibling, out align, out delimiterCount))
{
break;
}
totalDelimiterCount += delimiterCount;
isValidRow = true;
aligns.Add(new TableColumnDefinition() { Alignment = align });
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
break;
}
@@ -557,7 +579,27 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
break;
}
return isValidRow ? aligns : null;
// calculate the width of the columns in percent based on the delimiter count
if (!isValidRow || columnDefinitions == null)
{
return null;
}
if (Options.InferColumnWidthsFromSeparator)
{
foreach (var columnDefinition in columnDefinitions)
{
columnDefinition.Width = (columnDefinition.Width * 100) / totalDelimiterCount;
}
}
else
{
foreach (var columnDefinition in columnDefinitions)
{
columnDefinition.Width = 0;
}
}
return columnDefinitions;
}
private static bool IsLine(Inline inline)
@@ -632,10 +674,10 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
public int LineIndex { get; set; }
public List<Inline> ColumnAndLineDelimiters { get; } = new();
public List<Inline> ColumnAndLineDelimiters { get; } = [];
public List<TableCell> Cells { get; } = new();
public List<TableCell> Cells { get; } = [];
public List<Inline> EndOfLines { get; } = new();
public List<Inline> EndOfLines { get; } = [];
}
}

View File

@@ -17,12 +17,13 @@ public static class TableHelper
/// <param name="slice">The text slice.</param>
/// <param name="delimiterChar">The delimiter character (either `-` or `=`).</param>
/// <param name="align">The alignment of the column.</param>
/// <param name="delimiterCount">The number of delimiters.</param>
/// <returns>
/// <c>true</c> if parsing was successful
/// </returns>
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align)
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align, out int delimiterCount)
{
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out delimiterCount);
}
/// <summary>
@@ -37,7 +38,7 @@ public static class TableHelper
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align)
{
delimiterChar = '\0';
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align, out _);
}
/// <summary>
@@ -49,10 +50,10 @@ public static class TableHelper
/// <returns>
/// <c>true</c> if parsing was successful
/// </returns>
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align)
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align, out int delimiterCount)
{
align = null;
delimiterCount = 0;
slice.TrimStart();
var c = slice.CurrentChar;
bool hasLeft = false;
@@ -80,7 +81,8 @@ public static class TableHelper
}
// We expect at least one `-` delimiter char
if (slice.CountAndSkipChar(delimiterChar) == 0)
delimiterCount = slice.CountAndSkipChar(delimiterChar);
if (delimiterCount == 0)
{
return false;
}

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>
@@ -71,7 +71,7 @@ public class YamlFrontMatterParser : BlockParser
// If three dashes (optionally followed by whitespace)
// this is a YAML front matter block
if (count == 3 && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
if (count == 3 && c.IsWhiteSpaceOrZero() && line.TrimEnd())
{
bool hasFullYamlFrontMatter = false;
// We make sure that there is a closing frontmatter somewhere in the document
@@ -146,7 +146,7 @@ public class YamlFrontMatterParser : BlockParser
// If we have a closing fence, close it and discard the current line
// The line must contain only fence characters and optional following whitespace.
if (count == 3 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
if (count == 3 && !processor.IsCodeIndent && c.IsWhiteSpaceOrZero() && line.TrimEnd())
{
block.UpdateSpanEnd(line.Start - 1);

View File

@@ -1,2 +1,3 @@
global using System;
global using System.Collections.Frozen;
global using System.Collections.Generic;

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

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
@@ -19,26 +20,65 @@ public static class CharHelper
public const string ReplacementCharString = "\uFFFD";
private const char HighSurrogateStart = '\ud800';
private const char HighSurrogateEnd = '\udbff';
private const char LowSurrogateStart = '\udc00';
private const char LowSurrogateEnd = '\udfff';
private const string EmailUsernameSpecialChars = ".!#$%&'*+/=?^_`{|}~-+.~";
// We don't support LCDM
private static readonly Dictionary<char, int> romanMap = new Dictionary<char, int>(6) {
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 },
{ 'I', 1 }, { 'V', 5 }, { 'X', 10 }
};
// 2.1 Characters and lines
// A Unicode whitespace character is any code point in the Unicode Zs general category,
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
// CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
private const string AsciiWhitespaceChars = "\t\n\f\r ";
internal const string WhitespaceChars = AsciiWhitespaceChars + "\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000";
private static readonly char[] punctuationExceptions = { '', '-', '†', '‡' };
// 2.1 Characters and lines
// An ASCII punctuation character is
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+00212F),
// :, ;, <, =, >, ?, @ (U+003A0040),
// [, \, ], ^, _, ` (U+005B0060),
// {, |, }, or ~ (U+007B007E).
private const string AsciiPunctuationChars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
// Unicode P (punctuation) categories.
private const int UnicodePunctuationCategoryMask =
1 << (int)UnicodeCategory.ConnectorPunctuation |
1 << (int)UnicodeCategory.DashPunctuation |
1 << (int)UnicodeCategory.OpenPunctuation |
1 << (int)UnicodeCategory.ClosePunctuation |
1 << (int)UnicodeCategory.InitialQuotePunctuation |
1 << (int)UnicodeCategory.FinalQuotePunctuation |
1 << (int)UnicodeCategory.OtherPunctuation;
private const int UnicodePunctuationOrSpaceCategoryMask =
UnicodePunctuationCategoryMask |
1 << (int)UnicodeCategory.SpaceSeparator;
// 2.1 Characters and lines
// A Unicode punctuation character is a character in the Unicode P (punctuation) or S (symbol) general categories.
private const int CommonMarkPunctuationCategoryMask =
UnicodePunctuationCategoryMask |
1 << (int)UnicodeCategory.MathSymbol |
1 << (int)UnicodeCategory.CurrencySymbol |
1 << (int)UnicodeCategory.ModifierSymbol |
1 << (int)UnicodeCategory.OtherSymbol;
// We're not currently using these SearchValues instances for vectorized IndexOfAny-like searches, but for their efficient single Contains(char) checks.
private static readonly SearchValues<char> s_emailUsernameSpecialChar = SearchValues.Create(EmailUsernameSpecialChars);
private static readonly SearchValues<char> s_emailUsernameSpecialCharOrDigit = SearchValues.Create(EmailUsernameSpecialChars + "0123456789");
private static readonly SearchValues<char> s_asciiPunctuationChars = SearchValues.Create(AsciiPunctuationChars);
private static readonly SearchValues<char> s_asciiPunctuationCharsOrZero = SearchValues.Create(AsciiPunctuationChars + '\0');
private static readonly SearchValues<char> s_asciiPunctuationOrWhitespaceCharsOrZero = SearchValues.Create(AsciiPunctuationChars + AsciiWhitespaceChars + '\0');
private static readonly SearchValues<char> s_escapableSymbolChars = SearchValues.Create("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~•");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPunctuationException(char c) =>
c is '' or '-' or '†' or '‡';
public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose)
{
pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation);
c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation);
var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc);
var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c);
var prevIsExcepted = prevIsPunctuation && IsPunctuationException(pc);
var nextIsExcepted = nextIsPunctuation && IsPunctuationException(c);
// A left-flanking delimiter run is a delimiter run that is
// (1) not followed by Unicode whitespace, and either
@@ -99,8 +139,8 @@ public static class CharHelper
int result = 0;
for (int i = 0; i < text.Length; i++)
{
var candidate = romanMap[text[i]];
if ((uint)(i + 1) < text.Length && candidate < romanMap[text[i + 1]])
int candidate = RomanToArabic(text[i]);
if ((uint)(i + 1) < text.Length && candidate < RomanToArabic(text[i + 1]))
{
result -= candidate;
}
@@ -110,6 +150,20 @@ public static class CharHelper
}
}
return result;
// We don't support LCDM
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int RomanToArabic(char c)
{
Debug.Assert(IsRomanLetterPartial(c));
return (c | 0x20) switch
{
'i' => 1,
'v' => 5,
_ => 10
};
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -126,72 +180,67 @@ public static class CharHelper
return (column & (TabSize - 1)) != 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Contains(this char[] charList, char c)
{
foreach (char ch in charList)
{
if (ch == c)
{
return true;
}
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsWhitespace(this char c)
{
// 2.1 Characters and lines
// A Unicode whitespace character is any code point in the Unicode Zs general category,
// or a tab (U+0009), line feed (U+000A), form feed (U+000C), or carriage return (U+000D).
if (c <= ' ')
if (c < '\u00A0')
{
const long Mask =
(1L << ' ') |
(1L << '\t') |
(1L << '\n') |
(1L << '\f') |
(1L << '\r');
return (Mask & (1L << c)) != 0;
// Matches any of "\t\n\f\r ". See comments in HexConverter.IsHexChar for how these checks work:
// https://github.com/dotnet/runtime/blob/a2e1d21bb4faf914363968b812c990329ba92d8e/src/libraries/Common/src/System/HexConverter.cs#L392-L415
// https://gist.github.com/MihaZupan/b93ba180c2b5fbaaed993db2ade76b49
ulong shift = 30399299632234496UL << c;
ulong mask = (ulong)c - 64;
return (long)(shift & mask) < 0;
}
return c >= '\u00A0' && IsWhitespaceRare(c);
return IsWhitespaceRare(c);
}
static bool IsWhitespaceRare(char c)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsWhiteSpaceOrZero(this char c)
{
if (c < '\u00A0')
{
// return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
// Matches any of "\0\t\n\f\r ".
ulong shift = 9253771336487010304UL << c;
ulong mask = (ulong)c - 64;
return (long)(shift & mask) < 0;
}
if (c < 5760)
{
return c == '\u00A0';
}
else
{
return c <= 12288 &&
(c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288);
}
return IsWhitespaceRare(c);
}
private static bool IsWhitespaceRare(char c)
{
Debug.Assert(c >= '\u00A0');
// return CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
if (c < 5760)
{
return c == '\u00A0';
}
else
{
return c <= 12288 &&
(c == 5760 || IsInInclusiveRange(c, 8192, 8202) || c == 8239 || c == 8287 || c == 12288);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsControl(this char c)
{
return c < ' ' || char.IsControl(c);
return char.IsControl(c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEscapableSymbol(this char c)
{
// char.IsSymbol also works with Unicode symbols that cannot be escaped based on the specification.
return (c > ' ' && c < '0') || (c > '9' && c < 'A') || (c > 'Z' && c < 'a') || (c > 'z' && c < 127) || c == '•';
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsWhiteSpaceOrZero(this char c)
{
return IsZero(c) || IsWhitespace(c);
return s_escapableSymbolChars.Contains(c);
}
// Check if a char is a space or a punctuation
@@ -205,48 +254,46 @@ public static class CharHelper
else if (c <= 127)
{
space = c == '\0';
punctuation = c == '\0' || IsAsciiPunctuation(c);
punctuation = IsAsciiPunctuationOrZero(c);
}
else
{
// A Unicode punctuation character is an ASCII punctuation character
// or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps.
space = false;
UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c);
punctuation = category == UnicodeCategory.ConnectorPunctuation
|| category == UnicodeCategory.DashPunctuation
|| category == UnicodeCategory.OpenPunctuation
|| category == UnicodeCategory.ClosePunctuation
|| category == UnicodeCategory.InitialQuotePunctuation
|| category == UnicodeCategory.FinalQuotePunctuation
|| category == UnicodeCategory.OtherPunctuation;
punctuation = (CommonMarkPunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
}
}
// Same as CheckUnicodeCategory
internal static bool IsSpaceOrPunctuation(this char c)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsSpaceOrPunctuationForGFMAutoLink(char c)
{
if (IsWhitespace(c))
// Github Flavored Markdown's allowed set of domain characters differs from CommonMark's "punctuation" definition.
// CommonMark also counts symbols as punctuation, but GitHub will render e.g. http://☃.net as an autolink, despite
// the snowman emoji falling under the OtherSymbol (So) category.
if (c <= 127)
{
return true;
}
else if (c <= 127)
{
return c == '\0' || IsAsciiPunctuation(c);
return s_asciiPunctuationOrWhitespaceCharsOrZero.Contains(c);
}
else
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
return category == UnicodeCategory.ConnectorPunctuation
|| category == UnicodeCategory.DashPunctuation
|| category == UnicodeCategory.OpenPunctuation
|| category == UnicodeCategory.ClosePunctuation
|| category == UnicodeCategory.InitialQuotePunctuation
|| category == UnicodeCategory.FinalQuotePunctuation
|| category == UnicodeCategory.OtherPunctuation;
return NonAscii(c);
static bool NonAscii(char c) =>
(UnicodePunctuationOrSpaceCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
}
}
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
// zero or more characters other than ASCII control characters, space, <, and >.
//
// 2.1 Characters and lines
// An ASCII control character is a character between U+00001F (both including) or U+007F.
internal static readonly SearchValues<char> InvalidAutoLinkCharacters = SearchValues.Create(
// 0 is excluded because it can be slightly more expensive for SearchValues to handle, and we've already removed it from the input text.
"\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F" +
"\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" +
" <>\u007F");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsNewLineOrLineFeed(this char c)
{
@@ -286,7 +333,7 @@ public static class CharHelper
{
// 2.3 Insecure characters
// For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD).
return c == '\0' ? '\ufffd' : c;
return c == '\0' ? ReplacementChar : c;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -313,46 +360,33 @@ public static class CharHelper
return (uint)(c - '0') <= ('9' - '0');
}
public static bool IsAsciiPunctuation(this char c)
{
// 2.1 Characters and lines
// An ASCII punctuation character is
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+00212F),
// :, ;, <, =, >, ?, @ (U+003A0040),
// [, \, ], ^, _, ` (U+005B0060),
// {, |, }, or ~ (U+007B007E).
return c <= 127 && (
IsInInclusiveRange(c, 33, 47) ||
IsInInclusiveRange(c, 58, 64) ||
IsInInclusiveRange(c, 91, 96) ||
IsInInclusiveRange(c, 123, 126));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsAsciiPunctuationOrZero(this char c) =>
s_asciiPunctuationCharsOrZero.Contains(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEmailUsernameSpecialChar(char c)
{
return ".!#$%&'*+/=?^_`{|}~-+.~".IndexOf(c) >= 0;
}
public static bool IsAsciiPunctuation(this char c) =>
s_asciiPunctuationChars.Contains(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsHighSurrogate(char c)
{
return IsInInclusiveRange(c, HighSurrogateStart, HighSurrogateEnd);
}
public static bool IsEmailUsernameSpecialChar(char c) =>
s_emailUsernameSpecialChar.Contains(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsLowSurrogate(char c)
{
return IsInInclusiveRange(c, LowSurrogateStart, LowSurrogateEnd);
}
internal static bool IsEmailUsernameSpecialCharOrDigit(char c) =>
s_emailUsernameSpecialCharOrDigit.Contains(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInInclusiveRange(char c, char min, char max)
=> (uint)(c - min) <= (uint)(max - min);
public static bool IsHighSurrogate(char c) =>
char.IsHighSurrogate(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsInInclusiveRange(int value, uint min, uint max)
=> ((uint)value - min) <= (max - min);
public static bool IsLowSurrogate(char c) =>
char.IsLowSurrogate(c);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsInInclusiveRange(int value, uint min, uint max) =>
((uint)value - min) <= (max - min);
public static bool IsRightToLeft(int c)
{
@@ -729,11 +763,11 @@ public static class CharHelper
}
// Used by ListExtraItemParser to format numbers from 1 - 26
private static readonly string[] smallNumberStringCache = {
private static readonly string[] smallNumberStringCache = [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
"20", "21", "22", "23", "24", "25", "26",
};
];
internal static string SmallNumberToString(int number)
{

View File

@@ -20,7 +20,7 @@ public static class CharNormalizer
}
// This table was generated by the app UnicodeNormDApp
private static readonly Dictionary<char, string> CodeToAscii = new(1269)
private static readonly FrozenDictionary<char, string> CodeToAscii = new Dictionary<char, string>(1269)
{
{'Ḋ', "D"},
{'Ḍ', "D"},
@@ -1291,5 +1291,5 @@ public static class CharNormalizer
{'', "|"},
{'', "}"},
{'', "~"},
};
}.ToFrozenDictionary();
}

View File

@@ -2,14 +2,9 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Buffers;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
#if NETCOREAPP3_1_OR_GREATER
using System.Numerics;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace Markdig.Helpers;
@@ -19,13 +14,9 @@ namespace Markdig.Helpers;
/// <typeparam name="T"></typeparam>
public sealed class CharacterMap<T> where T : class
{
#if NETCOREAPP3_1_OR_GREATER
private readonly Vector128<byte> _asciiBitmap;
#endif
private readonly T[] asciiMap;
private readonly Dictionary<uint, T>? nonAsciiMap;
private readonly BoolVector128 isOpeningCharacter;
private readonly SearchValues<char> _values;
private readonly T[] _asciiMap;
private readonly FrozenDictionary<uint, T>? _nonAsciiMap;
/// <summary>
/// Initializes a new instance of the <see cref="CharacterMap{T}"/> class.
@@ -35,64 +26,44 @@ public sealed class CharacterMap<T> where T : class
public CharacterMap(IEnumerable<KeyValuePair<char, T>> maps)
{
if (maps is null) ThrowHelper.ArgumentNullException(nameof(maps));
var charSet = new HashSet<char>();
int maxChar = 0;
foreach (var map in maps)
{
var openingChar = map.Key;
charSet.Add(openingChar);
if (openingChar < 128)
{
maxChar = Math.Max(maxChar, openingChar);
if (openingChar == 0)
{
ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps));
}
}
else
{
nonAsciiMap ??= new Dictionary<uint, T>();
}
charSet.Add(map.Key);
}
OpeningCharacters = charSet.ToArray();
OpeningCharacters = [.. charSet];
Array.Sort(OpeningCharacters);
asciiMap = new T[maxChar + 1];
_asciiMap = new T[128];
Dictionary<uint, T>? nonAsciiMap = null;
foreach (var state in maps)
{
char openingChar = state.Key;
if (openingChar < 128)
{
asciiMap[openingChar] ??= state.Value;
isOpeningCharacter.Set(openingChar);
_asciiMap[openingChar] ??= state.Value;
}
else if (!nonAsciiMap!.ContainsKey(openingChar))
else
{
nonAsciiMap[openingChar] = state.Value;
nonAsciiMap ??= [];
if (!nonAsciiMap.ContainsKey(openingChar))
{
nonAsciiMap[openingChar] = state.Value;
}
}
}
#if NETCOREAPP3_1_OR_GREATER
if (nonAsciiMap is null)
_values = SearchValues.Create(OpeningCharacters);
if (nonAsciiMap is not null)
{
long bitmap_0_3 = 0;
long bitmap_4_7 = 0;
foreach (char openingChar in OpeningCharacters)
{
int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3);
if (position < 64) bitmap_0_3 |= 1L << position;
else bitmap_4_7 |= 1L << (position - 64);
}
_asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte();
_nonAsciiMap = nonAsciiMap.ToFrozenDictionary();
}
#endif
}
/// <summary>
@@ -110,7 +81,7 @@ public sealed class CharacterMap<T> where T : class
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
T[] asciiMap = this.asciiMap;
T[] asciiMap = _asciiMap;
if (openingChar < (uint)asciiMap.Length)
{
return asciiMap[openingChar];
@@ -118,13 +89,12 @@ public sealed class CharacterMap<T> where T : class
else
{
T? map = null;
nonAsciiMap?.TryGetValue(openingChar, out map);
_nonAsciiMap?.TryGetValue(openingChar, out map);
return map;
}
}
}
/// <summary>
/// Searches for an opening character from a registered parser in the specified string.
/// </summary>
@@ -132,167 +102,20 @@ public sealed class CharacterMap<T> where T : class
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IndexOfOpeningCharacter(string text, int start, int end)
{
Debug.Assert(text is not null);
Debug.Assert(start >= 0 && end >= 0);
Debug.Assert(end - start + 1 >= 0);
Debug.Assert(end - start + 1 <= text.Length);
if (nonAsciiMap is null)
ReadOnlySpan<char> span = text.AsSpan(start, end - start + 1);
int index = span.IndexOfAny(_values);
if (index >= 0)
{
#if NETCOREAPP3_1_OR_GREATER
if (Ssse3.IsSupported && BitConverter.IsLittleEndian)
{
// Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm
// Optimized for sets in the [1, 127] range
int lengthMinusOne = end - start;
int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128<short>.Count - 1);
int finalStart = start + charsToProcessVectorized;
if (start < finalStart)
{
ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start);
Vector128<byte> bitmap = _asciiBitmap;
do
{
// Load 32 bytes (16 chars) into two Vector128<short>s (chars)
// Drop the high byte of each char
// Pack the remaining bytes into a single Vector128<byte>
Vector128<byte> input = Sse2.PackUnsignedSaturate(
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref textStartRef)),
Unsafe.ReadUnaligned<Vector128<short>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref textStartRef, Vector128<short>.Count))));
// Extract the higher nibble of each character ((input >> 4) & 0xF)
Vector128<byte> higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF));
// Lookup the matching higher nibble for each character based on the lower nibble
// PSHUFB will set the result to 0 for any non-ASCII (> 127) character
Vector128<byte> bitsets = Ssse3.Shuffle(bitmap, input);
// Calculate a bitmask (1 << (higherNibble % 8)) for each character
Vector128<byte> bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles);
// Check which characters are present in the set
// We are relying on bitsets being zero for non-ASCII characters
Vector128<byte> result = Sse2.And(bitsets, bitmask);
if (!result.Equals(Vector128<byte>.Zero))
{
int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128<byte>.Zero));
return start + BitOperations.TrailingZeroCount((uint)resultMask);
}
start += 2 * Vector128<short>.Count;
textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128<short>.Count);
}
while (start != finalStart);
}
}
ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference());
for (; start <= end; start++)
{
if (IntPtr.Size == 4)
{
uint c = Unsafe.Add(ref textRef, start);
if (c < 128 && isOpeningCharacter[c])
{
return start;
}
}
else
{
ulong c = Unsafe.Add(ref textRef, start);
if (c < 128 && isOpeningCharacter[c])
{
return start;
}
}
}
#else
unsafe
{
fixed (char* pText = text)
{
for (int i = start; i <= end; i++)
{
char c = pText[i];
if (c < 128 && isOpeningCharacter[c])
{
return i;
}
}
}
}
#endif
return -1;
index += start;
}
else
{
return IndexOfOpeningCharacterNonAscii(text, start, end);
}
}
private int IndexOfOpeningCharacterNonAscii(string text, int start, int end)
{
#if NETCOREAPP3_1_OR_GREATER
ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference());
for (int i = start; i <= end; i++)
{
char c = Unsafe.Add(ref textRef, i);
if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c))
{
return i;
}
}
#else
unsafe
{
fixed (char* pText = text)
{
for (int i = start; i <= end; i++)
{
char c = pText[i];
if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c))
{
return i;
}
}
}
}
#endif
return -1;
return index;
}
}
internal unsafe struct BoolVector128
{
private fixed bool values[128];
public void Set(char c)
{
Debug.Assert(c < 128);
values[c] = true;
}
public readonly bool this[uint c]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
Debug.Assert(c < 128);
return values[c];
}
}
public readonly bool this[ulong c]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
Debug.Assert(c < 128 && IntPtr.Size == 8);
return values[c];
}
}
}

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

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
@@ -12,8 +13,8 @@ namespace Markdig.Helpers;
/// </summary>
public static class HtmlHelper
{
private static readonly char[] SearchBackAndAmp = { '\\', '&' };
private static readonly char[] SearchAmp = { '&' };
private static readonly char[] SearchBackAndAmp = ['\\', '&'];
private static readonly char[] SearchAmp = ['&'];
private static readonly string[] EscapeUrlsForAscii = new string[128];
static HtmlHelper()
@@ -393,40 +394,52 @@ public static class HtmlHelper
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder)
{
// https://spec.commonmark.org/0.31.2/#raw-html
// An HTML comment consists of <!-->, <!--->, or
// <!--, a string of characters not including the string -->, and -->.
// The caller already checked <!-
Debug.Assert(text.CurrentChar == '-' && text.PeekCharExtra(-1) == '!' && text.PeekCharExtra(-2) == '<');
var c = text.NextChar();
if (c != '-')
{
return false;
}
builder.Append('-');
builder.Append('-');
if (text.PeekChar() == '>')
c = text.NextChar();
if (c == '>')
{
// <!--> is considered valid.
builder.Append("-->");
text.SkipChar();
return true;
}
if (c == '-' && text.PeekChar() == '>')
{
// <!---> is also considered valid.
builder.Append("--->");
text.SkipChar();
text.SkipChar();
return true;
}
ReadOnlySpan<char> slice = text.AsSpan();
const string EndOfComment = "-->";
int endOfComment = slice.IndexOf(EndOfComment, StringComparison.Ordinal);
if (endOfComment < 0)
{
return false;
}
var countHyphen = 0;
while (true)
{
c = text.NextChar();
if (c == '\0')
{
return false;
}
if (countHyphen == 2)
{
if (c == '>')
{
builder.Append('>');
text.SkipChar();
return true;
}
return false;
}
countHyphen = c == '-' ? countHyphen + 1 : 0;
builder.Append(c);
}
builder.Append("--");
builder.Append(slice.Slice(0, endOfComment + EndOfComment.Length));
text.Start += endOfComment + EndOfComment.Length;
return true;
}
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder)

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

@@ -2,6 +2,8 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Markdig.Syntax;
@@ -19,6 +21,11 @@ public static class LinkHelper
}
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
{
return Urilize(headingText.AsSpan(), allowOnlyAscii, keepOpeningDigits);
}
public static string Urilize(ReadOnlySpan<char> headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
{
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]);
@@ -95,15 +102,24 @@ public static class LinkHelper
}
public static string UrilizeAsGfm(string headingText)
{
return UrilizeAsGfm(headingText.AsSpan());
}
public static string UrilizeAsGfm(ReadOnlySpan<char> headingText)
{
// Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
for (int i = 0; i < headingText.Length; i++)
{
var c = headingText[i];
if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_')
if (char.IsLetterOrDigit(c) || c == '-' || c == '_')
{
headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c));
headingBuffer.Append(char.ToLowerInvariant(c));
}
else if (c == ' ')
{
headingBuffer.Append('-');
}
}
return headingBuffer.ToString();
@@ -152,7 +168,7 @@ public static class LinkHelper
if (!c.IsAlpha())
{
// We may have an email char?
if (c.IsDigit() || CharHelper.IsEmailUsernameSpecialChar(c))
if (CharHelper.IsEmailUsernameSpecialCharOrDigit(c))
{
state = -1;
}
@@ -272,40 +288,34 @@ public static class LinkHelper
}
else
{
// scan an uri
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:)
// followed by zero or more characters other than ASCII whitespace and control characters, <, and >.
// 6.5 Autolinks - https://spec.commonmark.org/0.31.2/#autolinks
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:) followed by
// zero or more characters other than ASCII control characters, space, <, and >.
// If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space).
//
// 2.1 Characters and lines
// An ASCII control character is a character between U+00001F (both including) or U+007F.
while (true)
text.SkipChar();
ReadOnlySpan<char> slice = text.AsSpan();
Debug.Assert(!slice.Contains('\0'));
// This set of invalid characters includes '>'.
int end = slice.IndexOfAny(CharHelper.InvalidAutoLinkCharacters);
if ((uint)end < (uint)slice.Length && slice[end] == '>')
{
c = text.NextChar();
if (c == '\0')
{
break;
}
if (c == '>')
{
text.SkipChar();
link = builder.ToString();
return true;
}
// Chars valid for both scheme and email
if (c <= 127)
{
if (c > ' ' && c != '>')
{
builder.Append(c);
}
else break;
}
else if (!c.IsSpaceOrPunctuation())
{
builder.Append(c);
}
else break;
// We've found '>' and all characters before it are valid.
#if NET
link = string.Concat(builder.AsSpan(), slice.Slice(0, end));
builder.Dispose();
#else
builder.Append(slice.Slice(0, end));
link = builder.ToString();
#endif
text.Start += end + 1; // +1 to skip '>'
return true;
}
}
@@ -401,7 +411,7 @@ public static class LinkHelper
{
// Skip ')'
text.SkipChar();
title ??= string.Empty;
// not to normalize nulls into empty strings, since LinkInline.Title property is nullable.
}
return isValid;
@@ -531,87 +541,70 @@ public static class LinkHelper
enclosingCharacter = c;
var closingQuote = c == '(' ? ')' : c;
bool hasEscape = false;
// -1: undefined
// 0: has only spaces
// 1: has other characters
int hasOnlyWhiteSpacesSinceLastLine = -1;
while (true)
bool isLineBlank = false; // the first line is never blank
while ((c = text.NextChar()) != '\0')
{
c = text.NextChar();
if (c == '\r' || c == '\n')
{
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
if (isLineBlank)
{
if (hasOnlyWhiteSpacesSinceLastLine == 1)
{
break;
}
hasOnlyWhiteSpacesSinceLastLine = -1;
break;
}
if (hasEscape)
{
hasEscape = false;
buffer.Append('\\');
}
buffer.Append(c);
if (c == '\r' && text.PeekChar() == '\n')
{
buffer.Append('\n');
text.SkipChar();
}
continue;
}
if (c == '\0')
{
break;
isLineBlank = true;
}
if (c == closingQuote)
else if (hasEscape)
{
if (hasEscape)
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append(closingQuote);
hasEscape = false;
continue;
buffer.Append('\\');
}
buffer.Append(c);
}
else if (c == closingQuote)
{
// Skip last quote
text.SkipChar();
goto ReturnValid;
title = buffer.ToString();
return true;
}
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
isLineBlank = false;
}
hasEscape = false;
if (c.IsSpaceOrTab())
else
{
if (hasOnlyWhiteSpacesSinceLastLine < 0)
if (isLineBlank && !c.IsSpaceOrTab())
{
hasOnlyWhiteSpacesSinceLastLine = 1;
isLineBlank = false;
}
}
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
{
hasOnlyWhiteSpacesSinceLastLine = 0;
}
buffer.Append(c);
buffer.Append(c);
}
}
}
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseTitleTrivia<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
@@ -627,87 +620,70 @@ public static class LinkHelper
enclosingCharacter = c;
var closingQuote = c == '(' ? ')' : c;
bool hasEscape = false;
// -1: undefined
// 0: has only spaces
// 1: has other characters
int hasOnlyWhiteSpacesSinceLastLine = -1;
while (true)
bool isLineBlank = false; // the first line is never blank
while ((c = text.NextChar()) != '\0')
{
c = text.NextChar();
if (c == '\r' || c == '\n')
{
if (hasOnlyWhiteSpacesSinceLastLine >= 0)
if (isLineBlank)
{
if (hasOnlyWhiteSpacesSinceLastLine == 1)
{
break;
}
hasOnlyWhiteSpacesSinceLastLine = -1;
break;
}
if (hasEscape)
{
hasEscape = false;
buffer.Append('\\');
}
buffer.Append(c);
if (c == '\r' && text.PeekChar() == '\n')
{
buffer.Append('\n');
text.SkipChar();
}
continue;
}
if (c == '\0')
{
break;
isLineBlank = true;
}
if (c == closingQuote)
else if (hasEscape)
{
if (hasEscape)
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append(closingQuote);
hasEscape = false;
continue;
buffer.Append('\\');
}
buffer.Append(c);
}
else if (c == closingQuote)
{
// Skip last quote
text.SkipChar();
goto ReturnValid;
title = buffer.ToString();
return true;
}
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
isLineBlank = false;
}
hasEscape = false;
if (c.IsSpaceOrTab())
else
{
if (hasOnlyWhiteSpacesSinceLastLine < 0)
if (isLineBlank && !c.IsSpaceOrTab())
{
hasOnlyWhiteSpacesSinceLastLine = 1;
isLineBlank = false;
}
}
else if (c != '\n' && c != '\r' && text.PeekChar() != '\n')
{
hasOnlyWhiteSpacesSinceLastLine = 0;
}
buffer.Append(c);
buffer.Append(c);
}
}
}
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseUrl<T>(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator
@@ -744,12 +720,15 @@ public static class LinkHelper
break;
}
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
@@ -760,8 +739,6 @@ public static class LinkHelper
break;
}
hasEscape = false;
buffer.Append(c);
} while (c != '\0');
@@ -800,20 +777,21 @@ public static class LinkHelper
if (!isAutoLink)
{
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
// If we have an escape
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
hasEscape = false;
}
if (IsEndOfUri(c, isAutoLink))
@@ -891,12 +869,15 @@ public static class LinkHelper
break;
}
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
continue;
@@ -907,8 +888,6 @@ public static class LinkHelper
break;
}
hasEscape = false;
buffer.Append(c);
} while (c != '\0');
@@ -947,20 +926,21 @@ public static class LinkHelper
if (!isAutoLink)
{
if (hasEscape && !c.IsAsciiPunctuation())
if (hasEscape)
{
buffer.Append('\\');
hasEscape = false;
if (!c.IsAsciiPunctuation())
{
buffer.Append('\\');
}
}
// If we have an escape
if (c == '\\')
else if (c == '\\')
{
hasEscape = true;
c = text.NextChar();
continue;
}
hasEscape = false;
}
if (IsEndOfUri(c, isAutoLink))
@@ -1022,7 +1002,7 @@ public static class LinkHelper
return c == '\0' || c.IsSpaceOrTab() || c.IsControl() || (isAutoLink && c == '<'); // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
}
public static bool IsValidDomain(string link, int prefixLength)
public static bool IsValidDomain(string link, int prefixLength, bool allowDomainWithoutPeriod = false)
{
// https://github.github.com/gfm/#extended-www-autolink
// A valid domain consists of alphanumeric characters, underscores (_), hyphens (-) and periods (.).
@@ -1035,22 +1015,22 @@ public static class LinkHelper
bool segmentHasCharacters = false;
int lastUnderscoreSegment = -1;
for (int i = prefixLength; i < link.Length; i++)
for (int i = prefixLength; (uint)i < (uint)link.Length; i++)
{
char c = link[i];
if (c == '.') // New segment
{
if (!segmentHasCharacters)
return false;
segmentCount++;
segmentHasCharacters = false;
continue;
}
if (!c.IsAlphaNumeric())
{
if (c == '.') // New segment
{
if (!segmentHasCharacters)
return false;
segmentCount++;
segmentHasCharacters = false;
continue;
}
if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain name
break;
@@ -1058,7 +1038,7 @@ public static class LinkHelper
{
lastUnderscoreSegment = segmentCount;
}
else if (c != '-' && c.IsSpaceOrPunctuation())
else if (c != '-' && CharHelper.IsSpaceOrPunctuationForGFMAutoLink(c))
{
// An invalid character has been found
return false;
@@ -1068,7 +1048,7 @@ public static class LinkHelper
segmentHasCharacters = true;
}
return segmentCount != 1 && // At least one dot was present
return (segmentCount != 1 || allowDomainWithoutPeriod) && // At least one dot was present
segmentHasCharacters && // Last segment has valid characters
segmentCount - lastUnderscoreSegment >= 2; // No underscores are present in the last two segments of the domain
}
@@ -1145,7 +1125,7 @@ public static class LinkHelper
c = text.NextChar();
}
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
if (c != '\0' && c != '\n' && c != '\r')
{
// If we were able to parse the url but the title doesn't end with space,
// we are still returning a valid definition
@@ -1285,7 +1265,7 @@ public static class LinkHelper
c = text.NextChar();
}
if (c != '\0' && c != '\n' && c != '\r' && text.PeekChar() != '\n')
if (c != '\0' && c != '\n' && c != '\r')
{
// If we were able to parse the url but the title doesn't end with space,
// we are still returning a valid definition
@@ -1585,4 +1565,4 @@ public static class LinkHelper
label = buffer.ToString();
return true;
}
}
}

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()
@@ -334,9 +357,12 @@ public struct StringLineGroup : IEnumerable
goto Return;
MoveToNewLine:
SliceIndex++;
_offset = -1;
_currentSlice = _lines.Lines[SliceIndex];
if (SliceIndex < _lines.Lines.Length - 1)
{
SliceIndex++;
_offset = -1;
_currentSlice = _lines.Lines[SliceIndex];
}
Return:
return CurrentChar;
@@ -420,25 +446,21 @@ public struct StringLineGroup : IEnumerable
}
}
public readonly struct LineOffset
public readonly struct LineOffset(
int linePosition,
int column,
int offset,
int start,
int end)
{
public LineOffset(int linePosition, int column, int offset, int start, int end)
{
LinePosition = linePosition;
Column = column;
Offset = offset;
Start = start;
End = end;
}
public readonly int LinePosition = linePosition;
public readonly int LinePosition;
public readonly int Column = column;
public readonly int Column;
public readonly int Offset = offset;
public readonly int Offset;
public readonly int Start = start;
public readonly int Start;
public readonly int End;
public readonly int End = end;
}
}
}

View File

@@ -231,7 +231,7 @@ public struct StringSlice : ICharIterator
}
/// <summary>
/// Peeks a character at the specified offset from the current begining of the slice
/// Peeks a character at the specified offset from the current beginning of the slice
/// without using the range <see cref="Start"/> or <see cref="End"/>, returns `\0` if outside the <see cref="Text"/>.
/// </summary>
/// <param name="offset">The offset.</param>
@@ -291,7 +291,7 @@ public struct StringSlice : ICharIterator
var c = Text[i];
if (c.IsWhitespace())
{
if (c == '\0' || c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n'))
if (c == '\n' || (c == '\r' && i + 1 <= End && Text[i + 1] != '\n'))
{
return true;
}
@@ -475,7 +475,7 @@ public struct StringSlice : ICharIterator
}
#if NETCOREAPP3_1_OR_GREATER
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length);
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start), length);
#else
return text.AsSpan(start, length);
#endif

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,16 +5,16 @@
<Copyright>Alexandre Mutel</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<Authors>Alexandre Mutel</Authors>
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
<TargetFrameworks>net462;netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<PackageTags>Markdown CommonMark md html md2html</PackageTags>
<PackageReleaseNotes>https://github.com/lunet-io/markdig/blob/master/changelog.md</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/xoofx/markdig/blob/master/changelog.md</PackageReleaseNotes>
<PackageLicenseExpression>BSD-2-Clause</PackageLicenseExpression>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageIcon>markdig.png</PackageIcon>
<PackageProjectUrl>https://github.com/lunet-io/markdig</PackageProjectUrl>
<PackageProjectUrl>https://github.com/xoofx/markdig</PackageProjectUrl>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>10</LangVersion>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -24,22 +24,18 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<PackageReference Include="System.Memory" Version="4.5.4" />
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.6.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>
<ItemGroup>
<None Include="../../img/markdig.png" Pack="true" PackagePath="" />
<None Include="../../readme.md" Pack="true" PackagePath="/"/>
<PackageReference Include="MinVer" Version="3.1.0">
<PackageReference Include="MinVer" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.*" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.*" PrivateAssets="All"/>
</ItemGroup>
<Target Name="PatchVersion" AfterTargets="MinVer">

View File

@@ -2,8 +2,8 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using Markdig.Helpers;
@@ -17,21 +17,15 @@ namespace Markdig;
/// <summary>
/// Provides methods for parsing a Markdown string to a syntax tree and converting it to other formats.
/// </summary>
public static partial class Markdown
public static class Markdown
{
public static string Version
{
get
{
if (_Version == null)
_Version = ((AssemblyFileVersionAttribute)typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault())?.Version ?? "Unknown";
return _Version;
}
}
private static string? _Version;
[field: MaybeNull]
public static string Version => field ??= typeof(Markdown).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build();
private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build();
[field: MaybeNull]
private static MarkdownPipeline DefaultTrackTriviaPipeline => field ??= new MarkdownPipelineBuilder().EnableTrackTrivia().Build();
private static MarkdownPipeline GetPipeline(MarkdownPipeline? pipeline, string markdown)
{
@@ -96,8 +90,8 @@ public static partial class Markdown
/// <param name="markdown">A Markdown text.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <param name="context">A parser context used for the parsing.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown variable is null</exception>
/// <returns>The HTML string.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="markdown"/> is null.</exception>
public static string ToHtml(string markdown, MarkdownPipeline? pipeline = null, MarkdownParserContext? context = null)
{
if (markdown is null) ThrowHelper.ArgumentNullException_markdown();
@@ -114,8 +108,8 @@ public static partial class Markdown
/// </summary>
/// <param name="document">A Markdown document.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
/// <returns>The HTML string.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="document"/> is null.</exception>
public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pipeline = null)
{
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
@@ -137,8 +131,8 @@ public static partial class Markdown
/// <param name="document">A Markdown document.</param>
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="ArgumentNullException">if markdown document variable is null</exception>
/// <returns>The HTML string.</returns>
/// <exception cref="ArgumentNullException">If <paramref name="document"/> is null.</exception>
public static void ToHtml(this MarkdownDocument document, TextWriter writer, MarkdownPipeline? pipeline = null)
{
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
@@ -171,11 +165,7 @@ public static partial class Markdown
var document = MarkdownParser.Parse(markdown, pipeline, context);
using var rentedRenderer = pipeline.RentHtmlRenderer(writer);
HtmlRenderer renderer = rentedRenderer.Instance;
renderer.Render(document);
writer.Flush();
ToHtml(document, writer, pipeline);
return document;
}
@@ -212,7 +202,7 @@ public static partial class Markdown
{
if (markdown is null) ThrowHelper.ArgumentNullException_markdown();
MarkdownPipeline? pipeline = trackTrivia ? _defaultTrackTriviaPipeline : null;
MarkdownPipeline? pipeline = trackTrivia ? DefaultTrackTriviaPipeline : null;
return Parse(markdown, pipeline);
}
@@ -268,7 +258,7 @@ public static partial class Markdown
}
/// <summary>
/// Converts a Markdown string to HTML.
/// Converts a Markdown string to Plain text by using a <see cref="StringWriter"/> .
/// </summary>
/// <param name="markdown">A Markdown text.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>

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()
{
@@ -131,7 +127,7 @@ public sealed class MarkdownPipeline
}
}
internal readonly struct RentedHtmlRenderer : IDisposable
internal readonly ref struct RentedHtmlRenderer : IDisposable
{
private readonly HtmlRendererCache _cache;
public readonly HtmlRenderer Instance;

View File

@@ -16,7 +16,7 @@ namespace Markdig;
/// <remarks>NOTE: A pipeline is not thread-safe.</remarks>
public class MarkdownPipelineBuilder
{
private MarkdownPipeline? pipeline;
private MarkdownPipeline? _pipeline;
/// <summary>
/// Initializes a new instance of the <see cref="MarkdownPipeline" /> class.
@@ -24,8 +24,8 @@ public class MarkdownPipelineBuilder
public MarkdownPipelineBuilder()
{
// Add all default parsers
BlockParsers = new OrderedList<BlockParser>()
{
BlockParsers =
[
new ThematicBreakParser(),
new HeadingBlockParser(),
new QuoteBlockParser(),
@@ -35,10 +35,10 @@ public class MarkdownPipelineBuilder
new FencedCodeBlockParser(),
new IndentedCodeBlockParser(),
new ParagraphBlockParser(),
};
];
InlineParsers = new OrderedList<InlineParser>()
{
InlineParsers =
[
new HtmlEntityParser(),
new LinkInlineParser(),
new EscapeInlineParser(),
@@ -46,7 +46,7 @@ public class MarkdownPipelineBuilder
new CodeInlineParser(),
new AutolinkInlineParser(),
new LineBreakInlineParser(),
};
];
Extensions = new OrderedList<IMarkdownExtension>();
}
@@ -95,9 +95,9 @@ public class MarkdownPipelineBuilder
/// <exception cref="InvalidOperationException">An extension cannot be null</exception>
public MarkdownPipeline Build()
{
if (pipeline != null)
if (_pipeline != null)
{
return pipeline;
return _pipeline;
}
// TODO: Review the whole initialization process for extensions
@@ -115,7 +115,7 @@ public class MarkdownPipelineBuilder
extension.Setup(this);
}
pipeline = new MarkdownPipeline(
_pipeline = new MarkdownPipeline(
new OrderedList<IMarkdownExtension>(Extensions),
new BlockParserList(BlockParsers),
new InlineParserList(InlineParsers),
@@ -125,6 +125,6 @@ public class MarkdownPipelineBuilder
PreciseSourceLocation = PreciseSourceLocation,
TrackTrivia = TrackTrivia,
};
return pipeline;
return _pipeline;
}
}

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.IsWhiteSpaceOrZero() &&
line.TrimEnd())
{
block.UpdateSpanEnd(startBeforeTrim - 1);
var fencedBlock = (IFencedBlock)block;
fencedBlock.ClosingFencedCharCount = closingCount;
fence.ClosingFencedCharCount = closingCount;
if (processor.TrackTrivia)
{
fencedBlock.NewLine = processor.Line.NewLine;
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
fence.NewLine = line.NewLine;
fence.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
fence.TriviaAfter = new StringSlice(line.Text, processor.Start + closingCount, processor.Line.End);
}
// Don't keep the last line

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)
@@ -139,9 +139,7 @@ public class HtmlBlockParser : BlockParser
c = line.NextChar();
}
if (
!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhitespace() ||
c == '\0'))
if (!(c == '>' || (!hasLeadingClose && c == '/' && line.PeekChar() == '>') || c.IsWhiteSpaceOrZero()))
{
return BlockState.None;
}
@@ -297,7 +295,7 @@ public class HtmlBlockParser : BlockParser
return BlockState.Continue;
}
private static readonly CompactPrefixTree<int> HtmlTags = new(66, 94, 83)
private static readonly CompactPrefixTree<int> HtmlTags = new(67, 96, 86)
{
{ "address", 0 },
{ "article", 1 },
@@ -364,6 +362,7 @@ public class HtmlBlockParser : BlockParser
{ "title", 62 },
{ "tr", 63 },
{ "track", 64 },
{ "ul", 65 }
{ "ul", 65 },
{ "search", 66 },
};
}

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

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