Compare commits

..

175 Commits

Author SHA1 Message Date
Martijn Laarman
d47fbc757f Optimize PipeTable parsing: O(n²) → O(n) for 3.7x–85x speedup, enables 10K+ row tables (#922)
* Optimize PipeTable parsing: O(n²) → O(n) for large tables

Pipe tables were creating deeply nested tree structures where each pipe
delimiter contained all subsequent content as children, causing O(n²)
traversal complexity for n cells. This change restructures the parser to
use a flat sibling-based structure, treating tables as matrices rather
than nested trees.

Key changes:
- Set IsClosed=true on PipeTableDelimiterInline to prevent nesting
- Add PromoteNestedPipesToRootLevel() to flatten pipes nested in emphasis
- Update cell boundary detection to use sibling traversal
- Move EmphasisInlineParser before PipeTableParser in processing order
- Fix EmphasisInlineParser to continue past IsClosed delimiters
- Add ContainsParentOrSiblingOfType<T>() helper for flat structure detection

Performance improvements (measured on typical markdown content):

| Rows | Before    | After   | Speedup |
|------|-----------|---------|---------|
| 100  | 542 μs    | 150 μs  | 3.6x    |
| 500  | 23,018 μs | 763 μs  | 30x     |
| 1000 | 89,418 μs | 1,596 μs| 56x     |
| 1500 | 201,593 μs| 2,740 μs| 74x     |
| 5000 | CRASH     | 10,588 μs| ∞      |
| 10000| CRASH     | 18,551 μs| ∞      |

Tables with 5000+ rows previously crashed due to stack overflow from
recursive depth. They now parse successfully with linear time complexity.

* remove baseline results file

* Do not use System.Index and fix nullabillity checks for older platforms
2026-01-30 22:05:18 +01:00
prozolic
3602433b84 Replace null checks with IsEmpty property for ReadOnlySpan<char> (#916)
This change suppresses CA2265 warnings.
2026-01-30 22:01:50 +01:00
prozolic
1bac4afc9b Use Dictionary.TryAdd instead of ContainsKey and indexer by reducing lookups. (#917)
* Use Dictionary.TryAdd instead of ContainsKey and indexer by reducing lookups.

* Update src/Markdig/Parsers/ParserList.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 22:01:27 +01:00
Tatsunori Uchino
a89056d961 Recognize supplementary characters (#913)
* Recognize supplementary characters

* Internatize Rune

* Fix failing tests

* Fix extra comment error

* Remove extra local variable c

* Reorganize classes around Rune

* Prepare both Rune and char variants / make Rune variant public for .NET

* Make APIs in StringSlice.cs public only in modern .NET

* Throw exception if cannot obtain first Rune

* Add comments

* Add comment on PeekRuneExtra

* Use `Rune.TryCreate`

* Remove backtrack

* Fix parameter name in XML comment

* Don't throw when error in `Rune.DecodeFromUtf16`

* Fix RuneAt

* Add tests of Rune-related methods of `StringSlice`

* Make comment more tolerant of changes

* Tweak comment

* Fix comment

* Add `readonly`

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

* Move namespace of polyfilled Rune out of System.Text

* Apply suggestions from code review

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

* Fix regression by review suggestion

* Prepare constant for .NET Standard test

* Don't call `IsPunctuationException` if unnecessary

* PR feedback

---------

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2026-01-12 11:08:03 +01:00
Miha Zupan
cd7b9ca0ef Test netstandard (#915)
* Add GH Action to test netstandard 2.0 and 2.1

* Account for TFM changes in tests project
2025-11-17 18:46:26 +01:00
Alexandre Mutel
fb698598e4 Use central package management 2025-11-17 08:19:42 +01:00
mos379
12590e5fbe feat(link-helper): improve ASCII normalization handling (#911)
* feat(link-helper): improve ASCII normalization handling

Enhanced the `Urilize` method to better handle ASCII normalization and special characters. Added support for decomposing characters when `allowOnlyAscii` is true and skipping diacritical marks. Introduced handling for special German, Scandinavian, and Icelandic characters via new helper methods: `IsSpecialScandinavianOrGermanChar` and `NormalizeScandinavianOrGermanChar`.

Reorganized `using` directives for better clarity. Updated the processing loop in `Urilize` to handle normalized spans and ASCII equivalents more effectively. These changes improve link generation compatibility across various languages.

* Add tests for Scandinavian and German character normalization

Added tests for NormalizeScandinavianOrGermanChar method to validate character normalization for various special characters in both ASCII and non-ASCII contexts.

* test(link-helper): update ASCII transliteration tests

Updated test cases in `TestUrilizeOnlyAscii_Simple` to reflect
changes in `LinkHelper.Urilize` behavior. Non-ASCII characters
like `æ` and `ø` are now transliterated to their ASCII
equivalents (`ae` and `oe`) instead of being removed.
2025-11-10 22:01:35 +01:00
Miha Zupan
8c01cf0549 Add another test for pipe tables (#907) 2025-10-21 08:37:43 +02:00
Miha Zupan
bcbd8e47ac Lazily allocate ProcessInlinesBegin/End delegates on Blocks (#906) 2025-10-21 08:37:02 +02:00
Miha Zupan
d6e88f16f7 Fix pipe table parsing with a leading paragraph (#905)
* Fix pipe table parsing with a leading paragraph

* Use the alternative approach
2025-10-20 21:43:25 +02:00
Miha Zupan
03bdf60086 Add a basic fuzzing project (#903)
* Add basic fuzzing project

* Mark the project as non-packable
2025-10-17 08:09:28 +02:00
Miha Zupan
5c78932f55 Fix edge cases in EmphasisInlineParser (#902) 2025-10-17 08:07:15 +02:00
Miha Zupan
191e33ab32 Fix build warnings (#899) 2025-10-16 17:25:47 +02:00
Miha Zupan
800235ba7a Fix IndexOutOfRangeException in CodeInlineParser (#900) 2025-10-16 17:25:30 +02:00
Miha Zupan
d5f8a809a0 Move sln to slnx (#901) 2025-10-16 17:24:33 +02:00
Asttear
781d9b5365 Remove leading newline in block attributes (#896)
* Remove leading newline in block attributes

fix #895

* Add handling logic for `\r\n`
2025-10-05 11:21:12 +02:00
Phillip Haydon
543570224e Fix issue where an inline code block that spans multiple lines doesn't parse correctly (#893)
* fixes issue where an inline code block that spans multiple lines doesn't get treated as code

* Update src/Markdig.Tests/TestPipeTable.cs

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

* Apply suggestion from @MihaZupan

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

* Update src/Markdig.Tests/TestPipeTable.cs

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

* fix broken test

* removed unreachable code and added more tests

* Update src/Markdig.Tests/TestPipeTable.cs

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

* Update src/Markdig.Tests/TestPipeTable.cs

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

* removed uncessary inline code check

* Update src/Markdig/Parsers/Inlines/CodeInlineParser.cs

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

---------

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
Co-authored-by: Alexandre Mutel <alexandre_mutel@live.com>
2025-10-03 09:34:24 +02:00
Daniel Klecha
4dc0be88b4 add options for link inline (#894)
* add options for link inline

* create LinkOptions and associate it with all four parsers

* set EnableHtmlParsing to true by default
2025-10-03 09:22:51 +02:00
Phillip Haydon
0e9e80e1cd Fix for table depth error when cell contains backticks (#891)
* failing test

* fixed bug with table containing back tick which causes depth error
2025-09-21 16:26:02 +02:00
Alexandre Mutel
1b04599c44 Merge pull request #888 from prozolic/pullreq
Fixes issue #845
2025-09-11 07:55:51 +02:00
prozolic
5e6fb2d1c5 Add test for issue #845 list item blank line 2025-09-08 22:36:09 +09:00
prozolic
14406bc60d Fixes issue #845 2025-09-06 21:10:51 +09:00
Alexandre Mutel
2aa6780a30 Merge pull request #883 from messani/master
Add source position tracking for grid tables
2025-08-28 09:04:44 +02:00
Alexandre Mutel
c43646586c Merge pull request #885 from dannyp32/supportTableWithoutExtraLine
Add support for a table without an extra new line before it
2025-08-28 09:02:29 +02:00
Daniel Pino
d548b82bcd Add support for a table without an extra new line before it 2025-08-09 08:50:49 +00:00
Tibor Peluch
aab5543cb5 Code cleanup 2025-07-14 20:17:50 +02:00
Tibor Peluch
2e1d741aaf Cleaned up code, added tests for source position 2025-07-14 10:23:15 +02:00
Tibor Peluch
80c50e31e2 Attempt to fix tracking of tree node positions (line, column) inside GridTable 2025-07-11 13:25:03 +02:00
Alexandre Mutel
7ff8db9016 Merge pull request #877 from Mertsch/Mertsch-patch-1
Update readme.md
2025-06-19 08:41:54 +02:00
Alexandre Mutel
c69fb9ae73 Merge pull request #879 from stylefish/issue878
Fixes #878: RoundtripRenderer: render indent and 0 blocks for ordered lists
2025-06-19 08:41:10 +02:00
stylefish
5a3c206076 Fixes #878: render indent and 0 blocks 2025-06-16 11:26:23 +02:00
Mertsch
b92890094c Update readme.md 2025-06-12 14:26:00 +02:00
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
177 changed files with 7669 additions and 2575 deletions

View File

@@ -12,8 +12,9 @@ insert_final_newline = false
trim_trailing_whitespace = true
# Solution Files
[*.sln]
indent_style = tab
[*.slnx]
indent_size = 2
insert_final_newline = true
# XML Project Files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
@@ -35,3 +36,8 @@ insert_final_newline = true
# Bash Files
[*.sh]
end_of_line = lf
# C# files
[*.cs]
# License header
file_header_template = Copyright (c) Alexandre Mutel. All rights reserved.\nThis file is licensed under the BSD-Clause 2 license.\nSee the license.txt file in the project root for more information.

2
.gitattributes vendored
View File

@@ -1,3 +1,3 @@
* text=auto
*.cs text=auto diff=csharp
*.sln text=auto eol=crlf
*.slnx text=auto eol=crlf

View File

@@ -11,25 +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, 7.0, and 8.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
6.0.x
7.0.x
8.0.x
- name: Build, Test, Pack, Publish
shell: bash
run: |
dotnet tool install -g dotnet-releaser
dotnet-releaser run --nuget-token "${{secrets.NUGET_TOKEN}}" --github-token "${{secrets.GITHUB_TOKEN}}" src/dotnet-releaser.toml
uses: xoofx/.github/.github/workflows/dotnet.yml@main
with:
dotnet-version: |
6.0
8.0
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>

44
.github/workflows/test-netstandard.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Test netstandard
on: pull_request
jobs:
test-netstandard:
runs-on: ubuntu-latest
strategy:
matrix:
netstandard-version: ['netstandard2.0', 'netstandard2.1']
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
9.0.x
- name: Patch build to test ${{ matrix.netstandard-version }}
run: |
cd src
sed -i 's/<TargetFrameworks>.*<\/TargetFrameworks>/<TargetFrameworks>${{ matrix.netstandard-version }}<\/TargetFrameworks>/' Markdig/Markdig.targets
sed -i 's/<TargetFrameworks>.*<\/TargetFrameworks>/<TargetFrameworks>net8.0;net9.0<\/TargetFrameworks>/' Markdig.Tests/Markdig.Tests.csproj
echo "Markdig.targets TFMs:"
grep "TargetFrameworks" Markdig/Markdig.targets
echo "Markdig.Tests.csproj TFMs:"
grep "TargetFrameworks" Markdig.Tests/Markdig.Tests.csproj
- name: Restore dependencies
run: dotnet restore src/Markdig.Tests/Markdig.Tests.csproj
- name: Test Debug
run: |
dotnet build src/Markdig.Tests/Markdig.Tests.csproj -c Debug --no-restore
dotnet test src/Markdig.Tests/Markdig.Tests.csproj -c Debug --no-build
- name: Test Release
run: |
dotnet build src/Markdig.Tests/Markdig.Tests.csproj -c Release --no-restore
dotnet test src/Markdig.Tests/Markdig.Tests.csproj -c Release --no-build

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@
*.sln.docstates
*.nuget.props
*.nuget.targets
src/.idea
BenchmarkDotNet.Artifacts
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

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,8 +1,8 @@
# 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">
Markdig is a fast, powerful, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
Markdig is a fast, powerful, [CommonMark](https://commonmark.org/) compliant, extensible Markdown processor for .NET.
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
@@ -14,7 +14,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- 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)](https://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
- including **GFM fenced code blocks**.
- **Extensible** architecture
@@ -22,9 +22,9 @@ You can **try Markdig online** and compare it to other implementations on [babel
- [**Roundtrip support**](./src/Markdig/Roundtrip.md): Parses trivia (whitespace, newlines and other characters) to support lossless parse ⭢ render roundtrip. This enables changing markdown documents without introducing undesired trivia changes.
- Built-in with **20+ extensions**, including:
- 2 kind of tables:
- [**Pipe tables**](src/Markdig.Tests/Specs/PipeTableSpecs.md) (inspired from GitHub tables and [PanDoc - Pipe Tables](http://pandoc.org/README.html#extension-pipe_tables))
- [**Grid tables**](src/Markdig.Tests/Specs/GridTableSpecs.md) (inspired from [Pandoc - Grid Tables](http://pandoc.org/README.html#extension-grid_tables))
- [**Extra emphasis**](src/Markdig.Tests/Specs/EmphasisExtraSpecs.md) (inspired from [Pandoc - Emphasis](http://pandoc.org/README.html#strikeout) and [Markdown-it](https://markdown-it.github.io/))
- [**Pipe tables**](src/Markdig.Tests/Specs/PipeTableSpecs.md) (inspired from GitHub tables and [PanDoc - Pipe Tables](https://pandoc.org/MANUAL.html#extension-pipe_tables))
- [**Grid tables**](src/Markdig.Tests/Specs/GridTableSpecs.md) (inspired from [Pandoc - Grid Tables](https://pandoc.org/MANUAL.html#extension-grid_tables))
- [**Extra emphasis**](src/Markdig.Tests/Specs/EmphasisExtraSpecs.md) (inspired from [Pandoc - Emphasis](https://pandoc.org/MANUAL.html#strikeout) and [Markdown-it](https://markdown-it.github.io/))
- strike through `~~`,
- Subscript `~`
- Superscript `^`
@@ -33,7 +33,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- [**Special attributes**](src/Markdig.Tests/Specs/GenericAttributesSpecs.md) or attached HTML attributes (inspired from [PHP Markdown Extra - Special Attributes](https://michelf.ca/projects/php-markdown/extra/#spe-attr))
- [**Definition lists**](src/Markdig.Tests/Specs/DefinitionListSpecs.md) (inspired from [PHP Markdown Extra - Definitions Lists](https://michelf.ca/projects/php-markdown/extra/#def-list))
- [**Footnotes**](src/Markdig.Tests/Specs/FootnotesSpecs.md) (inspired from [PHP Markdown Extra - Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes))
- [**Auto-identifiers**](src/Markdig.Tests/Specs/AutoIdentifierSpecs.md) for headings (similar to [Pandoc - Auto Identifiers](http://pandoc.org/README.html#extension-auto_identifiers))
- [**Auto-identifiers**](src/Markdig.Tests/Specs/AutoIdentifierSpecs.md) for headings (similar to [Pandoc - Auto Identifiers](https://pandoc.org/MANUAL.html#extension-auto_identifiers))
- [**Auto-links**](src/Markdig.Tests/Specs/AutoLinks.md) generates links if a text starts with `http://` or `https://` or `ftp://` or `mailto:` or `www.xxx.yyy`
- [**Task Lists**](src/Markdig.Tests/Specs/TaskListSpecs.md) inspired from [Github Task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments).
- [**Extra bullet lists**](src/Markdig.Tests/Specs/ListExtraSpecs.md), supporting alpha bullet `a.` `b.` and roman bullet (`i`, `ii`...etc.)
@@ -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"](https://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
## Download
@@ -144,16 +144,16 @@ 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
Thanks to the fantastic work done by [John Mac Farlane](http://johnmacfarlane.net/) for the CommonMark specs and all the people involved in making Markdown a better standard!
Thanks to the fantastic work done by [John Mac Farlane](https://johnmacfarlane.net/) for the CommonMark specs and all the people involved in making Markdown a better standard!
This project would not have been possible without this huge foundation.
@@ -161,7 +161,7 @@ Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/Bench
Some decoding part (e.g HTML [EntityHelper.cs](https://github.com/lunet-io/markdig/blob/master/src/Markdig/Helpers/EntityHelper.cs)) have been re-used from [CommonMark.NET](https://github.com/Knagis/CommonMark.NET)
Thanks to the work done by @clarkd on the JIRA Link extension (https://github.com/clarkd/MarkdigJiraLinker), now included with this project!
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](https://xoofx.github.io/)

View File

@@ -0,0 +1,23 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" />
<PackageVersion Include="CommonMark.NET" Version="0.15.1" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MarkdownSharp" Version="2.0.5" />
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0" />
<PackageVersion Include="Microsoft.Diagnostics.Runtime" Version="3.1.512801" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MinVer" Version="6.0.0" />
<PackageVersion Include="NUnit" Version="4.4.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">
<PackageVersion Include="System.Memory" Version="4.6.3" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
@@ -19,12 +19,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.11" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.11" />
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.456101" />
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" />
<PackageReference Include="CommonMark.NET" />
<PackageReference Include="Markdown" />
<PackageReference Include="MarkdownSharp" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />

View File

@@ -0,0 +1,81 @@
// 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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Markdig;
namespace Testamina.Markdig.Benchmarks.PipeTable;
/// <summary>
/// Benchmark for pipe table parsing performance, especially for large tables.
/// Tests the performance of PipeTableParser with varying table sizes.
/// </summary>
[MemoryDiagnoser]
[GcServer(true)] // Use server GC to get more comprehensive GC stats
public class PipeTableBenchmark
{
private string _100Rows = null!;
private string _500Rows = null!;
private string _1000Rows = null!;
private string _1500Rows = null!;
private string _5000Rows = null!;
private string _10000Rows = null!;
private MarkdownPipeline _pipeline = null!;
[GlobalSetup]
public void Setup()
{
// Pipeline with pipe tables enabled (part of advanced extensions)
_pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
// Generate tables of various sizes
// Note: Before optimization, 5000+ rows hit depth limit due to nested tree structure.
// After optimization, these should work.
_100Rows = PipeTableGenerator.Generate(rows: 100, columns: 5);
_500Rows = PipeTableGenerator.Generate(rows: 500, columns: 5);
_1000Rows = PipeTableGenerator.Generate(rows: 1000, columns: 5);
_1500Rows = PipeTableGenerator.Generate(rows: 1500, columns: 5);
_5000Rows = PipeTableGenerator.Generate(rows: 5000, columns: 5);
_10000Rows = PipeTableGenerator.Generate(rows: 10000, columns: 5);
}
[Benchmark(Description = "PipeTable 100 rows x 5 cols")]
public string Parse100Rows()
{
return Markdown.ToHtml(_100Rows, _pipeline);
}
[Benchmark(Description = "PipeTable 500 rows x 5 cols")]
public string Parse500Rows()
{
return Markdown.ToHtml(_500Rows, _pipeline);
}
[Benchmark(Description = "PipeTable 1000 rows x 5 cols")]
public string Parse1000Rows()
{
return Markdown.ToHtml(_1000Rows, _pipeline);
}
[Benchmark(Description = "PipeTable 1500 rows x 5 cols")]
public string Parse1500Rows()
{
return Markdown.ToHtml(_1500Rows, _pipeline);
}
[Benchmark(Description = "PipeTable 5000 rows x 5 cols")]
public string Parse5000Rows()
{
return Markdown.ToHtml(_5000Rows, _pipeline);
}
[Benchmark(Description = "PipeTable 10000 rows x 5 cols")]
public string Parse10000Rows()
{
return Markdown.ToHtml(_10000Rows, _pipeline);
}
}

View File

@@ -0,0 +1,61 @@
// 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.Text;
namespace Testamina.Markdig.Benchmarks.PipeTable;
/// <summary>
/// Generates pipe table markdown content for benchmarking purposes.
/// </summary>
public static class PipeTableGenerator
{
private const int DefaultCellWidth = 10;
/// <summary>
/// Generates a pipe table in markdown format.
/// </summary>
/// <param name="rows">Number of data rows (excluding header)</param>
/// <param name="columns">Number of columns</param>
/// <param name="cellWidth">Width of each cell content (default: 10)</param>
/// <returns>Pipe table markdown string</returns>
public static string Generate(int rows, int columns, int cellWidth = DefaultCellWidth)
{
var sb = new StringBuilder();
// Header row
sb.Append('|');
for (int col = 0; col < columns; col++)
{
sb.Append(' ');
sb.Append($"Header {col + 1}".PadRight(cellWidth));
sb.Append(" |");
}
sb.AppendLine();
// Separator row (with dashes)
sb.Append('|');
for (int col = 0; col < columns; col++)
{
sb.Append(new string('-', cellWidth + 2));
sb.Append('|');
}
sb.AppendLine();
// Data rows
for (int row = 0; row < rows; row++)
{
sb.Append('|');
for (int col = 0; col < columns; col++)
{
sb.Append(' ');
sb.Append($"R{row + 1}C{col + 1}".PadRight(cellWidth));
sb.Append(" |");
}
sb.AppendLine();
}
return sb.ToString();
}
}

View File

@@ -7,6 +7,7 @@ using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Markdig;
using Testamina.Markdig.Benchmarks.PipeTable;
namespace Testamina.Markdig.Benchmarks;
@@ -68,7 +69,16 @@ public class Program
//config.Add(gcDiagnoser);
//var config = DefaultConfig.Instance;
BenchmarkRunner.Run<Program>(config);
// Run specific benchmarks based on command line arguments
if (args.Length > 0 && args[0] == "--pipetable")
{
BenchmarkRunner.Run<PipeTableBenchmark>(config);
}
else
{
BenchmarkRunner.Run<Program>(config);
}
//BenchmarkRunner.Run<TestDictionary>(config);
//BenchmarkRunner.Run<TestMatchPerf>();
//BenchmarkRunner.Run<TestStringPerf>();

4
src/Markdig.Fuzzing/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
corpus
libfuzzer-dotnet-windows.exe
crash-*
timeout-*

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpFuzz" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,71 @@
using Markdig;
using Markdig.Renderers.Roundtrip;
using Markdig.Syntax;
using SharpFuzz;
using System.Diagnostics;
using System.Text;
ReadOnlySpanAction fuzzTarget = ParseRenderFuzzer.FuzzTarget;
if (args.Length > 0)
{
// Run the target on existing inputs
string[] files = Directory.Exists(args[0])
? Directory.GetFiles(args[0])
: [args[0]];
Debugger.Launch();
foreach (string inputFile in files)
{
fuzzTarget(File.ReadAllBytes(inputFile));
}
}
else
{
Fuzzer.LibFuzzer.Run(fuzzTarget);
}
sealed class ParseRenderFuzzer
{
private static readonly MarkdownPipeline s_advancedPipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
private static readonly ResettableRoundtripRenderer _roundtripRenderer = new();
public static void FuzzTarget(ReadOnlySpan<byte> bytes)
{
string text = Encoding.UTF8.GetString(bytes);
try
{
MarkdownDocument document = Markdown.Parse(text);
_ = document.ToHtml();
document = Markdown.Parse(text, s_advancedPipeline);
_ = document.ToHtml(s_advancedPipeline);
document = Markdown.Parse(text, trackTrivia: true);
_ = document.ToHtml();
_roundtripRenderer.Reset();
_roundtripRenderer.Render(document);
_ = Markdown.Normalize(text);
_ = Markdown.ToPlainText(text);
}
catch (Exception ex) when (IsIgnorableException(ex)) { }
}
private static bool IsIgnorableException(Exception exception)
{
return exception.Message.Contains("Markdown elements in the input are too deeply nested", StringComparison.Ordinal);
}
private sealed class ResettableRoundtripRenderer : RoundtripRenderer
{
public ResettableRoundtripRenderer() : base(new StringWriter(new StringBuilder(1024 * 1024))) { }
public new void Reset() => base.Reset();
}
}

View File

@@ -0,0 +1,86 @@
param (
[string]$configuration = $null
)
Set-StrictMode -Version Latest
$libFuzzer = "libfuzzer-dotnet-windows.exe"
$outputDir = "bin"
function Get-LibFuzzer {
param (
[string]$Path
)
$libFuzzerUrl = "https://github.com/Metalnem/libfuzzer-dotnet/releases/download/v2025.05.02.0904/libfuzzer-dotnet-windows.exe"
$expectedHash = "17af5b3f6ff4d2c57b44b9a35c13051b570eb66f0557d00015df3832709050bf"
Write-Output "Downloading libFuzzer from $libFuzzerUrl..."
try {
$tempFile = "$Path.tmp"
Invoke-WebRequest -Uri $libFuzzerUrl -OutFile $tempFile -UseBasicParsing
$downloadedHash = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash
if ($downloadedHash -eq $ExpectedHash) {
Move-Item -Path $tempFile -Destination $Path -Force
Write-Output "libFuzzer downloaded successfully to $Path"
}
else {
Write-Error "Hash validation failed."
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
exit 1
}
}
catch {
Write-Error "Failed to download libFuzzer: $($_.Exception.Message)"
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
exit 1
}
}
# Check if libFuzzer exists, download if not
if (-not (Test-Path $libFuzzer)) {
Get-LibFuzzer -Path $libFuzzer
}
$toolListOutput = dotnet tool list --global sharpFuzz.CommandLine 2>$null
if (-not ($toolListOutput -match "sharpfuzz")) {
Write-Output "Installing sharpfuzz CLI"
dotnet tool install --global sharpFuzz.CommandLine
}
if (Test-Path $outputDir) {
Remove-Item -Recurse -Force $outputDir
}
if ($configuration -eq $null) {
$configuration = "Debug"
}
dotnet publish -c $configuration -o $outputDir
$project = Join-Path $outputDir "Markdig.Fuzzing.dll"
$fuzzingTarget = Join-Path $outputDir "Markdig.dll"
Write-Output "Instrumenting $fuzzingTarget"
& sharpfuzz $fuzzingTarget
if ($LastExitCode -ne 0) {
Write-Error "An error occurred while instrumenting $fuzzingTarget"
exit 1
}
New-Item -ItemType Directory -Force -Path corpus | Out-Null
$libFuzzerArgs = @("--target_path=dotnet", "--target_arg=$project", "-timeout=10", "corpus")
# Add any additional arguments passed to the script
if ($args) {
$libFuzzerArgs += $args
}
Write-Output "Starting libFuzzer with arguments: $libFuzzerArgs"
& ./$libFuzzer @libFuzzerArgs

View File

@@ -1,19 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>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>
<NoWarn>$(NoWarn);NETSDK1138</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="4.0.1" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
</ItemGroup>
<ItemGroup>
@@ -34,10 +36,10 @@
<InputSpecFiles Remove="Specs\readme.md" />
<!-- Allow Visual Studio up-to-date check to verify that nothing has changed - https://github.com/dotnet/project-system/blob/main/docs/up-to-date-check.md -->
<UpToDateCheckInput Include="@(InputSpecFiles)" />
<OutputSpecFiles Include="@(InputSpecFiles->'%(RelativeDir)%(Filename).generated.cs')" />
<OutputSpecFiles Include="@(InputSpecFiles-&gt;'%(RelativeDir)%(Filename).generated.cs')" />
</ItemGroup>
<Target Name="GeneratedSpecsFile" BeforeTargets="BeforeCompile;CoreCompile" Inputs="@(ItemSpecExecutable);@(InputSpecFiles)" Outputs="@(ItemSpecExecutable->'%(RelativeDir)%(Filename).timestamp');@(InputSpecFiles->'%(RelativeDir)%(Filename).generated.cs')">
<Target Name="GeneratedSpecsFile" BeforeTargets="BeforeCompile;CoreCompile" Inputs="@(ItemSpecExecutable);@(InputSpecFiles)" Outputs="@(ItemSpecExecutable-&gt;'%(RelativeDir)%(Filename).timestamp');@(InputSpecFiles-&gt;'%(RelativeDir)%(Filename).generated.cs')">
<Message Importance="high" Text="Regenerating Specs Files" />
<Exec Command="dotnet $(SpecExecutable)" />
<WriteLinesToFile File="$(SpecTimestamp)" Lines="$([System.DateTime]::Now)" />

View File

@@ -317,4 +317,96 @@ $$
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());
}
[Test]
public void TestIssue845ListItemBlankLine()
{
TestParser.TestSpec("-\n\n foo",@"
<ul>
<li></li>
</ul>
<p>foo</p>");
TestParser.TestSpec("-\n-\n\n foo",@"
<ul>
<li></li>
<li></li>
</ul>
<p>foo</p>");
TestParser.TestSpec("-\n\n-\n\n foo",@"
<ul>
<li></li>
<li></li>
</ul>
<p>foo</p>");
}
}

View File

@@ -25,6 +25,7 @@ public class TestUnorderedList
[TestCase("-\ti1")]
[TestCase("-\ti1\n-\ti2")]
[TestCase("-\ti1\n- i2\n-\ti3")]
[TestCase("- 1.\n- 2.")]
public void Test(string value)
{
RoundTrip(value);

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

@@ -98,5 +98,22 @@ namespace Markdig.Tests.Specs.GenericAttributes
TestParser.TestSpec("[Foo](url){data-x=1}\n\n[Foo](url){data-x='1'}\n\n[Foo](url){data-x=11}", "<p><a href=\"url\" data-x=\"1\">Foo</a></p>\n<p><a href=\"url\" data-x=\"1\">Foo</a></p>\n<p><a href=\"url\" data-x=\"11\">Foo</a></p>", "attributes|advanced", context: "Example 3\nSection Extensions / Generic Attributes\n");
}
// Attributes that occur immediately before a block element, on a line by themselves, affect that element
[Test]
public void ExtensionsGenericAttributes_Example004()
{
// Example 4
// Section: Extensions / Generic Attributes
//
// The following Markdown:
// {.center}
// A paragraph
//
// Should be rendered as:
// <p class="center">A paragraph</p>
TestParser.TestSpec("{.center}\nA paragraph", "<p class=\"center\">A paragraph</p>", "attributes|advanced", context: "Example 4\nSection Extensions / Generic Attributes\n");
}
}
}

View File

@@ -61,3 +61,12 @@ Attribute values can be one character long
<p><a href="url" data-x="1">Foo</a></p>
<p><a href="url" data-x="11">Foo</a></p>
````````````````````````````````
Attributes that occur immediately before a block element, on a line by themselves, affect that element
```````````````````````````````` example
{.center}
A paragraph
.
<p class="center">A paragraph</p>
````````````````````````````````

View File

@@ -386,5 +386,34 @@ namespace Markdig.Tests.Specs.GridTables
TestParser.TestSpec("+", "<ul>\n<li></li>\n</ul>", "gridtables|advanced", context: "Example 11\nSection Extensions / Grid Table\n");
}
// A table may begin right after a paragraph without an empty line in between:
[Test]
public void ExtensionsGridTable_Example012()
{
// Example 12
// Section: Extensions / Grid Table
//
// The following Markdown:
// Some
// **text**.
// +---+
// | A |
// +---+
//
// Should be rendered as:
// <p>Some
// <strong>text</strong>.</p>
// <table>
// <col style="width:100%" />
// <tbody>
// <tr>
// <td>A</td>
// </tr>
// </tbody>
// </table>
TestParser.TestSpec("Some\n**text**.\n+---+\n| A |\n+---+", "<p>Some\n<strong>text</strong>.</p>\n<table>\n<col style=\"width:100%\" />\n<tbody>\n<tr>\n<td>A</td>\n</tr>\n</tbody>\n</table>", "gridtables|advanced", context: "Example 12\nSection Extensions / Grid Table\n");
}
}
}

View File

@@ -285,3 +285,24 @@ An empty `+` on a line should result in a simple empty list output:
<li></li>
</ul>
````````````````````````````````
A table may begin right after a paragraph without an empty line in between:
```````````````````````````````` example
Some
**text**.
+---+
| A |
+---+
.
<p>Some
<strong>text</strong>.</p>
<table>
<col style="width:100%" />
<tbody>
<tr>
<td>A</td>
</tr>
</tbody>
</table>
````````````````````````````````

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

@@ -825,5 +825,190 @@ namespace Markdig.Tests.Specs.PipeTables
TestParser.TestSpec("a | b\n-- | - \n0 | 1 | 2", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n<th></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n<td>2</td>\n</tr>\n</tbody>\n</table>", "pipetables|advanced", context: "Example 25\nSection Extensions / Pipe Table\n");
}
// A table may begin right after a paragraph without an empty line in between:
[Test]
public void ExtensionsPipeTable_Example026()
{
// Example 26
// Section: Extensions / Pipe Table
//
// The following Markdown:
// Some
// **text**.
// | A |
// |---|
// | B |
//
// Should be rendered as:
// <p>Some
// <strong>text</strong>.</p>
// <table>
// <thead>
// <tr>
// <th>A</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>B</td>
// </tr>
// </tbody>
// </table>
TestParser.TestSpec("Some\n**text**.\n| A |\n|---|\n| B |", "<p>Some\n<strong>text</strong>.</p>\n<table>\n<thead>\n<tr>\n<th>A</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>B</td>\n</tr>\n</tbody>\n</table>", "pipetables|advanced", context: "Example 26\nSection Extensions / Pipe Table\n");
}
// Tables can be nested inside other blocks, like lists:
[Test]
public void ExtensionsPipeTable_Example027()
{
// Example 27
// Section: Extensions / Pipe Table
//
// The following Markdown:
// Bullet list
// * Table 1
//
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
// * Table 2
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
// * Table 3
// Lorem ipsum ...
// Lorem ipsum ...
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
//
// Ordered list
// 1. Table 1
//
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
// 2. Table 2
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
// 3. Table 3
// Lorem ipsum ...
// Lorem ipsum ...
// | Header 1 | Header 2 |
// |----------------|----------------|
// | Row 1 Column 1 | Row 1 Column 2 |
//
// Should be rendered as:
// <p>Bullet list</p>
// <ul>
// <li><p>Table 1</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// <li><p>Table 2</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// <li><p>Table 3
// Lorem ipsum ...
// Lorem ipsum ...</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// </ul>
// <p>Ordered list</p>
// <ol>
// <li><p>Table 1</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// <li><p>Table 2</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// <li><p>Table 3
// Lorem ipsum ...
// Lorem ipsum ...</p>
// <table>
// <thead>
// <tr>
// <th>Header 1</th>
// <th>Header 2</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Row 1 Column 1</td>
// <td>Row 1 Column 2</td>
// </tr>
// </tbody>
// </table></li>
// </ol>
TestParser.TestSpec("Bullet list\n* Table 1\n\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n* Table 2\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n* Table 3\n Lorem ipsum ...\n Lorem ipsum ...\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n\nOrdered list\n1. Table 1\n\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n2. Table 2\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |\n\n3. Table 3\n Lorem ipsum ...\n Lorem ipsum ...\n | Header 1 | Header 2 |\n |----------------|----------------|\n | Row 1 Column 1 | Row 1 Column 2 |", "<p>Bullet list</p>\n<ul>\n<li><p>Table 1</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 2</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 3\nLorem ipsum ...\nLorem ipsum ...</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n</ul>\n<p>Ordered list</p>\n<ol>\n<li><p>Table 1</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 2</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n<li><p>Table 3\nLorem ipsum ...\nLorem ipsum ...</p>\n<table>\n<thead>\n<tr>\n<th>Header 1</th>\n<th>Header 2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Row 1 Column 1</td>\n<td>Row 1 Column 2</td>\n</tr>\n</tbody>\n</table></li>\n</ol>", "pipetables|advanced", context: "Example 27\nSection Extensions / Pipe Table\n");
}
}
}

View File

@@ -612,4 +612,173 @@ a | b
</tr>
</tbody>
</table>
````````````````````````````````
A table may begin right after a paragraph without an empty line in between:
```````````````````````````````` example
Some
**text**.
| A |
|---|
| B |
.
<p>Some
<strong>text</strong>.</p>
<table>
<thead>
<tr>
<th>A</th>
</tr>
</thead>
<tbody>
<tr>
<td>B</td>
</tr>
</tbody>
</table>
````````````````````````````````
Tables can be nested inside other blocks, like lists:
```````````````````````````````` example
Bullet list
* Table 1
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
* Table 2
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
* Table 3
Lorem ipsum ...
Lorem ipsum ...
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
Ordered list
1. Table 1
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
2. Table 2
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
3. Table 3
Lorem ipsum ...
Lorem ipsum ...
| Header 1 | Header 2 |
|----------------|----------------|
| Row 1 Column 1 | Row 1 Column 2 |
.
<p>Bullet list</p>
<ul>
<li><p>Table 1</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
<li><p>Table 2</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
<li><p>Table 3
Lorem ipsum ...
Lorem ipsum ...</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
</ul>
<p>Ordered list</p>
<ol>
<li><p>Table 1</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
<li><p>Table 2</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
<li><p>Table 3
Lorem ipsum ...
Lorem ipsum ...</p>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Column 1</td>
<td>Row 1 Column 2</td>
</tr>
</tbody>
</table></li>
</ol>
````````````````````````````````

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

@@ -0,0 +1,10 @@
namespace Markdig.Tests;
public class TestCodeInline
{
[Test]
public void UnpairedCodeInlineWithTrailingChars()
{
TestParser.TestSpec("*`\n\f", "<p>*`</p>");
}
}

View File

@@ -148,6 +148,9 @@ public class TestEmphasisExtended
[TestCase("1Foo1", "<one-only>Foo</one-only>")]
[TestCase("1121", "1<one-only>2</one-only>")]
[TestCase("22322", "<two-only>3</two-only>")]
[TestCase("2223222", "2<two-only>32</two-only>")]
[TestCase("22223222", "22<two-only>32</two-only>")]
[TestCase("22223223222", "22223<two-only>3</two-only>2")]
[TestCase("2232", "2232")]
[TestCase("333", "333")]
[TestCase("3334333", "<three-only>4</three-only>")]

View File

@@ -22,6 +22,18 @@ public partial class TestEmphasisPlus
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <em><strong>Strong emphasis</strong></em> normal</p>", "");
}
[Test]
public void SupplementaryPunctuation()
{
TestParser.TestSpec("a*a∇*a\n\na*∇a*a\n\na*a𝜵*a\n\na*𝜵a*a\n\na*𐬼a*a\n\na*a𐬼*a", "<p>a*a∇*a</p>\n<p>a*∇a*a</p>\n<p>a*a𝜵*a</p>\n<p>a*𝜵a*a</p>\n<p>a*𐬼a*a</p>\n<p>a*a𐬼*a</p>", "");
}
[Test]
public void RecognizeSupplementaryChars()
{
TestParser.TestSpec("🌶️**𰻞**🍜**𰻞**🌶️**麺**🍜", "<p>🌶️<strong>𰻞</strong>🍜<strong>𰻞</strong>🌶️<strong>麺</strong>🍜</p>", "");
}
[Test]
public void OpenEmphasisHasConvenientContentStringSlice()
{

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()
{
@@ -304,8 +327,8 @@ public class TestLinkHelper
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bær", "br")]
[TestCase("bør", "br")]
[TestCase("bær", "baer")]
[TestCase("bør", "boer")]
[TestCase("bΘr", "br")]
[TestCase("四五", "")]
public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult)
@@ -320,6 +343,75 @@ public class TestLinkHelper
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
// Tests for NormalizeScandinavianOrGermanChar method mappings
// These special characters are always normalized (both allowOnlyAscii=true and false)
//
// Note: When allowOnlyAscii=true, NFD (Canonical Decomposition) is applied first:
// - German umlauts ä,ö,ü decompose to base letter + combining mark (ü -> u + ¨)
// The combining mark is then stripped, leaving just the base letter (ü -> u)
// - å decomposes similarly (å -> a + ˚ -> a)
// - But ø, æ, ß, þ, ð do NOT decompose, so they use NormalizeScandinavianOrGermanChar
//
// When allowOnlyAscii=false, NormalizeScandinavianOrGermanChar is used for ALL special chars
// German ß (Eszett/sharp s) - does NOT decompose with NFD
[TestCase("Straße", "strasse")] // ß -> ss (both allowOnlyAscii=true and false)
// Scandinavian æ, ø - do NOT decompose with NFD
[TestCase("æble", "aeble")] // æ -> ae (both modes)
[TestCase("Ærø", "aeroe")] // Æ -> Ae, ø -> oe (both modes, then lowercase)
[TestCase("København", "koebenhavn")] // ø -> oe (both modes)
[TestCase("Øresund", "oeresund")] // Ø -> Oe (both modes, then lowercase)
// Icelandic þ, ð - do NOT decompose with NFD
[TestCase("þing", "thing")] // þ (thorn) -> th (both modes)
[TestCase("bað", "bad")] // ð (eth) -> d (both modes)
// Mixed special characters (only chars that behave same in both modes)
[TestCase("øst-æble", "oest-aeble")] // ø->oe, æ->ae (both modes)
public void TestUrilizeScandinavianGermanChars(string input, string expectedResult)
{
// These transformations apply regardless of allowOnlyAscii flag
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
// Tests specific to allowOnlyAscii=true behavior
// German umlauts (ä, ö, ü) and å decompose with NFD, so they become base letter only
[TestCase("schön", "schon")] // ö decomposes to o (NFD strips combining mark)
[TestCase("Mädchen", "madchen")] // ä decomposes to a
[TestCase("Übung", "ubung")] // Ü decomposes to U (then lowercase to u)
[TestCase("Düsseldorf", "dusseldorf")] // ü decomposes to u
[TestCase("Käse", "kase")] // ä decomposes to a
[TestCase("gå", "ga")] // å decomposes to a
[TestCase("Ålesund", "alesund")] // Å decomposes to A (then lowercase)
[TestCase("grüßen", "grussen")] // ü decomposes to u, ß -> ss
[TestCase("Þór", "thor")] // Þ -> Th, ó decomposes to o (then lowercase)
[TestCase("Íslandsbanki", "islandsbanki")] // Í decomposes to I (then lowercase)
public void TestUrilizeOnlyAscii_GermanUmlautsDecompose(string input, string expectedResult)
{
// With allowOnlyAscii=true, these characters decompose via NFD and lose their diacritics
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
// Tests specific to allowOnlyAscii=false behavior
// All special chars use NormalizeScandinavianOrGermanChar (including ä, ö, ü, å)
[TestCase("schön", "schoen")] // ö -> oe (NormalizeScandinavianOrGermanChar)
[TestCase("Mädchen", "maedchen")] // ä -> ae
[TestCase("Übung", "uebung")] // Ü -> Ue (then lowercase)
[TestCase("Düsseldorf", "duesseldorf")] // ü -> ue
[TestCase("Käse", "kaese")] // ä -> ae
[TestCase("gå", "gaa")] // å -> aa
[TestCase("Ålesund", "aalesund")] // Å -> Aa (then lowercase)
[TestCase("grüßen", "gruessen")] // ü -> ue, ß -> ss
[TestCase("Þór", "thór")] // Þ -> Th (then lowercase 'th'), ó is kept as-is
[TestCase("Íslandsbanki", "íslandsbanki")] // í is kept as-is when allowOnlyAscii=false
public void TestUrilizeNonAscii_GermanUmlautsExpanded(string input, string expectedResult)
{
// With allowOnlyAscii=false, these characters use NormalizeScandinavianOrGermanChar
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("123", "")]
[TestCase("1,-b", "b")]
[TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1-
@@ -337,11 +429,11 @@ public class TestLinkHelper
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("bær", "bær")]
[TestCase("æ5el", "æ5el")]
[TestCase("-æ5el", "æ5el")]
[TestCase("-frø-", "frø")]
[TestCase("-fr-ø", "fr-ø")]
[TestCase("bær", "baer")]
[TestCase("æ5el", "ae5el")]
[TestCase("-æ5el", "ae5el")]
[TestCase("-frø-", "froe")]
[TestCase("-fr-ø", "fr-oe")]
public void TestUrilizeNonAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
@@ -370,4 +462,4 @@ public class TestLinkHelper
{
TestParser.TestSpec("[Foo]\n\n[Foo]: http://ünicode.com", "<p><a href=\"http://xn--nicode-2ya.com\">Foo</a></p>");
}
}
}

View File

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

View File

@@ -51,6 +51,16 @@ public class TestNormalize
});
}
[Test]
public void SyntaxHeadlineLevel7()
{
AssertSyntax("####### Headline", new HeadingBlock(null) {
HeaderChar = '#',
Level = 7,
Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")),
});
}
[Test]
public void SyntaxParagraph()
{

View File

@@ -1,5 +1,7 @@
using Markdig;
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Tests;
@@ -10,12 +12,195 @@ public sealed class TestPipeTable
[TestCase("| S | T |\r\n|---|---|\t\r\n| G | H |")]
[TestCase("| S | T |\r\n|---|---|\f\r\n| G | H |")]
[TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)]
[TestCase("a\r| S | T |\r|---|---|")]
[TestCase("a\n| S | T |\r|---|---|")]
[TestCase("a\r\n| S | T |\r|---|---|")]
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);
}
}
[Test]
public void TableWithUnbalancedCodeSpanParsesWithoutDepthLimitError()
{
const string markdown = """
| Count | A | B | C | D | E |
|-------|---|---|---|---|---|
| 0 | B | C | D | E | F |
| 1 | B | `C | D | E | F |
| 2 | B | `C | D | E | F |
| 3 | B | C | D | E | F |
| 4 | B | C | D | E | F |
| 5 | B | C | D | E | F |
| 6 | B | C | D | E | F |
| 7 | B | C | D | E | F |
| 8 | B | C | D | E | F |
| 9 | B | C | D | E | F |
| 10 | B | C | D | E | F |
| 11 | B | C | D | E | F |
| 12 | B | C | D | E | F |
| 13 | B | C | D | E | F |
| 14 | B | C | D | E | F |
| 15 | B | C | D | E | F |
| 16 | B | C | D | E | F |
| 17 | B | C | D | E | F |
| 18 | B | C | D | E | F |
| 19 | B | C | D | E | F |
""";
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
MarkdownDocument document = null!;
Assert.DoesNotThrow(() => document = Markdown.Parse(markdown, pipeline));
var tables = document.Descendants().OfType<Table>().ToArray();
Assert.That(tables, Has.Length.EqualTo(1));
string html = string.Empty;
Assert.DoesNotThrow(() => html = Markdown.ToHtml(markdown, pipeline));
Assert.That(html, Does.Contain("<table"));
Assert.That(html, Does.Contain("<td>`C</td>"));
}
[Test]
public void CodeInlineWithPipeDelimitersRemainsCodeInline()
{
const string markdown = "`|| hidden text ||`";
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var document = Markdown.Parse(markdown, pipeline);
var codeInline = document.Descendants().OfType<CodeInline>().SingleOrDefault();
Assert.IsNotNull(codeInline);
Assert.That(codeInline!.Content, Is.EqualTo("|| hidden text ||"));
Assert.That(document.ToHtml(), Is.EqualTo("<p><code>|| hidden text ||</code></p>\n"));
}
[Test]
public void MultiLineCodeInlineWithPipeDelimitersRendersAsCode()
{
string markdown =
"""
`
|| hidden text ||
`
""".ReplaceLineEndings("\n");
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Is.EqualTo("<p><code>|| hidden text ||</code></p>\n"));
}
[Test]
public void TableCellWithCodeInlineRendersCorrectly()
{
const string markdown =
"""
| Count | A | B | C | D | E |
|-------|---|---|---|---|---|
| 0 | B | C | D | E | F |
| 1 | B | `Code block` | D | E | F |
| 2 | B | C | D | E | F |
""";
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Does.Contain("<td><code>Code block</code></td>"));
}
[Test]
public void CodeInlineWithIndentedContentPreservesWhitespace()
{
const string markdown = "`\n foo\n`";
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var document = Markdown.Parse(markdown, pipeline);
var codeInline = document.Descendants().OfType<CodeInline>().Single();
Assert.That(codeInline.Content, Is.EqualTo("foo"));
Assert.That(Markdown.ToHtml(markdown, pipeline), Is.EqualTo("<p><code>foo</code></p>\n"));
}
[Test]
public void TableWithIndentedPipeAfterCodeInlineParsesCorrectly()
{
var markdown =
"""
`
|| hidden text ||
`
| Count | Value |
|-------|-------|
| 0 | B |
""".ReplaceLineEndings("\n");
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Does.Contain("<p><code>|| hidden text ||</code></p>"));
Assert.That(html, Does.Contain("<table"));
Assert.That(html, Does.Contain("<td>B</td>"));
}
}

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

@@ -31,4 +31,14 @@ public class TestSmartyPants
TestParser.TestSpec("<<test>>", "<p>&laquo;test&raquo;</p>", pipeline);
}
[Test]
public void RecognizesSupplementaryCharacters()
{
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants()
.Build();
TestParser.TestSpec("\"𝜵\"𠮷\"𝜵\"𩸽\"", "<p>&quot;𝜵&ldquo;𠮷&rdquo;𝜵&ldquo;𩸽&rdquo;</p>", pipeline);
}
}

View File

@@ -160,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()
{
@@ -522,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");
}
@@ -698,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");
}
@@ -788,6 +811,52 @@ 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 TestGridTable()
{
Check("0\n\n+-+-+\n|A|B|\n+=+=+\n|C|D|\n+-+-+", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
table ( 2, 0) 3-31
tablerow ( 3, 0) 9-13
tablecell ( 3, 0) 9-11
paragraph ( 3, 1) 10-10
literal ( 3, 1) 10-10
tablecell ( 3, 2) 11-13
paragraph ( 3, 3) 12-12
literal ( 3, 3) 12-12
tablerow ( 5, 0) 21-25
tablecell ( 5, 0) 21-23
paragraph ( 5, 1) 22-22
literal ( 5, 1) 22-22
tablecell ( 5, 2) 23-25
paragraph ( 5, 3) 24-24
literal ( 5, 3) 24-24", "gridtables");
}
[Test]
public void TestIndentedCode()
{

View File

@@ -0,0 +1,165 @@
// 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;
namespace Markdig.Tests;
[TestFixture]
public class TestStringSlice
{
#if NET
[Test]
public void TestRuneBmp()
{
var slice = new StringSlice("01234");
Assert.AreEqual('0', slice.CurrentRune.Value);
Assert.AreEqual(0, slice.Start);
Assert.AreEqual('1', slice.NextRune().Value);
Assert.AreEqual(1, slice.Start);
Assert.AreEqual('2', slice.NextRune().Value);
Assert.AreEqual(2, slice.Start);
Assert.AreEqual('2', slice.CurrentRune.Value);
Assert.AreEqual("234", slice.ToString());
Assert.AreEqual('3', slice.PeekRuneExtra(1).Value);
Assert.AreEqual('4', slice.PeekRuneExtra(2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(3).Value);
Assert.AreEqual('1', slice.PeekRuneExtra(-1).Value);
Assert.AreEqual('0', slice.PeekRuneExtra(-2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-3).Value);
Assert.AreEqual('0', slice.RuneAt(0).Value);
Assert.AreEqual('1', slice.RuneAt(1).Value);
Assert.AreEqual('2', slice.RuneAt(2).Value);
Assert.AreEqual('3', slice.RuneAt(3).Value);
Assert.AreEqual('4', slice.RuneAt(4).Value);
Assert.AreEqual(2, slice.Start);
}
[Test]
public void TestRuneSupplementaryOnly()
{
var slice = new StringSlice("𝟎𝟏𝟐𝟑𝟒");
Assert.AreEqual(10, slice.Length);
// 𝟎 = U+1D7CE, 𝟐 = U+1D7D0
Assert.AreEqual(0x1D7CE, slice.CurrentRune.Value); // 𝟎
Assert.AreEqual(0, slice.Start);
Assert.AreEqual(0x1D7CF, slice.NextRune().Value); // 𝟏
Assert.AreEqual(2, slice.Start);
Assert.AreEqual(0x1D7D0, slice.NextRune().Value); // 𝟐
Assert.AreEqual(4, slice.Start);
Assert.AreEqual(0x1D7D0, slice.CurrentRune.Value); // 𝟐
Assert.AreEqual("𝟐𝟑𝟒", slice.ToString());
// CurrentRune occupies 2 `char`s, so next Rune starts at index 2
Assert.AreEqual(0x1D7D1, slice.PeekRuneExtra(2).Value); // 𝟑
Assert.AreEqual(0x1D7D2, slice.PeekRuneExtra(4).Value); // 𝟒
Assert.AreEqual(0, slice.PeekRuneExtra(6).Value);
Assert.AreEqual(0x1D7CF, slice.PeekRuneExtra(-1).Value); // 𝟏
Assert.AreEqual(0x1D7CE, slice.PeekRuneExtra(-3).Value); // 𝟎
Assert.AreEqual(0, slice.PeekRuneExtra(-5).Value);
Assert.AreEqual(0x1D7CE, slice.RuneAt(0).Value); // 𝟎
Assert.AreEqual(0x1D7CF, slice.RuneAt(2).Value); // 𝟏
Assert.AreEqual(0x1D7D0, slice.RuneAt(4).Value); // 𝟐
Assert.AreEqual(0x1D7D1, slice.RuneAt(6).Value); // 𝟑
Assert.AreEqual(0x1D7D2, slice.RuneAt(8).Value); // 𝟒
// The following usages are not expected. You should take into consideration the `char`s that the Rune you just acquired occupies.
Assert.AreEqual(0, slice.PeekRuneExtra(-4).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(1).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(3).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(5).Value);
Assert.AreEqual(0, slice.RuneAt(1).Value);
Assert.AreEqual(0, slice.RuneAt(3).Value);
Assert.AreEqual(0, slice.RuneAt(5).Value);
Assert.AreEqual(0, slice.RuneAt(7).Value);
Assert.AreEqual(0, slice.RuneAt(9).Value);
Assert.AreEqual(4, slice.Start);
}
[Test]
public void TestRuneIsolatedHighSurrogate()
{
var slice = new StringSlice("\ud800\ud801\ud802\ud803\ud804");
Assert.AreEqual(0, slice.CurrentRune.Value);
Assert.AreEqual(0, slice.Start);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(0, slice.CurrentRune.Value);
Assert.AreEqual('\ud801', slice.CurrentChar);
Assert.AreEqual(1, slice.Start);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(2, slice.Start);
Assert.AreEqual('\ud802', slice.CurrentChar);
Assert.AreEqual(0, slice.CurrentRune.Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-3).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-1).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(1).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(3).Value);
Assert.AreEqual(0, slice.RuneAt(0).Value);
Assert.AreEqual(0, slice.RuneAt(1).Value);
Assert.AreEqual(0, slice.RuneAt(2).Value);
Assert.AreEqual(0, slice.RuneAt(3).Value);
Assert.AreEqual(0, slice.RuneAt(4).Value);
Assert.AreEqual(2, slice.Start);
}
[Test]
public void TestRuneIsolatedLowSurrogate()
{
var slice = new StringSlice("\udc00\udc01\udc02\udc03\udc04");
Assert.AreEqual(0, slice.CurrentRune.Value);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual('\udc01', slice.CurrentChar);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual('\udc02', slice.CurrentChar);
Assert.AreEqual(0, slice.CurrentRune.Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-3).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(-1).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(1).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(2).Value);
Assert.AreEqual(0, slice.PeekRuneExtra(3).Value);
Assert.AreEqual(0, slice.RuneAt(0).Value);
Assert.AreEqual(0, slice.RuneAt(1).Value);
Assert.AreEqual(0, slice.RuneAt(2).Value);
Assert.AreEqual(0, slice.RuneAt(3).Value);
Assert.AreEqual(0, slice.RuneAt(4).Value);
}
[Test]
public void TestMixedInput()
{
var slice = new StringSlice("a\udc00bc𝟑d𝟒\udc00");
Assert.AreEqual(10, slice.Length);
Assert.AreEqual('a', slice.CurrentRune.Value);
Assert.AreEqual(0, slice.Start);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(1, slice.Start);
Assert.AreEqual('b', slice.NextRune().Value);
Assert.AreEqual(2, slice.Start);
Assert.AreEqual('c', slice.NextRune().Value);
Assert.AreEqual(3, slice.Start);
Assert.AreEqual(0x1D7D1, slice.NextRune().Value);
Assert.AreEqual(4, slice.Start);
Assert.AreEqual('d', slice.NextRune().Value);
Assert.AreEqual(6, slice.Start);
Assert.AreEqual(0x1D7D2, slice.NextRune().Value);
Assert.AreEqual(7, slice.Start);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(9, slice.Start);
Assert.False(slice.IsEmpty);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(10, slice.Start);
Assert.True(slice.IsEmpty);
slice = new StringSlice(slice.Text + 'a', 7, 10);
Assert.AreEqual(0x1D7D2, slice.CurrentRune.Value);
Assert.AreEqual(0, slice.NextRune().Value);
Assert.AreEqual(9, slice.Start);
Assert.AreEqual('a', slice.NextRune().Value);
}
#endif
}

View File

@@ -8,7 +8,6 @@ namespace Markdig.Tests;
[TestFixture]
public class TestStringSliceList
{
// TODO: Add tests for StringSlice
// TODO: Add more tests for StringLineGroup
[Test]

View File

@@ -107,7 +107,7 @@ public class TestYamlFrontMatterExtension
}
}
Assert.Pass("No exception parsing and iterating through YAML front matter block lines");
// No exception parsing and iterating through YAML front matter block lines
}
}

View File

@@ -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.22.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" />
</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)
@@ -73,8 +73,6 @@ public class AbbreviationParser : BlockParser
private void DocumentOnProcessInlinesBegin(InlineProcessor inlineProcessor, Inline? inline)
{
inlineProcessor.Document.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin;
var abbreviations = inlineProcessor.Document.GetAbbreviations();
// Should not happen, but another extension could decide to remove them, so...
if (abbreviations is null)
@@ -89,6 +87,7 @@ public class AbbreviationParser : BlockParser
{
var literal = (LiteralInline)processor.Inline!;
var originalLiteral = literal;
var originalSpanEnd = literal.Span.End;
ContainerInline? container = null;
@@ -171,7 +170,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 +201,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;
@@ -24,6 +22,8 @@ public class AutoIdentifierExtension : IMarkdownExtension
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.
@@ -32,6 +32,8 @@ public class AutoIdentifierExtension : IMarkdownExtension
public AutoIdentifierExtension(AutoIdentifierOptions options)
{
_options = options;
_processInlinesBegin = DocumentOnProcessInlinesBegin;
_processInlinesEnd = HeadingBlock_ProcessInlinesEnd;
}
public void Setup(MarkdownPipelineBuilder pipeline)
@@ -87,19 +89,18 @@ 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;
var dictionary = (Dictionary<string, HeadingLinkReferenceDefinition>)doc.GetData(this)!;
foreach (var keyPair in dictionary)
{
@@ -119,7 +120,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()

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

@@ -2,9 +2,11 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers;
namespace Markdig.Extensions.AutoLinks;
public class AutoLinkOptions
public class AutoLinkOptions : LinkOptions
{
public AutoLinkOptions()
{
@@ -13,13 +15,13 @@ public class AutoLinkOptions
public string ValidPreviousCharacters { get; set; }
/// <summary>
/// Should the link open in a new window when clicked (false by default)
/// </summary>
public bool OpenInNewWindow { get; set; }
/// <summary>
/// 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
@@ -131,9 +134,6 @@ public class FootnoteParser : BlockParser
/// <param name="inline">The inline.</param>
private void Document_ProcessInlinesEnd(InlineProcessor state, Inline? inline)
{
// Unregister
state.Document.ProcessInlinesEnd -= Document_ProcessInlinesEnd;
var footnotes = (FootnoteGroup)state.Document.GetData(DocumentKey)!;
// Remove the footnotes from the document and readd them at the end
state.Document.Remove(footnotes);
@@ -170,10 +170,8 @@ public class FootnoteParser : BlockParser
paragraphBlock = new ParagraphBlock();
footnote.Add(paragraphBlock);
}
if (paragraphBlock.Inline == null)
{
paragraphBlock.Inline = new ContainerInline();
}
paragraphBlock.Inline ??= new ContainerInline();
foreach (var link in footnote.Links)
{

View File

@@ -23,7 +23,7 @@ public class GenericAttributesParser : InlineParser
/// </summary>
public GenericAttributesParser()
{
OpeningCharacters = new[] { '{' };
OpeningCharacters = ['{'];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
@@ -109,6 +109,15 @@ public class GenericAttributesParser : InlineParser
{
isValid = true;
line.SkipChar(); // skip }
// skip line breaks
if (line.CurrentChar == '\n')
{
line.SkipChar();
}
else if (line.CurrentChar == '\r' && line.PeekChar() == '\n')
{
line.Start += 2;
}
break;
}
@@ -124,7 +133,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 +145,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

@@ -3,13 +3,14 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers;
namespace Markdig.Extensions.JiraLinks;
/// <summary>
/// Available options for replacing JIRA links
/// </summary>
public class JiraLinkOptions
public class JiraLinkOptions : LinkOptions
{
/// <summary>
/// The base Url (e.g. `https://mycompany.atlassian.net`)
@@ -21,11 +22,6 @@ public class JiraLinkOptions
/// </summary>
public string BasePath { get; set; }
/// <summary>
/// Should the link open in a new window when clicked
/// </summary>
public bool OpenInNewWindow { get; set; }
public JiraLinkOptions(string baseUrl)
{
OpenInNewWindow = true; //default

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

@@ -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)
@@ -36,16 +36,15 @@ public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
// -- &ndash; 'ndash'
// --- — &mdash; 'mdash'
var pc = slice.PeekCharExtra(-1);
var c = slice.CurrentChar;
var openingChar = c;
var pc = slice.PeekRuneExtra(-1);
var openingChar = slice.CurrentChar;
var startingPosition = slice.Start;
// undefined first
var type = (SmartyPantType) 0;
switch (c)
switch (openingChar)
{
case '\'':
type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines
@@ -93,9 +92,9 @@ public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
}
// Skip char
c = slice.NextChar();
var next = slice.NextRune();
CharHelper.CheckOpenCloseDelimiter(pc, c, false, out bool canOpen, out bool canClose);
CharHelper.CheckOpenCloseDelimiter(pc, next, false, out bool canOpen, out bool canClose);
bool postProcess = false;
@@ -204,8 +203,6 @@ public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline? inline)
{
processor.Block!.ProcessInlinesEnd -= BlockOnProcessInlinesEnd;
var pants = (ListSmartyPants) processor.ParserStates[Index];
var openers = new Stack<Opener>(4);

View File

@@ -1,10 +1,11 @@
// 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.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
using System.Linq;
namespace Markdig.Extensions.Tables;
@@ -12,7 +13,7 @@ public class GridTableParser : BlockParser
{
public GridTableParser()
{
OpeningCharacters = new[] { '+' };
OpeningCharacters = ['+'];
}
public override BlockState TryOpen(BlockProcessor processor)
@@ -43,7 +44,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;
}
@@ -60,7 +61,12 @@ public class GridTableParser : BlockParser
}
// Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid)
tableState.AddLine(ref processor.Line);
var table = new Table(this);
var table = new Table(this)
{
Line = processor.LineIndex,
Column = processor.Column,
Span = { Start = lineStart }
};
table.SetData(typeof(GridTableState), tableState);
// Calculate the total width of all columns
@@ -94,10 +100,12 @@ public class GridTableParser : BlockParser
tableState.AddLine(ref processor.Line);
if (processor.CurrentChar == '+')
{
gridTable.UpdateSpanEnd(processor.Line.End);
return HandleNewRow(processor, tableState, gridTable);
}
if (processor.CurrentChar == '|')
{
gridTable.UpdateSpanEnd(processor.Line.End);
return HandleContents(processor, tableState, gridTable);
}
TerminateCurrentRow(processor, tableState, gridTable, true);
@@ -135,6 +143,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 +151,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 +167,7 @@ public class GridTableParser : BlockParser
}
}
private static bool IsRowSeperator(StringSlice slice)
private static bool IsRowSeparator(StringSlice slice)
{
char c = slice.CurrentChar;
do
@@ -181,8 +190,18 @@ public class GridTableParser : BlockParser
var columnSlice = columns[i];
if (columnSlice.CurrentCell != null)
{
currentRow ??= new TableRow();
if (currentRow == null)
{
TableCell firstCell = columns.First(c => c.CurrentCell != null).CurrentCell!;
TableCell lastCell = columns.Last(c => c.CurrentCell != null).CurrentCell!;
currentRow ??= new TableRow()
{
Span = new SourceSpan(firstCell.Span.Start, lastCell.Span.End),
Line = firstCell.Line
};
}
// If this cell does not already belong to a row
if (columnSlice.CurrentCell.Parent is null)
{
@@ -256,34 +275,35 @@ 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)
{
columnSlice.CurrentCell = new TableCell(this)
{
ColumnSpan = columnSlice.CurrentColumnSpan,
ColumnIndex = i
ColumnIndex = i,
Column = columnSlice.Start,
Line = processor.LineIndex,
Span = new SourceSpan(line.Start + columnSlice.Start, line.Start + columnSlice.End)
};
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);
}
// Process the content of the cell
columnSlice.BlockProcessor!.LineIndex = processor.LineIndex;
columnSlice.BlockProcessor.ProcessLine(sliceForCell);
columnSlice.BlockProcessor.ProcessLinePart(sliceForCell, sliceForCell.Start - line.Start);
}
// Go to next column

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

@@ -38,7 +38,7 @@ public class PipeTableExtension : IMarkdownExtension
var lineBreakParser = pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
{
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(lineBreakParser!, Options));
pipeline.InlineParsers.InsertAfter<EmphasisInlineParser>(new PipeTableParser(lineBreakParser!, Options));
}
}

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

@@ -3,7 +3,6 @@
// See the license.txt file in the project root for more information.
using System.Diagnostics;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Parsers.Inlines;
@@ -20,17 +19,17 @@ namespace Markdig.Extensions.Tables;
/// <seealso cref="IPostInlineProcessor" />
public class PipeTableParser : InlineParser, IPostInlineProcessor
{
private readonly LineBreakInlineParser lineBreakParser;
private readonly LineBreakInlineParser _lineBreakParser;
/// <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' };
_lineBreakParser = lineBreakParser ?? throw new ArgumentNullException(nameof(lineBreakParser));
OpeningCharacters = ['|', '\n', '\r'];
Options = options ?? new PipeTableOptions();
}
@@ -60,13 +59,12 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
if (tableState is null)
{
// A table could be preceded by an empty line or a line containing an inline
// that has not been added to the stack, so we consider this as a valid
// start for a table. Typically, with this, we can have an attributes {...}
// starting on the first line of a pipe table, even if the first line
// doesn't have a pipe
if (processor.Inline != null && (localLineIndex > 0 || c == '\n' || c == '\r'))
if (processor.Inline != null && (c == '\n' || c == '\r'))
{
return false;
}
@@ -75,6 +73,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
{
isFirstLineEmpty = true;
}
// Else setup a table processor
tableState = new TableState();
processor.ParserStates[Index] = tableState;
@@ -87,8 +86,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
tableState.IsInvalidTable = true;
}
tableState.LineHasPipe = false;
lineBreakParser.Match(processor, ref slice);
tableState.LineIndex++;
_lineBreakParser.Match(processor, ref slice);
if (!isFirstLineEmpty)
{
tableState.ColumnAndLineDelimiters.Add(processor.Inline!);
@@ -102,15 +100,11 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
Span = new SourceSpan(position, position),
Line = globalLineIndex,
Column = column,
LocalLineIndex = localLineIndex
LocalLineIndex = localLineIndex,
IsClosed = true // Creates flat sibling structure for O(n) traversal
};
var deltaLine = localLineIndex - tableState.LineIndex;
if (deltaLine > 0)
{
tableState.IsInvalidTable = true;
}
tableState.LineHasPipe = true;
tableState.LineIndex = localLineIndex;
slice.SkipChar(); // Skip the `|` character
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
@@ -132,6 +126,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
return true;
}
// With flat structure, pipes are siblings at root level
// Walk backwards from the last child to find pipe delimiters
var child = container.LastChild;
List<PipeTableDelimiterInline>? delimitersToRemove = null;
@@ -149,8 +145,8 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
break;
}
var subContainer = child as ContainerInline;
child = subContainer?.LastChild;
// Walk siblings instead of descending into containers
child = child.PreviousSibling;
}
// If we have found any delimiters, transform them to literals
@@ -193,19 +189,36 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
// Remove previous state
state.ParserStates[Index] = null!;
// Continue
if (tableState is null || container is null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex)
// Abort if not a valid table
if (tableState is null || container is null || tableState.IsInvalidTable || !tableState.LineHasPipe)
{
if (tableState is not null)
{
foreach (var inline in tableState.ColumnAndLineDelimiters)
{
if (inline is PipeTableDelimiterInline pipeDelimiter)
{
pipeDelimiter.ReplaceByLiteral();
}
}
}
return true;
}
// Detect the header row
var delimiters = tableState.ColumnAndLineDelimiters;
// TODO: we could optimize this by merging FindHeaderRow and the cell loop
var aligns = FindHeaderRow(delimiters);
if (Options.RequireHeaderSeparator && aligns is null)
{
// No valid header separator found - convert all pipe delimiters to literals
foreach (var inline in delimiters)
{
if (inline is PipeTableDelimiterInline pipeDelimiter)
{
pipeDelimiter.ReplaceByLiteral();
}
}
return true;
}
@@ -218,70 +231,43 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
attributes.CopyTo(table.GetAttributes());
}
state.BlockNew = table;
var cells = tableState.Cells;
cells.Clear();
//delimiters[0].DumpTo(state.DebugLog);
// Pipes may end up nested inside unmatched emphasis delimiters, e.g.:
// *a | b*|
// Promote them to root level so we have a flat sibling structure.
PromoteNestedPipesToRootLevel(delimiters, container);
// delimiters contain a list of `|` and `\n` delimiters
// The `|` delimiters are created as child containers.
// So the following:
// | a | b \n
// | d | e \n
// The inline tree is now flat: all pipes and line breaks are siblings at root level.
// For example, `| a | b \n| c | d \n` produces:
// [|] [a] [|] [b] [\n] [|] [c] [|] [d] [\n]
//
// Will generate a tree of the following node:
// |
// a
// |
// b
// \n
// |
// d
// |
// e
// \n
// When parsing delimiters, we need to recover whether a row is of the following form:
// 0) | a | b | \n
// 1) | a | b \n
// 2) a | b \n
// 3) a | b | \n
// Tables support four row formats:
// | a | b | (leading and trailing pipes)
// | a | b (leading pipe only)
// a | b (no leading or trailing pipes)
// a | b | (trailing pipe only)
// If the last element is not a line break, add a line break to homogenize parsing in the next loop
// Ensure the table ends with a line break to simplify row detection
var lastElement = delimiters[delimiters.Count - 1];
if (!(lastElement is LineBreakInline))
{
while (true)
// Find the actual last sibling (there may be content after the last delimiter)
while (lastElement.NextSibling != null)
{
if (lastElement is ContainerInline lastElementContainer)
{
var nextElement = lastElementContainer.LastChild;
if (nextElement != null)
{
lastElement = nextElement;
continue;
}
}
break;
lastElement = lastElement.NextSibling;
}
var endOfTable = new LineBreakInline();
// If the last element is a container, we have to add the EOL to its child
// otherwise only next sibling
if (lastElement is ContainerInline)
{
((ContainerInline)lastElement).AppendChild(endOfTable);
}
else
{
lastElement.InsertAfter(endOfTable);
}
lastElement.InsertAfter(endOfTable);
delimiters.Add(endOfTable);
tableState.EndOfLines.Add(endOfTable);
}
// Cell loop
// Reconstruct the table from the delimiters
int lastPipePos = 0;
// Build table rows and cells by iterating through delimiters
TableRow? row = null;
TableRow? firstRow = null;
for (int i = 0; i < delimiters.Count; i++)
@@ -296,66 +282,51 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
firstRow ??= row;
// If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like:
// 0) | a | b | \n
// 1) | a | b \n
// Skip leading pipe at start of row (e.g., `| a | b` or `| a | b |`)
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;
}
}
// We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`)
// So we iterate back to the first pipe or line break
// x
// 1) | a | b \n
// 2) a | b \n
// Find cell content by walking backwards from this delimiter to the previous pipe or line break.
// For `| a | b \n` at delimiter 'x':
// [|] [a] [x] [b] [\n]
// ^--- current delimiter
// Walk back: [a] is the cell content (stop at [|])
Inline? endOfCell = null;
Inline? beginOfCell = null;
var cellContentIt = delimiter;
while (true)
var cellContentIt = delimiter.PreviousSibling;
while (cellContentIt != null)
{
cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent;
if (cellContentIt is null || cellContentIt is LineBreakInline)
{
if (cellContentIt is LineBreakInline || cellContentIt is PipeTableDelimiterInline)
break;
}
// The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation)
if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent is null ))
{
beginOfCell = ((ContainerInline)cellContentIt).FirstChild;
if (endOfCell is null)
{
endOfCell = beginOfCell;
}
// Stop at the root ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation)
if (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent is null)
break;
}
beginOfCell = cellContentIt;
if (endOfCell is null)
{
endOfCell = beginOfCell;
}
endOfCell ??= beginOfCell;
cellContentIt = cellContentIt.PreviousSibling;
}
// If the current deilimiter is a pipe `|` OR
// If the current delimiter is a pipe `|` OR
// the beginOfCell/endOfCell are not null and
// either they are :
// either they are:
// - different
// - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal.
// Then we can add a cell to the current row
if (!isLine || (beginOfCell != null && endOfCell != null && ( beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline beingOfCellLiteral && beingOfCellLiteral.Content.IsEmptyOrWhitespace())))))
{
if (!isLine)
{
// 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();
}
// We trim whitespace at the beginning and ending of the cell
TrimStart(beginOfCell);
TrimEnd(endOfCell);
@@ -363,10 +334,20 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
var cellContainer = new ContainerInline();
// Copy elements from beginOfCell on the first level
// The pipe delimiter serves as a boundary - stop when we hit it
var cellIt = beginOfCell;
while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline))
{
var nextSibling = cellIt.NextSibling;
// Skip empty literals (can result from trimming)
if (cellIt is LiteralInline { Content.IsEmpty: true })
{
cellIt.Remove();
cellIt = nextSibling;
continue;
}
cellIt.Remove();
if (cellContainer.Span.IsEmpty)
{
@@ -379,8 +360,16 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
cellIt = nextSibling;
}
if (!isLine)
{
// Remove the pipe delimiter AFTER copying cell content
// This preserves the sibling chain during the copy loop
delimiter.Remove();
lastPipePos = delimiter.Span.End;
}
// Create the cell and add it to the pending row
var tableParagraph = new ParagraphBlock()
var tableParagraph = new ParagraphBlock
{
Span = cellContainer.Span,
Line = cellContainer.Line,
@@ -421,14 +410,18 @@ 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)
{
endOfLine.Remove();
}
// If we have a header row, we can remove it
// TODO: we could optimize this by merging FindHeaderRow and the previous loop
// Mark first row as header and remove the separator row if present
var tableRow = (TableRow)table[0];
tableRow.IsHeader = Options.RequireHeaderSeparator;
if (aligns != null)
@@ -438,11 +431,13 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
table.ColumnDefinitions.AddRange(aligns);
}
// Perform delimiter processor that are coming after this processor
// Perform all post-processors on cell content
// With InsertAfter, emphasis runs before pipe table, so we need to re-run from index 0
// to ensure emphasis delimiters in cells are properly matched
foreach (var cell in cells)
{
var paragraph = (ParagraphBlock) cell[0];
state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true);
state.PostProcessInlines(0, paragraph.Inline, null, true);
if (paragraph.Inline?.LastChild is not null)
{
paragraph.Inline.Span.End = paragraph.Inline.LastChild.Span.End;
@@ -463,13 +458,43 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
table.NormalizeUsingMaxWidth();
}
if (state.Block is ParagraphBlock { Inline.FirstChild: not null } leadingParagraph)
{
// The table was preceded by a non-empty paragraph, e.g.
// ```md
// Some text
// | Header |
// ```
//
// Keep the paragraph as-is and insert the table after it.
// Since we've already processed all the inlines in this table block,
// we can't insert it while the parent is still being processed.
// Hook up a callback that inserts the table after we're done with ProcessInlines for the parent block.
// We've processed inlines in the table, but not the leading paragraph itself yet.
state.PostProcessInlines(0, leadingParagraph.Inline, null, isFinalProcessing: true);
ContainerBlock parent = leadingParagraph.Parent!;
parent.ProcessInlinesEnd += (_, _) =>
{
parent.Insert(parent.IndexOf(leadingParagraph) + 1, table);
};
}
else
{
// Nothing interesting in the existing block, just replace it.
state.BlockNew = table;
}
// We don't want to continue procesing delimiters, as we are already processing them here
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)
{
@@ -478,7 +503,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')
{
@@ -493,7 +518,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]))
@@ -501,7 +527,7 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
continue;
}
// The last delimiter is always null,
// Parse the separator row (second row) to extract column alignments
for (int j = i + 1; j < delimiters.Count; j++)
{
var delimiter = delimiters[j];
@@ -513,42 +539,44 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
continue;
}
// Check the left side of a `|` delimiter
// Parse the content before this delimiter as a column definition (e.g., `:---`, `---:`, `:---:`)
// Skip if previous sibling is a pipe (empty cell) or whitespace
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))
!(delimiter.PreviousSibling is PipeTableDelimiterInline) &&
!(delimiter.PreviousSibling is LiteralInline li && li.Content.IsEmptyOrWhitespace()) &&
!ParseHeaderString(delimiter.PreviousSibling, out align, out delimiterCount))
{
break;
}
// Create aligns until we may have a header row
aligns ??= new List<TableColumnDefinition>();
columnDefinitions ??= new List<TableColumnDefinition>();
totalDelimiterCount += delimiterCount;
columnDefinitions.Add(new TableColumnDefinition() { Alignment = align, Width = delimiterCount});
aligns.Add(new TableColumnDefinition() { Alignment = align });
// If this is the last delimiter, we need to check the right side of the `|` delimiter
// If this is the last pipe, check for a trailing column definition (row without trailing pipe)
// e.g., `| :--- | ---:` has content after the last pipe
if (nextDelimiter is null)
{
var nextSibling = columnDelimiter != null
? columnDelimiter.FirstChild
: delimiter.NextSibling;
var nextSibling = delimiter.NextSibling;
// If there is no content after
// No trailing content means row ends with pipe: `| :--- |`
if (IsNullOrSpace(nextSibling))
{
isValidRow = true;
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;
}
@@ -562,7 +590,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)
@@ -596,9 +644,9 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
private static void TrimStart(Inline? inline)
{
while (inline is ContainerInline && !(inline is DelimiterInline))
while (inline is ContainerInline containerInline && !(containerInline is DelimiterInline))
{
inline = ((ContainerInline)inline).FirstChild;
inline = containerInline.FirstChild;
}
if (inline is LiteralInline literal)
@@ -609,6 +657,13 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
private static void TrimEnd(Inline? inline)
{
// Walk into containers to find the last leaf to trim
// Skip PipeTableDelimiterInline but walk into other containers (including emphasis)
while (inline is ContainerInline container && !(inline is PipeTableDelimiterInline))
{
inline = container.LastChild;
}
if (inline is LiteralInline literal)
{
literal.Content.TrimEnd();
@@ -629,18 +684,116 @@ public class PipeTableParser : InlineParser, IPostInlineProcessor
return false;
}
/// <summary>
/// Promotes nested pipe delimiters and line breaks to root level.
/// </summary>
/// <remarks>
/// Handles cases like `*a | b*|` where the pipe ends up inside an unmatched emphasis container.
/// After promotion, all delimiters become siblings at root level for consistent cell boundary detection.
/// </remarks>
private static void PromoteNestedPipesToRootLevel(List<Inline> delimiters, ContainerInline root)
{
for (int i = 0; i < delimiters.Count; i++)
{
var delimiter = delimiters[i];
// Handle both pipe delimiters and line breaks
bool isPipe = delimiter is PipeTableDelimiterInline;
bool isLineBreak = delimiter is LineBreakInline;
if (!isPipe && !isLineBreak)
continue;
// Skip if already at root level
if (delimiter.Parent == root)
continue;
// Find the top-level ancestor (direct child of root)
var ancestor = delimiter.Parent;
while (ancestor?.Parent != null && ancestor.Parent != root)
{
ancestor = ancestor.Parent;
}
if (ancestor is null || ancestor.Parent != root)
continue;
// Split: promote delimiter to be sibling of ancestor
SplitContainerAtDelimiter(delimiter, ancestor);
}
}
/// <summary>
/// Splits a container at the delimiter, promoting the delimiter to root level.
/// </summary>
/// <remarks>
/// For input `*a | b*`, the pipe is inside the emphasis container:
/// EmphasisDelimiter { "a", Pipe, "b" }
/// After splitting:
/// EmphasisDelimiter { "a" }, Pipe, Container { "b" }
/// </remarks>
private static void SplitContainerAtDelimiter(Inline delimiter, Inline ancestor)
{
if (delimiter.Parent is not { } parent) return;
// Collect content after the delimiter
var contentAfter = new List<Inline>();
var current = delimiter.NextSibling;
while (current != null)
{
contentAfter.Add(current);
current = current.NextSibling;
}
// Remove content after delimiter from parent
foreach (var inline in contentAfter)
{
inline.Remove();
}
// Remove delimiter from parent
delimiter.Remove();
// Insert delimiter after the ancestor (at root level)
ancestor.InsertAfter(delimiter);
// If there's content after, wrap in new container and insert after delimiter
if (contentAfter.Count > 0)
{
// Create new container matching the original parent type
var newContainer = CreateMatchingContainer(parent);
foreach (var inline in contentAfter)
{
newContainer.AppendChild(inline);
}
delimiter.InsertAfter(newContainer);
}
}
/// <summary>
/// Creates a container to wrap content split from the source container.
/// </summary>
private static ContainerInline CreateMatchingContainer(ContainerInline source)
{
// Emphasis processing runs before pipe table processing, so emphasis delimiters
// are already resolved. A plain ContainerInline suffices.
return new ContainerInline
{
Span = source.Span,
Line = source.Line,
Column = source.Column
};
}
private sealed class TableState
{
public bool IsInvalidTable { get; set; }
public bool LineHasPipe { get; set; }
public int LineIndex { get; set; }
public List<Inline> ColumnAndLineDelimiters { get; } = [];
public List<Inline> ColumnAndLineDelimiters { get; } = new();
public List<TableCell> Cells { get; } = [];
public List<TableCell> Cells { get; } = new();
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>
@@ -46,13 +47,14 @@ public static class TableHelper
/// <param name="slice">The text slice.</param>
/// <param name="delimiterChar">The delimiter character (either `-` or `=`). If `\0`, it will detect the character (either `-` or `=`)</param>
/// <param name="align">The alignment of the column.</param>
/// <param name="delimiterCount">The number of times <paramref name="delimiterChar"/> appeared in the column header.</param>
/// <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 +82,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;

View File

@@ -1,10 +1,12 @@
// 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.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
namespace Markdig.Helpers;
@@ -19,29 +21,103 @@ 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";
// 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 '‡';
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPunctuationException(Rune c) =>
c.IsBmp && IsPunctuationException((char)c.Value);
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);
CheckOpenCloseDelimiter(
prevIsWhiteSpace,
prevIsPunctuation,
prevIsPunctuation && IsPunctuationException(pc),
nextIsWhiteSpace,
nextIsPunctuation,
nextIsPunctuation && IsPunctuationException(c),
enableWithinWord,
out canOpen,
out canClose);
}
var prevIsExcepted = prevIsPunctuation && IsPunctuationException(pc);
var nextIsExcepted = nextIsPunctuation && IsPunctuationException(c);
#if NET
public
#else
internal
#endif
static void CheckOpenCloseDelimiter(Rune pc, Rune 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);
CheckOpenCloseDelimiter(
prevIsWhiteSpace,
prevIsPunctuation,
prevIsPunctuation && IsPunctuationException(pc),
nextIsWhiteSpace,
nextIsPunctuation,
nextIsPunctuation && IsPunctuationException(c),
enableWithinWord,
out canOpen,
out canClose);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CheckOpenCloseDelimiter(bool prevIsWhiteSpace, bool prevIsPunctuation, bool prevIsExcepted, bool nextIsWhiteSpace, bool nextIsPunctuation, bool nextIsExcepted, bool enableWithinWord, out bool canOpen, out bool canClose)
{
// A left-flanking delimiter run is a delimiter run that is
// (1) not followed by Unicode whitespace, and either
// (2a) not followed by a punctuation character or
@@ -62,13 +138,13 @@ public static class CharHelper
if (!enableWithinWord)
{
var temp = canOpen;
// A single _ character can open emphasis iff it is part of a left-flanking delimiter run and either
// (a) not part of a right-flanking delimiter run or
// A single _ character can open emphasis iff it is part of a left-flanking delimiter run and either
// (a) not part of a right-flanking delimiter run or
// (b) part of a right-flanking delimiter run preceded by punctuation.
canOpen = canOpen && (!canClose || prevIsPunctuation);
// A single _ character can close emphasis iff it is part of a right-flanking delimiter run and either
// (a) not part of a left-flanking delimiter run or
// (a) not part of a left-flanking delimiter run or
// (b) part of a left-flanking delimiter run followed by punctuation.
canClose = canClose && (!temp || nextIsPunctuation);
}
@@ -101,8 +177,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;
}
@@ -112,6 +188,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)]
@@ -128,39 +218,73 @@ public static class CharHelper
return (column & (TabSize - 1)) != 0;
}
/// <summary>
/// <see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#unicode-whitespace-character">Unicode whitespace character</see>.
/// </summary>
/// <param name="c">The character to evaluate.</param>
/// <returns><see langword="true"/> if the character is a Unicode whitespace character</returns>
[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)
/// <summary>
/// <see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#unicode-whitespace-character">Unicode whitespace character</see>.
/// </summary>
/// <param name="r">The character to evaluate. A supplementary character is also accepted.</param>
/// <returns><see langword="true"/> if the character is a Unicode whitespace character</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if NET
public
#else
internal
#endif
static bool IsWhitespace(this Rune r) => r.IsBmp && IsWhitespace((char)r.Value);
// Note: there is no supplementary character whose Unicode category is Zs (at least as of Unicode 17).
// https://www.compart.com/en/unicode/category/Zs
[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);
}
}
@@ -174,16 +298,15 @@ public static class CharHelper
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 == '•';
return s_escapableSymbolChars.Contains(c);
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsWhiteSpaceOrZero(this char c)
{
return IsZero(c) || IsWhitespace(c);
}
// Check if a char is a space or a punctuation
/// <summary>
/// Checks the Unicode category of the given character and determines whether it is a whitespace or punctuation character.
/// </summary>
/// <param name="c">The character to check.</param>
/// <param name="space">Output parameter indicating whether the character is a whitespace character.</param>
/// <param name="punctuation">Output parameter indicating whether the character is a punctuation character.</param>
public static void CheckUnicodeCategory(this char c, out bool space, out bool punctuation)
{
if (IsWhitespace(c))
@@ -194,52 +317,76 @@ 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.
const int PunctuationCategoryMask =
1 << (int)UnicodeCategory.ConnectorPunctuation |
1 << (int)UnicodeCategory.DashPunctuation |
1 << (int)UnicodeCategory.OpenPunctuation |
1 << (int)UnicodeCategory.ClosePunctuation |
1 << (int)UnicodeCategory.InitialQuotePunctuation |
1 << (int)UnicodeCategory.FinalQuotePunctuation |
1 << (int)UnicodeCategory.OtherPunctuation;
space = false;
punctuation = (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
punctuation = (CommonMarkPunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
}
}
// Same as CheckUnicodeCategory
internal static bool IsSpaceOrPunctuation(this char c)
/// <summary>
/// Check if a character is a <see href="https://spec.commonmark.org/0.31.2/#unicode-whitespace-character">Unicode whitespace</see> or <see href="https://spec.commonmark.org/0.31.2/#unicode-punctuation-character">punctuation character</see>.
/// </summary>
/// <param name="r">The character to evaluate. A supplementary character is also accepted.</param>
/// <param name="space"><see langword="true"/> if the character is an <see href="https://spec.commonmark.org/0.31.2/#unicode-whitespace-character">Unicode whitespace character</see></param>
/// <param name="punctuation"><see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#unicode-punctuation-character">Unicode punctuation character</see></param>
#if NET
public
#else
internal
#endif
static void CheckUnicodeCategory(this Rune r, out bool space, out bool punctuation)
{
if (IsWhitespace(c))
if (IsWhitespace(r))
{
return true;
space = true;
punctuation = false;
}
else if (c <= 127)
else if (r.Value <= 127)
{
return c == '\0' || IsAsciiPunctuation(c);
space = r.Value == 0;
punctuation = r.IsBmp && IsAsciiPunctuationOrZero((char)r.Value);
}
else
{
const int PunctuationCategoryMask =
1 << (int)UnicodeCategory.ConnectorPunctuation |
1 << (int)UnicodeCategory.DashPunctuation |
1 << (int)UnicodeCategory.OpenPunctuation |
1 << (int)UnicodeCategory.ClosePunctuation |
1 << (int)UnicodeCategory.InitialQuotePunctuation |
1 << (int)UnicodeCategory.FinalQuotePunctuation |
1 << (int)UnicodeCategory.OtherPunctuation;
return (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0;
space = false;
punctuation = (CommonMarkPunctuationCategoryMask & (1 << (int)Rune.GetUnicodeCategory(r))) != 0;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsSpaceOrPunctuationForGFMAutoLink(char 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 s_asciiPunctuationOrWhitespaceCharsOrZero.Contains(c);
}
else
{
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)
{
@@ -252,22 +399,37 @@ public static class CharHelper
return c == '\0';
}
/// <summary>
/// Returns <see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#space">space</see> (U+0020).
/// </summary>
/// <param name="c">The character to evaluate</param>
/// <returns><see langword="true"/> if the character is a space</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsSpace(this char c)
{
// 2.1 Characters and lines
// 2.1 Characters and lines
// A space is U+0020.
return c == ' ';
}
/// <summary>
/// Returns <see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#tab">tab</see> (U+0009).
/// </summary>
/// <param name="c">The character to evaluate</param>
/// <returns><see langword="true"/> if the character is a tab</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTab(this char c)
{
// 2.1 Characters and lines
// 2.1 Characters and lines
// A space is U+0009.
return c == '\t';
}
/// <summary>
/// Returns <see langword="true"/> if the character is a <see href="https://spec.commonmark.org/0.31.2/#space">space</see> (U+0020) or <see href="https://spec.commonmark.org/0.31.2/#tab">tab</see> (U+0009).
/// </summary>
/// <param name="c">The character to evaluate.</param>
/// <returns><see langword="true"/> if the character is a space or tab</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsSpaceOrTab(this char c)
{
@@ -279,7 +441,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)]
@@ -306,46 +468,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)
{
@@ -722,11 +871,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

@@ -4,7 +4,6 @@
using System.Buffers;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Markdig.Helpers;
@@ -17,7 +16,7 @@ public sealed class CharacterMap<T> where T : class
{
private readonly SearchValues<char> _values;
private readonly T[] _asciiMap;
private readonly Dictionary<uint, T>? _nonAsciiMap;
private readonly FrozenDictionary<uint, T>? _nonAsciiMap;
/// <summary>
/// Initializes a new instance of the <see cref="CharacterMap{T}"/> class.
@@ -35,10 +34,11 @@ public sealed class CharacterMap<T> where T : class
charSet.Add(map.Key);
}
OpeningCharacters = charSet.ToArray();
OpeningCharacters = [.. charSet];
Array.Sort(OpeningCharacters);
_asciiMap = new T[128];
Dictionary<uint, T>? nonAsciiMap = null;
foreach (var state in maps)
{
@@ -49,16 +49,18 @@ public sealed class CharacterMap<T> where T : class
}
else
{
_nonAsciiMap ??= new Dictionary<uint, T>();
nonAsciiMap ??= [];
if (!_nonAsciiMap.ContainsKey(openingChar))
{
_nonAsciiMap[openingChar] = state.Value;
}
nonAsciiMap.TryAdd(openingChar, state.Value);
}
}
_values = SearchValues.Create(OpeningCharacters);
if (nonAsciiMap is not null)
{
_nonAsciiMap = nonAsciiMap.ToFrozenDictionary();
}
}
/// <summary>

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

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