Compare commits

...

143 Commits

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

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

* Fix comment

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

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

* Update src/Markdig/MarkdownExtensions.cs

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

* Fix parsing of alert block with multiple children blocks

* Allow null for BlockParser ctor argument of QuoteBlock

---------

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-03-14 08:09:42 +01:00
teethache
3c7edaa82d Fix math source span. 2024-03-11 20:35:54 +08:00
Alexandre Mutel
fcb56fb037 Merge pull request #773 from hhyyrylainen/patch-1
Fixed a typo in MathSpecs.md
2024-02-29 08:21:10 +01:00
Henri Hyyryläinen
50bc6cadfc Fix typo in MathSpecs.md 2024-02-18 18:58:04 +02:00
Alexandre Mutel
201aa4ef73 Try to use the reusable workflow 2024-02-17 07:11:33 +01:00
Alexandre Mutel
adce9797d6 Use ubuntu for the CI 2024-02-17 06:49:39 +01:00
Alexandre Mutel
fb71dff0ec Fix tests 2024-02-17 06:48:47 +01:00
Alexandre Mutel
399570941a Remove net7.0 from CI 2024-02-17 06:47:31 +01:00
Alexandre Mutel
2f903697e2 Update projects to net8.0 2024-02-17 06:43:07 +01:00
Alexandre Mutel
eb8fe15679 Update ci.yml with nuget config 2024-02-13 09:29:26 +01:00
Alexandre Mutel
8f008e45ca Add nuget.org only config 2024-02-13 09:29:26 +01:00
Alexandre Mutel
0014ec4138 Merge pull request #769 from carbon/cq3
Eliminate various string allocations
2024-02-13 08:25:40 +01:00
Jason Nelson
2ca05ccad7 Eliminate string allocation in CodeInlineRenderer 2024-02-09 13:13:31 -08:00
Jason Nelson
6a15c804bc Add test coverage for headlines with > 6 # characters 2024-02-09 12:54:52 -08:00
Jason Nelson
0446959623 Add TextRendererBase.Write(char c, int count) method, and eliminate various string allocations 2024-02-09 12:54:30 -08:00
Alexandre Mutel
e6afddbaa0 Merge pull request #761 from carbon/collection-expressions
Use C# 12 syntax
2023-12-18 21:11:50 +01:00
Jason Nelson
a377239e91 Use null-coalescing assignment operator 2023-12-14 20:08:53 -08:00
Jason Nelson
35aa304faf Remove unused using statement 2023-12-14 19:56:08 -08:00
Jason Nelson
e4568979ec Fix typo 2023-12-14 19:55:45 -08:00
Jason Nelson
3470ec0d54 Make various members readonly on SourceSpan 2023-12-14 19:55:37 -08:00
Jason Nelson
113ef7f215 Use primary constructors (part 2) 2023-12-14 19:50:09 -08:00
Jason Nelson
4cb4b68883 Use collection expressions (part 5) 2023-12-14 19:43:15 -08:00
Jason Nelson
64ae344b74 Use collection expressions (part 4) 2023-12-14 15:41:07 -08:00
Jason Nelson
b5f3c9fc67 Use collection expressions (part 3) 2023-12-14 12:57:11 -08:00
Jason Nelson
8a88fd0557 Use collection expressions (part 2) 2023-12-14 12:46:40 -08:00
Jason Nelson
cc7623989d Fix typo on private method 2023-12-14 12:39:19 -08:00
Jason Nelson
b6a7acf5fc Use primary constructors 2023-12-14 12:35:22 -08:00
Jason Nelson
804a6f0dbc Use accelerated IndexOfAny in one more case 2023-12-14 12:32:52 -08:00
Jason Nelson
342e264988 Use collection expressions 2023-12-14 12:32:34 -08:00
Alexandre Mutel
f52ecee0b9 Update packages 2023-12-14 06:24:19 +01:00
Alexandre Mutel
a092ec23b3 Merge pull request #760 from zickb/fix_source_span_calculation_for_linebreak_inline
Fix source span calculation for LineBreakInline
2023-12-14 06:18:09 +01:00
Benni
6f1dce6306 Fix last LineBreakInline source span in multi block scenario 2023-12-14 02:53:02 +01:00
Alexandre Mutel
040a778d87 Merge pull request #759 from Akarinnnnn/fix-757
Make StringLineGroup returns a count limited Enumerator
2023-12-12 19:13:52 +01:00
Fa鸽
2ae2cf9263 Add tests for non-boxed enumerator of StringLineGroup. 2023-12-12 16:18:10 +08:00
Fa鸽
ba1e562d2f Fix complication error of StringLineGroup. 2023-12-12 16:15:55 +08:00
Fa鸽
65a02e44ec Add Enumerator GetEnumerator() for StringLineGroup 2023-12-12 15:45:30 +08:00
Alexandre Mutel
e78833ae30 Update src/Markdig/Helpers/StringLineGroup.cs 2023-12-12 08:00:54 +01:00
Fa鸽
2ab716bec1 Make StringLineGroup returns a count limited Enumerator
Fixes #757, before we return the array enumerator directly, enumerate it will get phantom empty lines.
2023-12-11 19:57:51 +08:00
Alexandre Mutel
feeb1867ce Merge pull request #753 from MihaZupan/perf-nov23-3
A few more perf improvements
2023-11-29 10:00:01 +01:00
Miha Zupan
f3aa7e73e3 Avoid Dictionary lookups in RendererBase.Write 2023-11-26 02:27:26 +01:00
Miha Zupan
dce5572356 Create inlining boundaries in MarkdownParser.Parse 2023-11-26 00:59:12 +01:00
Miha Zupan
dbbabd2221 Avoid redundant work in FencedBlockParserBase.TryContinue 2023-11-26 00:59:06 +01:00
Miha Zupan
22145c2fb0 Add UnicodeUtility helper 2023-11-26 00:58:57 +01:00
Miha Zupan
2517003edc Speed up code block arguments string parsing 2023-11-25 21:36:04 +01:00
Miha Zupan
50a3d02c2c Remove NoInlining from throw helper 2023-11-25 20:15:40 +01:00
Alexandre Mutel
40fb2b8249 Merge pull request #751 from MihaZupan/net8
.NET 8.0 and a few other perf improvements
2023-11-25 10:08:32 +01:00
Miha Zupan
5c54968807 Fix polyfill namespace 2023-11-25 03:25:37 +01:00
Miha Zupan
58ea46d58b Also install 6.0, 7.0 SDKs 2023-11-25 02:33:59 +01:00
Miha Zupan
f557e57ab1 Optimize WriteEscapeUrl 2023-11-24 03:34:45 +01:00
Miha Zupan
87aa32e1bd Optimize WriteEscape 2023-11-24 02:57:12 +01:00
Miha Zupan
4f1cb9da08 Avoid allocating strings for known emphasis character fallbacks 2023-11-24 02:44:47 +01:00
Miha Zupan
5cff880c90 Remove temporary string allocations in AutoIdentifierExtension 2023-11-24 02:44:08 +01:00
Miha Zupan
c7aec822b0 Speed up a few character checks 2023-11-24 02:41:08 +01:00
Miha Zupan
b0bde46cc1 Defer position calculations in LiteralInlineParser 2023-11-24 02:39:16 +01:00
Miha Zupan
7803417e5c Rewrite CodeInline matching to make use of vectorization 2023-11-24 02:37:45 +01:00
Miha Zupan
047c4cbcbb Skip _lineBits read on MarkdownObject creation 2023-11-24 02:28:25 +01:00
Miha Zupan
e4f57ca21e Fix build warnings 2023-11-24 02:26:51 +01:00
Miha Zupan
1f1364e69b Add SearchValues polyfill and use it in CharacterMap 2023-11-24 02:23:55 +01:00
Miha Zupan
4eea9db35c Add .NET 8.0 target 2023-11-24 02:17:47 +01:00
Alexandre Mutel
cce7284b84 Merge pull request #749 from michaelvolz/patch-1
Update Visual Studio editor link
2023-11-14 18:36:26 +01:00
Michael A. Volz (Flynn)
8e1e0b9bf3 Update Visual Studio editor link
Markdown Editor v2 (Visual Studio 2022)

This is a complete rewrite of the original Markdown Editor with tons of fixes, tweeks, and performance improvements.
2023-11-14 17:50:51 +01:00
Alexandre Mutel
7d40bc118b Merge pull request #736 from zickb/better_literal_delimiter_content_string_slice
Better literal delimiter content string slice
2023-08-30 07:29:44 +02:00
Benni
dba94a2371 Add documentation for the new constructor. 2023-08-30 00:55:35 +02:00
Benni
6d75eed3bb Don't break external users of the public constructor 2023-08-30 00:53:05 +02:00
Benni
ccb75fd5f0 Merge branch 'master' into better_literal_delimiter_content_string_slice 2023-08-30 00:50:18 +02:00
Benni
06eb6ba774 Better content string slice of delimiter literals:
instead of creating a new StringSlice only containing the delimiter chars, use the provided StringSlice from the match method with an appropriate start and end index
2023-08-30 00:40:10 +02:00
Alexandre Mutel
f15e9f020e Merge pull request #733 from zickb/fix_source_span_calculation
Fix source span calculation
2023-08-26 16:04:01 +02:00
Benni
a70ca6304f Fix source span of paragraphs in table cells and cleanup InlineProcessor 2023-08-25 08:54:30 +02:00
Benni
d26822be05 fix inline source spans calculation 2023-08-22 23:38:37 +02:00
Alexandre Mutel
5e3416f8b7 Merge pull request #726 from mkapahnke/make_allow_null_internal
Make AllowNull internal to prevent conflicts
2023-08-04 06:40:22 +02:00
Alexandre Mutel
012a57d361 Merge pull request #724 from DeveloPoel/markdown.cs_findings
Made the markdown class not partial and fixed the ToPlainText summary.
2023-08-04 06:39:58 +02:00
Alexandre Mutel
053a18c684 Merge pull request #723 from RickStrahl/MediaExtensions-RelativePathSupport
Add relative path support for Audio and Video Urls to  MediaLinks Extension
2023-08-04 06:39:38 +02:00
Maximilian Kapahnke
13265453ac make internal 2023-07-25 18:23:23 +02:00
developoel
8ea0783834 Made the markdown class not partial and fixed the ToPlainText summary. 2023-07-18 14:09:19 +02:00
Rick Strahl
3d29430337 Update src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs
Co-authored-by: Günther Foidl <gue@korporal.at>
2023-07-13 12:04:52 -07:00
Rick Strahl
81bc58c6c9 Add support for relative Urls for Video and Audio links. 2023-07-11 14:58:43 -07:00
Rick Strahl
bfe3800130 Allow for use of .NET 7.0 SDK (Major Version Roll Forward) 2023-07-11 12:13:05 -07:00
Alexandre Mutel
b7cb169fd3 Merge pull request #710 from valterc/fix-line-group-oob
Add line count check to avoid out of range
2023-04-22 13:23:04 +01:00
Valter Costa
512b28256a Improved tests 2023-04-18 17:44:41 +01:00
Valter Costa
cd5d11eeff Fix index check 2023-04-18 17:44:23 +01:00
Valter Costa
a9118774a8 Added line count check to avoid out of bounds 2023-04-18 12:46:48 +01:00
Alexandre Mutel
8155a1e3d6 Update specs 2023-02-27 07:39:46 +01:00
Alexandre Mutel
0a167248fd Merge pull request #701 from yufeih/yufeih/yamlfrontmatter
Allow YAML front matter in the middle of the document
2023-02-26 21:29:25 +01:00
Yufei Huang
9df67b7934 Allow YAML front matter in the middle of the document 2023-02-23 21:12:50 +08:00
Alexandre Mutel
8f8a145f0e Merge pull request #696 from artempyanykh/fix-setext-span
Fix incorrect setext heading source span
2023-02-16 06:41:11 +01:00
Artem Pyanykh
a6cd283183 Fix incorrect setext heading source span 2023-02-11 17:09:00 +00:00
Alexandre Mutel
851713fad9 Merge pull request #693 from carbon/fsn2
Enable ImplictUsings and use file-scoped namespaces
2023-01-24 07:46:44 +01:00
Jason Nelson
9422764f98 [Markdig] Use global usings 2023-01-21 16:41:18 -08:00
Jason Nelson
74f978ed2d [Markdig] Use file-scoped namespaces 2023-01-21 16:35:41 -08:00
Jason Nelson
66aaffaef1 [mctoc] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:53:44 -08:00
Jason Nelson
a18d8dee4f [UnicodeNormDApp] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:51:58 -08:00
Jason Nelson
96934214db [SpecFileGen] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:51:19 -08:00
Jason Nelson
ca03b7df4f [Benchmarks] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:50:40 -08:00
Jason Nelson
4660426719 [WebApp] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:49:44 -08:00
Jason Nelson
7ef2959d6e [Tests] Enable ImplictUsings and use file-scoped namespaces 2023-01-18 19:48:52 -08:00
Alexandre Mutel
78c4efb9cc Merge pull request #690 from carbon/net46
Bump  .NET framework target to net462
2023-01-14 18:19:51 +01:00
Jason Nelson
641f3fe0c1 Remove ArrayHelper 2023-01-09 21:52:02 -08:00
Jason Nelson
83145a323b Add back missing using statement 2023-01-09 20:11:44 -08:00
Jason Nelson
1abc804228 Bump .NET framework target to net462 2023-01-09 20:03:49 -08:00
Alexandre Mutel
25323f080c Merge pull request #685 from carbon/netcoreapp3.1
Drop netcoreapp3.1 target
2022-12-20 21:03:43 +01:00
Jason Nelson
38a15ef2a9 Drop netcoreapp3.1 target 2022-12-16 20:17:49 -08:00
Alexandre Mutel
f9bed5d270 Merge pull request #678 from waldyrious/copyedit-grid-tables-docs
Copyedit the grid table extension documentation
2022-12-04 18:38:24 +01:00
Waldir Pimenta
3f7d5c68c2 Copyedit the grid table extension documentation 2022-11-25 11:47:14 +00:00
Alexandre Mutel
936fe35460 Merge pull request #672 from LukeTOBrien/feature/medialink-controls
Option to set controls attribute for MediaLinks
2022-10-18 08:47:46 +02:00
Alexandre Mutel
0dc4857213 Merge pull request #674 from wbaldoumas/contriuting-docs
Add Contributing Docs
2022-10-18 08:47:27 +02:00
Alexandre Mutel
956f109419 Update contributing.md 2022-10-18 08:47:11 +02:00
Will
b1f3229812 Link contributing docs from readme 2022-10-14 08:32:15 -07:00
Will
3ceff1d076 Add Contributing Docs 2022-10-14 08:27:08 -07:00
Luke
e2e557b1c4 Change to make option more explicit 2022-10-14 16:23:53 +01:00
Luke
e155792be5 Option to set controls attribute for MediaLinks 2022-10-13 15:01:51 +01:00
Alexandre Mutel
98c687b4ed Merge pull request #666 from meziantou/issues/665
Fix ANE when parsing empty documents with trackTrivia enabled
2022-09-27 07:24:22 +02:00
Gérald Barré
8e4a732efe Fix ANE when parsing empty documents with trackTrivia enabled 2022-09-26 21:55:28 -04:00
Alexandre Mutel
bce4b70dc6 Merge pull request #649 from MihaZupan/commonmark-whitespace-punctuation
Align Whitespace and Punctuation definitions with CommonMark
2022-08-12 07:46:24 +02:00
Alexandre Mutel
1f71520de9 Merge pull request #650 from gfoidl/htmlhelper-TryParseHtmlTagOpenTag_remove_branches
Remove some branches in HtmlHelper.TryParseHtmlTagOpenTag by using bitmask
2022-08-12 07:45:31 +02:00
Günther Foidl
bfd7b6460c Remove some branches in HtmlHelper.TryParseHtmlTagOpenTag by using bitmasks 2022-07-21 12:11:03 +02:00
Miha Zupan
0e26ec5382 Align Whitespace and Punctuation definitions with CommonMark 2022-07-17 20:22:26 +02:00
Alexandre Mutel
5f80d86265 Merge pull request #642 from mnaoumov/issue-579
More accurate check for YAML renderers
2022-06-15 07:52:51 +02:00
Alexandre Mutel
b85cc0daf5 Merge pull request #644 from PaulVrugt/master
added support for ToPlainText for pipetables extension
2022-06-15 07:52:06 +02:00
Paul Vrugt
76073e81c0 added support for ToPlainText for pipetables extension 2022-06-08 19:47:48 +02:00
Michael Naumov
5b6621d729 Add tests to cover all possible cases of adding Yaml renderers 2022-06-07 15:06:14 -06:00
Michael Naumov
9723eda455 More accurate check for YAML renderers 2022-06-07 14:02:40 -06:00
Alexandre Mutel
7228ad5072 Merge pull request #638 from mnaoumov/issue-579
Add YamlFrontMatterRoundtripRenderer
2022-06-07 21:40:58 +02:00
Alexandre Mutel
96f55d0aa6 Merge pull request #637 from MihaZupan/perf-parse-overhead
Reduce the overhead of Parse calls
2022-06-07 21:38:08 +02:00
Alexandre Mutel
2d69ac4499 Merge pull request #624 from mattj23/docs
Additional parser documentation
2022-06-07 21:37:17 +02:00
Michael Naumov
5e91b9b763 Add YamlFrontMatterRoundtripRenderer 2022-05-17 16:46:24 -06:00
Miha Zupan
e6255de62b Avoid locking overhead in ObjectCache 2022-05-14 19:04:51 +02:00
Miha Zupan
c7b8772669 Avoid SelfPipeline search overhead 2022-05-14 19:03:31 +02:00
Alexandre Mutel
89a10ee76b Remove old benchmark graphs 2022-04-23 17:37:06 +02:00
Alexandre Mutel
7676079b4e Update doc readme with new benchmarks 2022-04-23 17:36:17 +02:00
Alexandre Mutel
495abab743 Update benchmark code and dependencies 2022-04-23 16:51:16 +02:00
Alexandre Mutel
210b39e8fb Improve RenderBase optimization with Type.GetTypeHandle (#632) 2022-04-23 16:21:09 +02:00
Alexandre Mutel
f09d030fd3 Cleanup code after #632 2022-04-23 14:05:26 +02:00
Alexandre Mutel
f2ca6be7a6 Fix name for SpecFileGen 2022-04-23 07:59:18 +02:00
Alexandre Mutel
94e07d11ce Revert optimization for RendererBase to use a plain type for the dictionary key (#632) 2022-04-23 07:46:26 +02:00
Alexandre Mutel
263041e899 Merge pull request #632 from MihaZupan/wasm-mt-ptr
Don't try to use the MT pointer on unsupported platforms
2022-04-23 07:38:16 +02:00
Miha Zupan
e8f9274b64 Don't try to use the MT pointer on unsupported platforms 2022-04-22 20:36:52 +02:00
Alexandre Mutel
b32e71aaeb Merge pull request #630 from MihaZupan/containerblock-copyto-628
Fix typos in ContainerBlock
2022-04-22 07:33:56 +02:00
Miha Zupan
f991b2123b Fix typos in ContainerBlock 2022-04-21 20:48:13 +02:00
Matt Jarvis
f0c200fc28 Updates to parsing documentation, inlines 2022-04-15 18:16:07 -04:00
Matt Jarvis
70184179b7 Merge remote-tracking branch 'upstream/master' into docs 2022-04-15 15:45:16 -04:00
Matt Jarvis
4169e538af Merge remote-tracking branch 'upstream/master' into docs 2022-04-02 22:22:11 -04:00
398 changed files with 35800 additions and 34980 deletions

View File

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

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>

36
contributing.md Normal file
View File

@@ -0,0 +1,36 @@
# How to Contribute
Thanks for your interest in contributing to `Markdig`! Here are a few general guidelines on contributing and
reporting bugs that we ask you to review. Following these guidelines helps to communicate that you respect the time of
the contributors managing and developing this open source project.
## Reporting Issues
Before reporting a new issue, please ensure that the issue was not already reported or fixed by searching through our
[issues list](https://github.com/xoofx/markdig/issues).
When creating a new issue, please be sure to include a **title and clear description**, as much relevant information as
possible, and, if possible, a test case.
## Sending Pull Requests
Before sending a new pull request, take a look at existing pull requests and issues to see if the proposed change or fix
has been discussed in the past, or if the change was already implemented but not yet released.
We expect new pull requests to include tests for any affected behavior, and, as we follow semantic versioning, we may
reserve breaking changes until the next major version release.
## Other Ways to Contribute
We welcome anyone that wants to contribute to `Markdig` to triage and reply to open issues to help troubleshoot
and fix existing bugs. Here is what you can do:
- Help ensure that existing issues follows the recommendations from the _[Reporting Issues](#reporting-issues)_ section,
providing feedback to the issue's author on what might be missing.
instructions and code samples.
- Review existing pull requests, and testing patches against real existing applications that use `Markdig`.
- Write a test, or add a missing test case to an existing test.
Thanks again for your interest on contributing to `Markdig`!
:heart:

View File

@@ -125,34 +125,3 @@ public class BlinkExtension : IMarkdownExtension
}
}
```
## Parsers
Markdig has two types of parsers, both of which derive from `ParserBase<TProcessor>`.
Block parsers, derived from `BlockParser`, identify block elements from lines in the source text and push them onto the abstract syntax tree. Inline parsers, derived from `InlineParser`, identify inline elements from `LeafBlock` elements and push them into an attached container.
Both inline and block parsers are regex-free, and instead work on finding opening characters and then making fast read-only views into the source text.
### Block Parser
**(The contents of this section I am very unsure of, this is from my reading of the code but I could use some guidance here)**
**(Does `CanInterrupt` specifically refer to interrupting a paragraph block?)**
In order to be added to the parsing pipeline, all block parsers must be derived from `BlockParser`.
Internally, the main parsing algorithm will be stepping through the source text, using the `HasOpeningCharacter(char c)` method of the block parser collection to pre-identify parsers which *could* be opening a block at a given position in the text based on the active character. Thus any derived implementation needs to set the value of the `char[]? OpeningCharacter` property with the initial characters that might begin the block.
If a parser can potentially open a block at a place in the source text it should expect to have the `TryOpen(BlockProcessor processor)` method called. This is a virtual method that must be implemented on any derived class. The `BlockProcessor` argument is a reference to an object which stores the current state of parsing and the position in the source.
**(What are the rules concerning how the `BlockState` return type should work for `TryOpen`? I see examples returning `None`, `Continue`, `BreakDiscard`, `ContinueDiscard`. How does the return value change the algorithm behavior?)**
**(Should a new block always be pushed into `processor.NewBlocks` in the `TryOpen` method?)**
As the main parsing algorithm moves forward, it will then call `TryContinue(...)` on blocks that were opened in `TryOpen(..)`.
**(Is this where/how you close a block? Is there anything that needs to be done to perform that beyond `block.UpdateSpanEnd` and returning `BlockState.Break`?)**
### Inline Parser

View File

@@ -71,7 +71,7 @@ var pipeline = new MarkdownPipelineBuilder()
var document = Markdown.Parse(markdownText, pipeline);
```
## The Parser and the Pipeline
## Markdown.Parse and the MarkdownPipeline
As metioned in the [Introduction](#introduction), Markdig's parsing machinery involves two surface components: the `Markdown.Parse(...)` method, and the `MarkdownPipeline` type. The main parsing algorithm (not to be confused with individual `BlockParser` and `InlineParser` components) lives in the `Markdown.Parse(...)` static method. The `MarkdownPipeline` is responsible for configuring the behavior of the parser.
@@ -117,6 +117,8 @@ This section discusses the pipeline builder and the concept of *extensions* in m
### Extensions (IMarkdownExtension)
***Note**: This section discusses how to consume extensions by adding them to pipeline. For a discussion on how to implement an extension, refer to the [Extensions/Parsers](parsing-extensions.md) document.*
Extensions are the primary mechanism for modifying the parsers in the pipeline.
An extension is any class which implements the `IMarkdownExtension` interface found in [IMarkdownExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/IMarkdownExtension.cs). This interface consists solely of two `Setup(...)` overloads, which both take a `MarkdownPipelineBuilder` as the first argument.
@@ -125,8 +127,6 @@ When the `MarkdownPipelineBuilder.Build()` method is invoked as the final stage
Because of this, *some* extensions may need to be ordered in relation to others, for instance if they modify a parser that gets added by a different extension. The `OrderedList<T>` class contains convenience methods to this end, which aid in finding other extensions by type and then being able to added an item before or after them.
For a discussion on how to implement an extension, refer to the [Extensions/Parsers](parsing-extensions.md) document.
### The MarkdownPipelineBuilder
Because the `MarkdownPipeline` is a sealed internal class, it cannot (and *should* not be attempted to) be created directly. Rather, the `MarkdownPipelineBuilder` manages the requisite construction of the pipeline after the configuration has been provided by the client code.
@@ -225,3 +225,112 @@ Internally, the fluent interface wraps manual operations on the three primary co
All three collections are `OrderedList<T>`, which is a collection type custom to Markdig which contains special methods for finding and inserting derived types. With the builder created, manual configuration can be performed by accessing these collections and their elements and modifying them as necessary.
***Warning**: be aware that it should not be necessary to directly modify either the `BlockParsers` or the `InlineParsers` collections directly during the pipeline configuration. Rather, these can and should be modified whenever possible through the `Setup(...)` method of extensions, which will be deferred until the pipeline is actually built and will allow for ordering such that operations dependent on other operations can be accounted for.*
## Block and Inline Parsers
Let's dive deeper into the parsing system. With a configured pipeline, the `Markdown.Parse` method will run through two two conceptual passes to produce the abstract syntax tree.
1. First, `BlockProcessor.ProcessLine` is called on the file's lines, one by one, trying to identify block elements in the source
2. Next, an `InlineProcessor` is created or borrowed and run on each block to identify inline elements.
These two conceptual operations dictate Markdig's two types of parsers, both of which derive from `ParserBase<TProcessor>`.
Block parsers, derived from `BlockParser`, identify block elements from lines in the source text and push them onto the abstract syntax tree. Inline parsers, derived from `InlineParser`, identify inline elements from `LeafBlock` elements and push them into an attached container: the `ContainerInline? LeafBlock.Inline` property.
Both inline and block parsers are regex-free, and instead work on finding opening characters and then making fast read-only views into the source text.
### Block Parser
**(The contents of this section I am very unsure of, this is from my reading of the code but I could use some guidance here)**
**(Does `CanInterrupt` specifically refer to interrupting a paragraph block?)**
In order to be added to the parsing pipeline, all block parsers must be derived from `BlockParser`.
Internally, the main parsing algorithm will be stepping through the source text, using the `HasOpeningCharacter(char c)` method of the block parser collection to pre-identify parsers which *could* be opening a block at a given position in the text based on the active character. Thus any derived implementation needs to set the value of the `char[]? OpeningCharacter` property with the initial characters that might begin the block.
If a parser can potentially open a block at a place in the source text it should expect to have the `TryOpen(BlockProcessor processor)` method called. This is a virtual method that must be implemented on any derived class. The `BlockProcessor` argument is a reference to an object which stores the current state of parsing and the position in the source.
**(What are the rules concerning how the `BlockState` return type should work for `TryOpen`? I see examples returning `None`, `Continue`, `BreakDiscard`, `ContinueDiscard`. How does the return value change the algorithm behavior?)**
**(Should a new block always be pushed into `processor.NewBlocks` in the `TryOpen` method?)**
As the main parsing algorithm moves forward, it will then call `TryContinue(...)` on blocks that were opened in `TryOpen(..)`.
**(Is this where/how you close a block? Is there anything that needs to be done to perform that beyond `block.UpdateSpanEnd` and returning `BlockState.Break`?)**
### Inline Parsers
Inline parsers extract inline markdown elements from the source, but their starting point is the text of each individual `LeafBlock` produced by the block parsing process. To understand the role of each inline parser it is necessary to first understand the inline parsing process as a whole.
#### The Inline Parsing Process
After the block parsing process has occurred, the abstract syntax tree of the document has been populated only with block elements, starting from the root `MarkdownDocument` node and ending with the individual `LeafBlock` derived block elements, most of which will be `ParagraphBlocks`, but also include things like `CodeBlocks`, `HeadingBlocks`, `FigureCaptions`, and so on.
At this point, the parsing machinery will iterate through each `LeafBlock` one by one, creating and assigning its `LeafBlock.Inline` property with an empty `ContainerInline`, and then sweeping through the `LeafBlock`'s text running the inline parsers. This occurs by the following process:
Starting at the first character of the text it will run through all of its `InlineParser` objects which have that character as a possible opening character for the type of inline they extract. The parsers will run in order (as such ordering is the *only* way which conflicts between parsers are resolved, and thus is important to the overall behavior of the parsing system) and the `Match(...)` method will be called on each candidate parser, in order, until one of them returns `true`.
The `Match(...)` method will be passed a slice of the text beginning at the *specific character* being processed and running until the end of the `LeafBlock`'s complete text. If the parser can create an `Inline` element it will do so and return `true`, otherwise it will return `false`. The parser will store the created `Inline` object in the processor's `InlineProcessor.Inline` property, which as passed into the `Match(...)` method as an argument. The parser will also advance the start of the working `StringSlice` by the characters consumed in the match.
* If the parser has created an inline element and returned `true`, that element is pushed into the deepest open `ContainerInline`
* If `false` was returned, a default `LiteralInlineParser` will run instead:
* If the `InlineProcessor.Inline` property already has an existing `LiteralInline` in it, these characters will be added to the existing `LiteralInline`, effectively growing it
* If no `LiteralInline` exists in the `InlineProcessor.Inline` property, a new one will be created containing the consumed characters and pushed into the deepest open `ContainerInline`
After that, the working text of the `LeafBlock` has been conceptually shortened by the advancing start of the working `StringSlice`, moving the starting character forward. If there is still text remaining, the process repeats from the new starting character until all of the text is consumed.
At this point, when all of the source text from the `LeafBlock` has been consumed, a post-processing step occurs. `InlineParser` objects in the pipeline which also implement `IPostInlineProcessor` are invoked on the `LeafBlock`'s root `ContainerInline`. This, for example, is the mechanism by which the unstructured output of the `EmphasisInlineParser` is then restructured into cleanly nested `EmphasisInline` and `LiteralInline` elements.
#### Responsibilities of an Inline Parser
Like the block parsers, an inline parser must provide an array of opening characters with the `char[]? OpeningCharacter` property.
However, inline parsers only require one other method, the `Match(InlineProcessor processor, ref StringSlice slice)` method, which is expected to determine if a match for the related inline is located at the starting character of the slice.
Within the `Match` method a parser should:
1. Determine if a match begins at the starting character of the `slice` argument
2. If no match exists, the method should return `false` and not advance the `Start` property of the `slice` argument
3. If a match does exist, perform the following actions:
* Instantiate the appropriate `Inline` derived class and assign it to the processor argument with `processor.Inline = myInlineObject`
* Advance the `Start` property of the `slice` argument by the number of characters contained in the match, for example by using the `NextChar()`, `SkipChar()`, or other helper methods of the `StringSlice` class
* Return `true`
While parsing, the `InlineProcessor` performing the processing, which is available to the `Match` function through the `processor` argument, contains a number of properties which can be used to access the current state of parsing. For example, the `processor.Inline` property is the mechanism for returning a new inline element, but before assignment it contains the last created inline, which in turn can be accessed for its parents.
Additionally, in the case of inlines which can be expected to contain other inlines, a possible strategy is to inject an inline element derived from `DelimiterInline` when the opening delimiter is detected, then to replace the opening delimiter with the final desired element when the closing delimiter is found. This is the strategy used by the `LinkInlineParser`, for example. In such cases the tools described in the next section, such as the `ReplaceBy` method, can be used. Note that if this method is used the post-processing should be invoked on the `InlineProcessor` in order to finalize any emphasis elements. For example, in the following code adapted from the `LinkInlineParser`:
```csharp
var parent = processor.Inline?.FirstParentOfType<MyDelimiterInline>();
if (parent is null) return;
var myInline = new MySpecialInline { /* set span and other parameters here */ };
// Replace the delimiter inline with the final inline type, adopting all of its children
parent.ReplaceBy(myInline);
// Notifies processor as we are creating an inline locally
processor.Inline = myInline;
// Process emphasis delimiters
processor.PostProcessInlines(0, myInline, null, false);
```
#### Inline Post-Processing
The purpose of post-processing inlines is typically to re-structure inline elements after the initial parsing is complete and the entire structure of the inline elements within a parent container is now available in a way it was not during the parsing process. Generally this consists of removing, replacing, and re-ordering `Inline` elements.
To this end, the `Inline` abstract base class contains several helper methods intended to allow manipulation of inline elements during the post-processing phase.
|Method|Purpose|
|-|-|
|`InsertAfter(...)`|Takes a new inline as an argument and inserts it into the same parent container after this instance|
|`InsertBefore(...)`|Takes a new inline as an argument and inserts it into the same parent container before this instance|
|`Remove()`|Removes this inline from its parent container|
|`ReplaceBy(...)`|Removes this instance and replaces it with a new inline specified in the argument. Has an option to move all of the original inline's children into the new inline.|
Additionally, the `PreviousSibling` and `NextSibling` properties can be used to determine the siblings of an inline element within its parent container. The `FirstParentOfType<T>()` method can be used to search for a parent element, which is often useful when searching for `DelimiterInline` derived elements, which are implemented as containers.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -12,7 +12,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
- Checkout [Markdown Editor v2 for Visual Studio 2022](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor2) powered by Markdig!
- Converter to **HTML**
- Passing more than **600+ tests** from the latest [CommonMark specs (0.30)](http://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
@@ -101,6 +101,10 @@ var result = Markdown.ToHtml("This is a text with some *emphasis*", pipeline);
You can have a look at the [MarkdownExtensions](https://github.com/lunet-io/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) that describes all actionable extensions (by modifying the MarkdownPipeline)
## Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. For detailed contributing guidelines, please see [contributing.md](contributing.md).
## Build
In order to build Markdig, you need to install [.NET 6.0](https://dotnet.microsoft.com/en-us/download)
@@ -112,82 +116,34 @@ This software is released under the [BSD-Clause 2 license](https://github.com/lu
## Benchmarking
This is an early preview of the benchmarking against various implementations:
The latest benchmark was collected on April 23 2022, against the following implementations:
**C implementations**:
- [cmark](https://github.com/jgm/cmark) (version: 0.25.0): Reference C implementation of CommonMark, no support for extensions
- [Moonshine](https://github.com/brandonc/moonshine) (version: : popular C Markdown processor
**.NET implementations**:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.5.x): itself
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.11.0): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMark.NET(pipe_tables)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) (version: 1.5.0): another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 1.13.0): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Marked.NET](https://github.com/T-Alex/MarkedNet) (version: 1.0.5) port of original [marked.js](https://github.com/chjj/marked) project
- [Microsoft.DocAsCode.MarkdownLite](https://github.com/dotnet/docfx/tree/dev/src/Microsoft.DocAsCode.MarkdownLite) (version: 2.0.1) used by the [docfx](https://github.com/dotnet/docfx) project
### Analysis of the results:
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
- **Among the best in CPU**, Extremely competitive and often faster than other implementations (not feature wise equivalent)
- **15% to 30% less allocations** and GC pressure
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:
![BenchMark CPU Time](img/BenchmarkCPU.png)
![BenchMark Memory](img/BenchmarkMemory.png)
### Performance for x86:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.30.2): itself
- [cmark](https://github.com/commonmark/cmark) (version: 0.30.2): Reference C implementation of CommonMark, no support for extensions
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.15.1): CommonMark implementation for .NET, no support for extensions, port of cmark, deprecated.
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 2.0.5): Open source C# implementation of Markdown processor, as featured previously on Stack Overflow, regexp based.
```
BenchmarkDotNet-Dev=v0.9.7.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
JitModules=clrjit-v4.6.1080.0
// * Summary *
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK=6.0.202
[Host] : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
DefaultJob : .NET 6.0.4 (6.0.422.16404), X64 RyuJIT
Method | Median | StdDev |Scaled | Gen 0 | Gen 1| Gen 2|Bytes Allocated/Op |
--------------------------- |------------ |---------- |------ | ------ |------|---------|------------------ |
Markdig | 5.5316 ms | 0.0372 ms | 0.71 | 56.00| 21.00| 49.00| 1,285,917.31 |
CommonMark.NET(master) | 4.7035 ms | 0.0422 ms | 0.60 | 113.00| 7.00| 49.00| 1,502,404.60 |
CommonMark.NET(pipe_tables) | 5.6164 ms | 0.0298 ms | 0.72 | 111.00| 56.00| 49.00| 1,863,128.13 |
MarkdownDeep | 7.8193 ms | 0.0334 ms | 1.00 | 120.00| 56.00| 49.00| 1,884,854.85 |
cmark | 4.2698 ms | 0.1526 ms | 0.55 | -| -| -| NA |
Moonshine | 6.0929 ms | 0.1053 ms | 1.28 | -| -| -| NA |
Marked.NET | 207.3169 ms | 5.2628 ms | 26.51 | 0.00| 0.00| 0.00| 303,125,228.65 |
MarkdownSharp | 675.0185 ms | 2.8447 ms | 86.32 | 40.00| 27.00| 41.00| 2,413,394.17 |
Microsoft DocfxMarkdownLite | 166.3357 ms | 0.4529 ms | 21.27 |4,452.00|948.00|11,167.00| 180,218,359.60 |
| Method | Mean | Error | StdDev |
|------------------ |-----------:|----------:|----------:|
| markdig | 1.979 ms | 0.0221 ms | 0.0185 ms |
| cmark | 2.571 ms | 0.0081 ms | 0.0076 ms |
| CommonMark.NET | 2.016 ms | 0.0169 ms | 0.0158 ms |
| MarkdownSharp | 221.455 ms | 1.4442 ms | 1.3509 ms |
```
### Performance for x64:
- Markdig is roughly **x100 times faster than MarkdownSharp**
- **20% faster than the reference cmark C implementation**
```
BenchmarkDotNet-Dev=v0.9.6.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
JitModules=clrjit-v4.6.1080.0
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
--------------------- |---------- |---------- |------- |------- |------ |------------------- |
TestMarkdig | 5.5276 ms | 0.0402 ms | 109.00 | 96.00 | 84.00 | 1,537,027.66 |
TestCommonMarkNet | 4.4661 ms | 0.1190 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
TestCommonMarkNetNew | 5.3151 ms | 0.0815 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
```
## Donate

View File

@@ -1,40 +1,38 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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;
using System.Runtime.InteropServices;
using System.Text;
namespace Testamina.Markdig.Benchmarks
{
public static class CommonMarkLib
{
public static string ToHtml(string text)
{
unsafe
{
var textAsArray = Encoding.UTF8.GetBytes(text);
namespace Testamina.Markdig.Benchmarks;
fixed (void* ptext = textAsArray)
public static class CommonMarkLib
{
public static string ToHtml(string text)
{
unsafe
{
var textAsArray = Encoding.UTF8.GetBytes(text);
fixed (void* ptext = textAsArray)
{
var ptr = (byte*)cmark_markdown_to_html(new IntPtr(ptext), text.Length);
int length = 0;
while (ptr[length] != 0)
{
var ptr = (byte*)cmark_markdown_to_html(new IntPtr(ptext), text.Length);
int length = 0;
while (ptr[length] != 0)
{
length++;
}
var buffer = new byte[length];
Marshal.Copy(new IntPtr(ptr), buffer, 0, length);
var result = Encoding.UTF8.GetString(buffer);
Marshal.FreeHGlobal(new IntPtr(ptr));
return result;
length++;
}
var buffer = new byte[length];
Marshal.Copy(new IntPtr(ptr), buffer, 0, length);
var result = Encoding.UTF8.GetString(buffer);
Marshal.FreeHGlobal(new IntPtr(ptr));
return result;
}
}
// char *cmark_markdown_to_html(const char *text, size_t len, int options);
[DllImport("cmark", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr cmark_markdown_to_html(IntPtr charBuffer, int len, int options = 0);
}
// char *cmark_markdown_to_html(const char *text, size_t len, int options);
[DllImport("cmark", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr cmark_markdown_to_html(IntPtr charBuffer, int len, int options = 0);
}

View File

@@ -1,47 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Remove="spec.md" />
</ItemGroup>
<ItemGroup>
<Reference Include="CommonMarkNew, Version=0.1.0.0, Culture=neutral, PublicKeyToken=001ef8810438905d, processorArchitecture=MSIL">
<HintPath>lib\CommonMarkNew.dll</HintPath>
<SpecificVersion>False</SpecificVersion>
<Aliases>newcmark</Aliases>
<Private>True</Private>
</Reference>
<Reference Include="MoonShine">
<HintPath>lib\MoonShine.dll</HintPath>
</Reference>
<Reference Include="MarkdownDeep">
<HintPath>lib\MarkdownDeep.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include="cmark.dll">
<HintPath>cmark.dll</HintPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="libsundown.dll">
<HintPath>libsundown.dll</HintPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="spec.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.12" />
<PackageReference Include="CommonMark.NET" Version="0.15.1" />
<PackageReference Include="Markdown" Version="2.2.1" />
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.74" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="3.1.506101" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Markdig\Markdig.csproj" />

View File

@@ -1,147 +1,76 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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.
extern alias newcmark;
using System;
using System.Diagnostics;
using System.IO;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Markdig;
namespace Testamina.Markdig.Benchmarks
namespace Testamina.Markdig.Benchmarks;
//[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit, processCount: 1, warmupIterationCount: 2)]
public class Program
{
//[BenchmarkTask(platform: BenchmarkPlatform.X64, jitVersion: BenchmarkJitVersion.RyuJit, processCount: 1, warmupIterationCount: 2)]
public class Program
private string text;
public Program()
{
private string text;
//text = File.ReadAllText("progit.md");
text = File.ReadAllText("spec.md");
}
public Program()
{
//text = File.ReadAllText("progit.md");
text = File.ReadAllText("spec.md");
}
//[Benchmark(Description = "TestMarkdig", OperationsPerInvoke = 4096)]
[Benchmark(Description = "markdig")]
public void TestMarkdig()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
Markdown.ToHtml(text);
//File.WriteAllText("spec.html", writer.ToString());
}
//[Benchmark(Description = "TestMarkdig", OperationsPerInvoke = 4096)]
[Benchmark]
public void TestMarkdig()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
Markdown.ToHtml(text);
//File.WriteAllText("spec.html", writer.ToString());
}
[Benchmark(Description = "cmark")]
public void TestCommonMarkCpp()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
CommonMarkLib.ToHtml(text);
//File.WriteAllText("spec.html", writer.ToString());
}
[Benchmark]
public void TestCommonMarkCpp()
{
//var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
CommonMarkLib.ToHtml(text);
//File.WriteAllText("spec.html", writer.ToString());
}
[Benchmark(Description = "CommonMark.NET")]
public void TestCommonMarkNet()
{
////var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
// var reader = new StringReader(text);
//CommonMark.CommonMarkConverter.Parse(reader);
//CommonMark.CommonMarkConverter.Parse(reader);
//reader.Dispose();
//var writer = new StringWriter();
CommonMark.CommonMarkConverter.Convert(text);
//writer.Flush();
//writer.ToString();
}
[Benchmark]
public void TestCommonMarkNet()
{
////var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
// var reader = new StringReader(text);
//CommonMark.CommonMarkConverter.Parse(reader);
//CommonMark.CommonMarkConverter.Parse(reader);
//reader.Dispose();
//var writer = new StringWriter();
CommonMark.CommonMarkConverter.Convert(text);
//writer.Flush();
//writer.ToString();
}
[Benchmark(Description = "MarkdownSharp")]
public void TestMarkdownSharp()
{
new MarkdownSharp.Markdown().Transform(text);
}
[Benchmark]
public void TestCommonMarkNetNew()
{
////var reader = new StreamReader(File.Open("spec.md", FileMode.Open));
// var reader = new StringReader(text);
//CommonMark.CommonMarkConverter.Parse(reader);
//CommonMark.CommonMarkConverter.Parse(reader);
//reader.Dispose();
//var writer = new StringWriter();
newcmark::CommonMark.CommonMarkConverter.Convert(text);
//writer.Flush();
//writer.ToString();
}
static void Main(string[] args)
{
var config = ManualConfig.Create(DefaultConfig.Instance);
//var gcDiagnoser = new MemoryDiagnoser();
//config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 });
//config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 });
//config.Add(gcDiagnoser);
[Benchmark]
public void TestMarkdownDeep()
{
new MarkdownDeep.Markdown().Transform(text);
}
[Benchmark]
public void TestMarkdownSharp()
{
new MarkdownSharp.Markdown().Transform(text);
}
[Benchmark]
public void TestMoonshine()
{
Sundown.MoonShine.Markdownify(text);
}
static void Main(string[] args)
{
bool markdig = args.Length == 0;
bool simpleBench = false;
if (simpleBench)
{
var clock = Stopwatch.StartNew();
var program = new Program();
GC.Collect(2, GCCollectionMode.Forced, true);
var gc0 = GC.CollectionCount(0);
var gc1 = GC.CollectionCount(1);
var gc2 = GC.CollectionCount(2);
const int count = 12*64;
for (int i = 0; i < count; i++)
{
if (markdig)
{
program.TestMarkdig();
}
else
{
program.TestCommonMarkNetNew();
}
}
clock.Stop();
Console.WriteLine((markdig ? "MarkDig" : "CommonMark") + $" => time: {(double)clock.ElapsedMilliseconds/count}ms (total {clock.ElapsedMilliseconds}ms)");
DumpGC(gc0, gc1, gc2);
}
else
{
//new TestMatchPerf().TestMatch();
var config = ManualConfig.Create(DefaultConfig.Instance);
//var gcDiagnoser = new MemoryDiagnoser();
//config.Add(new Job { Mode = Mode.SingleRun, LaunchCount = 2, WarmupCount = 2, IterationTime = 1024, TargetCount = 10 });
//config.Add(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 });
//config.Add(gcDiagnoser);
//var config = DefaultConfig.Instance;
BenchmarkRunner.Run<Program>(config);
//BenchmarkRunner.Run<TestDictionary>(config);
//BenchmarkRunner.Run<TestMatchPerf>();
//BenchmarkRunner.Run<TestStringPerf>();
}
}
private static void DumpGC(int gc0, int gc1, int gc2)
{
Console.WriteLine($"gc0: {GC.CollectionCount(0)-gc0}");
Console.WriteLine($"gc1: {GC.CollectionCount(1)-gc1}");
Console.WriteLine($"gc2: {GC.CollectionCount(2)-gc2}");
}
//var config = DefaultConfig.Instance;
BenchmarkRunner.Run<Program>(config);
//BenchmarkRunner.Run<TestDictionary>(config);
//BenchmarkRunner.Run<TestMatchPerf>();
//BenchmarkRunner.Run<TestStringPerf>();
}
}

View File

@@ -2,81 +2,79 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace Testamina.Markdig.Benchmarks
namespace Testamina.Markdig.Benchmarks;
public class TextRegexHelper
{
public class TextRegexHelper
private readonly Dictionary<string, string> replacers;
private readonly Regex regex;
public TextRegexHelper(Dictionary<string, string> replacers)
{
private readonly Dictionary<string, string> replacers;
private readonly Regex regex;
this.replacers = replacers;
var builder = new StringBuilder();
public TextRegexHelper(Dictionary<string, string> replacers)
// (?<1>:value:?)|(?<1>:noo:?)
foreach (var replace in replacers)
{
this.replacers = replacers;
var builder = new StringBuilder();
// (?<1>:value:?)|(?<1>:noo:?)
foreach (var replace in replacers)
var matchStr = Regex.Escape(replace.Key);
if (builder.Length > 0)
{
var matchStr = Regex.Escape(replace.Key);
if (builder.Length > 0)
{
builder.Append('|');
}
builder.Append("(?<1>").Append(matchStr).Append("?)");
builder.Append('|');
}
regex = new Regex(builder.ToString());
builder.Append("(?<1>").Append(matchStr).Append("?)");
}
public bool TryMatch(string text, int offset, out string matchText, out string replaceText)
{
replaceText = null;
matchText = null;
var result = regex.Match(text, offset);
if (!result.Success)
{
return false;
}
matchText = result.Groups[1].Value;
replaceText = replacers[matchText];
return true;
}
regex = new Regex(builder.ToString());
}
/*
public class TestMatchPerf
public bool TryMatch(string text, int offset, out string matchText, out string replaceText)
{
private readonly TextMatchHelper matcher;
public TestMatchPerf()
replaceText = null;
matchText = null;
var result = regex.Match(text, offset);
if (!result.Success)
{
var replacers = new Dictionary<string, string>();
for (int i = 0; i < 1000; i++)
{
replacers.Add($":z{i}:", i.ToString());
}
replacers.Add(":abc:", "yes");
matcher = new TextMatchHelper(new HashSet<string>(replacers.Keys));
return false;
}
[Benchmark]
public void TestMatch()
{
matchText = result.Groups[1].Value;
replaceText = replacers[matchText];
return true;
}
}
for (int i = 0; i < 1000; i++)
{
string matchText;
//var text = ":z150: this is a long string";
var text = ":z1:";
matcher.TryMatch(text, 0, text.Length, out matchText);
}
/*
public class TestMatchPerf
{
private readonly TextMatchHelper matcher;
public TestMatchPerf()
{
var replacers = new Dictionary<string, string>();
for (int i = 0; i < 1000; i++)
{
replacers.Add($":z{i}:", i.ToString());
}
replacers.Add(":abc:", "yes");
matcher = new TextMatchHelper(new HashSet<string>(replacers.Keys));
}
[Benchmark]
public void TestMatch()
{
for (int i = 0; i < 1000; i++)
{
string matchText;
//var text = ":z150: this is a long string";
var text = ":z1:";
matcher.TryMatch(text, 0, text.Length, out matchText);
}
}
*/
}
}
*/

View File

@@ -1,58 +1,57 @@
using System.IO;
using BenchmarkDotNet.Attributes;
using Markdig.Renderers;
namespace Testamina.Markdig.Benchmarks
namespace Testamina.Markdig.Benchmarks;
public class TestStringPerf
{
public class TestStringPerf
private string text;
public TestStringPerf()
{
private string text;
public TestStringPerf()
{
text = new string('a', 1000);
}
[Benchmark]
public void TestIndexOfAny()
{
var writer = new HtmlRenderer(new StringWriter());
for (int i = 0; i < 100; i++)
{
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
}
}
//[Benchmark]
//public void TestCustomIndexOfAny()
//{
// var writer = new HtmlRenderer(new StringWriter());
// for (int i = 0; i < 100; i++)
// {
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// }
//}
text = new string('a', 1000);
}
[Benchmark]
public void TestIndexOfAny()
{
var writer = new HtmlRenderer(new StringWriter());
for (int i = 0; i < 100; i++)
{
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
writer.WriteEscape(text, 0, text.Length);
}
}
//[Benchmark]
//public void TestCustomIndexOfAny()
//{
// var writer = new HtmlRenderer(new StringWriter());
// for (int i = 0; i < 100; i++)
// {
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// writer.WriteEscapeOptimized(text, 0, text.Length);
// }
//}
}

Binary file not shown.

View File

@@ -0,0 +1,5 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
global using Assert = NUnit.Framework.Legacy.ClassicAssert;

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<ImplicitUsings>enable</ImplicitUsings>
<StartupObject>Markdig.Tests.Program</StartupObject>
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.dll</SpecExecutable>
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net6.0\SpecFileGen.timestamp</SpecTimestamp>
<SpecExecutable>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net8.0\SpecFileGen.dll</SpecExecutable>
<SpecTimestamp>$(MSBuildProjectDirectory)\..\SpecFileGen\bin\$(Configuration)\net8.0\SpecFileGen.timestamp</SpecTimestamp>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
@@ -20,6 +21,10 @@
<ProjectReference Include="..\SpecFileGen\SpecFileGen.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
</ItemGroup>
<ItemGroup>
<ItemSpecExecutable Include="$(SpecExecutable)" />
<InputSpecFiles Include="Specs\*.md" />

View File

@@ -1,192 +1,192 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Markdig.Extensions.AutoLinks;
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class MiscTests
{
public class MiscTests
[Test]
public void LinkWithInvalidNonAsciiDomainNameIsIgnored()
{
[Test]
public void LinkWithInvalidNonAsciiDomainNameIsIgnored()
// Url from https://github.com/lunet-io/markdig/issues/438
_ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) ");
// Valid IDN
TestParser.TestSpec("[foo](http://ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.com\">foo</a></p>");
TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.xn--nicode-2ya.com\">foo</a></p>");
// Invalid IDN
TestParser.TestSpec("[foo](http://ünicode..com)", "<p><a href=\"http://%C3%BCnicode..com\">foo</a></p>");
}
[TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508
[TestCase("link [foo][bar]")]
[TestCase("link [][foo][bar][]")]
[TestCase("link [][foo][bar][[]]")]
[TestCase("link [foo] [bar]")]
[TestCase("link [[foo] [] [bar] [[abc]def]]")]
[TestCase("[]")]
[TestCase("[ ]")]
[TestCase("[bar][]")]
[TestCase("[bar][ foo]")]
[TestCase("[bar][foo ][]")]
[TestCase("[bar][fo[ ]o ][][]")]
[TestCase("[a]b[c[d[e]f]g]h")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")]
public void LinkTextMayContainBalancedBrackets(string linkText)
{
string markdown = $"[{linkText}](/uri)";
string expected = $@"<p><a href=""/uri"">{linkText}</a></p>";
TestParser.TestSpec(markdown, expected);
// Make the link text unbalanced
foreach (var bracketIndex in linkText
.Select((c, i) => new Tuple<char, int>(c, i))
.Where(t => t.Item1 == '[' || t.Item1 == ']')
.Select(t => t.Item2))
{
// Url from https://github.com/lunet-io/markdig/issues/438
_ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) ");
string brokenLinkText = linkText.Remove(bracketIndex, 1);
// Valid IDN
TestParser.TestSpec("[foo](http://ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.com\">foo</a></p>");
TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.xn--nicode-2ya.com\">foo</a></p>");
markdown = $"[{brokenLinkText}](/uri)";
expected = $@"<p><a href=""/uri"">{brokenLinkText}</a></p>";
// Invalid IDN
TestParser.TestSpec("[foo](http://ünicode..com)", "<p><a href=\"http://%C3%BCnicode..com\">foo</a></p>");
string actual = Markdown.ToHtml(markdown);
Assert.AreNotEqual(expected, actual);
}
}
[TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508
[TestCase("link [foo][bar]")]
[TestCase("link [][foo][bar][]")]
[TestCase("link [][foo][bar][[]]")]
[TestCase("link [foo] [bar]")]
[TestCase("link [[foo] [] [bar] [[abc]def]]")]
[TestCase("[]")]
[TestCase("[ ]")]
[TestCase("[bar][]")]
[TestCase("[bar][ foo]")]
[TestCase("[bar][foo ][]")]
[TestCase("[bar][fo[ ]o ][][]")]
[TestCase("[a]b[c[d[e]f]g]h")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")]
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")]
public void LinkTextMayContainBalancedBrackets(string linkText)
[Theory]
[TestCase('[', 9 * 1024, true, false)]
[TestCase('[', 11 * 1024, true, true)]
[TestCase('[', 100, false, false)]
[TestCase('[', 150, false, true)]
[TestCase('>', 100, true, false)]
[TestCase('>', 150, true, true)]
public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow)
{
var markdown = new string(c, count);
TestDelegate test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown);
if (shouldThrow)
{
string markdown = $"[{linkText}](/uri)";
string expected = $@"<p><a href=""/uri"">{linkText}</a></p>";
TestParser.TestSpec(markdown, expected);
// Make the link text unbalanced
foreach (var bracketIndex in linkText
.Select((c, i) => new Tuple<char, int>(c, i))
.Where(t => t.Item1 == '[' || t.Item1 == ']')
.Select(t => t.Item2))
{
string brokenLinkText = linkText.Remove(bracketIndex, 1);
markdown = $"[{brokenLinkText}](/uri)";
expected = $@"<p><a href=""/uri"">{brokenLinkText}</a></p>";
string actual = Markdown.ToHtml(markdown);
Assert.AreNotEqual(expected, actual);
}
Exception e = Assert.Throws<ArgumentException>(test);
Assert.True(e.Message.Contains("depth limit"));
}
[Theory]
[TestCase('[', 9 * 1024, true, false)]
[TestCase('[', 11 * 1024, true, true)]
[TestCase('[', 100, false, false)]
[TestCase('[', 150, false, true)]
[TestCase('>', 100, true, false)]
[TestCase('>', 150, true, true)]
public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow)
else
{
var markdown = new string(c, count);
TestDelegate test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown);
if (shouldThrow)
{
Exception e = Assert.Throws<ArgumentException>(test);
Assert.True(e.Message.Contains("depth limit"));
}
else
{
test();
}
test();
}
}
[Test]
public void IsIssue356Corrected()
[Test]
public void IsIssue356Corrected()
{
string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97";
string expected = @"<p><a href=""https://foo.bar/path/%5C#m4mv5W0GYKZpGvfA.97"">https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97</a></p>";
TestParser.TestSpec($"<{input}>", expected);
TestParser.TestSpec(input, expected, "autolinks|advanced");
}
[Test]
public void IsIssue365Corrected()
{
// The scheme must be escaped too...
string input = "![image](\"onclick=\"alert&amp;#40;'click'&amp;#41;\"://)";
string expected = "<p><img src=\"%22onclick=%22alert&amp;#40;%27click%27&amp;#41;%22://\" alt=\"image\" /></p>";
TestParser.TestSpec(input, expected);
}
[Test]
public void TestAltTextIsCorrectlyEscaped()
{
TestParser.TestSpec(
@"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)",
@"<p><img src=""girl.png"" alt=""This is image alt text with quotation ' and double quotation &quot;hello&quot; world"" /></p>");
}
[Test]
public void TestChangelogPRLinksMatchDescription()
{
string solutionFolder = Path.GetFullPath(Path.Combine(TestParser.TestsDirectory, "../.."));
string changelogPath = Path.Combine(solutionFolder, "changelog.md");
string changelog = File.ReadAllText(changelogPath);
var matches = Regex.Matches(changelog, @"\(\[\(PR #(\d+)\)\]\(.*?pull\/(\d+)\)\)");
Assert.Greater(matches.Count, 0);
foreach (Match match in matches)
{
string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97";
string expected = @"<p><a href=""https://foo.bar/path/%5C#m4mv5W0GYKZpGvfA.97"">https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97</a></p>";
TestParser.TestSpec($"<{input}>", expected);
TestParser.TestSpec(input, expected, "autolinks|advanced");
Assert.True(int.TryParse(match.Groups[1].Value, out int textNr));
Assert.True(int.TryParse(match.Groups[2].Value, out int linkNr));
Assert.AreEqual(textNr, linkNr);
}
}
[Test]
public void IsIssue365Corrected()
{
// The scheme must be escaped too...
string input = "![image](\"onclick=\"alert&amp;#40;'click'&amp;#41;\"://)";
string expected = "<p><img src=\"%22onclick=%22alert&amp;#40;%27click%27&amp;#41;%22://\" alt=\"image\" /></p>";
[Test]
public void TestFixHang()
{
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md"));
_ = Markdown.ToHtml(input);
}
TestParser.TestSpec(input, expected);
}
[Test]
public void TestInvalidHtmlEntity()
{
var input = "9&ddr;&*&ddr;&de<64><65>__";
TestParser.TestSpec(input, "<p>9&amp;ddr;&amp;*&amp;ddr;&amp;de<64><65>__</p>");
}
[Test]
public void TestAltTextIsCorrectlyEscaped()
{
TestParser.TestSpec(
@"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)",
@"<p><img src=""girl.png"" alt=""This is image alt text with quotation ' and double quotation &quot;hello&quot; world"" /></p>");
}
[Test]
public void TestInvalidCharacterHandling()
{
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md"));
_ = Markdown.ToHtml(input);
}
[Test]
public void TestChangelogPRLinksMatchDescription()
{
string solutionFolder = Path.GetFullPath(Path.Combine(TestParser.TestsDirectory, "../.."));
string changelogPath = Path.Combine(solutionFolder, "changelog.md");
string changelog = File.ReadAllText(changelogPath);
var matches = Regex.Matches(changelog, @"\(\[\(PR #(\d+)\)\]\(.*?pull\/(\d+)\)\)");
Assert.Greater(matches.Count, 0);
foreach (Match match in matches)
{
Assert.True(int.TryParse(match.Groups[1].Value, out int textNr));
Assert.True(int.TryParse(match.Groups[2].Value, out int linkNr));
Assert.AreEqual(textNr, linkNr);
}
}
[Test]
public void TestInvalidCodeEscape()
{
var input = "```**Header** ";
_ = Markdown.ToHtml(input);
}
[Test]
public void TestFixHang()
{
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md"));
_ = Markdown.ToHtml(input);
}
[Test]
public void TestEmphasisAndHtmlEntity()
{
var markdownText = "*Unlimited-Fun&#174;*&#174;";
TestParser.TestSpec(markdownText, "<p><em>Unlimited-Fun®</em>®</p>");
}
[Test]
public void TestInvalidHtmlEntity()
{
var input = "9&ddr;&*&ddr;&de<64><65>__";
TestParser.TestSpec(input, "<p>9&amp;ddr;&amp;*&amp;ddr;&amp;de<64><65>__</p>");
}
[Test]
public void TestInvalidCharacterHandling()
{
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md"));
_ = Markdown.ToHtml(input);
}
[Test]
public void TestInvalidCodeEscape()
{
var input = "```**Header** ";
_ = Markdown.ToHtml(input);
}
[Test]
public void TestEmphasisAndHtmlEntity()
{
var markdownText = "*Unlimited-Fun&#174;*&#174;";
TestParser.TestSpec(markdownText, "<p><em>Unlimited-Fun®</em>®</p>");
}
[Test]
public void TestThematicInsideCodeBlockInsideList()
{
var input = @"1. In the :
[Test]
public void TestThematicInsideCodeBlockInsideList()
{
var input = @"1. In the :
```
Id DisplayName Description
-- ----------- -----------
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
```";
TestParser.TestSpec(input, @"<ol>
TestParser.TestSpec(input, @"<ol>
<li><p>In the :</p>
<pre><code>Id DisplayName Description
-- ----------- -----------
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
</code></pre></li>
</ol>");
}
}
[Test]
public void VisualizeMathExpressions()
{
string math = @"Math expressions
[Test]
public void VisualizeMathExpressions()
{
string math = @"Math expressions
$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
@@ -199,98 +199,122 @@ $$
<div class=""math"">
\begin{align}
\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
& = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
& \approx \frac{73}{12}\left(1 - \frac{1}{2\cdot73^2}\right)
\end{align}
</div>
";
//Console.WriteLine("Math Expressions:\n");
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var html = Markdown.ToHtml(math, pl);
//Console.WriteLine(html);
}
//Console.WriteLine("Math Expressions:\n");
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var html = Markdown.ToHtml(math, pl);
//Console.WriteLine(html);
}
[Test]
public void InlineMathExpression()
{
string math = @"Math expressions
[Test]
public void InlineMathExpression()
{
string math = @"Math expressions
$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
";
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var html = Markdown.ToHtml(math, pl);
var html = Markdown.ToHtml(math, pl);
var test1 = html.Contains("<p><span class=\"math\">\\(");
var test2 = html.Contains("\\)</span></p>");
if (!test1 || !test2)
{
Console.WriteLine(html);
}
Assert.IsTrue(test1, "Leading bracket missing");
Assert.IsTrue(test2, "Trailing bracket missing");
var test1 = html.Contains("<p><span class=\"math\">\\(");
var test2 = html.Contains("\\)</span></p>");
if (!test1 || !test2)
{
Console.WriteLine(html);
}
[Test]
public void BlockMathExpression()
{
string math = @"Math expressions
Assert.IsTrue(test1, "Leading bracket missing");
Assert.IsTrue(test2, "Trailing bracket missing");
}
[Test]
public void BlockMathExpression()
{
string math = @"Math expressions
$$
\frac{n!}{k!(n-k)!} = \binom{n}{k}
$$
";
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
var html = Markdown.ToHtml(math, pl);
var test1 = html.Contains("<div class=\"math\">\n\\[");
var test2 = html.Contains("\\]</div>");
if (!test1 || !test2)
{
Console.WriteLine(html);
}
Assert.IsTrue(test1, "Leading bracket missing");
Assert.IsTrue(test2, "Trailing bracket missing");
}
[Test]
public void CanDisableParsingHeadings()
var html = Markdown.ToHtml(math, pl);
var test1 = html.Contains("<div class=\"math\">\n\\[");
var test2 = html.Contains("\\]</div>");
if (!test1 || !test2)
{
var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build();
TestParser.TestSpec("Foo\n===", "<h1>Foo</h1>");
TestParser.TestSpec("Foo\n===", "<p>Foo\n===</p>", noHeadingsPipeline);
TestParser.TestSpec("# Heading 1", "<h1>Heading 1</h1>");
TestParser.TestSpec("# Heading 1", "<p># Heading 1</p>", noHeadingsPipeline);
// Does not also disable link reference definitions
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>");
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>", noHeadingsPipeline);
Console.WriteLine(html);
}
[Test]
public void CanOpenAutoLinksInNewWindow()
{
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build();
Assert.IsTrue(test1, "Leading bracket missing");
Assert.IsTrue(test2, "Trailing bracket missing");
}
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\" target=\"_blank\">www.foo.bar</a></p>", newWindowPipeline);
}
[Test]
public void CanDisableParsingHeadings()
{
var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build();
[Test]
public void CanUseHttpsPrefixForWWWAutoLinks()
{
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build();
TestParser.TestSpec("Foo\n===", "<h1>Foo</h1>");
TestParser.TestSpec("Foo\n===", "<p>Foo\n===</p>", noHeadingsPipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"https://www.foo.bar\">www.foo.bar</a></p>", httpsPipeline);
}
TestParser.TestSpec("# Heading 1", "<h1>Heading 1</h1>");
TestParser.TestSpec("# Heading 1", "<p># Heading 1</p>", noHeadingsPipeline);
// Does not also disable link reference definitions
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>");
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>", noHeadingsPipeline);
}
[Test]
public void CanOpenAutoLinksInNewWindow()
{
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build();
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\" target=\"_blank\">www.foo.bar</a></p>", newWindowPipeline);
}
[Test]
public void CanUseHttpsPrefixForWWWAutoLinks()
{
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build();
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
TestParser.TestSpec("www.foo.bar", "<p><a href=\"https://www.foo.bar\">www.foo.bar</a></p>", httpsPipeline);
}
[Test]
public void RootInlineHasCorrectSourceSpan()
{
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
pipeline.TrackTrivia = true;
var document = Markdown.Parse("0123456789\n", pipeline);
var expectedSourceSpan = new SourceSpan(0, 10);
Assert.That(((LeafBlock)document.LastChild).Inline.Span == expectedSourceSpan);
}
[Test]
public void RootInlineInTableCellHasCorrectSourceSpan()
{
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseAdvancedExtensions().Build();
pipeline.TrackTrivia = true;
var document = Markdown.Parse("| a | b |\n| --- | --- |\n| <span id=\"dest\"></span><span id=\"DEST\"></span>*dest*<br/> | \\[in\\] The address of the result of the operation.<br/> |", pipeline);
var paragraph = (ParagraphBlock)((TableCell)((TableRow)((Table)document.LastChild).LastChild).First()).LastChild;
Assert.That(paragraph.Inline.Span.Start == paragraph.Inline.FirstChild.Span.Start);
Assert.That(paragraph.Inline.Span.End == paragraph.Inline.LastChild.Span.End);
}
}

View File

@@ -2,18 +2,15 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
namespace Markdig.Tests;
namespace Markdig.Tests
class Program
{
class Program
public static void Main(string[] args)
{
public static void Main(string[] args)
{
Console.WriteLine("Run NUnit tests runner with this");
// Uncomment the following line to debug a specific tests more easily
//var tests = new Specs.CommonMarkV_0_29.TestLeafBlocksSetextHeadings();
//tests.LeafBlocksSetextHeadings_Example063();
}
Console.WriteLine("Run NUnit tests runner with this");
// Uncomment the following line to debug a specific tests more easily
//var tests = new Specs.CommonMarkV_0_29.TestLeafBlocksSetextHeadings();
//tests.LeafBlocksSetextHeadings_Example063();
}
}

View File

@@ -1,55 +1,53 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestAtxHeading
{
[TestFixture]
public class TestAtxHeading
[TestCase("# h")]
[TestCase("# h ")]
[TestCase("# h\n#h")]
[TestCase("# h\n #h")]
[TestCase("# h\n # h")]
[TestCase("# h\n # h ")]
[TestCase(" # h \n # h ")]
public void Test(string value)
{
[TestCase("# h")]
[TestCase("# h ")]
[TestCase("# h\n#h")]
[TestCase("# h\n #h")]
[TestCase("# h\n # h")]
[TestCase("# h\n # h ")]
[TestCase(" # h \n # h ")]
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
[TestCase("\n# h\n\np")]
[TestCase("\n# h\n\np\n")]
[TestCase("\n# h\n\np\n\n")]
[TestCase("\n\n# h\n\np\n\n")]
[TestCase("\n\n# h\np\n\n")]
[TestCase("\n\n# h\np\n\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("\n# h\n\np")]
[TestCase("\n# h\n\np\n")]
[TestCase("\n# h\n\np\n\n")]
[TestCase("\n\n# h\n\np\n\n")]
[TestCase("\n\n# h\np\n\n")]
[TestCase("\n\n# h\np\n\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("\n# h")]
[TestCase("\n# h\n")]
[TestCase("\n# h\r")]
[TestCase("\n# h\r\n")]
[TestCase("\n# h")]
[TestCase("\n# h\n")]
[TestCase("\n# h\r")]
[TestCase("\n# h\r\n")]
[TestCase("\r# h")]
[TestCase("\r# h\n")]
[TestCase("\r# h\r")]
[TestCase("\r# h\r\n")]
[TestCase("\r# h")]
[TestCase("\r# h\n")]
[TestCase("\r# h\r")]
[TestCase("\r# h\r\n")]
[TestCase("\r\n# h")]
[TestCase("\r\n# h\n")]
[TestCase("\r\n# h\r")]
[TestCase("\r\n# h\r\n")]
[TestCase("\r\n# h")]
[TestCase("\r\n# h\n")]
[TestCase("\r\n# h\r")]
[TestCase("\r\n# h\r\n")]
[TestCase("# h\n\n ")]
[TestCase("# h\n\n ")]
[TestCase("# h\n\n ")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("# h\n\n ")]
[TestCase("# h\n\n ")]
[TestCase("# h\n\n ")]
public void TestNewline(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,107 +1,105 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestFencedCodeBlock
{
[TestFixture]
public class TestFencedCodeBlock
[TestCase("```\nc\n```")]
[TestCase("```\nc\n```\n")]
[TestCase("\n```\nc\n```")]
[TestCase("\n\n```\nc\n```")]
[TestCase("```\nc\n```\n")]
[TestCase("```\nc\n```\n\n")]
[TestCase("\n```\nc\n```\n")]
[TestCase("\n```\nc\n```\n\n")]
[TestCase("\n\n```\nc\n```\n")]
[TestCase("\n\n```\nc\n```\n\n")]
[TestCase(" ```\nc\n````")]
[TestCase("```\nc\n````")]
[TestCase("p\n\n```\nc\n```")]
[TestCase("```\n c\n```")]
[TestCase("```\nc \n```")]
[TestCase("```\n c \n```")]
[TestCase(" ``` \n c \n ``` ")]
[TestCase("\t```\t\n\tc\t\n\t```\t")]
[TestCase("\v```\v\n\vc\v\n\v```\v")]
[TestCase("\f```\f\n\fc\f\n\f```\f")]
public void Test(string value)
{
[TestCase("```\nc\n```")]
[TestCase("```\nc\n```\n")]
[TestCase("\n```\nc\n```")]
[TestCase("\n\n```\nc\n```")]
[TestCase("```\nc\n```\n")]
[TestCase("```\nc\n```\n\n")]
[TestCase("\n```\nc\n```\n")]
[TestCase("\n```\nc\n```\n\n")]
[TestCase("\n\n```\nc\n```\n")]
[TestCase("\n\n```\nc\n```\n\n")]
RoundTrip(value);
}
[TestCase(" ```\nc\n````")]
[TestCase("```\nc\n````")]
[TestCase("p\n\n```\nc\n```")]
[TestCase("~~~ aa ``` ~~~\nfoo\n~~~")]
[TestCase("~~~ aa ``` ~~~\nfoo\n~~~ ")]
public void TestTilde(string value)
{
RoundTrip(value);
}
[TestCase("```\n c\n```")]
[TestCase("```\nc \n```")]
[TestCase("```\n c \n```")]
[TestCase("```\n c \n```")]
[TestCase("```\n c \r```")]
[TestCase("```\n c \r\n```")]
[TestCase("```\r c \n```")]
[TestCase("```\r c \r```")]
[TestCase("```\r c \r\n```")]
[TestCase("```\r\n c \n```")]
[TestCase("```\r\n c \r```")]
[TestCase("```\r\n c \r\n```")]
[TestCase(" ``` \n c \n ``` ")]
[TestCase("\t```\t\n\tc\t\n\t```\t")]
[TestCase("\v```\v\n\vc\v\n\v```\v")]
[TestCase("\f```\f\n\fc\f\n\f```\f")]
public void Test(string value)
{
RoundTrip(value);
}
[TestCase("```\n c \n```\n")]
[TestCase("```\n c \r```\n")]
[TestCase("```\n c \r\n```\n")]
[TestCase("```\r c \n```\n")]
[TestCase("```\r c \r```\n")]
[TestCase("```\r c \r\n```\n")]
[TestCase("```\r\n c \n```\n")]
[TestCase("```\r\n c \r```\n")]
[TestCase("```\r\n c \r\n```\n")]
[TestCase("~~~ aa ``` ~~~\nfoo\n~~~")]
[TestCase("~~~ aa ``` ~~~\nfoo\n~~~ ")]
public void TestTilde(string value)
{
RoundTrip(value);
}
[TestCase("```\n c \n```\r")]
[TestCase("```\n c \r```\r")]
[TestCase("```\n c \r\n```\r")]
[TestCase("```\r c \n```\r")]
[TestCase("```\r c \r```\r")]
[TestCase("```\r c \r\n```\r")]
[TestCase("```\r\n c \n```\r")]
[TestCase("```\r\n c \r```\r")]
[TestCase("```\r\n c \r\n```\r")]
[TestCase("```\n c \n```")]
[TestCase("```\n c \r```")]
[TestCase("```\n c \r\n```")]
[TestCase("```\r c \n```")]
[TestCase("```\r c \r```")]
[TestCase("```\r c \r\n```")]
[TestCase("```\r\n c \n```")]
[TestCase("```\r\n c \r```")]
[TestCase("```\r\n c \r\n```")]
[TestCase("```\n c \n```\r\n")]
[TestCase("```\n c \r```\r\n")]
[TestCase("```\n c \r\n```\r\n")]
[TestCase("```\r c \n```\r\n")]
[TestCase("```\r c \r```\r\n")]
[TestCase("```\r c \r\n```\r\n")]
[TestCase("```\r\n c \n```\r\n")]
[TestCase("```\r\n c \r```\r\n")]
[TestCase("```\r\n c \r\n```\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("```\n c \n```\n")]
[TestCase("```\n c \r```\n")]
[TestCase("```\n c \r\n```\n")]
[TestCase("```\r c \n```\n")]
[TestCase("```\r c \r```\n")]
[TestCase("```\r c \r\n```\n")]
[TestCase("```\r\n c \n```\n")]
[TestCase("```\r\n c \r```\n")]
[TestCase("```\r\n c \r\n```\n")]
[TestCase("```i a\n```")]
[TestCase("```i a a2\n```")]
[TestCase("```i a a2 a3\n```")]
[TestCase("```i a a2 a3 a4\n```")]
[TestCase("```\n c \n```\r")]
[TestCase("```\n c \r```\r")]
[TestCase("```\n c \r\n```\r")]
[TestCase("```\r c \n```\r")]
[TestCase("```\r c \r```\r")]
[TestCase("```\r c \r\n```\r")]
[TestCase("```\r\n c \n```\r")]
[TestCase("```\r\n c \r```\r")]
[TestCase("```\r\n c \r\n```\r")]
[TestCase("```i\ta\n```")]
[TestCase("```i\ta a2\n```")]
[TestCase("```i\ta a2 a3\n```")]
[TestCase("```i\ta a2 a3 a4\n```")]
[TestCase("```\n c \n```\r\n")]
[TestCase("```\n c \r```\r\n")]
[TestCase("```\n c \r\n```\r\n")]
[TestCase("```\r c \n```\r\n")]
[TestCase("```\r c \r```\r\n")]
[TestCase("```\r c \r\n```\r\n")]
[TestCase("```\r\n c \n```\r\n")]
[TestCase("```\r\n c \r```\r\n")]
[TestCase("```\r\n c \r\n```\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("```i a\n```")]
[TestCase("```i a a2\n```")]
[TestCase("```i a a2 a3\n```")]
[TestCase("```i a a2 a3 a4\n```")]
[TestCase("```i\ta\n```")]
[TestCase("```i\ta a2\n```")]
[TestCase("```i\ta a2 a3\n```")]
[TestCase("```i\ta a2 a3 a4\n```")]
[TestCase("```i\ta \n```")]
[TestCase("```i\ta a2 \n```")]
[TestCase("```i\ta a2 a3 \n```")]
[TestCase("```i\ta a2 a3 a4 \n```")]
public void TestInfoArguments(string value)
{
RoundTrip(value);
}
[TestCase("```i\ta \n```")]
[TestCase("```i\ta a2 \n```")]
[TestCase("```i\ta a2 a3 \n```")]
[TestCase("```i\ta a2 a3 a4 \n```")]
public void TestInfoArguments(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,20 +1,18 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestHtmlBlock
{
[TestFixture]
public class TestHtmlBlock
[TestCase("<br>")]
[TestCase("<br>\n")]
[TestCase("<br>\n\n")]
[TestCase("<div></div>\n\n# h")]
[TestCase("p\n\n<div></div>\n")]
[TestCase("<div></div>\n\n# h")]
public void Test(string value)
{
[TestCase("<br>")]
[TestCase("<br>\n")]
[TestCase("<br>\n\n")]
[TestCase("<div></div>\n\n# h")]
[TestCase("p\n\n<div></div>\n")]
[TestCase("<div></div>\n\n# h")]
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
}

View File

@@ -1,86 +1,84 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestIndentedCodeBlock
{
[TestFixture]
public class TestIndentedCodeBlock
// A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content.
// l = line
[TestCase(" l")]
[TestCase(" l")]
[TestCase("\tl")]
[TestCase("\t\tl")]
[TestCase("\tl1\n l1")]
[TestCase("\n l")]
[TestCase("\n\n l")]
[TestCase("\n l\n")]
[TestCase("\n l\n\n")]
[TestCase("\n\n l\n")]
[TestCase("\n\n l\n\n")]
[TestCase(" l\n l")]
[TestCase(" l\n l\n l")]
// two newlines are needed for indented codeblock start after paragraph
[TestCase("p\n\n l")]
[TestCase("p\n\n l\n")]
[TestCase("p\n\n l\n\n")]
[TestCase("p\n\n l\n l")]
[TestCase("p\n\n l\n l")]
[TestCase(" l\n\np\n\n l")]
[TestCase(" l l\n\np\n\n l l")]
public void Test(string value)
{
// A codeblock is indented with 4 spaces. After the 4th space, whitespace is interpreted as content.
// l = line
[TestCase(" l")]
[TestCase(" l")]
[TestCase("\tl")]
[TestCase("\t\tl")]
[TestCase("\tl1\n l1")]
RoundTrip(value);
}
[TestCase("\n l")]
[TestCase("\n\n l")]
[TestCase("\n l\n")]
[TestCase("\n l\n\n")]
[TestCase("\n\n l\n")]
[TestCase("\n\n l\n\n")]
[TestCase(" l\n")]
[TestCase(" l\r")]
[TestCase(" l\r\n")]
[TestCase(" l\n l")]
[TestCase(" l\n l\n l")]
[TestCase(" l\n l")]
[TestCase(" l\n l\n")]
[TestCase(" l\n l\r")]
[TestCase(" l\n l\r\n")]
[TestCase(" l\r l")]
[TestCase(" l\r l\n")]
[TestCase(" l\r l\r")]
[TestCase(" l\r l\r\n")]
// two newlines are needed for indented codeblock start after paragraph
[TestCase("p\n\n l")]
[TestCase("p\n\n l\n")]
[TestCase("p\n\n l\n\n")]
[TestCase(" l\r\n l")]
[TestCase(" l\r\n l\n")]
[TestCase(" l\r\n l\r")]
[TestCase(" l\r\n l\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("p\n\n l\n l")]
[TestCase("p\n\n l\n l")]
[TestCase(" l\n\n l\n")]
[TestCase(" l\n\n\n l\n")]
public void TestNewlinesInBetweenResultInOneCodeBlock(string value)
{
var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder.EnableTrackTrivia();
MarkdownPipeline pipeline = pipelineBuilder.Build();
var markdownDocument = Markdown.Parse(value, pipeline);
[TestCase(" l\n\np\n\n l")]
[TestCase(" l l\n\np\n\n l l")]
public void Test(string value)
{
RoundTrip(value);
}
Assert.AreEqual(1, markdownDocument.Count);
}
[TestCase(" l\n")]
[TestCase(" l\r")]
[TestCase(" l\r\n")]
[TestCase(" l\n l")]
[TestCase(" l\n l\n")]
[TestCase(" l\n l\r")]
[TestCase(" l\n l\r\n")]
[TestCase(" l\r l")]
[TestCase(" l\r l\n")]
[TestCase(" l\r l\r")]
[TestCase(" l\r l\r\n")]
[TestCase(" l\r\n l")]
[TestCase(" l\r\n l\n")]
[TestCase(" l\r\n l\r")]
[TestCase(" l\r\n l\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase(" l\n\n l\n")]
[TestCase(" l\n\n\n l\n")]
public void TestNewlinesInBetweenResultInOneCodeBlock(string value)
{
var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder.EnableTrackTrivia();
MarkdownPipeline pipeline = pipelineBuilder.Build();
var markdownDocument = Markdown.Parse(value, pipeline);
Assert.AreEqual(1, markdownDocument.Count);
}
[TestCase(" l\n\np")]
[TestCase(" l\n\n\np")]
[TestCase(" l\n\n\n\np")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase(" l\n\np")]
[TestCase(" l\n\n\np")]
[TestCase(" l\n\n\n\np")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,214 +1,212 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestLinkReferenceDefinition
{
[TestFixture]
public class TestLinkReferenceDefinition
[TestCase(@"[a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@"[a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@"[a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l"" ")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase("[a]:\t/r")]
[TestCase("[a]:\t/r\t")]
[TestCase("[a]:\t/r\t\"l\"")]
[TestCase("[a]:\t/r\t\"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r\t")]
[TestCase("[a]: \t/r\t\"l\"")]
[TestCase("[a]: \t/r\t\"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r\t")]
[TestCase("[a]:\t /r\t\"l\"")]
[TestCase("[a]:\t /r\t\"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r\t")]
[TestCase("[a]: \t /r\t\"l\"")]
[TestCase("[a]: \t /r\t\"l\"\t")]
[TestCase("[a]:\t/r \t")]
[TestCase("[a]:\t/r \t\"l\"")]
[TestCase("[a]:\t/r \t\"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r \t")]
[TestCase("[a]: \t/r \t\"l\"")]
[TestCase("[a]: \t/r \t\"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r \t")]
[TestCase("[a]:\t /r \t\"l\"")]
[TestCase("[a]:\t /r \t\"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r \t")]
[TestCase("[a]: \t /r \t\"l\"")]
[TestCase("[a]: \t /r \t\"l\"\t")]
[TestCase("[a]:\t/r\t ")]
[TestCase("[a]:\t/r\t \"l\"")]
[TestCase("[a]:\t/r\t \"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r\t ")]
[TestCase("[a]: \t/r\t \"l\"")]
[TestCase("[a]: \t/r\t \"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r\t ")]
[TestCase("[a]:\t /r\t \"l\"")]
[TestCase("[a]:\t /r\t \"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r\t ")]
[TestCase("[a]: \t /r\t \"l\"")]
[TestCase("[a]: \t /r\t \"l\"\t")]
[TestCase("[a]:\t/r \t ")]
[TestCase("[a]:\t/r \t \"l\"")]
[TestCase("[a]:\t/r \t \"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r \t ")]
[TestCase("[a]: \t/r \t \"l\"")]
[TestCase("[a]: \t/r \t \"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r \t ")]
[TestCase("[a]:\t /r \t \"l\"")]
[TestCase("[a]:\t /r \t \"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r \t ")]
[TestCase("[a]: \t /r \t \"l\"")]
[TestCase("[a]: \t /r \t \"l\"\t")]
public void Test(string value)
{
[TestCase(@"[a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
RoundTrip(value);
}
[TestCase(@"[a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase(@" [a]: /r")]
[TestCase("[a]: /r\n[b]: /r\n")]
[TestCase("[a]: /r\n[b]: /r\n[c] /r\n")]
public void TestMultiple(string value)
{
RoundTrip(value);
}
[TestCase(@"[a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase(@" [a]: /r ")]
[TestCase("[a]:\f/r\f\"l\"")]
[TestCase("[a]:\v/r\v\"l\"")]
public void TestUncommonWhitespace(string value)
{
RoundTrip(value);
}
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l"" ")]
[TestCase(@"[a]: /r ""l""")]
[TestCase(@"[a]: /r ""l"" ")]
[TestCase("[a]:\n/r\n\"t\"")]
[TestCase("[a]:\n/r\r\"t\"")]
[TestCase("[a]:\n/r\r\n\"t\"")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase("[a]:\r/r\n\"t\"")]
[TestCase("[a]:\r/r\r\"t\"")]
[TestCase("[a]:\r/r\r\n\"t\"")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase("[a]:\r\n/r\n\"t\"")]
[TestCase("[a]:\r\n/r\r\"t\"")]
[TestCase("[a]:\r\n/r\r\n\"t\"")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase(@" [a]: /r ""l""")]
[TestCase(@" [a]: /r ""l"" ")]
[TestCase("[a]:\n/r\n\"t\nt\"")]
[TestCase("[a]:\n/r\n\"t\rt\"")]
[TestCase("[a]:\n/r\n\"t\r\nt\"")]
[TestCase("[a]:\t/r")]
[TestCase("[a]:\t/r\t")]
[TestCase("[a]:\t/r\t\"l\"")]
[TestCase("[a]:\t/r\t\"l\"\t")]
[TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")]
[TestCase("[a]:\n/r\n\n[a],")]
[TestCase("[a]: /r\n[b]: /r\n\n[a],")]
public void TestNewlines(string value)
{
RoundTrip(value);
}
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r\t")]
[TestCase("[a]: \t/r\t\"l\"")]
[TestCase("[a]: \t/r\t\"l\"\t")]
[TestCase("[ a]: /r")]
[TestCase("[a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[ a]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[a a]: /r")]
[TestCase("[a\va]: /r")]
[TestCase("[a\fa]: /r")]
[TestCase("[a\ta]: /r")]
[TestCase("[\va]: /r")]
[TestCase("[\fa]: /r")]
[TestCase("[\ta]: /r")]
[TestCase(@"[\]]: /r")]
public void TestLabel(string value)
{
RoundTrip(value);
}
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r\t")]
[TestCase("[a]:\t /r\t\"l\"")]
[TestCase("[a]:\t /r\t\"l\"\t")]
[TestCase("[a]: /r ()")]
[TestCase("[a]: /r (t)")]
[TestCase("[a]: /r ( t)")]
[TestCase("[a]: /r (t )")]
[TestCase("[a]: /r ( t )")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r\t")]
[TestCase("[a]: \t /r\t\"l\"")]
[TestCase("[a]: \t /r\t\"l\"\t")]
[TestCase("[a]: /r ''")]
[TestCase("[a]: /r 't'")]
[TestCase("[a]: /r ' t'")]
[TestCase("[a]: /r 't '")]
[TestCase("[a]: /r ' t '")]
public void Test_Title(string value)
{
RoundTrip(value);
}
[TestCase("[a]:\t/r \t")]
[TestCase("[a]:\t/r \t\"l\"")]
[TestCase("[a]:\t/r \t\"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r \t")]
[TestCase("[a]: \t/r \t\"l\"")]
[TestCase("[a]: \t/r \t\"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r \t")]
[TestCase("[a]:\t /r \t\"l\"")]
[TestCase("[a]:\t /r \t\"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r \t")]
[TestCase("[a]: \t /r \t\"l\"")]
[TestCase("[a]: \t /r \t\"l\"\t")]
[TestCase("[a]:\t/r\t ")]
[TestCase("[a]:\t/r\t \"l\"")]
[TestCase("[a]:\t/r\t \"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r\t ")]
[TestCase("[a]: \t/r\t \"l\"")]
[TestCase("[a]: \t/r\t \"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r\t ")]
[TestCase("[a]:\t /r\t \"l\"")]
[TestCase("[a]:\t /r\t \"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r\t ")]
[TestCase("[a]: \t /r\t \"l\"")]
[TestCase("[a]: \t /r\t \"l\"\t")]
[TestCase("[a]:\t/r \t ")]
[TestCase("[a]:\t/r \t \"l\"")]
[TestCase("[a]:\t/r \t \"l\"\t")]
[TestCase("[a]: \t/r")]
[TestCase("[a]: \t/r \t ")]
[TestCase("[a]: \t/r \t \"l\"")]
[TestCase("[a]: \t/r \t \"l\"\t")]
[TestCase("[a]:\t /r")]
[TestCase("[a]:\t /r \t ")]
[TestCase("[a]:\t /r \t \"l\"")]
[TestCase("[a]:\t /r \t \"l\"\t")]
[TestCase("[a]: \t /r")]
[TestCase("[a]: \t /r \t ")]
[TestCase("[a]: \t /r \t \"l\"")]
[TestCase("[a]: \t /r \t \"l\"\t")]
public void Test(string value)
{
RoundTrip(value);
}
[TestCase("[a]: /r\n[b]: /r\n")]
[TestCase("[a]: /r\n[b]: /r\n[c] /r\n")]
public void TestMultiple(string value)
{
RoundTrip(value);
}
[TestCase("[a]:\f/r\f\"l\"")]
[TestCase("[a]:\v/r\v\"l\"")]
public void TestUncommonWhitespace(string value)
{
RoundTrip(value);
}
[TestCase("[a]:\n/r\n\"t\"")]
[TestCase("[a]:\n/r\r\"t\"")]
[TestCase("[a]:\n/r\r\n\"t\"")]
[TestCase("[a]:\r/r\n\"t\"")]
[TestCase("[a]:\r/r\r\"t\"")]
[TestCase("[a]:\r/r\r\n\"t\"")]
[TestCase("[a]:\r\n/r\n\"t\"")]
[TestCase("[a]:\r\n/r\r\"t\"")]
[TestCase("[a]:\r\n/r\r\n\"t\"")]
[TestCase("[a]:\n/r\n\"t\nt\"")]
[TestCase("[a]:\n/r\n\"t\rt\"")]
[TestCase("[a]:\n/r\n\"t\r\nt\"")]
[TestCase("[a]:\r\n /r\t \n \t \"t\r\nt\" ")]
[TestCase("[a]:\n/r\n\n[a],")]
[TestCase("[a]: /r\n[b]: /r\n\n[a],")]
public void TestNewlines(string value)
{
RoundTrip(value);
}
[TestCase("[ a]: /r")]
[TestCase("[a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[ a]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[ a ]: /r")]
[TestCase("[a a]: /r")]
[TestCase("[a\va]: /r")]
[TestCase("[a\fa]: /r")]
[TestCase("[a\ta]: /r")]
[TestCase("[\va]: /r")]
[TestCase("[\fa]: /r")]
[TestCase("[\ta]: /r")]
[TestCase(@"[\]]: /r")]
public void TestLabel(string value)
{
RoundTrip(value);
}
[TestCase("[a]: /r ()")]
[TestCase("[a]: /r (t)")]
[TestCase("[a]: /r ( t)")]
[TestCase("[a]: /r (t )")]
[TestCase("[a]: /r ( t )")]
[TestCase("[a]: /r ''")]
[TestCase("[a]: /r 't'")]
[TestCase("[a]: /r ' t'")]
[TestCase("[a]: /r 't '")]
[TestCase("[a]: /r ' t '")]
public void Test_Title(string value)
{
RoundTrip(value);
}
[TestCase("[a]: /r\n===\n[a]")]
public void TestSetextHeader(string value)
{
RoundTrip(value);
}
[TestCase("[a]: /r\n===\n[a]")]
public void TestSetextHeader(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,23 +1,21 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestNoBlocksFoundBlock
{
[TestFixture]
public class TestNoBlocksFoundBlock
[TestCase("\r")]
[TestCase("\n")]
[TestCase("\r\n")]
[TestCase("\t")]
[TestCase("\v")]
[TestCase("\f")]
[TestCase(" ")]
[TestCase(" ")]
[TestCase(" ")]
public void Test(string value)
{
[TestCase("\r")]
[TestCase("\n")]
[TestCase("\r\n")]
[TestCase("\t")]
[TestCase("\v")]
[TestCase("\f")]
[TestCase(" ")]
[TestCase(" ")]
[TestCase(" ")]
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
}

View File

@@ -1,191 +1,189 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestOrderedList
{
[TestFixture]
public class TestOrderedList
[TestCase("1. i")]
[TestCase("1. i")]
[TestCase("1. i ")]
[TestCase("1. i ")]
[TestCase("1. i ")]
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase("1. i\n")]
[TestCase("1. i\n")]
[TestCase("1. i \n")]
[TestCase("1. i \n")]
[TestCase("1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase("1. i\n2. j")]
[TestCase("1. i\n2. j")]
[TestCase("1. i \n2. j")]
[TestCase("1. i \n2. j")]
[TestCase("1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase("1. i\n2. j\n")]
[TestCase("1. i\n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase("1. i\n2. j\n3. k")]
[TestCase("1. i\n2. j\n3. k\n")]
public void Test(string value)
{
[TestCase("1. i")]
[TestCase("1. i")]
[TestCase("1. i ")]
[TestCase("1. i ")]
[TestCase("1. i ")]
RoundTrip(value);
}
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i")]
[TestCase(" 1. i")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase(" 1. i ")]
[TestCase("1. i\n")]
[TestCase("1. i\n")]
[TestCase("1. i \n")]
[TestCase("1. i \n")]
[TestCase("1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i\n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase(" 1. i \n")]
[TestCase("1. i\n2. j")]
[TestCase("1. i\n2. j")]
[TestCase("1. i \n2. j")]
[TestCase("1. i \n2. j")]
[TestCase("1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i\n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase(" 1. i \n2. j")]
[TestCase("1. i\n2. j\n")]
[TestCase("1. i\n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase("1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i\n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase(" 1. i \n2. j\n")]
[TestCase("1. i\n2. j\n3. k")]
[TestCase("1. i\n2. j\n3. k\n")]
public void Test(string value)
{
RoundTrip(value);
}
[TestCase("10. i")]
[TestCase("11. i")]
[TestCase("10. i\n12. i")]
[TestCase("2. i\n3. i")]
public void Test_MoreThenOneStart(string value)
{
RoundTrip(value);
}
[TestCase("10. i")]
[TestCase("11. i")]
[TestCase("10. i\n12. i")]
[TestCase("2. i\n3. i")]
public void Test_MoreThenOneStart(string value)
{
RoundTrip(value);
}
[TestCase("\n1. i")]
[TestCase("\r1. i")]
[TestCase("\r\n1. i")]
[TestCase("\n1. i")]
[TestCase("\r1. i")]
[TestCase("\r\n1. i")]
[TestCase("\n1. i\n")]
[TestCase("\r1. i\n")]
[TestCase("\r\n1. i\n")]
[TestCase("\n1. i\n")]
[TestCase("\r1. i\n")]
[TestCase("\r\n1. i\n")]
[TestCase("\n1. i\r")]
[TestCase("\r1. i\r")]
[TestCase("\r\n1. i\r")]
[TestCase("\n1. i\r")]
[TestCase("\r1. i\r")]
[TestCase("\r\n1. i\r")]
[TestCase("\n1. i\r\n")]
[TestCase("\r1. i\r\n")]
[TestCase("\r\n1. i\r\n")]
[TestCase("\n1. i\r\n")]
[TestCase("\r1. i\r\n")]
[TestCase("\r\n1. i\r\n")]
[TestCase("1. i\n2. i")]
[TestCase("\n1. i\n2. i")]
[TestCase("\r1. i\n2. i")]
[TestCase("\r\n1. i\n2. i")]
[TestCase("1. i\n2. i")]
[TestCase("\n1. i\n2. i")]
[TestCase("\r1. i\n2. i")]
[TestCase("\r\n1. i\n2. i")]
[TestCase("1. i\r2. i")]
[TestCase("\n1. i\r2. i")]
[TestCase("\r1. i\r2. i")]
[TestCase("\r\n1. i\r2. i")]
[TestCase("1. i\r2. i")]
[TestCase("\n1. i\r2. i")]
[TestCase("\r1. i\r2. i")]
[TestCase("\r\n1. i\r2. i")]
[TestCase("1. i\r\n2. i")]
[TestCase("\n1. i\r\n2. i")]
[TestCase("\r1. i\r\n2. i")]
[TestCase("\r\n1. i\r\n2. i")]
[TestCase("1. i\r\n2. i")]
[TestCase("\n1. i\r\n2. i")]
[TestCase("\r1. i\r\n2. i")]
[TestCase("\r\n1. i\r\n2. i")]
[TestCase("1. i\n2. i\n")]
[TestCase("\n1. i\n2. i\n")]
[TestCase("\r1. i\n2. i\n")]
[TestCase("\r\n1. i\n2. i\n")]
[TestCase("1. i\n2. i\n")]
[TestCase("\n1. i\n2. i\n")]
[TestCase("\r1. i\n2. i\n")]
[TestCase("\r\n1. i\n2. i\n")]
[TestCase("1. i\r2. i\r")]
[TestCase("\n1. i\r2. i\r")]
[TestCase("\r1. i\r2. i\r")]
[TestCase("\r\n1. i\r2. i\r")]
[TestCase("1. i\r2. i\r")]
[TestCase("\n1. i\r2. i\r")]
[TestCase("\r1. i\r2. i\r")]
[TestCase("\r\n1. i\r2. i\r")]
[TestCase("1. i\r\n2. i\r\n")]
[TestCase("\n1. i\r\n2. i\r\n")]
[TestCase("\r1. i\r\n2. i\r\n")]
[TestCase("\r\n1. i\r\n2. i\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("1. i\r\n2. i\r\n")]
[TestCase("\n1. i\r\n2. i\r\n")]
[TestCase("\r1. i\r\n2. i\r\n")]
[TestCase("\r\n1. i\r\n2. i\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("1. i\n 1. i")]
[TestCase("1. i\n 1. i\n")]
[TestCase("1. i\n 1. i\n 2. i")]
[TestCase("1. i\n 2. i\n 3. i")]
[TestCase("1. i\n 1. i")]
[TestCase("1. i\n 1. i\n")]
[TestCase("1. i\n 1. i\n 2. i")]
[TestCase("1. i\n 2. i\n 3. i")]
[TestCase("1. i\n\t1. i")]
[TestCase("1. i\n\t1. i\n2. i")]
public void TestMultipleLevels(string value)
{
RoundTrip(value);
}
[TestCase("1. i\n\t1. i")]
[TestCase("1. i\n\t1. i\n2. i")]
public void TestMultipleLevels(string value)
{
RoundTrip(value);
}
[TestCase("1. c")]
[TestCase("1. c")]
public void Test_IndentedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("1. c")]
[TestCase("1. c")]
public void Test_IndentedCodeBlock(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,247 +1,245 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestParagraph
{
[TestFixture]
public class TestParagraph
[TestCase("p")]
[TestCase(" p")]
[TestCase("p ")]
[TestCase(" p ")]
[TestCase("p\np")]
[TestCase(" p\np")]
[TestCase("p \np")]
[TestCase(" p \np")]
[TestCase("p\n p")]
[TestCase(" p\n p")]
[TestCase("p \n p")]
[TestCase(" p \n p")]
[TestCase("p\np ")]
[TestCase(" p\np ")]
[TestCase("p \np ")]
[TestCase(" p \np ")]
[TestCase("p\n\n p ")]
[TestCase(" p\n\n p ")]
[TestCase("p \n\n p ")]
[TestCase(" p \n\n p ")]
[TestCase("p\n\np")]
[TestCase(" p\n\np")]
[TestCase("p \n\np")]
[TestCase(" p \n\np")]
[TestCase("p\n\n p")]
[TestCase(" p\n\n p")]
[TestCase("p \n\n p")]
[TestCase(" p \n\n p")]
[TestCase("p\n\np ")]
[TestCase(" p\n\np ")]
[TestCase("p \n\np ")]
[TestCase(" p \n\np ")]
[TestCase("p\n\n p ")]
[TestCase(" p\n\n p ")]
[TestCase("p \n\n p ")]
[TestCase(" p \n\n p ")]
[TestCase("\np")]
[TestCase("\n p")]
[TestCase("\np ")]
[TestCase("\n p ")]
[TestCase("\np\np")]
[TestCase("\n p\np")]
[TestCase("\np \np")]
[TestCase("\n p \np")]
[TestCase("\np\n p")]
[TestCase("\n p\n p")]
[TestCase("\np \n p")]
[TestCase("\n p \n p")]
[TestCase("\np\np ")]
[TestCase("\n p\np ")]
[TestCase("\np \np ")]
[TestCase("\n p \np ")]
[TestCase("\np\n\n p ")]
[TestCase("\n p\n\n p ")]
[TestCase("\np \n\n p ")]
[TestCase("\n p \n\n p ")]
[TestCase("\np\n\np")]
[TestCase("\n p\n\np")]
[TestCase("\np \n\np")]
[TestCase("\n p \n\np")]
[TestCase("\np\n\n p")]
[TestCase("\n p\n\n p")]
[TestCase("\np \n\n p")]
[TestCase("\n p \n\n p")]
[TestCase("\np\n\np ")]
[TestCase("\n p\n\np ")]
[TestCase("\np \n\np ")]
[TestCase("\n p \n\np ")]
[TestCase("\np\n\n p ")]
[TestCase("\n p\n\n p ")]
[TestCase("\np \n\n p ")]
[TestCase("\n p \n\n p ")]
[TestCase("p p")]
[TestCase("p\tp")]
[TestCase("p \tp")]
[TestCase("p \t p")]
[TestCase("p \tp")]
// special cases
[TestCase(" p \n\n\n\n p \n\n")]
[TestCase("\n\np")]
[TestCase("p\n")]
[TestCase("p\n\n")]
[TestCase("p\np\n p")]
[TestCase("p\np\n p\n p")]
public void Test(string value)
{
[TestCase("p")]
[TestCase(" p")]
[TestCase("p ")]
[TestCase(" p ")]
[TestCase("p\np")]
[TestCase(" p\np")]
[TestCase("p \np")]
[TestCase(" p \np")]
[TestCase("p\n p")]
[TestCase(" p\n p")]
[TestCase("p \n p")]
[TestCase(" p \n p")]
[TestCase("p\np ")]
[TestCase(" p\np ")]
[TestCase("p \np ")]
[TestCase(" p \np ")]
[TestCase("p\n\n p ")]
[TestCase(" p\n\n p ")]
[TestCase("p \n\n p ")]
[TestCase(" p \n\n p ")]
[TestCase("p\n\np")]
[TestCase(" p\n\np")]
[TestCase("p \n\np")]
[TestCase(" p \n\np")]
[TestCase("p\n\n p")]
[TestCase(" p\n\n p")]
[TestCase("p \n\n p")]
[TestCase(" p \n\n p")]
[TestCase("p\n\np ")]
[TestCase(" p\n\np ")]
[TestCase("p \n\np ")]
[TestCase(" p \n\np ")]
[TestCase("p\n\n p ")]
[TestCase(" p\n\n p ")]
[TestCase("p \n\n p ")]
[TestCase(" p \n\n p ")]
[TestCase("\np")]
[TestCase("\n p")]
[TestCase("\np ")]
[TestCase("\n p ")]
[TestCase("\np\np")]
[TestCase("\n p\np")]
[TestCase("\np \np")]
[TestCase("\n p \np")]
[TestCase("\np\n p")]
[TestCase("\n p\n p")]
[TestCase("\np \n p")]
[TestCase("\n p \n p")]
[TestCase("\np\np ")]
[TestCase("\n p\np ")]
[TestCase("\np \np ")]
[TestCase("\n p \np ")]
[TestCase("\np\n\n p ")]
[TestCase("\n p\n\n p ")]
[TestCase("\np \n\n p ")]
[TestCase("\n p \n\n p ")]
[TestCase("\np\n\np")]
[TestCase("\n p\n\np")]
[TestCase("\np \n\np")]
[TestCase("\n p \n\np")]
[TestCase("\np\n\n p")]
[TestCase("\n p\n\n p")]
[TestCase("\np \n\n p")]
[TestCase("\n p \n\n p")]
[TestCase("\np\n\np ")]
[TestCase("\n p\n\np ")]
[TestCase("\np \n\np ")]
[TestCase("\n p \n\np ")]
[TestCase("\np\n\n p ")]
[TestCase("\n p\n\n p ")]
[TestCase("\np \n\n p ")]
[TestCase("\n p \n\n p ")]
[TestCase("p p")]
[TestCase("p\tp")]
[TestCase("p \tp")]
[TestCase("p \t p")]
[TestCase("p \tp")]
// special cases
[TestCase(" p \n\n\n\n p \n\n")]
[TestCase("\n\np")]
[TestCase("p\n")]
[TestCase("p\n\n")]
[TestCase("p\np\n p")]
[TestCase("p\np\n p\n p")]
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
[TestCase("\n")]
[TestCase("\r\n")]
[TestCase("\r")]
[TestCase("\n")]
[TestCase("\r\n")]
[TestCase("\r")]
[TestCase("p\n")]
[TestCase("p\r")]
[TestCase("p\r\n")]
[TestCase("p\n")]
[TestCase("p\r")]
[TestCase("p\r\n")]
[TestCase("p\np")]
[TestCase("p\rp")]
[TestCase("p\r\np")]
[TestCase("p\np")]
[TestCase("p\rp")]
[TestCase("p\r\np")]
[TestCase("p\np\n")]
[TestCase("p\rp\n")]
[TestCase("p\r\np\n")]
[TestCase("p\np\n")]
[TestCase("p\rp\n")]
[TestCase("p\r\np\n")]
[TestCase("p\np\r")]
[TestCase("p\rp\r")]
[TestCase("p\r\np\r")]
[TestCase("p\np\r")]
[TestCase("p\rp\r")]
[TestCase("p\r\np\r")]
[TestCase("p\np\r\n")]
[TestCase("p\rp\r\n")]
[TestCase("p\r\np\r\n")]
[TestCase("p\np\r\n")]
[TestCase("p\rp\r\n")]
[TestCase("p\r\np\r\n")]
[TestCase("\np\n")]
[TestCase("\np\r")]
[TestCase("\np\r\n")]
[TestCase("\np\n")]
[TestCase("\np\r")]
[TestCase("\np\r\n")]
[TestCase("\np\np")]
[TestCase("\np\rp")]
[TestCase("\np\r\np")]
[TestCase("\np\np")]
[TestCase("\np\rp")]
[TestCase("\np\r\np")]
[TestCase("\np\np\n")]
[TestCase("\np\rp\n")]
[TestCase("\np\r\np\n")]
[TestCase("\np\np\n")]
[TestCase("\np\rp\n")]
[TestCase("\np\r\np\n")]
[TestCase("\np\np\r")]
[TestCase("\np\rp\r")]
[TestCase("\np\r\np\r")]
[TestCase("\np\np\r")]
[TestCase("\np\rp\r")]
[TestCase("\np\r\np\r")]
[TestCase("\np\np\r\n")]
[TestCase("\np\rp\r\n")]
[TestCase("\np\r\np\r\n")]
[TestCase("\np\np\r\n")]
[TestCase("\np\rp\r\n")]
[TestCase("\np\r\np\r\n")]
[TestCase("\rp\n")]
[TestCase("\rp\r")]
[TestCase("\rp\r\n")]
[TestCase("\rp\n")]
[TestCase("\rp\r")]
[TestCase("\rp\r\n")]
[TestCase("\rp\np")]
[TestCase("\rp\rp")]
[TestCase("\rp\r\np")]
[TestCase("\rp\np")]
[TestCase("\rp\rp")]
[TestCase("\rp\r\np")]
[TestCase("\rp\np\n")]
[TestCase("\rp\rp\n")]
[TestCase("\rp\r\np\n")]
[TestCase("\rp\np\n")]
[TestCase("\rp\rp\n")]
[TestCase("\rp\r\np\n")]
[TestCase("\rp\np\r")]
[TestCase("\rp\rp\r")]
[TestCase("\rp\r\np\r")]
[TestCase("\rp\np\r")]
[TestCase("\rp\rp\r")]
[TestCase("\rp\r\np\r")]
[TestCase("\rp\np\r\n")]
[TestCase("\rp\rp\r\n")]
[TestCase("\rp\r\np\r\n")]
[TestCase("\rp\np\r\n")]
[TestCase("\rp\rp\r\n")]
[TestCase("\rp\r\np\r\n")]
[TestCase("\r\np\n")]
[TestCase("\r\np\r")]
[TestCase("\r\np\r\n")]
[TestCase("\r\np\n")]
[TestCase("\r\np\r")]
[TestCase("\r\np\r\n")]
[TestCase("\r\np\np")]
[TestCase("\r\np\rp")]
[TestCase("\r\np\r\np")]
[TestCase("\r\np\np")]
[TestCase("\r\np\rp")]
[TestCase("\r\np\r\np")]
[TestCase("\r\np\np\n")]
[TestCase("\r\np\rp\n")]
[TestCase("\r\np\r\np\n")]
[TestCase("\r\np\np\n")]
[TestCase("\r\np\rp\n")]
[TestCase("\r\np\r\np\n")]
[TestCase("\r\np\np\r")]
[TestCase("\r\np\rp\r")]
[TestCase("\r\np\r\np\r")]
[TestCase("\r\np\np\r")]
[TestCase("\r\np\rp\r")]
[TestCase("\r\np\r\np\r")]
[TestCase("\r\np\np\r\n")]
[TestCase("\r\np\rp\r\n")]
[TestCase("\r\np\r\np\r\n")]
[TestCase("\r\np\np\r\n")]
[TestCase("\r\np\rp\r\n")]
[TestCase("\r\np\r\np\r\n")]
[TestCase("p\n")]
[TestCase("p\n\n")]
[TestCase("p\n\n\n")]
[TestCase("p\n\n\n\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("p\n")]
[TestCase("p\n\n")]
[TestCase("p\n\n\n")]
[TestCase("p\n\n\n\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase(" \n")]
[TestCase(" \r")]
[TestCase(" \r\n")]
[TestCase(" \n")]
[TestCase(" \r")]
[TestCase(" \r\n")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \np")]
[TestCase(" \rp")]
[TestCase(" \r\np")]
[TestCase(" \n ")]
[TestCase(" \r ")]
[TestCase(" \r\n ")]
[TestCase(" \n ")]
[TestCase(" \r ")]
[TestCase(" \r\n ")]
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
public void Test_WhitespaceWithNewline(string value)
{
RoundTrip(value);
}
[TestCase(" \np ")]
[TestCase(" \rp ")]
[TestCase(" \r\np ")]
public void Test_WhitespaceWithNewline(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,286 +1,284 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestQuoteBlock
{
[TestFixture]
public class TestQuoteBlock
[TestCase(">q")]
[TestCase(" >q")]
[TestCase(" >q")]
[TestCase(" >q")]
[TestCase("> q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase("> q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase(">q\n>q")]
[TestCase(">q\n >q")]
[TestCase(">q\n >q")]
[TestCase(">q\n >q")]
[TestCase(">q\n> q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n> q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(" >q\n>q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n>q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase("> q\n>q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase("> q\n>q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(">q\n>q\n>q")]
[TestCase(">q\n>\n>q")]
[TestCase(">q\np\n>q")]
[TestCase(">q\n>\n>\n>q")]
[TestCase(">q\n>\n>\n>\n>q")]
[TestCase(">q\n>\n>q\n>\n>q")]
[TestCase("p\n\n> **q**\n>p\n")]
[TestCase("> q\np\n> q")] // lazy
[TestCase("> q\n> q\np")] // lazy
[TestCase(">>q")]
[TestCase(" > > q")]
[TestCase("> **q**\n>p\n")]
[TestCase("> **q**")]
public void Test(string value)
{
[TestCase(">q")]
[TestCase(" >q")]
[TestCase(" >q")]
[TestCase(" >q")]
[TestCase("> q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase("> q")]
[TestCase(" > q")]
[TestCase(" > q")]
[TestCase(" > q")]
RoundTrip(value);
}
[TestCase(">q\n>q")]
[TestCase(">q\n >q")]
[TestCase(">q\n >q")]
[TestCase(">q\n >q")]
[TestCase(">q\n> q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n> q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase(">q\n > q")]
[TestCase("> q")] // 5
[TestCase("> q")] // 6
[TestCase(" > q")] //5
[TestCase(" > q")] //6
[TestCase(" > \tq")]
[TestCase("> q\n> q")] // 5, 5
[TestCase("> q\n> q")] // 5, 6
[TestCase("> q\n> q")] // 6, 5
[TestCase("> q\n> q")] // 6, 6
[TestCase("> q\n\n> 5")] // 5, 5
public void TestIndentedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase(" >q\n>q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase("\n> q")]
[TestCase("\n> q\n")]
[TestCase("\n> q\n\n")]
[TestCase("> q\n\np")]
[TestCase("p\n\n> q\n\n# h")]
[TestCase(" >q\n>q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n >q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n> q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
[TestCase(" >q\n > q")]
//https://github.com/lunet-io/markdig/issues/480
//[TestCase(">\np")]
//[TestCase(">**b**\n>\n>p\n>\np\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("> q\n>q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n\n# h\n")]
public void TestAtxHeader(string value)
{
RoundTrip(value);
}
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(">- i")]
[TestCase("> - i")]
[TestCase(">- i\n>- i")]
[TestCase(">- >p")]
[TestCase("> - >p")]
[TestCase(">- i1\n>- i2\n")]
[TestCase("> **p** p\n>- i1\n>- i2\n")]
public void TestUnorderedList(string value)
{
RoundTrip(value);
}
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase("> *q*\n>p\n")]
[TestCase("> *q*")]
public void TestEmphasis(string value)
{
RoundTrip(value);
}
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase("> **q**\n>p\n")]
[TestCase("> **q**")]
public void TestStrongEmphasis(string value)
{
RoundTrip(value);
}
[TestCase("> q\n>q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n >q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n> q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase("> q\n > q")]
[TestCase(">p\n")]
[TestCase(">p\r")]
[TestCase(">p\r\n")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(">p\n>p")]
[TestCase(">p\r>p")]
[TestCase(">p\r\n>p")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(">p\n>p\n")]
[TestCase(">p\r>p\n")]
[TestCase(">p\r\n>p\n")]
[TestCase(" > q\n>q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n >q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n> q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(" > q\n > q")]
[TestCase(">p\n>p\r")]
[TestCase(">p\r>p\r")]
[TestCase(">p\r\n>p\r")]
[TestCase(">q\n>q\n>q")]
[TestCase(">q\n>\n>q")]
[TestCase(">q\np\n>q")]
[TestCase(">q\n>\n>\n>q")]
[TestCase(">q\n>\n>\n>\n>q")]
[TestCase(">q\n>\n>q\n>\n>q")]
[TestCase("p\n\n> **q**\n>p\n")]
[TestCase(">p\n>p\r\n")]
[TestCase(">p\r>p\r\n")]
[TestCase(">p\r\n>p\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("> q\np\n> q")] // lazy
[TestCase("> q\n> q\np")] // lazy
[TestCase(">>q")]
[TestCase(" > > q")]
[TestCase("> **q**\n>p\n")]
[TestCase("> **q**")]
public void Test(string value)
{
RoundTrip(value);
}
[TestCase("> q")] // 5
[TestCase("> q")] // 6
[TestCase(" > q")] //5
[TestCase(" > q")] //6
[TestCase(" > \tq")]
[TestCase("> q\n> q")] // 5, 5
[TestCase("> q\n> q")] // 5, 6
[TestCase("> q\n> q")] // 6, 5
[TestCase("> q\n> q")] // 6, 6
[TestCase("> q\n\n> 5")] // 5, 5
public void TestIndentedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("\n> q")]
[TestCase("\n> q\n")]
[TestCase("\n> q\n\n")]
[TestCase("> q\n\np")]
[TestCase("p\n\n> q\n\n# h")]
//https://github.com/lunet-io/markdig/issues/480
//[TestCase(">\np")]
//[TestCase(">**b**\n>\n>p\n>\np\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("> q\n\n# h\n")]
public void TestAtxHeader(string value)
{
RoundTrip(value);
}
[TestCase(">- i")]
[TestCase("> - i")]
[TestCase(">- i\n>- i")]
[TestCase(">- >p")]
[TestCase("> - >p")]
[TestCase(">- i1\n>- i2\n")]
[TestCase("> **p** p\n>- i1\n>- i2\n")]
public void TestUnorderedList(string value)
{
RoundTrip(value);
}
[TestCase("> *q*\n>p\n")]
[TestCase("> *q*")]
public void TestEmphasis(string value)
{
RoundTrip(value);
}
[TestCase("> **q**\n>p\n")]
[TestCase("> **q**")]
public void TestStrongEmphasis(string value)
{
RoundTrip(value);
}
[TestCase(">p\n")]
[TestCase(">p\r")]
[TestCase(">p\r\n")]
[TestCase(">p\n>p")]
[TestCase(">p\r>p")]
[TestCase(">p\r\n>p")]
[TestCase(">p\n>p\n")]
[TestCase(">p\r>p\n")]
[TestCase(">p\r\n>p\n")]
[TestCase(">p\n>p\r")]
[TestCase(">p\r>p\r")]
[TestCase(">p\r\n>p\r")]
[TestCase(">p\n>p\r\n")]
[TestCase(">p\r>p\r\n")]
[TestCase(">p\r\n>p\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase(">\n>q")]
[TestCase(">\n>\n>q")]
[TestCase(">q\n>\n>q")]
[TestCase(">q\n>\n>\n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n>\t\n>q")]
[TestCase(">q\n>\v\n>q")]
[TestCase(">q\n>\f\n>q")]
public void TestEmptyLines(string value)
{
RoundTrip(value);
}
[TestCase(">\n>q")]
[TestCase(">\n>\n>q")]
[TestCase(">q\n>\n>q")]
[TestCase(">q\n>\n>\n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n> \n>q")]
[TestCase(">q\n>\t\n>q")]
[TestCase(">q\n>\v\n>q")]
[TestCase(">q\n>\f\n>q")]
public void TestEmptyLines(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,53 +1,51 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestSetextHeading
{
[TestFixture]
public class TestSetextHeading
[TestCase("h1\n===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n=== ")] //3
[TestCase("h1 \n===")] //3
[TestCase("h1\\\n===")] //3
[TestCase("h1\n === ")] //3
[TestCase("h1\nh1 l2\n===")] //3
[TestCase("h1\n====")] // 4
[TestCase("h1\n ====")] // 4
[TestCase("h1\n==== ")] // 4
[TestCase("h1\n ==== ")] // 4
[TestCase("h1\n===\nh1\n===")] //3
[TestCase("\\>h1\n===")] //3
public void Test(string value)
{
[TestCase("h1\n===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n ===")] //3
[TestCase("h1\n=== ")] //3
[TestCase("h1 \n===")] //3
[TestCase("h1\\\n===")] //3
[TestCase("h1\n === ")] //3
[TestCase("h1\nh1 l2\n===")] //3
[TestCase("h1\n====")] // 4
[TestCase("h1\n ====")] // 4
[TestCase("h1\n==== ")] // 4
[TestCase("h1\n ==== ")] // 4
[TestCase("h1\n===\nh1\n===")] //3
[TestCase("\\>h1\n===")] //3
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
[TestCase("h1\r===")]
[TestCase("h1\n===")]
[TestCase("h1\r\n===")]
[TestCase("h1\r===")]
[TestCase("h1\n===")]
[TestCase("h1\r\n===")]
[TestCase("h1\r===\r")]
[TestCase("h1\n===\r")]
[TestCase("h1\r\n===\r")]
[TestCase("h1\r===\r")]
[TestCase("h1\n===\r")]
[TestCase("h1\r\n===\r")]
[TestCase("h1\r===\n")]
[TestCase("h1\n===\n")]
[TestCase("h1\r\n===\n")]
[TestCase("h1\r===\n")]
[TestCase("h1\n===\n")]
[TestCase("h1\r\n===\n")]
[TestCase("h1\r===\r\n")]
[TestCase("h1\n===\r\n")]
[TestCase("h1\r\n===\r\n")]
[TestCase("h1\r===\r\n")]
[TestCase("h1\n===\r\n")]
[TestCase("h1\r\n===\r\n")]
[TestCase("h1\n===\n\n\nh2---\n\n")]
[TestCase("h1\r===\r\r\rh2---\r\r")]
[TestCase("h1\r\n===\r\n\r\n\r\nh2---\r\n\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("h1\n===\n\n\nh2---\n\n")]
[TestCase("h1\r===\r\r\rh2---\r\r")]
[TestCase("h1\r\n===\r\n\r\n\r\nh2---\r\n\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,53 +1,51 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestThematicBreak
{
[TestFixture]
public class TestThematicBreak
[TestCase("---")]
[TestCase(" ---")]
[TestCase(" ---")]
[TestCase(" ---")]
[TestCase("--- ")]
[TestCase(" --- ")]
[TestCase(" --- ")]
[TestCase(" --- ")]
[TestCase("- - -")]
[TestCase(" - - -")]
[TestCase(" - - - ")]
[TestCase("-- -")]
[TestCase("---\n")]
[TestCase("---\n\n")]
[TestCase("---\np")]
[TestCase("---\n\np")]
[TestCase("---\n# h")]
[TestCase("p\n\n---")]
// Note: "p\n---" is parsed as setext heading
public void Test(string value)
{
[TestCase("---")]
[TestCase(" ---")]
[TestCase(" ---")]
[TestCase(" ---")]
[TestCase("--- ")]
[TestCase(" --- ")]
[TestCase(" --- ")]
[TestCase(" --- ")]
[TestCase("- - -")]
[TestCase(" - - -")]
[TestCase(" - - - ")]
[TestCase("-- -")]
[TestCase("---\n")]
[TestCase("---\n\n")]
[TestCase("---\np")]
[TestCase("---\n\np")]
[TestCase("---\n# h")]
[TestCase("p\n\n---")]
// Note: "p\n---" is parsed as setext heading
public void Test(string value)
{
RoundTrip(value);
}
RoundTrip(value);
}
[TestCase("\n---")]
[TestCase("\r---")]
[TestCase("\r\n---")]
[TestCase("\n---")]
[TestCase("\r---")]
[TestCase("\r\n---")]
[TestCase("\n---\n")]
[TestCase("\r---\n")]
[TestCase("\r\n---\n")]
[TestCase("\n---\n")]
[TestCase("\r---\n")]
[TestCase("\r\n---\n")]
[TestCase("\n---\r")]
[TestCase("\r---\r")]
[TestCase("\r\n---\r")]
[TestCase("\n---\r")]
[TestCase("\r---\r")]
[TestCase("\r\n---\r")]
[TestCase("\n---\r\n")]
[TestCase("\r---\r\n")]
[TestCase("\r\n---\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("\n---\r\n")]
[TestCase("\r---\r\n")]
[TestCase("\r\n---\r\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
}

View File

@@ -1,183 +1,181 @@
using NUnit.Framework;
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestUnorderedList
{
[TestFixture]
public class TestUnorderedList
// i = item
[TestCase("- i1")]
[TestCase("- i1 ")]
[TestCase("- i1\n")]
[TestCase("- i1\n\n")]
[TestCase("- i1\n- i2")]
[TestCase("- i1\n - i2")]
[TestCase("- i1\n - i1.1\n - i1.2")]
[TestCase("- i1 \n- i2 \n")]
[TestCase("- i1 \n- i2 \n")]
[TestCase(" - i1")]
[TestCase(" - i1")]
[TestCase(" - i1")]
[TestCase("- i1\n\n- i1")]
[TestCase("- i1\n\n\n- i1")]
[TestCase("- i1\n - i1.1\n - i1.1.1\n")]
[TestCase("-\ti1")]
[TestCase("-\ti1\n-\ti2")]
[TestCase("-\ti1\n- i2\n-\ti3")]
public void Test(string value)
{
// i = item
[TestCase("- i1")]
[TestCase("- i1 ")]
[TestCase("- i1\n")]
[TestCase("- i1\n\n")]
[TestCase("- i1\n- i2")]
[TestCase("- i1\n - i2")]
[TestCase("- i1\n - i1.1\n - i1.2")]
[TestCase("- i1 \n- i2 \n")]
[TestCase("- i1 \n- i2 \n")]
[TestCase(" - i1")]
[TestCase(" - i1")]
[TestCase(" - i1")]
[TestCase("- i1\n\n- i1")]
[TestCase("- i1\n\n\n- i1")]
[TestCase("- i1\n - i1.1\n - i1.1.1\n")]
RoundTrip(value);
}
[TestCase("-\ti1")]
[TestCase("-\ti1\n-\ti2")]
[TestCase("-\ti1\n- i2\n-\ti3")]
public void Test(string value)
{
RoundTrip(value);
}
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q1\n - > q2")]
public void TestBlockQuote(string value)
{
RoundTrip(value);
}
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase("- > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q")]
[TestCase(" - > q1\n - > q2")]
public void TestBlockQuote(string value)
{
RoundTrip(value);
}
[TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline
[TestCase("- i1\n\n\np\n")]
[TestCase("- i1\n\np")]
[TestCase("- i1\n\np\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("- i1\n\np\n")] // TODO: listblock should render newline, apparently last paragraph of last listitem dont have newline
[TestCase("- i1\n\n\np\n")]
[TestCase("- i1\n\np")]
[TestCase("- i1\n\np\n")]
public void TestParagraph(string value)
{
RoundTrip(value);
}
[TestCase("- i1\n\n---\n")]
[TestCase("- i1\n\n\n---\n")]
public void TestThematicBreak(string value)
{
RoundTrip(value);
}
[TestCase("- i1\n\n---\n")]
[TestCase("- i1\n\n\n---\n")]
public void TestThematicBreak(string value)
{
RoundTrip(value);
}
[TestCase("- c")] // 5
[TestCase("- c\n c")] // 5, 6
[TestCase(" - c\n c")] // 5, 6
[TestCase(" - c\n c")] // 5, 7
[TestCase("- c\n c")] // 6, 6
[TestCase(" - c\n c")] // 6, 6
[TestCase(" - c\n c")] // 6, 7
public void TestIndentedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("- c")] // 5
[TestCase("- c\n c")] // 5, 6
[TestCase(" - c\n c")] // 5, 6
[TestCase(" - c\n c")] // 5, 7
[TestCase("- c\n c")] // 6, 6
[TestCase(" - c\n c")] // 6, 6
[TestCase(" - c\n c")] // 6, 7
public void TestIndentedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("- ```a```")]
[TestCase("- ```\n a\n```")]
[TestCase("- i1\n - i1.1\n ```\n c\n ```")]
[TestCase("- i1\n - i1.1\n ```\nc\n```")]
[TestCase("- i1\n - i1.1\n ```\nc\n```\n")]
public void TestFencedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("- ```a```")]
[TestCase("- ```\n a\n```")]
[TestCase("- i1\n - i1.1\n ```\n c\n ```")]
[TestCase("- i1\n - i1.1\n ```\nc\n```")]
[TestCase("- i1\n - i1.1\n ```\nc\n```\n")]
public void TestFencedCodeBlock(string value)
{
RoundTrip(value);
}
[TestCase("\n- i")]
[TestCase("\r- i")]
[TestCase("\r\n- i")]
[TestCase("\n- i")]
[TestCase("\r- i")]
[TestCase("\r\n- i")]
[TestCase("\n- i\n")]
[TestCase("\r- i\n")]
[TestCase("\r\n- i\n")]
[TestCase("\n- i\n")]
[TestCase("\r- i\n")]
[TestCase("\r\n- i\n")]
[TestCase("\n- i\r")]
[TestCase("\r- i\r")]
[TestCase("\r\n- i\r")]
[TestCase("\n- i\r")]
[TestCase("\r- i\r")]
[TestCase("\r\n- i\r")]
[TestCase("\n- i\r\n")]
[TestCase("\r- i\r\n")]
[TestCase("\r\n- i\r\n")]
[TestCase("\n- i\r\n")]
[TestCase("\r- i\r\n")]
[TestCase("\r\n- i\r\n")]
[TestCase("- i\n- j")]
[TestCase("- i\r- j")]
[TestCase("- i\r\n- j")]
[TestCase("- i\n- j")]
[TestCase("- i\r- j")]
[TestCase("- i\r\n- j")]
[TestCase("\n- i\n- j")]
[TestCase("\n- i\r- j")]
[TestCase("\n- i\r\n- j")]
[TestCase("\n- i\n- j")]
[TestCase("\n- i\r- j")]
[TestCase("\n- i\r\n- j")]
[TestCase("\r- i\n- j")]
[TestCase("\r- i\r- j")]
[TestCase("\r- i\r\n- j")]
[TestCase("\r- i\n- j")]
[TestCase("\r- i\r- j")]
[TestCase("\r- i\r\n- j")]
[TestCase("\r\n- i\n- j")]
[TestCase("\r\n- i\r- j")]
[TestCase("\r\n- i\r\n- j")]
[TestCase("\r\n- i\n- j")]
[TestCase("\r\n- i\r- j")]
[TestCase("\r\n- i\r\n- j")]
[TestCase("- i\n- j\n")]
[TestCase("- i\r- j\n")]
[TestCase("- i\r\n- j\n")]
[TestCase("- i\n- j\n")]
[TestCase("- i\r- j\n")]
[TestCase("- i\r\n- j\n")]
[TestCase("\n- i\n- j\n")]
[TestCase("\n- i\r- j\n")]
[TestCase("\n- i\r\n- j\n")]
[TestCase("\n- i\n- j\n")]
[TestCase("\n- i\r- j\n")]
[TestCase("\n- i\r\n- j\n")]
[TestCase("\r- i\n- j\n")]
[TestCase("\r- i\r- j\n")]
[TestCase("\r- i\r\n- j\n")]
[TestCase("\r- i\n- j\n")]
[TestCase("\r- i\r- j\n")]
[TestCase("\r- i\r\n- j\n")]
[TestCase("\r\n- i\n- j\n")]
[TestCase("\r\n- i\r- j\n")]
[TestCase("\r\n- i\r\n- j\n")]
[TestCase("\r\n- i\n- j\n")]
[TestCase("\r\n- i\r- j\n")]
[TestCase("\r\n- i\r\n- j\n")]
[TestCase("- i\n- j\r")]
[TestCase("- i\r- j\r")]
[TestCase("- i\r\n- j\r")]
[TestCase("- i\n- j\r")]
[TestCase("- i\r- j\r")]
[TestCase("- i\r\n- j\r")]
[TestCase("\n- i\n- j\r")]
[TestCase("\n- i\r- j\r")]
[TestCase("\n- i\r\n- j\r")]
[TestCase("\n- i\n- j\r")]
[TestCase("\n- i\r- j\r")]
[TestCase("\n- i\r\n- j\r")]
[TestCase("\r- i\n- j\r")]
[TestCase("\r- i\r- j\r")]
[TestCase("\r- i\r\n- j\r")]
[TestCase("\r- i\n- j\r")]
[TestCase("\r- i\r- j\r")]
[TestCase("\r- i\r\n- j\r")]
[TestCase("\r\n- i\n- j\r")]
[TestCase("\r\n- i\r- j\r")]
[TestCase("\r\n- i\r\n- j\r")]
[TestCase("\r\n- i\n- j\r")]
[TestCase("\r\n- i\r- j\r")]
[TestCase("\r\n- i\r\n- j\r")]
[TestCase("- i\n- j\r\n")]
[TestCase("- i\r- j\r\n")]
[TestCase("- i\r\n- j\r\n")]
[TestCase("- i\n- j\r\n")]
[TestCase("- i\r- j\r\n")]
[TestCase("- i\r\n- j\r\n")]
[TestCase("\n- i\n- j\r\n")]
[TestCase("\n- i\r- j\r\n")]
[TestCase("\n- i\r\n- j\r\n")]
[TestCase("\n- i\n- j\r\n")]
[TestCase("\n- i\r- j\r\n")]
[TestCase("\n- i\r\n- j\r\n")]
[TestCase("\r- i\n- j\r\n")]
[TestCase("\r- i\r- j\r\n")]
[TestCase("\r- i\r\n- j\r\n")]
[TestCase("\r- i\n- j\r\n")]
[TestCase("\r- i\r- j\r\n")]
[TestCase("\r- i\r\n- j\r\n")]
[TestCase("\r\n- i\n- j\r\n")]
[TestCase("\r\n- i\r- j\r\n")]
[TestCase("\r\n- i\r\n- j\r\n")]
[TestCase("\r\n- i\n- j\r\n")]
[TestCase("\r\n- i\r- j\r\n")]
[TestCase("\r\n- i\r\n- j\r\n")]
[TestCase("- i\n")]
[TestCase("- i\n\n")]
[TestCase("- i\n\n\n")]
[TestCase("- i\n\n\n\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
[TestCase("- i\n")]
[TestCase("- i\n\n")]
[TestCase("- i\n\n\n")]
[TestCase("- i\n\n\n\n")]
public void TestNewline(string value)
{
RoundTrip(value);
}
}

View File

@@ -0,0 +1,15 @@
using static Markdig.Tests.TestRoundtrip;
namespace Markdig.Tests.RoundtripSpecs;
[TestFixture]
public class TestYamlFrontMatterBlock
{
[TestCase("---\nkey1: value1\nkey2: value2\n---\n\nContent\n")]
[TestCase("No front matter")]
[TestCase("Looks like front matter but actually is not\n---\nkey1: value1\nkey2: value2\n---")]
public void FrontMatterBlockIsPreserved(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

@@ -17,7 +17,8 @@ namespace Markdig.Tests.Specs.GridTables
//
// ## Grid Table
//
// A grid table allows to have multiple lines per cells and allows to span cells over multiple columns. The following shows a simple grid table
// A grid table allows having multiple lines per cells and allows spanning cells over multiple columns.
// The following shows a simple grid table:
//
// ```
// +---------+---------+
@@ -38,17 +39,20 @@ namespace Markdig.Tests.Specs.GridTables
// ```
//
// **Rule #1**
// The first line of a grid table must a **row separator**. It must start with the column separator character `+` used to separate columns in a row separator. Each column separator is:
// - starting by optional spaces
// - followed by an optional `:` to specify left align, followed by optional spaces
// - followed by a sequence of at least one `-` character, followed by optional spaces
// - followed by an optional `:` to specify right align (or center align if left align is also defined)
// - ending by optional spaces
// The first line of a grid table must be a **row separator**.
// It must start with the column separator character `+` used to separate columns in a row separator.
//
// The first row separator must be followed by a *regular row*. A regular row must start with the character `|` that is starting at the same position than the column separator `+` of the first row separator.
// Each column separator:
// - starts with optional spaces
// - followed by an optional `:` to specify left align, followed by optional spaces
// - followed by a sequence of one or more `-` characters, followed by optional spaces
// - followed by an optional `:` to specify right align (or center align if left align is also defined)
// - ends with optional spaces
//
// The first row separator must be followed by a *regular row*.
// A regular row must start with a `|` character starting at the same position as the column separator `+` of the first row separator.
//
// The following is a valid row separator
// The following is a valid row separator:
[Test]
public void ExtensionsGridTable_Example001()
{
@@ -74,7 +78,7 @@ namespace Markdig.Tests.Specs.GridTables
TestParser.TestSpec("+---------+---------+\n| This is | a table |", "<table>\n<col style=\"width:50%\" />\n<col style=\"width:50%\" />\n<tbody>\n<tr>\n<td>This is</td>\n<td>a table</td>\n</tr>\n</tbody>\n</table>", "gridtables|advanced", context: "Example 1\nSection Extensions / Grid Table\n");
}
// The following is not a valid row separator
// The following is not a valid row separator:
[Test]
public void ExtensionsGridTable_Example002()
{
@@ -93,7 +97,8 @@ namespace Markdig.Tests.Specs.GridTables
}
// **Rule #2**
// A regular row can continue a previous regular row when column separator `|` are positioned at the same position than the previous line. If they are positioned at the same location, the column may span over multiple columns:
// A regular row can continue a previous regular row when the column separators `|` are positioned at the same position as those of the previous line.
// If they are positioned at the same location, the column may span over multiple columns:
[Test]
public void ExtensionsGridTable_Example003()
{
@@ -187,15 +192,13 @@ namespace Markdig.Tests.Specs.GridTables
TestParser.TestSpec("+---------+---------+\n| This is | a table with a longer text in the second column", "<table>\n<col style=\"width:50%\" />\n<col style=\"width:50%\" />\n<tbody>\n<tr>\n<td>This is</td>\n<td>a table with a longer text in the second column</td>\n</tr>\n</tbody>\n</table>", "gridtables|advanced", context: "Example 5\nSection Extensions / Grid Table\n");
}
// The respective width of the columns are calculated from the ratio between the total size of the first table row without counting the `+`: `+----+--------+----+` would be divided between:
// The respective widths of the columns are calculated from the ratio between the total size of the first table row without counting the `+`: `+----+--------+----+` would be divided between:
//
// Total size is : 16
// - `----` → 4 characters
// - `--------` → 8 characters
// - `----` → 4 characters
//
// - `----` -> 4
// - `--------` -> 8
// - `----` -> 4
//
// So the width would be 4/16 = 25%, 8/16 = 50%, 4/16 = 25%
// The total size is 16 characters, so the widths would be 4/16 = 25%, 8/16 = 50%, and 4/16 = 25%.
[Test]
public void ExtensionsGridTable_Example006()
{
@@ -296,7 +299,7 @@ namespace Markdig.Tests.Specs.GridTables
TestParser.TestSpec("+---+---+---+\n| AAAAA | B |\n+---+---+ B +\n| D | E | B |\n+ D +---+---+\n| D | CCCCC |\n+---+---+---+", "<table>\n<col style=\"width:33.33%\" />\n<col style=\"width:33.33%\" />\n<col style=\"width:33.33%\" />\n<tbody>\n<tr>\n<td colspan=\"2\">AAAAA</td>\n<td rowspan=\"2\">B\nB\nB</td>\n</tr>\n<tr>\n<td rowspan=\"2\">D\nD\nD</td>\n<td>E</td>\n</tr>\n<tr>\n<td colspan=\"2\">CCCCC</td>\n</tr>\n</tbody>\n</table>", "gridtables|advanced", context: "Example 8\nSection Extensions / Grid Table\n");
}
// A grid table may have cells with both colspan and rowspan:
// A grid table may have cells with both `colspan` and `rowspan`:
[Test]
public void ExtensionsGridTable_Example009()
{

View File

@@ -4,7 +4,8 @@ This section describes the different extensions supported:
## Grid Table
A grid table allows to have multiple lines per cells and allows to span cells over multiple columns. The following shows a simple grid table
A grid table allows having multiple lines per cells and allows spanning cells over multiple columns.
The following shows a simple grid table:
```
+---------+---------+
@@ -25,17 +26,20 @@ A grid table allows to have multiple lines per cells and allows to span cells ov
```
**Rule #1**
The first line of a grid table must a **row separator**. It must start with the column separator character `+` used to separate columns in a row separator. Each column separator is:
- starting by optional spaces
- followed by an optional `:` to specify left align, followed by optional spaces
- followed by a sequence of at least one `-` character, followed by optional spaces
- followed by an optional `:` to specify right align (or center align if left align is also defined)
- ending by optional spaces
The first line of a grid table must be a **row separator**.
It must start with the column separator character `+` used to separate columns in a row separator.
The first row separator must be followed by a *regular row*. A regular row must start with the character `|` that is starting at the same position than the column separator `+` of the first row separator.
Each column separator:
- starts with optional spaces
- followed by an optional `:` to specify left align, followed by optional spaces
- followed by a sequence of one or more `-` characters, followed by optional spaces
- followed by an optional `:` to specify right align (or center align if left align is also defined)
- ends with optional spaces
The first row separator must be followed by a *regular row*.
A regular row must start with a `|` character starting at the same position as the column separator `+` of the first row separator.
The following is a valid row separator
The following is a valid row separator:
```````````````````````````````` example
+---------+---------+
@@ -53,8 +57,8 @@ The following is a valid row separator
</table>
````````````````````````````````
The following is not a valid row separator:
The following is not a valid row separator
```````````````````````````````` example
|-----xxx----+---------+
| This is | not a table
@@ -64,7 +68,8 @@ The following is not a valid row separator
````````````````````````````````
**Rule #2**
A regular row can continue a previous regular row when column separator `|` are positioned at the same position than the previous line. If they are positioned at the same location, the column may span over multiple columns:
A regular row can continue a previous regular row when the column separators `|` are positioned at the same position as those of the previous line.
If they are positioned at the same location, the column may span over multiple columns:
```````````````````````````````` example
+---------+---------+---------+
@@ -134,15 +139,13 @@ The last column separator `|` may be omitted:
</table>
````````````````````````````````
The respective width of the columns are calculated from the ratio between the total size of the first table row without counting the `+`: `+----+--------+----+` would be divided between:
The respective widths of the columns are calculated from the ratio between the total size of the first table row without counting the `+`: `+----+--------+----+` would be divided between:
Total size is : 16
- `----` → 4 characters
- `--------` → 8 characters
- `----` → 4 characters
- `----` -> 4
- `--------` -> 8
- `----` -> 4
So the width would be 4/16 = 25%, 8/16 = 50%, 4/16 = 25%
The total size is 16 characters, so the widths would be 4/16 = 25%, 8/16 = 50%, and 4/16 = 25%.
```````````````````````````````` example
+----+--------+----+
@@ -165,7 +168,6 @@ So the width would be 4/16 = 25%, 8/16 = 50%, 4/16 = 25%
Alignment might be specified on the first row using the character `:`:
```````````````````````````````` example
+-----+:---:+-----+
| A | B | C |
@@ -220,7 +222,7 @@ D</td>
</table>
````````````````````````````````
A grid table may have cells with both colspan and rowspan:
A grid table may have cells with both `colspan` and `rowspan`:
```````````````````````````````` example
+---+---+---+
@@ -276,7 +278,6 @@ A grid table may not have irregularly shaped cells:
An empty `+` on a line should result in a simple empty list output:
```````````````````````````````` example
+
.

View File

@@ -279,7 +279,7 @@ namespace Markdig.Tests.Specs.Math
{
// ## Math Block
//
// The match block can spawn on multiple lines by having a $$ starting on a line.
// The math block can spawn on multiple lines by having a $$ starting on a line.
// It is working as a fenced code block.
[Test]
public void ExtensionsMathBlock_Example017()

View File

@@ -127,7 +127,7 @@ $$ math $$ starting at a line
## Math Block
The match block can spawn on multiple lines by having a $$ starting on a line.
The math block can spawn on multiple lines by having a $$ starting on a line.
It is working as a fenced code block.
```````````````````````````````` example

View File

@@ -0,0 +1,90 @@
using System.Globalization;
using Markdig.Helpers;
namespace Markdig.Tests;
public class TestCharHelper
{
// An ASCII punctuation character is
// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., / (U+00212F),
// :, ;, <, =, >, ?, @ (U+003A0040),
// [, \, ], ^, _, ` (U+005B0060),
// {, |, }, or ~ (U+007B007E).
private static readonly HashSet<char> s_asciiPunctuation = new()
{
'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/',
':', ';', '<', '=', '>', '?', '@',
'[', '\\', ']', '^', '_', '`',
'{', '|', '}', '~'
};
// 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()
{
UnicodeCategory.ConnectorPunctuation,
UnicodeCategory.DashPunctuation,
UnicodeCategory.ClosePunctuation,
UnicodeCategory.FinalQuotePunctuation,
UnicodeCategory.InitialQuotePunctuation,
UnicodeCategory.OtherPunctuation,
UnicodeCategory.OpenPunctuation
};
private static bool ExpectedIsPunctuation(char c)
{
return c <= 127
? s_asciiPunctuation.Contains(c)
: s_punctuationCategories.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' ||
CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator;
}
[Test]
public void IsWhitespace()
{
for (int i = char.MinValue; i <= char.MaxValue; i++)
{
char c = (char)i;
Assert.AreEqual(ExpectedIsWhitespace(c), CharHelper.IsWhitespace(c));
}
}
[Test]
public void CheckUnicodeCategory()
{
for (int i = char.MinValue; i <= char.MaxValue; i++)
{
char c = (char)i;
bool expectedSpace = c == 0 || ExpectedIsWhitespace(c);
bool expectedPunctuation = c == 0 || ExpectedIsPunctuation(c);
CharHelper.CheckUnicodeCategory(c, out bool spaceActual, out bool punctuationActual);
Assert.AreEqual(expectedSpace, spaceActual);
Assert.AreEqual(expectedPunctuation, punctuationActual);
}
}
[Test]
public void IsSpaceOrPunctuation()
{
for (int i = char.MinValue; i <= char.MaxValue; i++)
{
char c = (char)i;
bool expected = c == 0 || ExpectedIsWhitespace(c) || ExpectedIsPunctuation(c);
Assert.AreEqual(expected, CharHelper.IsSpaceOrPunctuation(c));
}
}
}

View File

@@ -1,42 +1,39 @@
using NUnit.Framework;
namespace Markdig.Tests;
namespace Markdig.Tests
[TestFixture]
public class TestConfigureNewLine
{
[TestFixture]
public class TestConfigureNewLine
[Test]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>\n<em>2</em></p>\n")]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>\n<em>2</em></p>\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>\r\n<em>2</em></p>\r\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>\r\n<em>2</em></p>\r\n")]
[TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>!!!<em>2</em></p>!!!")]
[TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>!!!<em>2</em></p>!!!")]
public void TestHtmlOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected)
{
[Test]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>\n<em>2</em></p>\n")]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>\n<em>2</em></p>\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>\r\n<em>2</em></p>\r\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>\r\n<em>2</em></p>\r\n")]
[TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\n*2*\n", /* expected: */ "<p><em>1</em>!!!<em>2</em></p>!!!")]
[TestCase(/* newLineForWriting: */ "!!!" , /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "<p><em>1</em>!!!<em>2</em></p>!!!")]
public void TestHtmlOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected)
{
var pipeline = new MarkdownPipelineBuilder()
.ConfigureNewLine(newLineForWriting)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.ConfigureNewLine(newLineForWriting)
.Build();
var actual = Markdown.ToHtml(markdownText, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToHtml(markdownText, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\n2\n")]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\n2\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\r\n2\r\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\r\n2\r\n")]
[TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1!!!2!!!")]
[TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1!!!2!!!")]
public void TestPlainOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected)
{
var pipeline = new MarkdownPipelineBuilder()
.ConfigureNewLine(newLineForWriting)
.Build();
[Test]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\n2\n")]
[TestCase(/* newLineForWriting: */ "\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\n2\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1\r\n2\r\n")]
[TestCase(/* newLineForWriting: */ "\r\n", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1\r\n2\r\n")]
[TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\n*2*\n", /* expected: */ "1!!!2!!!")]
[TestCase(/* newLineForWriting: */ "!!!", /* markdownText: */ "*1*\r\n*2*\r\n", /* expected: */ "1!!!2!!!")]
public void TestPlainOutputWhenConfiguringNewLine(string newLineForWriting, string markdownText, string expected)
{
var pipeline = new MarkdownPipelineBuilder()
.ConfigureNewLine(newLineForWriting)
.Build();
var actual = Markdown.ToPlainText(markdownText, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToPlainText(markdownText, pipeline);
Assert.AreEqual(expected, actual);
}
}

View File

@@ -1,92 +1,186 @@
using System;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestContainerBlocks
{
public class TestContainerBlocks
private class MockContainerBlock : ContainerBlock
{
private class MockContainerBlock : ContainerBlock
public MockContainerBlock()
: base(null)
{
public MockContainerBlock()
: base(null)
{
}
}
[Test]
public void CanBeCleared()
{
ContainerBlock container = new MockContainerBlock();
Assert.AreEqual(0, container.Count);
Assert.Null(container.LastChild);
var paragraph = new ParagraphBlock();
Assert.Null(paragraph.Parent);
container.Add(paragraph);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container, paragraph.Parent);
Assert.AreSame(paragraph, container.LastChild);
container.Clear();
Assert.AreEqual(0, container.Count);
Assert.Null(container.LastChild);
Assert.Null(paragraph.Parent);
}
[Test]
public void CanBeInsertedInto()
{
ContainerBlock container = new MockContainerBlock();
var one = new ParagraphBlock();
container.Insert(0, one);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container, one.Parent);
var two = new ParagraphBlock();
container.Insert(1, two);
Assert.AreEqual(2, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container[1], two);
Assert.AreSame(container, two.Parent);
var three = new ParagraphBlock();
container.Insert(0, three);
Assert.AreEqual(3, container.Count);
Assert.AreSame(container[0], three);
Assert.AreSame(container[1], one);
Assert.AreSame(container[2], two);
Assert.AreSame(container, three.Parent);
Assert.Throws<ArgumentNullException>(() => container.Insert(0, null));
Assert.Throws<ArgumentOutOfRangeException>(() => container.Insert(4, new ParagraphBlock()));
Assert.Throws<ArgumentOutOfRangeException>(() => container.Insert(-1, new ParagraphBlock()));
Assert.Throws<ArgumentException>(() => container.Insert(0, one)); // one already has a parent
}
[Test]
public void CanBeSet()
{
ContainerBlock container = new MockContainerBlock();
var one = new ParagraphBlock();
container.Insert(0, one);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container, one.Parent);
var two = new ParagraphBlock();
container[0] = two;
Assert.AreSame(container, two.Parent);
Assert.Null(one.Parent);
Assert.Throws<ArgumentException>(() => container[0] = two); // two already has a parent
}
}
[Test]
public void CanBeCleared()
{
ContainerBlock container = new MockContainerBlock();
Assert.AreEqual(0, container.Count);
Assert.Null(container.LastChild);
var paragraph = new ParagraphBlock();
Assert.Null(paragraph.Parent);
container.Add(paragraph);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container, paragraph.Parent);
Assert.AreSame(paragraph, container.LastChild);
container.Clear();
Assert.AreEqual(0, container.Count);
Assert.Null(container.LastChild);
Assert.Null(paragraph.Parent);
}
[Test]
public void CanBeInsertedInto()
{
ContainerBlock container = new MockContainerBlock();
var one = new ParagraphBlock();
container.Insert(0, one);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container, one.Parent);
var two = new ParagraphBlock();
container.Insert(1, two);
Assert.AreEqual(2, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container[1], two);
Assert.AreSame(container, two.Parent);
var three = new ParagraphBlock();
container.Insert(0, three);
Assert.AreEqual(3, container.Count);
Assert.AreSame(container[0], three);
Assert.AreSame(container[1], one);
Assert.AreSame(container[2], two);
Assert.AreSame(container, three.Parent);
Assert.Throws<ArgumentNullException>(() => container.Insert(0, null));
Assert.Throws<ArgumentOutOfRangeException>(() => container.Insert(4, new ParagraphBlock()));
Assert.Throws<ArgumentOutOfRangeException>(() => container.Insert(-1, new ParagraphBlock()));
Assert.Throws<ArgumentException>(() => container.Insert(0, one)); // one already has a parent
}
[Test]
public void CanBeSet()
{
ContainerBlock container = new MockContainerBlock();
var one = new ParagraphBlock();
container.Insert(0, one);
Assert.AreEqual(1, container.Count);
Assert.AreSame(container[0], one);
Assert.AreSame(container, one.Parent);
var two = new ParagraphBlock();
container[0] = two;
Assert.AreSame(container, two.Parent);
Assert.Null(one.Parent);
Assert.Throws<ArgumentException>(() => container[0] = two); // two already has a parent
}
[Test]
public void Contains()
{
var container = new MockContainerBlock();
var block = new ParagraphBlock();
Assert.False(container.Contains(block));
container.Add(block);
Assert.True(container.Contains(block));
container.Add(new ParagraphBlock());
Assert.True(container.Contains(block));
container.Insert(0, new ParagraphBlock());
Assert.True(container.Contains(block));
}
[Test]
public void Remove()
{
var container = new MockContainerBlock();
var block = new ParagraphBlock();
Assert.False(container.Remove(block));
Assert.AreEqual(0, container.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => container.RemoveAt(0));
Assert.AreEqual(0, container.Count);
container.Add(block);
Assert.AreEqual(1, container.Count);
Assert.True(container.Remove(block));
Assert.AreEqual(0, container.Count);
Assert.False(container.Remove(block));
Assert.AreEqual(0, container.Count);
container.Add(block);
Assert.AreEqual(1, container.Count);
container.RemoveAt(0);
Assert.AreEqual(0, container.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => container.RemoveAt(0));
Assert.AreEqual(0, container.Count);
container.Add(new ParagraphBlock { Column = 1 });
container.Add(new ParagraphBlock { Column = 2 });
container.Add(new ParagraphBlock { Column = 3 });
container.Add(new ParagraphBlock { Column = 4 });
Assert.AreEqual(4, container.Count);
container.RemoveAt(2);
Assert.AreEqual(3, container.Count);
Assert.AreEqual(4, container[2].Column);
Assert.True(container.Remove(container[1]));
Assert.AreEqual(2, container.Count);
Assert.AreEqual(1, container[0].Column);
Assert.AreEqual(4, container[1].Column);
Assert.Throws<IndexOutOfRangeException>(() => _ = container[2]);
}
[Test]
public void CopyTo()
{
var container = new MockContainerBlock();
var destination = new Block[4];
container.CopyTo(destination, 0);
container.CopyTo(destination, 1);
container.CopyTo(destination, -1);
container.CopyTo(destination, 5);
Assert.Null(destination[0]);
container.Add(new ParagraphBlock());
container.CopyTo(destination, 0);
Assert.NotNull(destination[0]);
Assert.Null(destination[1]);
Assert.Null(destination[2]);
Assert.Null(destination[3]);
container.CopyTo(destination, 2);
Assert.NotNull(destination[0]);
Assert.Null(destination[1]);
Assert.NotNull(destination[2]);
Assert.Null(destination[3]);
Array.Clear(destination);
container.Add(new ParagraphBlock());
container.CopyTo(destination, 1);
Assert.Null(destination[0]);
Assert.NotNull(destination[1]);
Assert.NotNull(destination[2]);
Assert.Null(destination[3]);
Assert.Throws<IndexOutOfRangeException>(() => container.CopyTo(destination, 3));
}
}

View File

@@ -1,41 +1,38 @@
using System;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestContainerInlines
{
public class TestContainerInlines
private class MockLeafBlock : LeafBlock
{
private class MockLeafBlock : LeafBlock
public MockLeafBlock()
: base(null)
{
public MockLeafBlock()
: base(null)
{
}
}
[Test]
public void CanBeAddedToLeafBlock()
{
var leafBlock1 = new MockLeafBlock();
var one = new ContainerInline();
Assert.Null(one.ParentBlock);
leafBlock1.Inline = one;
Assert.AreSame(leafBlock1, one.ParentBlock);
var two = new ContainerInline();
Assert.Null(two.ParentBlock);
leafBlock1.Inline = two;
Assert.AreSame(leafBlock1, two.ParentBlock);
Assert.Null(one.ParentBlock);
var leafBlock2 = new MockLeafBlock();
Assert.Throws<ArgumentException>(() => leafBlock2.Inline = two);
}
}
[Test]
public void CanBeAddedToLeafBlock()
{
var leafBlock1 = new MockLeafBlock();
var one = new ContainerInline();
Assert.Null(one.ParentBlock);
leafBlock1.Inline = one;
Assert.AreSame(leafBlock1, one.ParentBlock);
var two = new ContainerInline();
Assert.Null(two.ParentBlock);
leafBlock1.Inline = two;
Assert.AreSame(leafBlock1, two.ParentBlock);
Assert.Null(one.ParentBlock);
var leafBlock2 = new MockLeafBlock();
Assert.Throws<ArgumentException>(() => leafBlock2.Inline = two);
}
}

View File

@@ -1,128 +1,124 @@
using System;
using System.Collections.Generic;
using Markdig.Extensions.Emoji;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestCustomEmojis
{
[TestFixture]
public class TestCustomEmojis
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomEmoji(string input, string expected)
{
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomEmoji(string input, string expected)
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
emojiToUnicode[":smiley:"] = "♥";
emojiToUnicode[":smiley:"] = "♥";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomSmiley(string input, string expected)
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>:confused:</p>\n")] // default emoji does not work
[TestCase(":/", "<p>:/</p>\n")] // default smiley does not work
public void TestCustomSmiley(string input, string expected)
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":)", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomEmoji(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
[Test]
[TestCase(":smiley:", "<p>♥</p>\n")]
[TestCase(":)", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomEmoji(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
emojiToUnicode[":smiley:"] = "♥";
emojiToUnicode[":smiley:"] = "♥";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomSmiley(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
[Test]
[TestCase(":testheart:", "<p>♥</p>\n")]
[TestCase("hello", "<p>♥</p>\n")]
[TestCase(":confused:", "<p>😕</p>\n")] // default emoji still works
[TestCase(":/", "<p>😕</p>\n")] // default smiley still works
public void TestOverrideDefaultWithCustomSmiley(string input, string expected)
{
var emojiToUnicode = EmojiMapping.GetDefaultEmojiShortcodeToUnicode();
var smileyToEmoji = EmojiMapping.GetDefaultSmileyToEmojiShortcode();
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
emojiToUnicode[":testheart:"] = "♥";
smileyToEmoji["hello"] = ":testheart:";
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var customMapping = new EmojiMapping(emojiToUnicode, smileyToEmoji);
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(customEmojiMapping: customMapping)
.Build();
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
var actual = Markdown.ToHtml(input, pipeline);
Assert.AreEqual(expected, actual);
}
[Test]
public void TestCustomEmojiValidation()
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
[Test]
public void TestCustomEmojiValidation()
{
var emojiToUnicode = new Dictionary<string, string>();
var smileyToEmoji = new Dictionary<string, string>();
Assert.Throws<ArgumentNullException>(() => new EmojiMapping(null, smileyToEmoji));
Assert.Throws<ArgumentNullException>(() => new EmojiMapping(emojiToUnicode, null));
Assert.Throws<ArgumentNullException>(() => new EmojiMapping(null, smileyToEmoji));
Assert.Throws<ArgumentNullException>(() => new EmojiMapping(emojiToUnicode, null));
emojiToUnicode.Add("null-value", null);
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
emojiToUnicode.Clear();
emojiToUnicode.Add("null-value", null);
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
emojiToUnicode.Clear();
smileyToEmoji.Add("null-value", null);
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
smileyToEmoji.Clear();
smileyToEmoji.Add("null-value", null);
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
smileyToEmoji.Clear();
smileyToEmoji.Add("foo", "something-that-does-not-exist-in-emojiToUnicode");
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
smileyToEmoji.Clear();
smileyToEmoji.Add("foo", "something-that-does-not-exist-in-emojiToUnicode");
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
smileyToEmoji.Clear();
emojiToUnicode.Add("a", "aaa");
emojiToUnicode.Add("b", "bbb");
emojiToUnicode.Add("c", "ccc");
smileyToEmoji.Add("a", "c"); // "a" already exists in emojiToUnicode
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
}
emojiToUnicode.Add("a", "aaa");
emojiToUnicode.Add("b", "bbb");
emojiToUnicode.Add("c", "ccc");
smileyToEmoji.Add("a", "c"); // "a" already exists in emojiToUnicode
Assert.Throws<ArgumentException>(() => new EmojiMapping(emojiToUnicode, smileyToEmoji));
}
}

View File

@@ -1,150 +1,145 @@
using NUnit.Framework;
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System.Linq;
using System.Collections.Generic;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestDescendantsOrder
{
[TestFixture]
public class TestDescendantsOrder
public static void TestSchemas(MarkdownDocument[] specsSyntaxTrees)
{
public static void TestSchemas(MarkdownDocument[] specsSyntaxTrees)
foreach (var syntaxTree in specsSyntaxTrees)
{
foreach (var syntaxTree in specsSyntaxTrees)
AssertIEnumerablesAreEqual(
Descendants_Legacy(syntaxTree),
syntaxTree.Descendants());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
syntaxTree.Descendants<ParagraphBlock>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
(syntaxTree as ContainerBlock).Descendants<ParagraphBlock>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<LiteralInline>(),
syntaxTree.Descendants<LiteralInline>());
foreach (LiteralInline literalInline in syntaxTree.Descendants<LiteralInline>())
{
Assert.AreSame(Array.Empty<ListBlock>(), literalInline.Descendants<ListBlock>());
Assert.AreSame(Array.Empty<ParagraphBlock>(), literalInline.Descendants<ParagraphBlock>());
Assert.AreSame(Array.Empty<ContainerInline>(), literalInline.Descendants<ContainerInline>());
}
foreach (ContainerInline containerInline in syntaxTree.Descendants<ContainerInline>())
{
AssertIEnumerablesAreEqual(
Descendants_Legacy(syntaxTree),
syntaxTree.Descendants());
containerInline.FindDescendants<LiteralInline>(),
containerInline.Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
syntaxTree.Descendants<ParagraphBlock>());
containerInline.FindDescendants<LiteralInline>(),
(containerInline as MarkdownObject).Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<ParagraphBlock>(),
(syntaxTree as ContainerBlock).Descendants<ParagraphBlock>());
AssertIEnumerablesAreEqual(
syntaxTree.Descendants().OfType<LiteralInline>(),
syntaxTree.Descendants<LiteralInline>());
foreach (LiteralInline literalInline in syntaxTree.Descendants<LiteralInline>())
if (containerInline.FirstChild is null)
{
Assert.AreSame(ArrayHelper.Empty<ListBlock>(), literalInline.Descendants<ListBlock>());
Assert.AreSame(ArrayHelper.Empty<ParagraphBlock>(), literalInline.Descendants<ParagraphBlock>());
Assert.AreSame(ArrayHelper.Empty<ContainerInline>(), literalInline.Descendants<ContainerInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), containerInline.Descendants<LiteralInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), containerInline.FindDescendants<LiteralInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), (containerInline as MarkdownObject).Descendants<LiteralInline>());
}
foreach (ContainerInline containerInline in syntaxTree.Descendants<ContainerInline>())
Assert.AreSame(Array.Empty<ListBlock>(), containerInline.Descendants<ListBlock>());
Assert.AreSame(Array.Empty<ParagraphBlock>(), containerInline.Descendants<ParagraphBlock>());
}
foreach (ParagraphBlock paragraphBlock in syntaxTree.Descendants<ParagraphBlock>())
{
AssertIEnumerablesAreEqual(
(paragraphBlock as MarkdownObject).Descendants<LiteralInline>(),
paragraphBlock.Descendants<LiteralInline>());
Assert.AreSame(Array.Empty<ParagraphBlock>(), paragraphBlock.Descendants<ParagraphBlock>());
}
foreach (ContainerBlock containerBlock in syntaxTree.Descendants<ContainerBlock>())
{
AssertIEnumerablesAreEqual(
containerBlock.Descendants<LiteralInline>(),
(containerBlock as MarkdownObject).Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
containerBlock.Descendants<ParagraphBlock>(),
(containerBlock as MarkdownObject).Descendants<ParagraphBlock>());
if (containerBlock.Count == 0)
{
AssertIEnumerablesAreEqual(
containerInline.FindDescendants<LiteralInline>(),
containerInline.Descendants<LiteralInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), containerBlock.Descendants<LiteralInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), (containerBlock as Block).Descendants<LiteralInline>());
Assert.AreSame(Array.Empty<LiteralInline>(), (containerBlock as MarkdownObject).Descendants<LiteralInline>());
}
}
}
}
AssertIEnumerablesAreEqual(
containerInline.FindDescendants<LiteralInline>(),
(containerInline as MarkdownObject).Descendants<LiteralInline>());
private static void AssertIEnumerablesAreEqual<T>(IEnumerable<T> first, IEnumerable<T> second)
{
var firstList = new List<T>(first);
var secondList = new List<T>(second);
if (containerInline.FirstChild is null)
Assert.AreEqual(firstList.Count, secondList.Count);
for (int i = 0; i < firstList.Count; i++)
{
Assert.AreSame(firstList[i], secondList[i]);
}
}
private static IEnumerable<MarkdownObject> Descendants_Legacy(MarkdownObject markdownObject)
{
// TODO: implement a recursiveless method
var block = markdownObject as ContainerBlock;
if (block != null)
{
foreach (var subBlock in block)
{
yield return subBlock;
foreach (var sub in Descendants_Legacy(subBlock))
{
yield return sub;
}
// Visit leaf block that have inlines
var leafBlock = subBlock as LeafBlock;
if (leafBlock?.Inline != null)
{
foreach (var subInline in Descendants_Legacy(leafBlock.Inline))
{
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), containerInline.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), containerInline.FindDescendants<LiteralInline>());
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), (containerInline as MarkdownObject).Descendants<LiteralInline>());
}
Assert.AreSame(ArrayHelper.Empty<ListBlock>(), containerInline.Descendants<ListBlock>());
Assert.AreSame(ArrayHelper.Empty<ParagraphBlock>(), containerInline.Descendants<ParagraphBlock>());
}
foreach (ParagraphBlock paragraphBlock in syntaxTree.Descendants<ParagraphBlock>())
{
AssertIEnumerablesAreEqual(
(paragraphBlock as MarkdownObject).Descendants<LiteralInline>(),
paragraphBlock.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper.Empty<ParagraphBlock>(), paragraphBlock.Descendants<ParagraphBlock>());
}
foreach (ContainerBlock containerBlock in syntaxTree.Descendants<ContainerBlock>())
{
AssertIEnumerablesAreEqual(
containerBlock.Descendants<LiteralInline>(),
(containerBlock as MarkdownObject).Descendants<LiteralInline>());
AssertIEnumerablesAreEqual(
containerBlock.Descendants<ParagraphBlock>(),
(containerBlock as MarkdownObject).Descendants<ParagraphBlock>());
if (containerBlock.Count == 0)
{
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), containerBlock.Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), (containerBlock as Block).Descendants<LiteralInline>());
Assert.AreSame(ArrayHelper.Empty<LiteralInline>(), (containerBlock as MarkdownObject).Descendants<LiteralInline>());
yield return subInline;
}
}
}
}
private static void AssertIEnumerablesAreEqual<T>(IEnumerable<T> first, IEnumerable<T> second)
else
{
var firstList = new List<T>(first);
var secondList = new List<T>(second);
Assert.AreEqual(firstList.Count, secondList.Count);
for (int i = 0; i < firstList.Count; i++)
var inline = markdownObject as ContainerInline;
if (inline != null)
{
Assert.AreSame(firstList[i], secondList[i]);
}
}
private static IEnumerable<MarkdownObject> Descendants_Legacy(MarkdownObject markdownObject)
{
// TODO: implement a recursiveless method
var block = markdownObject as ContainerBlock;
if (block != null)
{
foreach (var subBlock in block)
var child = inline.FirstChild;
while (child != null)
{
yield return subBlock;
var next = child.NextSibling;
yield return child;
foreach (var sub in Descendants_Legacy(subBlock))
foreach (var sub in Descendants_Legacy(child))
{
yield return sub;
}
// Visit leaf block that have inlines
var leafBlock = subBlock as LeafBlock;
if (leafBlock?.Inline != null)
{
foreach (var subInline in Descendants_Legacy(leafBlock.Inline))
{
yield return subInline;
}
}
}
}
else
{
var inline = markdownObject as ContainerInline;
if (inline != null)
{
var child = inline.FirstChild;
while (child != null)
{
var next = child.NextSibling;
yield return child;
foreach (var sub in Descendants_Legacy(child))
{
yield return sub;
}
child = next;
}
child = next;
}
}
}

View File

@@ -1,169 +1,166 @@
using System.Diagnostics;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestEmphasisExtended
{
[TestFixture]
public class TestEmphasisExtended
class EmphasisTestExtension : IMarkdownExtension
{
class EmphasisTestExtension : IMarkdownExtension
public void Setup(MarkdownPipelineBuilder pipeline)
{
public void Setup(MarkdownPipelineBuilder pipeline)
var emphasisParser = pipeline.InlineParsers.Find<EmphasisInlineParser>();
Debug.Assert(emphasisParser != null);
foreach (var emphasis in EmphasisTestDescriptors)
{
var emphasisParser = pipeline.InlineParsers.Find<EmphasisInlineParser>();
Debug.Assert(emphasisParser != null);
foreach (var emphasis in EmphasisTestDescriptors)
{
emphasisParser.EmphasisDescriptors.Add(
new EmphasisDescriptor(emphasis.Character, emphasis.Minimum, emphasis.Maximum, true));
}
emphasisParser.TryCreateEmphasisInlineList.Add((delimiterChar, delimiterCount) =>
{
return delimiterChar == '*' || delimiterChar == '_'
? null
: new CustomEmphasisInline() { DelimiterChar = delimiterChar, DelimiterCount = delimiterCount };
});
emphasisParser.EmphasisDescriptors.Add(
new EmphasisDescriptor(emphasis.Character, emphasis.Minimum, emphasis.Maximum, true));
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
emphasisParser.TryCreateEmphasisInlineList.Add((delimiterChar, delimiterCount) =>
{
renderer.ObjectRenderers.Insert(0, new EmphasisRenderer());
}
class EmphasisRenderer : HtmlObjectRenderer<CustomEmphasisInline>
{
protected override void Write(HtmlRenderer renderer, CustomEmphasisInline obj)
{
var tag = EmphasisTestDescriptors.First(test => test.Character == obj.DelimiterChar).Tags[obj.DelimiterCount];
renderer.Write(tag.OpeningTag);
renderer.WriteChildren(obj);
renderer.Write(tag.ClosingTag);
}
}
return delimiterChar is '*' or '_'
? null
: new CustomEmphasisInline() { DelimiterChar = delimiterChar, DelimiterCount = delimiterCount };
});
}
class Tag
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
#pragma warning disable CS0649
public int Level;
#pragma warning restore CS0649
public string RawTag;
public string OpeningTag;
public string ClosingTag;
public Tag(string tag)
{
RawTag = tag;
OpeningTag = "<" + tag + ">";
ClosingTag = "</" + tag + ">";
}
public static implicit operator Tag(string tag)
=> new Tag(tag);
renderer.ObjectRenderers.Insert(0, new EmphasisRenderer());
}
class EmphasisTestDescriptor
{
public char Character;
public int Minimum;
public int Maximum;
public Dictionary<int, Tag> Tags = new Dictionary<int, Tag>();
private EmphasisTestDescriptor(char character, int min, int max)
class EmphasisRenderer : HtmlObjectRenderer<CustomEmphasisInline>
{
protected override void Write(HtmlRenderer renderer, CustomEmphasisInline obj)
{
Character = character;
Minimum = min;
Maximum = max;
}
public EmphasisTestDescriptor(char character, int min, int max, params Tag[] tags)
: this(character, min, max)
{
Debug.Assert(tags.Length == max - min + 1);
foreach (var tag in tags)
{
Tags.Add(min++, tag);
}
}
public EmphasisTestDescriptor(char character, int min, int max, string tag)
: this(character, min, max, new Tag(tag)) { }
}
class CustomEmphasisInline : EmphasisInline { }
static readonly EmphasisTestDescriptor[] EmphasisTestDescriptors = new[]
{
// Min Max
new EmphasisTestDescriptor('"', 1, 1, "quotation"),
new EmphasisTestDescriptor(',', 1, 2, "comma", "extra-comma"),
new EmphasisTestDescriptor('!', 2, 3, "warning", "error"),
new EmphasisTestDescriptor('=', 1, 3, "equal", "really-equal", "congruent"),
new EmphasisTestDescriptor('1', 1, 1, "one-only"),
new EmphasisTestDescriptor('2', 2, 2, "two-only"),
new EmphasisTestDescriptor('3', 3, 3, "three-only"),
};
var tag = EmphasisTestDescriptors.First(test => test.Character == obj.DelimiterChar).Tags[obj.DelimiterCount];
static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Use<EmphasisTestExtension>().Build();
[Test]
[TestCase("*foo**", "<em>foo</em>*")]
[TestCase("**foo*", "*<em>foo</em>")]
[TestCase("***foo***", "<em><strong>foo</strong></em>")]
[TestCase("**_foo_**", "<strong><em>foo</em></strong>")]
[TestCase("_**foo**_", "<em><strong>foo</strong></em>")]
[TestCase("\"foo\"", "<quotation>foo</quotation>")]
[TestCase("\"\"foo\"\"", "<quotation><quotation>foo</quotation></quotation>")]
[TestCase("\"foo\"\"", "<quotation>foo</quotation>&quot;")]
[TestCase("\"\"foo\"", "&quot;<quotation>foo</quotation>")]
[TestCase(", foo", ", foo")]
[TestCase(", foo,", ", foo,")]
[TestCase(",some, foo,", "<comma>some</comma> foo,")]
[TestCase(",,foo,,", "<extra-comma>foo</extra-comma>")]
[TestCase(",foo,,", "<comma>foo</comma>,")]
[TestCase(",,,foo,,,", "<comma><extra-comma>foo</extra-comma></comma>")]
[TestCase("*foo*&_foo_", "<em>foo</em>&amp;<em>foo</em>")]
[TestCase("!1!", "!1!")]
[TestCase("!!2!!", "<warning>2</warning>")]
[TestCase("!!!3!!!", "<error>3</error>")]
[TestCase("!!!34!!!!", "<error>34</error>!")]
[TestCase("!!!!43!!!", "!<error>43</error>")]
[TestCase("!!!!44!!!!", "!<error>44!</error>")] // This is a new case - should the second ! be before or after </error>?
[TestCase("!!!!!5!!!!!", "<warning><error>5</error></warning>")]
[TestCase("!!!!!!6!!!!!!", "<error><error>6</error></error>")]
[TestCase("!! !mixed!!!", "!! !mixed!!!")] // can't open the delimiter because of the whitespace
[TestCase("=", "=")]
[TestCase("==", "==")]
[TestCase("====", "====")]
[TestCase("=a", "=a")]
[TestCase("=a=", "<equal>a</equal>")]
[TestCase("==a=", "=<equal>a</equal>")]
[TestCase("==a==", "<really-equal>a</really-equal>")]
[TestCase("==a===", "<really-equal>a</really-equal>=")]
[TestCase("===a===", "<congruent>a</congruent>")]
[TestCase("====a====", "<equal><congruent>a</congruent></equal>")]
[TestCase("=====a=====", "<really-equal><congruent>a</congruent></really-equal>")]
[TestCase("1", "1")]
[TestCase("1 1", "1 1")]
[TestCase("1Foo1", "<one-only>Foo</one-only>")]
[TestCase("1121", "1<one-only>2</one-only>")]
[TestCase("22322", "<two-only>3</two-only>")]
[TestCase("2232", "2232")]
[TestCase("333", "333")]
[TestCase("3334333", "<three-only>4</three-only>")]
[TestCase("33334333", "3<three-only>4</three-only>")]
[TestCase("33343333", "<three-only>4</three-only>3")]
[TestCase("122122", "<one-only>22</one-only>22")]
[TestCase("221221", "<two-only>1</two-only>1")]
[TestCase("122foo221", "<one-only><two-only>foo</two-only></one-only>")]
[TestCase("122foo122", "<one-only>22foo</one-only>22")]
[TestCase("!!!!!Attention:!! \"==1+1== 2\",but ===333 and 222===, mod 111!!!",
"<error><warning>Attention:</warning> <quotation><really-equal><one-only>+</one-only></really-equal> 2</quotation><comma>but <congruent>333 and 222</congruent></comma> mod 111</error>")]
public void TestEmphasis(string markdown, string expectedHtml)
{
TestParser.TestSpec(markdown, "<p>" + expectedHtml + "</p>", Pipeline);
renderer.Write(tag.OpeningTag);
renderer.WriteChildren(obj);
renderer.Write(tag.ClosingTag);
}
}
}
class Tag
{
#pragma warning disable CS0649
public int Level;
#pragma warning restore CS0649
public string RawTag;
public string OpeningTag;
public string ClosingTag;
public Tag(string tag)
{
RawTag = tag;
OpeningTag = "<" + tag + ">";
ClosingTag = "</" + tag + ">";
}
public static implicit operator Tag(string tag)
=> new Tag(tag);
}
class EmphasisTestDescriptor
{
public char Character;
public int Minimum;
public int Maximum;
public Dictionary<int, Tag> Tags = new Dictionary<int, Tag>();
private EmphasisTestDescriptor(char character, int min, int max)
{
Character = character;
Minimum = min;
Maximum = max;
}
public EmphasisTestDescriptor(char character, int min, int max, params Tag[] tags)
: this(character, min, max)
{
Debug.Assert(tags.Length == max - min + 1);
foreach (var tag in tags)
{
Tags.Add(min++, tag);
}
}
public EmphasisTestDescriptor(char character, int min, int max, string tag)
: this(character, min, max, new Tag(tag)) { }
}
class CustomEmphasisInline : EmphasisInline { }
static readonly EmphasisTestDescriptor[] EmphasisTestDescriptors = new[]
{
// Min Max
new EmphasisTestDescriptor('"', 1, 1, "quotation"),
new EmphasisTestDescriptor(',', 1, 2, "comma", "extra-comma"),
new EmphasisTestDescriptor('!', 2, 3, "warning", "error"),
new EmphasisTestDescriptor('=', 1, 3, "equal", "really-equal", "congruent"),
new EmphasisTestDescriptor('1', 1, 1, "one-only"),
new EmphasisTestDescriptor('2', 2, 2, "two-only"),
new EmphasisTestDescriptor('3', 3, 3, "three-only"),
};
static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().Use<EmphasisTestExtension>().Build();
[Test]
[TestCase("*foo**", "<em>foo</em>*")]
[TestCase("**foo*", "*<em>foo</em>")]
[TestCase("***foo***", "<em><strong>foo</strong></em>")]
[TestCase("**_foo_**", "<strong><em>foo</em></strong>")]
[TestCase("_**foo**_", "<em><strong>foo</strong></em>")]
[TestCase("\"foo\"", "<quotation>foo</quotation>")]
[TestCase("\"\"foo\"\"", "<quotation><quotation>foo</quotation></quotation>")]
[TestCase("\"foo\"\"", "<quotation>foo</quotation>&quot;")]
[TestCase("\"\"foo\"", "&quot;<quotation>foo</quotation>")]
[TestCase(", foo", ", foo")]
[TestCase(", foo,", ", foo,")]
[TestCase(",some, foo,", "<comma>some</comma> foo,")]
[TestCase(",,foo,,", "<extra-comma>foo</extra-comma>")]
[TestCase(",foo,,", "<comma>foo</comma>,")]
[TestCase(",,,foo,,,", "<comma><extra-comma>foo</extra-comma></comma>")]
[TestCase("*foo*&_foo_", "<em>foo</em>&amp;<em>foo</em>")]
[TestCase("!1!", "!1!")]
[TestCase("!!2!!", "<warning>2</warning>")]
[TestCase("!!!3!!!", "<error>3</error>")]
[TestCase("!!!34!!!!", "<error>34</error>!")]
[TestCase("!!!!43!!!", "!<error>43</error>")]
[TestCase("!!!!44!!!!", "!<error>44!</error>")] // This is a new case - should the second ! be before or after </error>?
[TestCase("!!!!!5!!!!!", "<warning><error>5</error></warning>")]
[TestCase("!!!!!!6!!!!!!", "<error><error>6</error></error>")]
[TestCase("!! !mixed!!!", "!! !mixed!!!")] // can't open the delimiter because of the whitespace
[TestCase("=", "=")]
[TestCase("==", "==")]
[TestCase("====", "====")]
[TestCase("=a", "=a")]
[TestCase("=a=", "<equal>a</equal>")]
[TestCase("==a=", "=<equal>a</equal>")]
[TestCase("==a==", "<really-equal>a</really-equal>")]
[TestCase("==a===", "<really-equal>a</really-equal>=")]
[TestCase("===a===", "<congruent>a</congruent>")]
[TestCase("====a====", "<equal><congruent>a</congruent></equal>")]
[TestCase("=====a=====", "<really-equal><congruent>a</congruent></really-equal>")]
[TestCase("1", "1")]
[TestCase("1 1", "1 1")]
[TestCase("1Foo1", "<one-only>Foo</one-only>")]
[TestCase("1121", "1<one-only>2</one-only>")]
[TestCase("22322", "<two-only>3</two-only>")]
[TestCase("2232", "2232")]
[TestCase("333", "333")]
[TestCase("3334333", "<three-only>4</three-only>")]
[TestCase("33334333", "3<three-only>4</three-only>")]
[TestCase("33343333", "<three-only>4</three-only>3")]
[TestCase("122122", "<one-only>22</one-only>22")]
[TestCase("221221", "<two-only>1</two-only>1")]
[TestCase("122foo221", "<one-only><two-only>foo</two-only></one-only>")]
[TestCase("122foo122", "<one-only>22foo</one-only>22")]
[TestCase("!!!!!Attention:!! \"==1+1== 2\",but ===333 and 222===, mod 111!!!",
"<error><warning>Attention:</warning> <quotation><really-equal><one-only>+</one-only></really-equal> 2</quotation><comma>but <congruent>333 and 222</congruent></comma> mod 111</error>")]
public void TestEmphasis(string markdown, string expectedHtml)
{
TestParser.TestSpec(markdown, "<p>" + expectedHtml + "</p>", Pipeline);
}
}

View File

@@ -1,45 +1,43 @@
using NUnit.Framework;
using Markdig.Extensions.EmphasisExtras;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestEmphasisExtraOptions
{
[TestFixture]
public class TestEmphasisExtraOptions
[Test]
public void OnlyStrikethrough_Single()
{
[Test]
public void OnlyStrikethrough_Single()
{
TestParser.TestSpec("~foo~", "<p>~foo~</p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build());
}
TestParser.TestSpec("~foo~", "<p>~foo~</p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build());
}
[Test]
public void OnlyStrikethrough_Double()
{
TestParser.TestSpec("~~foo~~", "<p><del>foo</del></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build());
}
[Test]
public void OnlyStrikethrough_Double()
{
TestParser.TestSpec("~~foo~~", "<p><del>foo</del></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough).Build());
}
[Test]
public void OnlySubscript_Single()
{
TestParser.TestSpec("~foo~", "<p><sub>foo</sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void OnlySubscript_Single()
{
TestParser.TestSpec("~foo~", "<p><sub>foo</sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void OnlySubscript_Double()
{
TestParser.TestSpec("~~foo~~", "<p><sub><sub>foo</sub></sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void OnlySubscript_Double()
{
TestParser.TestSpec("~~foo~~", "<p><sub><sub>foo</sub></sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void SubscriptAndStrikethrough_Single()
{
TestParser.TestSpec("~foo~", "<p><sub>foo</sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void SubscriptAndStrikethrough_Single()
{
TestParser.TestSpec("~foo~", "<p><sub>foo</sub></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void SubscriptAndStrikethrough_Double()
{
TestParser.TestSpec("~~foo~~", "<p><del>foo</del></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build());
}
[Test]
public void SubscriptAndStrikethrough_Double()
{
TestParser.TestSpec("~~foo~~", "<p><del>foo</del></p>", new MarkdownPipelineBuilder().UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Subscript).Build());
}
}

View File

@@ -1,23 +1,37 @@
// 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 NUnit.Framework;
namespace Markdig.Tests
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Tests;
[TestFixture]
public partial class TestEmphasisPlus
{
[TestFixture]
public partial class TestEmphasisPlus
[Test]
public void StrongNormal()
{
[Test]
public void StrongNormal()
{
TestParser.TestSpec("***Strong emphasis*** normal", "<p><em><strong>Strong emphasis</strong></em> normal</p>", "");
}
TestParser.TestSpec("***Strong emphasis*** normal", "<p><em><strong>Strong emphasis</strong></em> normal</p>", "");
}
[Test]
public void NormalStrongNormal()
{
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <em><strong>Strong emphasis</strong></em> normal</p>", "");
}
[Test]
public void NormalStrongNormal()
{
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <em><strong>Strong emphasis</strong></em> normal</p>", "");
}
[Test]
public void OpenEmphasisHasConvenientContentStringSlice()
{
var pipeline = new MarkdownPipelineBuilder().Build();
var document = Markdown.Parse("test*test", pipeline);
var emphasisDelimiterLiteral = (LiteralInline)((ParagraphBlock)document.LastChild).Inline.ElementAt(1);
Assert.That(emphasisDelimiterLiteral.Content.Text == "test*test");
Assert.That(emphasisDelimiterLiteral.Content.Start == 4);
Assert.That(emphasisDelimiterLiteral.Content.End == 4);
}
}

View File

@@ -1,48 +1,45 @@
using NUnit.Framework;
namespace Markdig.Tests;
namespace Markdig.Tests
[TestFixture]
public class TestExceptionNotThrown
{
[TestFixture]
public class TestExceptionNotThrown
[Test]
public void DoesNotThrowIndexOutOfRangeException1()
{
[Test]
public void DoesNotThrowIndexOutOfRangeException1()
Assert.DoesNotThrow(() =>
{
Assert.DoesNotThrow(() =>
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+", pipeline);
});
}
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+", pipeline);
});
}
[Test]
public void DoesNotThrowIndexOutOfRangeException2()
[Test]
public void DoesNotThrowIndexOutOfRangeException2()
{
Assert.DoesNotThrow(() =>
{
Assert.DoesNotThrow(() =>
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+--\n|\n+0", pipeline);
});
}
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+--\n|\n+0", pipeline);
});
}
[Test]
public void DoesNotThrowIndexOutOfRangeException3()
[Test]
public void DoesNotThrowIndexOutOfRangeException3()
{
Assert.DoesNotThrow(() =>
{
Assert.DoesNotThrow(() =>
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+\n0", pipeline);
});
}
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+\n0", pipeline);
});
}
[Test]
public void DoesNotThrowIndexOutOfRangeException4()
[Test]
public void DoesNotThrowIndexOutOfRangeException4()
{
Assert.DoesNotThrow(() =>
{
Assert.DoesNotThrow(() =>
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+0", pipeline);
});
}
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
Markdown.ToHtml("+-\n|\n+0", pipeline);
});
}
}

View File

@@ -1,189 +1,186 @@
using Markdig.Helpers;
using NUnit.Framework;
using System;
using System.Text;
using System.Threading.Tasks;
namespace Markdig.Tests
using Markdig.Helpers;
namespace Markdig.Tests;
[TestFixture]
public class TestFastStringWriter
{
[TestFixture]
public class TestFastStringWriter
private const string NewLineReplacement = "~~NEW_LINE~~";
private FastStringWriter _writer = new();
[SetUp]
public void Setup()
{
private const string NewLineReplacement = "~~NEW_LINE~~";
private FastStringWriter _writer = new();
[SetUp]
public void Setup()
_writer = new FastStringWriter
{
_writer = new FastStringWriter
{
NewLine = NewLineReplacement
};
}
NewLine = NewLineReplacement
};
}
public void AssertToString(string value)
{
value = value.Replace("\n", NewLineReplacement);
Assert.AreEqual(value, _writer.ToString());
Assert.AreEqual(value, _writer.ToString());
}
public void AssertToString(string value)
{
value = value.Replace("\n", NewLineReplacement);
Assert.AreEqual(value, _writer.ToString());
Assert.AreEqual(value, _writer.ToString());
}
[Test]
public async Task NewLine()
{
Assert.AreEqual("\n", new FastStringWriter().NewLine);
[Test]
public async Task NewLine()
{
Assert.AreEqual("\n", new FastStringWriter().NewLine);
_writer.NewLine = "\r";
Assert.AreEqual("\r", _writer.NewLine);
_writer.NewLine = "\r";
Assert.AreEqual("\r", _writer.NewLine);
_writer.NewLine = "foo";
Assert.AreEqual("foo", _writer.NewLine);
_writer.NewLine = "foo";
Assert.AreEqual("foo", _writer.NewLine);
_writer.WriteLine();
await _writer.WriteLineAsync();
_writer.WriteLine("bar");
Assert.AreEqual("foofoobarfoo", _writer.ToString());
}
_writer.WriteLine();
await _writer.WriteLineAsync();
_writer.WriteLine("bar");
Assert.AreEqual("foofoobarfoo", _writer.ToString());
}
[Test]
public async Task FlushCloseDispose()
[Test]
public async Task FlushCloseDispose()
{
_writer.Write('a');
// Nops
_writer.Close();
_writer.Dispose();
await _writer.DisposeAsync();
_writer.Flush();
await _writer.FlushAsync();
_writer.Write('b');
AssertToString("ab");
}
[Test]
public async Task Write_Char()
{
_writer.Write('a');
AssertToString("a");
_writer.Write('b');
AssertToString("ab");
_writer.Write('\0');
_writer.Write('\r');
_writer.Write('\u1234');
AssertToString("ab\0\r\u1234");
_writer.Reset();
AssertToString("");
_writer.Write('a');
_writer.WriteLine('b');
_writer.Write('c');
_writer.Write('d');
_writer.WriteLine('e');
AssertToString("ab\ncde\n");
await _writer.WriteAsync('f');
await _writer.WriteLineAsync('g');
AssertToString("ab\ncde\nfg\n");
_writer.Reset();
for (int i = 0; i < 2050; i++)
{
_writer.Write('a');
// Nops
_writer.Close();
_writer.Dispose();
await _writer.DisposeAsync();
_writer.Flush();
await _writer.FlushAsync();
_writer.Write('b');
AssertToString("ab");
}
[Test]
public async Task Write_Char()
{
_writer.Write('a');
AssertToString("a");
_writer.Write('b');
AssertToString("ab");
_writer.Write('\0');
_writer.Write('\r');
_writer.Write('\u1234');
AssertToString("ab\0\r\u1234");
_writer.Reset();
AssertToString("");
_writer.Write('a');
_writer.WriteLine('b');
_writer.Write('c');
_writer.Write('d');
_writer.WriteLine('e');
AssertToString("ab\ncde\n");
await _writer.WriteAsync('f');
await _writer.WriteLineAsync('g');
AssertToString("ab\ncde\nfg\n");
_writer.Reset();
for (int i = 0; i < 2050; i++)
{
_writer.Write('a');
AssertToString(new string('a', i + 1));
}
}
[Test]
public async Task Write_String()
{
_writer.Write("foo");
AssertToString("foo");
_writer.WriteLine("bar");
AssertToString("foobar\n");
await _writer.WriteAsync("baz");
await _writer.WriteLineAsync("foo");
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050));
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_Span()
{
_writer.Write("foo".AsSpan());
AssertToString("foo");
_writer.WriteLine("bar".AsSpan());
AssertToString("foobar\n");
await _writer.WriteAsync("baz".AsMemory());
await _writer.WriteLineAsync("foo".AsMemory());
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050).AsSpan());
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_CharArray()
{
_writer.Write("foo".ToCharArray());
AssertToString("foo");
_writer.WriteLine("bar".ToCharArray());
AssertToString("foobar\n");
await _writer.WriteAsync("baz".ToCharArray());
await _writer.WriteLineAsync("foo".ToCharArray());
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050).ToCharArray());
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_CharArrayWithIndexes()
{
_writer.Write("foo".ToCharArray(), 1, 1);
AssertToString("o");
_writer.WriteLine("bar".ToCharArray(), 0, 2);
AssertToString("oba\n");
await _writer.WriteAsync("baz".ToCharArray(), 0, 1);
await _writer.WriteLineAsync("foo".ToCharArray(), 0, 3);
AssertToString("oba\nbfoo\n");
_writer.Write(new string('a', 1050).ToCharArray(), 10, 1035);
AssertToString("oba\nbfoo\n" + new string('a', 1035));
}
[Test]
public async Task Write_StringBuilder()
{
_writer.Write(new StringBuilder("foo"));
AssertToString("foo");
_writer.WriteLine(new StringBuilder("bar"));
AssertToString("foobar\n");
await _writer.WriteAsync(new StringBuilder("baz"));
await _writer.WriteLineAsync(new StringBuilder("foo"));
AssertToString("foobar\nbazfoo\n");
var sb = new StringBuilder("foo");
sb.Append('a', 1050);
_writer.Write(sb);
AssertToString("foobar\nbazfoo\nfoo" + new string('a', 1050));
AssertToString(new string('a', i + 1));
}
}
[Test]
public async Task Write_String()
{
_writer.Write("foo");
AssertToString("foo");
_writer.WriteLine("bar");
AssertToString("foobar\n");
await _writer.WriteAsync("baz");
await _writer.WriteLineAsync("foo");
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050));
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_Span()
{
_writer.Write("foo".AsSpan());
AssertToString("foo");
_writer.WriteLine("bar".AsSpan());
AssertToString("foobar\n");
await _writer.WriteAsync("baz".AsMemory());
await _writer.WriteLineAsync("foo".AsMemory());
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050).AsSpan());
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_CharArray()
{
_writer.Write("foo".ToCharArray());
AssertToString("foo");
_writer.WriteLine("bar".ToCharArray());
AssertToString("foobar\n");
await _writer.WriteAsync("baz".ToCharArray());
await _writer.WriteLineAsync("foo".ToCharArray());
AssertToString("foobar\nbazfoo\n");
_writer.Write(new string('a', 1050).ToCharArray());
AssertToString("foobar\nbazfoo\n" + new string('a', 1050));
}
[Test]
public async Task Write_CharArrayWithIndexes()
{
_writer.Write("foo".ToCharArray(), 1, 1);
AssertToString("o");
_writer.WriteLine("bar".ToCharArray(), 0, 2);
AssertToString("oba\n");
await _writer.WriteAsync("baz".ToCharArray(), 0, 1);
await _writer.WriteLineAsync("foo".ToCharArray(), 0, 3);
AssertToString("oba\nbfoo\n");
_writer.Write(new string('a', 1050).ToCharArray(), 10, 1035);
AssertToString("oba\nbfoo\n" + new string('a', 1035));
}
[Test]
public async Task Write_StringBuilder()
{
_writer.Write(new StringBuilder("foo"));
AssertToString("foo");
_writer.WriteLine(new StringBuilder("bar"));
AssertToString("foobar\n");
await _writer.WriteAsync(new StringBuilder("baz"));
await _writer.WriteLineAsync(new StringBuilder("foo"));
AssertToString("foobar\nbazfoo\n");
var sb = new StringBuilder("foo");
sb.Append('a', 1050);
_writer.Write(sb);
AssertToString("foobar\nbazfoo\nfoo" + new string('a', 1050));
}
}

View File

@@ -1,46 +1,43 @@
using System.Linq;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestFencedCodeBlocks
{
public class TestFencedCodeBlocks
[Test]
[TestCase("c#", "c#", "")]
[TestCase("C#", "C#", "")]
[TestCase(" c#", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" \tc# ", "c#", "")]
[TestCase("\t c# \t", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" c# foo", "c#", "foo")]
[TestCase(" c# \t fOo \t", "c#", "fOo")]
[TestCase("in\\%fo arg\\%ument", "in%fo", "arg%ument")]
[TestCase("info&#9; arg&acute;ument", "info\t", "arg\u00B4ument")]
public void TestInfoAndArguments(string infoString, string expectedInfo, string expectedArguments)
{
[Test]
[TestCase("c#", "c#", "")]
[TestCase("C#", "C#", "")]
[TestCase(" c#", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" \tc# ", "c#", "")]
[TestCase("\t c# \t", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" c# foo", "c#", "foo")]
[TestCase(" c# \t fOo \t", "c#", "fOo")]
[TestCase("in\\%fo arg\\%ument", "in%fo", "arg%ument")]
[TestCase("info&#9; arg&acute;ument", "info\t", "arg\u00B4ument")]
public void TestInfoAndArguments(string infoString, string expectedInfo, string expectedArguments)
Test('`');
Test('~');
void Test(char fencedChar)
{
Test('`');
Test('~');
const string Contents = "Foo\nBar\n";
void Test(char fencedChar)
{
const string Contents = "Foo\nBar\n";
var fence = new string(fencedChar, 3);
string markdownText = $"{fence}{infoString}\n{Contents}\n{fence}\n";
string fence = new string(fencedChar, 3);
string markdownText = $"{fence}{infoString}\n{Contents}\n{fence}\n";
MarkdownDocument document = Markdown.Parse(markdownText);
MarkdownDocument document = Markdown.Parse(markdownText);
FencedCodeBlock codeBlock = document.Descendants<FencedCodeBlock>().Single();
FencedCodeBlock codeBlock = document.Descendants<FencedCodeBlock>().Single();
Assert.AreEqual(fencedChar, codeBlock.FencedChar);
Assert.AreEqual(3, codeBlock.OpeningFencedCharCount);
Assert.AreEqual(3, codeBlock.ClosingFencedCharCount);
Assert.AreEqual(expectedInfo, codeBlock.Info);
Assert.AreEqual(expectedArguments, codeBlock.Arguments);
Assert.AreEqual(Contents, codeBlock.Lines.ToString());
}
Assert.AreEqual(fencedChar, codeBlock.FencedChar);
Assert.AreEqual(3, codeBlock.OpeningFencedCharCount);
Assert.AreEqual(3, codeBlock.ClosingFencedCharCount);
Assert.AreEqual(expectedInfo, codeBlock.Info);
Assert.AreEqual(expectedArguments, codeBlock.Arguments);
Assert.AreEqual(Contents, codeBlock.Lines.ToString());
}
}
}

View File

@@ -3,94 +3,91 @@
// See the license.txt file in the project root for more information.
using Markdig.Renderers.Html;
using NUnit.Framework;
using System.Collections.Generic;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture()]
public class TestHtmlAttributes
{
[TestFixture()]
public class TestHtmlAttributes
[Test]
public void TestAddClass()
{
[Test]
public void TestAddClass()
{
var attributes = new HtmlAttributes();
attributes.AddClass("test");
Assert.NotNull(attributes.Classes);
Assert.AreEqual(new List<string>() { "test" }, attributes.Classes);
var attributes = new HtmlAttributes();
attributes.AddClass("test");
Assert.NotNull(attributes.Classes);
Assert.AreEqual(new List<string>() { "test" }, attributes.Classes);
attributes.AddClass("test");
Assert.AreEqual(1, attributes.Classes.Count);
attributes.AddClass("test");
Assert.AreEqual(1, attributes.Classes.Count);
attributes.AddClass("test1");
Assert.AreEqual(new List<string>() { "test", "test1" }, attributes.Classes);
}
attributes.AddClass("test1");
Assert.AreEqual(new List<string>() { "test", "test1" }, attributes.Classes);
}
[Test]
public void TestAddProperty()
{
var attributes = new HtmlAttributes();
attributes.AddProperty("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
[Test]
public void TestAddProperty()
{
var attributes = new HtmlAttributes();
attributes.AddProperty("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
attributes.AddPropertyIfNotExist("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
attributes.AddPropertyIfNotExist("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
attributes.AddPropertyIfNotExist("key2", "2");
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, attributes.Properties);
}
attributes.AddPropertyIfNotExist("key2", "2");
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, attributes.Properties);
}
[Test]
public void TestCopyTo()
{
var from = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
[Test]
public void TestCopyTo()
{
var from = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
var to = new HtmlAttributes();
from.CopyTo(to);
var to = new HtmlAttributes();
from.CopyTo(to);
Assert.True(ReferenceEquals(from.Classes, to.Classes));
Assert.True(ReferenceEquals(from.Properties, to.Properties));
Assert.True(ReferenceEquals(from.Classes, to.Classes));
Assert.True(ReferenceEquals(from.Properties, to.Properties));
// From: Classes From: Properties To: Classes To: Properties
// test1: null null null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.CopyTo(to, false, false);
Assert.Null(to.Classes);
Assert.Null(to.Properties);
// From: Classes From: Properties To: Classes To: Properties
// test1: null null null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.CopyTo(to, false, false);
Assert.Null(to.Classes);
Assert.Null(to.Properties);
// test2: ["test"] ["key1", "1"] null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1")}, to.Properties);
// test2: ["test"] ["key1", "1"] null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1")}, to.Properties);
// test3: null null ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, to.Properties);
// test3: null null ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, to.Properties);
// test4: ["test1"] ["key2", "2"] ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test1");
from.AddProperty("key2", "2");
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test", "test1" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, to.Properties);
}
// test4: ["test1"] ["key2", "2"] ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test1");
from.AddProperty("key2", "2");
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test", "test1" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, to.Properties);
}
}

View File

@@ -1,30 +1,28 @@
// 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 NUnit.Framework;
using Markdig.Helpers;
namespace Markdig.Tests
{
[TestFixture]
public class TestHtmlHelper
{
[Test]
public void TestParseHtmlTagSimple()
{
var inputTag = "<a>";
var text = new StringSlice(inputTag);
Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag));
Assert.AreEqual(inputTag, outputTag);
}
namespace Markdig.Tests;
[Test]
public void TestParseHtmlTagSimpleWithAttribute()
{
var inputTag = "<a href='http://google.com'>";
var text = new StringSlice(inputTag);
Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag));
Assert.AreEqual(inputTag, outputTag);
}
[TestFixture]
public class TestHtmlHelper
{
[Test]
public void TestParseHtmlTagSimple()
{
var inputTag = "<a>";
var text = new StringSlice(inputTag);
Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag));
Assert.AreEqual(inputTag, outputTag);
}
[Test]
public void TestParseHtmlTagSimpleWithAttribute()
{
var inputTag = "<a href='http://google.com'>";
var text = new StringSlice(inputTag);
Assert.True(HtmlHelper.TryParseHtmlTag(ref text, out string outputTag));
Assert.AreEqual(inputTag, outputTag);
}
}

View File

@@ -1,25 +1,23 @@
using NUnit.Framework;
using System.Text.RegularExpressions;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestImageAltText
{
[TestFixture]
public class TestImageAltText
[Test]
[TestCase("![](image.jpg)", "")]
[TestCase("![foo](image.jpg)", "foo")]
[TestCase("![][1]\n\n[1]: image.jpg", "")]
[TestCase("![bar][1]\n\n[1]: image.jpg", "bar")]
[TestCase("![](image.jpg 'title')", "")]
[TestCase("![foo](image.jpg 'title')", "foo")]
[TestCase("![][1]\n\n[1]: image.jpg 'title'", "")]
[TestCase("![bar][1]\n\n[1]: image.jpg 'title'", "bar")]
public void TestImageHtmlAltText(string markdown, string expectedAltText)
{
[Test]
[TestCase("![](image.jpg)", "")]
[TestCase("![foo](image.jpg)", "foo")]
[TestCase("![][1]\n\n[1]: image.jpg", "")]
[TestCase("![bar][1]\n\n[1]: image.jpg", "bar")]
[TestCase("![](image.jpg 'title')", "")]
[TestCase("![foo](image.jpg 'title')", "foo")]
[TestCase("![][1]\n\n[1]: image.jpg 'title'", "")]
[TestCase("![bar][1]\n\n[1]: image.jpg 'title'", "bar")]
public void TestImageHtmlAltText(string markdown, string expectedAltText)
{
string html = Markdown.ToHtml(markdown);
string actualAltText = Regex.Match(html, "alt=\"(.*?)\"").Groups[1].Value;
Assert.AreEqual(expectedAltText, actualAltText);
}
string html = Markdown.ToHtml(markdown);
string actualAltText = Regex.Match(html, "alt=\"(.*?)\"").Groups[1].Value;
Assert.AreEqual(expectedAltText, actualAltText);
}
}

View File

@@ -1,67 +1,65 @@
using Markdig.Helpers;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestLazySubstring
{
public class TestLazySubstring
[Theory]
[TestCase("")]
[TestCase("a")]
[TestCase("foo")]
public void LazySubstring_ReturnsCorrectSubstring(string text)
{
[Theory]
[TestCase("")]
[TestCase("a")]
[TestCase("foo")]
public void LazySubstring_ReturnsCorrectSubstring(string text)
{
var substring = new LazySubstring(text);
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
var substring = new LazySubstring(text);
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(text, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(text, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
}
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(text, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(text.Length, substring.Length);
}
[Theory]
[TestCase("", 0, 0)]
[TestCase("a", 0, 0)]
[TestCase("a", 1, 0)]
[TestCase("a", 0, 1)]
[TestCase("foo", 1, 0)]
[TestCase("foo", 1, 1)]
[TestCase("foo", 1, 2)]
[TestCase("foo", 0, 3)]
public void LazySubstring_ReturnsCorrectSubstring(string text, int start, int length)
{
var substring = new LazySubstring(text, start, length);
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
[Theory]
[TestCase("", 0, 0)]
[TestCase("a", 0, 0)]
[TestCase("a", 1, 0)]
[TestCase("a", 0, 1)]
[TestCase("foo", 1, 0)]
[TestCase("foo", 1, 1)]
[TestCase("foo", 1, 2)]
[TestCase("foo", 0, 3)]
public void LazySubstring_ReturnsCorrectSubstring(string text, int start, int length)
{
var substring = new LazySubstring(text, start, length);
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
string expectedSubstring = text.Substring(start, length);
string expectedSubstring = text.Substring(start, length);
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(start, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(expectedSubstring, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreSame(substring.ToString(), substring.ToString());
Assert.AreEqual(expectedSubstring, substring.ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
}
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(expectedSubstring, substring.AsSpan().ToString());
Assert.AreEqual(0, substring.Offset);
Assert.AreEqual(length, substring.Length);
}
}

View File

@@ -2,126 +2,124 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using NUnit.Framework;
using Markdig.Helpers;
namespace Markdig.Tests
namespace Markdig.Tests;
/// <summary>
/// Test for <see cref="LineReader"/>.
/// </summary>
[TestFixture]
public class TestLineReader
{
/// <summary>
/// Test for <see cref="LineReader"/>.
/// </summary>
[TestFixture]
public class TestLineReader
[Test]
public void TestEmpty()
{
[Test]
public void TestEmpty()
{
var lineReader = new LineReader("");
Assert.Null(lineReader.ReadLine().Text);
}
var lineReader = new LineReader("");
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyLf()
{
var lineReader = new LineReader("\n\n\n");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(1, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyLf()
{
var lineReader = new LineReader("\n\n\n");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(1, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyCr()
{
var lineReader = new LineReader("\r\r\r");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(1, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyCr()
{
var lineReader = new LineReader("\r\r\r");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(1, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyCrLf()
{
var lineReader = new LineReader("\r\n\r\n\r\n");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLinesOnlyCrLf()
{
var lineReader = new LineReader("\r\n\r\n\r\n");
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(2, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual(string.Empty, lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestNoEndOfLine()
{
var lineReader = new LineReader("123");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestNoEndOfLine()
{
var lineReader = new LineReader("123");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLf()
{
var lineReader = new LineReader("123\n");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLf()
{
var lineReader = new LineReader("123\n");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLf2()
{
// When limited == true, we limit the internal buffer exactly after the first new line char '\n'
var lineReader = new LineReader("123\n456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestLf2()
{
// When limited == true, we limit the internal buffer exactly after the first new line char '\n'
var lineReader = new LineReader("123\n456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCr()
{
var lineReader = new LineReader("123\r");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCr()
{
var lineReader = new LineReader("123\r");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCr2()
{
var lineReader = new LineReader("123\r456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCr2()
{
var lineReader = new LineReader("123\r456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(4, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCrLf()
{
// When limited == true, we limit the internal buffer exactly after the first new line char '\r'
// and we check that we don't get a new line for `\n`
var lineReader = new LineReader("123\r\n");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(5, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCrLf()
{
// When limited == true, we limit the internal buffer exactly after the first new line char '\r'
// and we check that we don't get a new line for `\n`
var lineReader = new LineReader("123\r\n");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(5, lineReader.SourcePosition);
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCrLf2()
{
var lineReader = new LineReader("123\r\n456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(5, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
[Test]
public void TestCrLf2()
{
var lineReader = new LineReader("123\r\n456");
Assert.AreEqual("123", lineReader.ReadLine().ToString());
Assert.AreEqual(5, lineReader.SourcePosition);
Assert.AreEqual("456", lineReader.ReadLine().ToString());
Assert.Null(lineReader.ReadLine().Text);
}
}

View File

@@ -2,374 +2,372 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using NUnit.Framework;
using Markdig.Helpers;
using Markdig.Syntax;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestLinkHelper
{
[TestFixture]
public class TestLinkHelper
[Test]
public void TestUrlSimple()
{
[Test]
public void TestUrlSimple()
{
var text = new StringSlice("toto tutu");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("toto", link);
Assert.AreEqual(' ', text.CurrentChar);
}
var text = new StringSlice("toto tutu");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("toto", link);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlUrl()
{
var text = new StringSlice("http://google.com)");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual(')', text.CurrentChar);
}
[Test]
public void TestUrlUrl()
{
var text = new StringSlice("http://google.com)");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual(')', text.CurrentChar);
}
[Test]
[TestCase("http://google.com.")]
[TestCase("http://google.com. ")]
public void TestUrlTrailingFullStop(string uri)
{
var text = new StringSlice(uri);
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _, true));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual('.', text.CurrentChar);
}
[Test]
[TestCase("http://google.com.")]
[TestCase("http://google.com. ")]
public void TestUrlTrailingFullStop(string uri)
{
var text = new StringSlice(uri);
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _, true));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual('.', text.CurrentChar);
}
[Test]
public void TestUrlNestedParenthesis()
{
var text = new StringSlice("(toto)tutu(tata) nooo");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("(toto)tutu(tata)", link);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlNestedParenthesis()
{
var text = new StringSlice("(toto)tutu(tata) nooo");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("(toto)tutu(tata)", link);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlAlternate()
{
var text = new StringSlice("<toto_tata_tutu> nooo");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("toto_tata_tutu", link);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlAlternate()
{
var text = new StringSlice("<toto_tata_tutu> nooo");
Assert.True(LinkHelper.TryParseUrl(ref text, out string link, out _));
Assert.AreEqual("toto_tata_tutu", link);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlAlternateInvalid()
{
var text = new StringSlice("<toto_tata_tutu");
Assert.False(LinkHelper.TryParseUrl(ref text, out string link, out _));
}
[Test]
public void TestUrlAlternateInvalid()
{
var text = new StringSlice("<toto_tata_tutu");
Assert.False(LinkHelper.TryParseUrl(ref text, out string link, out _));
}
[Test]
public void TestTitleSimple()
{
var text = new StringSlice(@"'tata\tutu\''");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual(@"tata\tutu'", title);
}
[Test]
public void TestTitleSimple()
{
var text = new StringSlice(@"'tata\tutu\''");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual(@"tata\tutu'", title);
}
[Test]
public void TestTitleSimpleAlternate()
{
var text = new StringSlice(@"""tata\tutu\"""" ");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual(@"tata\tutu""", title);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestTitleSimpleAlternate()
{
var text = new StringSlice(@"""tata\tutu\"""" ");
Assert.True(LinkHelper.TryParseTitle(ref text, out string title, out _));
Assert.AreEqual(@"tata\tutu""", title);
Assert.AreEqual(' ', text.CurrentChar);
}
[Test]
public void TestUrlAndTitle()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"(http://google.com 'this is a title')ABC");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("this is a title", title);
Assert.AreEqual(new SourceSpan(1, 17), linkSpan);
Assert.AreEqual(new SourceSpan(19, 35), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitle()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"(http://google.com 'this is a title')ABC");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("this is a title", title);
Assert.AreEqual(new SourceSpan(1, 17), linkSpan);
Assert.AreEqual(new SourceSpan(19, 35), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty()
{
// 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(new SourceSpan(1, 2), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty()
{
// 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(new SourceSpan(1, 2), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty2()
{
// 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(new SourceSpan(2, 3), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty2()
{
// 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(new SourceSpan(2, 3), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlEmptyWithTitleWithMultipleSpaces()
{
// 0 1 2
// 0123456789012345678901234567
var text = new StringSlice(@"( <> 'toto' )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("toto", title);
Assert.AreEqual(new SourceSpan(4, 5), linkSpan);
Assert.AreEqual(new SourceSpan(12, 17), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlEmptyWithTitleWithMultipleSpaces()
{
// 0 1 2
// 0123456789012345678901234567
var text = new StringSlice(@"( <> 'toto' )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("toto", title);
Assert.AreEqual(new SourceSpan(4, 5), linkSpan);
Assert.AreEqual(new SourceSpan(12, 17), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlEmpty()
{
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(SourceSpan.Empty, linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlEmpty()
{
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(SourceSpan.Empty, linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestMultipleLines()
{
// 0 1 2 3
// 01 2345678901234567890 1234567890123456789
var text = new StringSlice("(\n<http://google.com>\n 'toto' )A");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("toto", title);
Assert.AreEqual(new SourceSpan(2, 20), linkSpan);
Assert.AreEqual(new SourceSpan(26, 31), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestMultipleLines()
{
// 0 1 2 3
// 01 2345678901234567890 1234567890123456789
var text = new StringSlice("(\n<http://google.com>\n 'toto' )A");
Assert.True(LinkHelper.TryParseInlineLink(ref text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("toto", title);
Assert.AreEqual(new SourceSpan(2, 20), linkSpan);
Assert.AreEqual(new SourceSpan(26, 31), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestLabelSimple()
{
// 01234
var text = new StringSlice("[foo]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual("foo", label);
}
[Test]
public void TestLabelSimple()
{
// 01234
var text = new StringSlice("[foo]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual("foo", label);
}
[Test]
public void TestLabelEscape()
{
// 012345678
var text = new StringSlice(@"[fo\[\]o]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 7), labelSpan);
Assert.AreEqual(@"fo[]o", label);
}
[Test]
public void TestLabelEscape()
{
// 012345678
var text = new StringSlice(@"[fo\[\]o]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 7), labelSpan);
Assert.AreEqual(@"fo[]o", label);
}
[Test]
public void TestLabelEscape2()
{
// 0123
var text = new StringSlice(@"[\]]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 2), labelSpan);
Assert.AreEqual(@"]", label);
}
[Test]
public void TestLabelEscape2()
{
// 0123
var text = new StringSlice(@"[\]]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(1, 2), labelSpan);
Assert.AreEqual(@"]", label);
}
[Test]
public void TestLabelInvalids()
{
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"a"), out string label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"["), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[\x]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[[]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ ]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ \t \n ]"), out label));
}
[Test]
public void TestLabelInvalids()
{
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"a"), out string label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"["), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[\x]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[[]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ ]"), out label));
Assert.False(LinkHelper.TryParseLabel(new StringSlice(@"[ \t \n ]"), out label));
}
[Test]
public void TestLabelWhitespaceCollapsedAndTrim()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[ fo o z ]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(6, 17), labelSpan);
Assert.AreEqual(@"fo o z", label);
}
[Test]
public void TestLabelWhitespaceCollapsedAndTrim()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[ fo o z ]");
Assert.True(LinkHelper.TryParseLabel(ref text, out string label, out SourceSpan labelSpan));
Assert.AreEqual(new SourceSpan(6, 17), labelSpan);
Assert.AreEqual(@"fo o z", label);
}
[Test]
public void TestlLinkReferenceDefinitionSimple()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[foo]: /toto 'title'");
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan));
Assert.AreEqual(@"foo", label);
Assert.AreEqual(@"/toto", url);
Assert.AreEqual(@"title", title);
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual(new SourceSpan(7, 11), urlSpan);
Assert.AreEqual(new SourceSpan(13, 19), titleSpan);
[Test]
public void TestlLinkReferenceDefinitionSimple()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[foo]: /toto 'title'");
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan));
Assert.AreEqual(@"foo", label);
Assert.AreEqual(@"/toto", url);
Assert.AreEqual(@"title", title);
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual(new SourceSpan(7, 11), urlSpan);
Assert.AreEqual(new SourceSpan(13, 19), titleSpan);
}
}
[Test]
public void TestAutoLinkUrlSimple()
{
var text = new StringSlice(@"<http://google.com>");
Assert.True(LinkHelper.TryParseAutolink(ref text, out string url, out bool isEmail));
Assert.False(isEmail);
Assert.AreEqual("http://google.com", url);
}
[Test]
public void TestAutoLinkUrlSimple()
{
var text = new StringSlice(@"<http://google.com>");
Assert.True(LinkHelper.TryParseAutolink(ref text, out string url, out bool isEmail));
Assert.False(isEmail);
Assert.AreEqual("http://google.com", url);
}
[Test]
public void TestAutoLinkEmailSimple()
{
var text = new StringSlice(@"<user@host.com>");
Assert.True(LinkHelper.TryParseAutolink(ref text, out string email, out bool isEmail));
Assert.True(isEmail);
Assert.AreEqual("user@host.com", email);
}
[Test]
public void TestAutoLinkEmailSimple()
{
var text = new StringSlice(@"<user@host.com>");
Assert.True(LinkHelper.TryParseAutolink(ref text, out string email, out bool isEmail));
Assert.True(isEmail);
Assert.AreEqual("user@host.com", email);
}
[Test]
public void TestAutolinkInvalid()
{
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out string text, out bool isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<ab"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<user@>"), out text, out isEmail));
}
[Test]
public void TestAutolinkInvalid()
{
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@""), out string text, out bool isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<ab"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<user@>"), out text, out isEmail));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in...
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "applications")]
[TestCase("33", "")]
public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in...
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "applications")]
[TestCase("33", "")]
public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "-dogs--in-my-house")]
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "3-applications")]
[TestCase("33", "33")]
public void TestUrilizeGfm(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.UrilizeAsGfm(input));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "-dogs--in-my-house")]
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "3-applications")]
[TestCase("33", "33")]
public void TestUrilizeGfm(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.UrilizeAsGfm(input));
}
[TestCase("abc", "abc")]
[TestCase("a-c", "a-c")]
[TestCase("a c", "a-c")]
[TestCase("a_c", "a_c")]
[TestCase("a.c", "a.c")]
[TestCase("a,c", "ac")]
[TestCase("a--", "a")] // Not Pandoc-equivalent: a--
[TestCase("a__", "a")] // Not Pandoc-equivalent: a__
[TestCase("a..", "a")] // Not Pandoc-equivalent: a..
[TestCase("a??", "a")]
[TestCase("a ", "a")]
[TestCase("a--d", "a-d")]
[TestCase("a__d", "a_d")]
[TestCase("a??d", "ad")]
[TestCase("a d", "a-d")]
[TestCase("a..d", "a.d")]
[TestCase("-bc", "bc")]
[TestCase("_bc", "bc")]
[TestCase(" bc", "bc")]
[TestCase("?bc", "bc")]
[TestCase(".bc", "bc")]
[TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.-
public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("abc", "abc")]
[TestCase("a-c", "a-c")]
[TestCase("a c", "a-c")]
[TestCase("a_c", "a_c")]
[TestCase("a.c", "a.c")]
[TestCase("a,c", "ac")]
[TestCase("a--", "a")] // Not Pandoc-equivalent: a--
[TestCase("a__", "a")] // Not Pandoc-equivalent: a__
[TestCase("a..", "a")] // Not Pandoc-equivalent: a..
[TestCase("a??", "a")]
[TestCase("a ", "a")]
[TestCase("a--d", "a-d")]
[TestCase("a__d", "a_d")]
[TestCase("a??d", "ad")]
[TestCase("a d", "a-d")]
[TestCase("a..d", "a.d")]
[TestCase("-bc", "bc")]
[TestCase("_bc", "bc")]
[TestCase(" bc", "bc")]
[TestCase("?bc", "bc")]
[TestCase(".bc", "bc")]
[TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.-
public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bær", "br")]
[TestCase("bør", "br")]
[TestCase("bΘr", "br")]
[TestCase("四五", "")]
public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bær", "br")]
[TestCase("bør", "br")]
[TestCase("bΘr", "br")]
[TestCase("四五", "")]
public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bár", "bar")]
[TestCase("àrrivé", "arrive")]
public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bár", "bar")]
[TestCase("àrrivé", "arrive")]
public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("123", "")]
[TestCase("1,-b", "b")]
[TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1-
[TestCase("ab3", "ab3")]
[TestCase("ab3de", "ab3de")]
public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("123", "")]
[TestCase("1,-b", "b")]
[TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1-
[TestCase("ab3", "ab3")]
[TestCase("ab3de", "ab3de")]
public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("一二三四五", "一二三四五")]
[TestCase("一,-b", "一-b")]
public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("一二三四五", "一二三四五")]
[TestCase("一,-b", "一-b")]
public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("bær", "bær")]
[TestCase("æ5el", "æ5el")]
[TestCase("-æ5el", "æ5el")]
[TestCase("-frø-", "frø")]
[TestCase("-fr-ø", "fr-ø")]
public void TestUrilizeNonAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("bær", "bær")]
[TestCase("æ5el", "æ5el")]
[TestCase("-æ5el", "æ5el")]
[TestCase("-frø-", "frø")]
[TestCase("-fr-ø", "fr-ø")]
public void TestUrilizeNonAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
// Just to be sure, test for characters expressly forbidden in URI fragments:
[TestCase("b#r", "br")]
[TestCase("b%r", "br")] // Invalid except as an escape character
[TestCase("b^r", "br")]
[TestCase("b[r", "br")]
[TestCase("b]r", "br")]
[TestCase("b{r", "br")]
[TestCase("b}r", "br")]
[TestCase("b<r", "br")]
[TestCase("b>r", "br")]
[TestCase(@"b\r", "br")]
[TestCase(@"b""r", "br")]
[TestCase(@"Requirement 😀", "requirement")]
public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
// Just to be sure, test for characters expressly forbidden in URI fragments:
[TestCase("b#r", "br")]
[TestCase("b%r", "br")] // Invalid except as an escape character
[TestCase("b^r", "br")]
[TestCase("b[r", "br")]
[TestCase("b]r", "br")]
[TestCase("b{r", "br")]
[TestCase("b}r", "br")]
[TestCase("b<r", "br")]
[TestCase("b>r", "br")]
[TestCase(@"b\r", "br")]
[TestCase(@"b""r", "br")]
[TestCase(@"Requirement 😀", "requirement")]
public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[Test]
public void TestUnicodeInDomainNameOfLinkReferenceDefinition()
{
TestParser.TestSpec("[Foo]\n\n[Foo]: http://ünicode.com", "<p><a href=\"http://xn--nicode-2ya.com\">Foo</a></p>");
}
[Test]
public void TestUnicodeInDomainNameOfLinkReferenceDefinition()
{
TestParser.TestSpec("[Foo]\n\n[Foo]: http://ünicode.com", "<p><a href=\"http://xn--nicode-2ya.com\">Foo</a></p>");
}
}

View File

@@ -1,44 +1,40 @@
using System;
using System.IO;
using Markdig.Parsers;
using Markdig.Renderers;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestLinkRewriter
{
public class TestLinkRewriter
[Test]
public void ReplacesRelativeLinks()
{
[Test]
public void ReplacesRelativeLinks()
{
TestSpec(s => "abc" + s, "Link: [hello](/relative.jpg)", "abc/relative.jpg");
TestSpec(s => s + "xyz", "Link: [hello](relative.jpg)", "relative.jpgxyz");
TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg");
TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg");
}
TestSpec(s => "abc" + s, "Link: [hello](/relative.jpg)", "abc/relative.jpg");
TestSpec(s => s + "xyz", "Link: [hello](relative.jpg)", "relative.jpgxyz");
TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg");
TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg");
}
[Test]
public void ReplacesRelativeImageSources()
{
TestSpec(s => "abc" + s, "Image: ![alt text](/image.jpg)", "abc/image.jpg");
TestSpec(s => "abc" + s, "Image: ![alt text](image.jpg \"title\")", "abcimage.jpg");
TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg");
}
[Test]
public void ReplacesRelativeImageSources()
{
TestSpec(s => "abc" + s, "Image: ![alt text](/image.jpg)", "abc/image.jpg");
TestSpec(s => "abc" + s, "Image: ![alt text](image.jpg \"title\")", "abcimage.jpg");
TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg");
}
public static void TestSpec(Func<string,string> linkRewriter, string markdown, string expectedLink)
{
var pipeline = new MarkdownPipelineBuilder().Build();
public static void TestSpec(Func<string,string> linkRewriter, string markdown, string expectedLink)
{
var pipeline = new MarkdownPipelineBuilder().Build();
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
renderer.LinkRewriter = linkRewriter;
pipeline.Setup(renderer);
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
renderer.LinkRewriter = linkRewriter;
pipeline.Setup(renderer);
var document = MarkdownParser.Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
var document = MarkdownParser.Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\""));
}
Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\""));
}
}

View File

@@ -1,217 +1,214 @@
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
using System.IO;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestMarkdigCoreApi
{
public class TestMarkdigCoreApi
[Test]
public void TestToHtml()
{
[Test]
public void TestToHtml()
for (int i = 0; i < 5; i++)
{
for (int i = 0; i < 5; i++)
{
string html = Markdown.ToHtml("This is a text with some *emphasis*");
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
string html = Markdown.ToHtml("This is a text with some *emphasis*");
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
html = Markdown.ToHtml("This is a text with a https://link.tld/");
Assert.AreNotEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
html = Markdown.ToHtml("This is a text with a https://link.tld/");
Assert.AreNotEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
}
[Test]
public void TestToHtmlWithPipeline()
{
var pipeline = new MarkdownPipelineBuilder()
.Build();
for (int i = 0; i < 5; i++)
{
string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline);
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline);
Assert.AreNotEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
[Test]
public void TestToHtmlWithPipeline()
pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
var pipeline = new MarkdownPipelineBuilder()
.Build();
string html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline);
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
}
for (int i = 0; i < 5; i++)
{
string html = Markdown.ToHtml("This is a text with some *emphasis*", pipeline);
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
[Test]
public void TestToHtmlWithWriter()
{
var writer = new StringWriter();
html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline);
Assert.AreNotEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
string html = Markdown.ToHtml("This is a text with a https://link.tld/", pipeline);
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
}
for (int i = 0; i < 5; i++)
{
_ = Markdown.ToHtml("This is a text with some *emphasis*", writer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
[Test]
public void TestToHtmlWithWriter()
writer = new StringWriter();
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
_ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestDocumentToHtmlWithWriter()
{
var writer = new StringWriter();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with some *emphasis*");
document.ToHtml(writer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with a https://link.tld/", pipeline);
document.ToHtml(writer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestConvert()
{
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
for (int i = 0; i < 5; i++)
{
_ = Markdown.Convert("This is a text with some *emphasis*", renderer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
renderer = new HtmlRenderer(writer);
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
_ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestParse()
{
const string markdown = "This is a text with some *emphasis*";
var pipeline = new MarkdownPipelineBuilder()
.UsePreciseSourceLocation()
.Build();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse(markdown, pipeline);
Assert.AreEqual(1, document.LineCount);
Assert.AreEqual(markdown.Length, document.Span.Length);
Assert.AreEqual(1, document.LineStartIndexes.Count);
Assert.AreEqual(0, document.LineStartIndexes[0]);
Assert.AreEqual(1, document.Count);
ParagraphBlock paragraph = document[0] as ParagraphBlock;
Assert.NotNull(paragraph);
Assert.AreEqual(markdown.Length, paragraph.Span.Length);
LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline;
Assert.NotNull(literal);
Assert.AreEqual("This is a text with some ", literal.ToString());
EmphasisInline emphasis = literal.NextSibling as EmphasisInline;
Assert.NotNull(emphasis);
Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length);
LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline;
Assert.NotNull(emphasisLiteral);
Assert.AreEqual("emphasis", emphasisLiteral.ToString());
Assert.Null(emphasisLiteral.NextSibling);
Assert.Null(emphasis.NextSibling);
}
}
[Test]
public void TestNormalize()
{
for (int i = 0; i < 5; i++)
{
string normalized = Markdown.Normalize("Heading\n=======");
Assert.AreEqual("# Heading", normalized);
}
}
[Test]
public void TestNormalizeWithWriter()
{
for (int i = 0; i < 5; i++)
{
var writer = new StringWriter();
for (int i = 0; i < 5; i++)
{
_ = Markdown.ToHtml("This is a text with some *emphasis*", writer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
_ = Markdown.ToHtml("This is a text with a https://link.tld/", writer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
_ = Markdown.Normalize("Heading\n=======", writer);
string normalized = writer.ToString();
Assert.AreEqual("# Heading", normalized);
}
}
[Test]
public void TestDocumentToHtmlWithWriter()
[Test]
public void TestToPlainText()
{
for (int i = 0; i < 5; i++)
{
string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!");
Assert.AreEqual("Hello, world!\n", plainText);
}
}
[Test]
public void TestToPlainTextWithWriter()
{
for (int i = 0; i < 5; i++)
{
var writer = new StringWriter();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with some *emphasis*");
document.ToHtml(writer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse("This is a text with a https://link.tld/", pipeline);
document.ToHtml(writer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestConvert()
{
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
for (int i = 0; i < 5; i++)
{
_ = Markdown.Convert("This is a text with some *emphasis*", renderer);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with some <em>emphasis</em></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
writer = new StringWriter();
renderer = new HtmlRenderer(writer);
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < 5; i++)
{
_ = Markdown.Convert("This is a text with a https://link.tld/", renderer, pipeline);
string html = writer.ToString();
Assert.AreEqual("<p>This is a text with a <a href=\"https://link.tld/\">https://link.tld/</a></p>\n", html);
writer.GetStringBuilder().Length = 0;
}
}
[Test]
public void TestParse()
{
const string markdown = "This is a text with some *emphasis*";
var pipeline = new MarkdownPipelineBuilder()
.UsePreciseSourceLocation()
.Build();
for (int i = 0; i < 5; i++)
{
MarkdownDocument document = Markdown.Parse(markdown, pipeline);
Assert.AreEqual(1, document.LineCount);
Assert.AreEqual(markdown.Length, document.Span.Length);
Assert.AreEqual(1, document.LineStartIndexes.Count);
Assert.AreEqual(0, document.LineStartIndexes[0]);
Assert.AreEqual(1, document.Count);
ParagraphBlock paragraph = document[0] as ParagraphBlock;
Assert.NotNull(paragraph);
Assert.AreEqual(markdown.Length, paragraph.Span.Length);
LiteralInline literal = paragraph.Inline.FirstChild as LiteralInline;
Assert.NotNull(literal);
Assert.AreEqual("This is a text with some ", literal.ToString());
EmphasisInline emphasis = literal.NextSibling as EmphasisInline;
Assert.NotNull(emphasis);
Assert.AreEqual("*emphasis*".Length, emphasis.Span.Length);
LiteralInline emphasisLiteral = emphasis.FirstChild as LiteralInline;
Assert.NotNull(emphasisLiteral);
Assert.AreEqual("emphasis", emphasisLiteral.ToString());
Assert.Null(emphasisLiteral.NextSibling);
Assert.Null(emphasis.NextSibling);
}
}
[Test]
public void TestNormalize()
{
for (int i = 0; i < 5; i++)
{
string normalized = Markdown.Normalize("Heading\n=======");
Assert.AreEqual("# Heading", normalized);
}
}
[Test]
public void TestNormalizeWithWriter()
{
for (int i = 0; i < 5; i++)
{
var writer = new StringWriter();
_ = Markdown.Normalize("Heading\n=======", writer);
string normalized = writer.ToString();
Assert.AreEqual("# Heading", normalized);
}
}
[Test]
public void TestToPlainText()
{
for (int i = 0; i < 5; i++)
{
string plainText = Markdown.ToPlainText("*Hello*, [world](http://example.com)!");
Assert.AreEqual("Hello, world!\n", plainText);
}
}
[Test]
public void TestToPlainTextWithWriter()
{
for (int i = 0; i < 5; i++)
{
var writer = new StringWriter();
_ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer);
string plainText = writer.ToString();
Assert.AreEqual("Hello, world!\n", plainText);
}
_ = Markdown.ToPlainText("*Hello*, [world](http://example.com)!", writer);
string plainText = writer.ToString();
Assert.AreEqual("Hello, world!\n", plainText);
}
}
}

View File

@@ -1,104 +1,112 @@
using Markdig.Extensions.MediaLinks;
using NUnit.Framework;
using System;
using System.Text.RegularExpressions;
namespace Markdig.Tests
using Markdig.Extensions.MediaLinks;
namespace Markdig.Tests;
[TestFixture]
public class TestMediaLinks
{
[TestFixture]
public class TestMediaLinks
private MarkdownPipeline GetPipeline(MediaOptions options = null)
{
private MarkdownPipeline GetPipeline(MediaOptions options = null)
return new MarkdownPipelineBuilder()
.UseMediaLinks(options)
.Build();
}
private MarkdownPipeline GetPipelineWithBootstrap(MediaOptions options = null)
{
return new MarkdownPipelineBuilder()
.UseBootstrap()
.UseMediaLinks(options)
.Build();
}
[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.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")]
[TestCase("![ok.ru](https://ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" class=\"odnoklassniki\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![ok.ru](//ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" class=\"odnoklassniki\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
public void TestBuiltInHosts(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
}
[TestCase("![static video relative path](./video.mp4)",
"<p><video width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"./video.mp4\"></source></video></p>\n")]
[TestCase("![static audio relative path](./audio.mp3)",
"<p><audio width=\"500\" controls=\"\"><source type=\"audio/mpeg\" src=\"./audio.mp3\"></source></audio></p>\n")]
public void TestBuiltInHostsWithRelativePaths(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
}
private class TestHostProvider : IHostProvider
{
public string Class { get; } = "regex";
public bool AllowFullScreen { get; }
public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl)
{
return new MarkdownPipelineBuilder()
.UseMediaLinks(options)
.Build();
iframeUrl = null;
var uri = isSchemaRelative ? "//" + mediaUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme, UriFormat.UriEscaped) : mediaUri.ToString();
if (!matcher.IsMatch(uri))
return false;
iframeUrl = matcher.Replace(uri, replacement);
return true;
}
private MarkdownPipeline GetPipelineWithBootstrap(MediaOptions options = null)
private Regex matcher;
private string replacement;
public TestHostProvider(string provider, string replace)
{
return new MarkdownPipelineBuilder()
.UseBootstrap()
.UseMediaLinks(options)
.Build();
}
[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.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")]
[TestCase("![ok.ru](https://ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" class=\"odnoklassniki\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
[TestCase("![ok.ru](//ok.ru/video/26870090463)", "<p><iframe src=\"https://ok.ru/videoembed/26870090463\" class=\"odnoklassniki\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
public void TestBuiltInHosts(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipeline());
Assert.AreEqual(html, expected);
}
private class TestHostProvider : IHostProvider
{
public string Class { get; } = "regex";
public bool AllowFullScreen { get; }
public bool TryHandle(Uri mediaUri, bool isSchemaRelative, out string iframeUrl)
{
iframeUrl = null;
var uri = isSchemaRelative ? "//" + mediaUri.GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Scheme, UriFormat.UriEscaped) : mediaUri.ToString();
if (!matcher.IsMatch(uri))
return false;
iframeUrl = matcher.Replace(uri, replacement);
return true;
}
private Regex matcher;
private string replacement;
public TestHostProvider(string provider, string replace)
{
matcher = new Regex(provider);
replacement = replace;
}
}
[Test]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](//sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^//sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4?token=aaabbb\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")]
public void TestCustomHostProvider(string markdown, string expected, string provider, string replace)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Hosts =
{
new TestHostProvider(provider, replace),
}
}));
Assert.AreEqual(html, expected);
}
[Test]
[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.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("![static mp4](//sample.com/video.mp4)", "<p><video class=\"k\" width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n", "k")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"k youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n", "k")]
public void TestCustomClass(string markdown, string expected, string klass)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Class = klass,
}));
Assert.AreEqual(html, expected);
}
[Test]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video class=\"img-fluid\" width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"img-fluid youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
public void TestWithBootstrap(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipelineWithBootstrap());
Assert.AreEqual(html, expected);
matcher = new Regex(provider);
replacement = replace;
}
}
[Test]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](//sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^//sample.com/(.+)$", @"https://example.com/$1")]
[TestCase("![p1](https://sample.com/video.mp4)", "<p><iframe src=\"https://example.com/video.mp4?token=aaabbb\" class=\"regex\" width=\"500\" height=\"281\" frameborder=\"0\"></iframe></p>\n", @"^https?://sample.com/(.+)$", @"https://example.com/$1?token=aaabbb")]
public void TestCustomHostProvider(string markdown, string expected, string provider, string replace)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Hosts =
{
new TestHostProvider(provider, replace),
}
}));
Assert.AreEqual(html, expected);
}
[Test]
[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.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("![static mp4](//sample.com/video.mp4)", "<p><video class=\"k\" width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n", "k")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"k youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n", "k")]
public void TestCustomClass(string markdown, string expected, string klass)
{
string html = Markdown.ToHtml(markdown, GetPipeline(new MediaOptions
{
Class = klass,
}));
Assert.AreEqual(html, expected);
}
[Test]
[TestCase("![static mp4](//sample.com/video.mp4)", "<p><video class=\"img-fluid\" width=\"500\" height=\"281\" controls=\"\"><source type=\"video/mp4\" src=\"//sample.com/video.mp4\"></source></video></p>\n")]
[TestCase(@"![youtube.com](https://www.youtube.com/watch?v=mswPy5bt3TQ)", "<p><iframe src=\"https://www.youtube.com/embed/mswPy5bt3TQ\" class=\"img-fluid youtube\" width=\"500\" height=\"281\" frameborder=\"0\" allowfullscreen=\"\"></iframe></p>\n")]
public void TestWithBootstrap(string markdown, string expected)
{
string html = Markdown.ToHtml(markdown, GetPipelineWithBootstrap());
Assert.AreEqual(html, expected);
}
}

View File

@@ -1,31 +1,28 @@
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestNewLine
{
[TestFixture]
public class TestNewLine
[TestCase("a \nb", "<p>a<br />\nb</p>\n")]
[TestCase("a\\\nb", "<p>a<br />\nb</p>\n")]
[TestCase("a `b\nc`", "<p>a <code>b c</code></p>\n")]
[TestCase("# Text A\nText B\n\n## Text C", "<h1>Text A</h1>\n<p>Text B</p>\n<h2>Text C</h2>\n")]
public void Test(string value, string expectedHtml)
{
[TestCase("a \nb", "<p>a<br />\nb</p>\n")]
[TestCase("a\\\nb", "<p>a<br />\nb</p>\n")]
[TestCase("a `b\nc`", "<p>a <code>b c</code></p>\n")]
[TestCase("# Text A\nText B\n\n## Text C", "<h1>Text A</h1>\n<p>Text B</p>\n<h2>Text C</h2>\n")]
public void Test(string value, string expectedHtml)
{
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value));
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value.Replace("\n", "\r\n")));
}
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value));
Assert.AreEqual(expectedHtml, Markdown.ToHtml(value.Replace("\n", "\r\n")));
}
[Test()]
public void TestEscapeLineBreak()
{
var input = "test\\\r\ntest1\r\n";
var doc = Markdown.Parse(input);
var inlines = doc.Descendants<LineBreakInline>().ToList();
Assert.AreEqual(1, inlines.Count, "Invalid number of LineBreakInline");
Assert.True(inlines[0].IsBackslash);
}
[Test()]
public void TestEscapeLineBreak()
{
var input = "test\\\r\ntest1\r\n";
var doc = Markdown.Parse(input);
var inlines = doc.Descendants<LineBreakInline>().ToList();
Assert.AreEqual(1, inlines.Count, "Invalid number of LineBreakInline");
Assert.True(inlines[0].IsBackslash);
}
}

View File

@@ -2,180 +2,187 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using NUnit.Framework;
using Markdig.Helpers;
using Markdig.Renderers.Normalize;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System.IO;
using Markdig.Renderers.Normalize;
using Markdig.Helpers;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestNormalize
{
[TestFixture]
public class TestNormalize
[Test]
public void SyntaxCodeBlock()
{
[Test]
public void SyntaxCodeBlock()
AssertSyntax("````csharp\npublic void HelloWorld()\n{\n}\n````", new FencedCodeBlock(null)
{
AssertSyntax("````csharp\npublic void HelloWorld()\n{\n}\n````", new FencedCodeBlock(null)
FencedChar = '`',
OpeningFencedCharCount = 4,
ClosingFencedCharCount = 4,
Info = "csharp",
Lines = new StringLineGroup(4)
{
FencedChar = '`',
OpeningFencedCharCount = 4,
ClosingFencedCharCount = 4,
Info = "csharp",
Lines = new StringLineGroup(4)
{
new StringSlice("public void HelloWorld()"),
new StringSlice("{"),
new StringSlice("}"),
}
});
new StringSlice("public void HelloWorld()"),
new StringSlice("{"),
new StringSlice("}"),
}
});
AssertSyntax(" public void HelloWorld()\n {\n }", new CodeBlock(null)
AssertSyntax(" public void HelloWorld()\n {\n }", new CodeBlock(null)
{
Lines = new StringLineGroup(4)
{
Lines = new StringLineGroup(4)
{
new StringSlice("public void HelloWorld()"),
new StringSlice("{"),
new StringSlice("}"),
}
});
}
new StringSlice("public void HelloWorld()"),
new StringSlice("{"),
new StringSlice("}"),
}
});
}
[Test]
public void SyntaxHeadline()
[Test]
public void SyntaxHeadline()
{
AssertSyntax("## Headline", new HeadingBlock(null)
{
AssertSyntax("## Headline", new HeadingBlock(null)
{
HeaderChar = '#',
Level = 2,
Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")),
});
}
HeaderChar = '#',
Level = 2,
Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")),
});
}
[Test]
public void SyntaxParagraph()
[Test]
public void SyntaxHeadlineLevel7()
{
AssertSyntax("####### Headline", new HeadingBlock(null) {
HeaderChar = '#',
Level = 7,
Inline = new ContainerInline().AppendChild(new LiteralInline("Headline")),
});
}
[Test]
public void SyntaxParagraph()
{
AssertSyntax("This is a normal paragraph", new ParagraphBlock()
{
AssertSyntax("This is a normal paragraph", new ParagraphBlock()
{
Inline = new ContainerInline()
.AppendChild(new LiteralInline("This is a normal paragraph")),
});
Inline = new ContainerInline()
.AppendChild(new LiteralInline("This is a normal paragraph")),
});
AssertSyntax("This is a\nnormal\nparagraph", new ParagraphBlock()
{
Inline = new ContainerInline()
.AppendChild(new LiteralInline("This is a"))
.AppendChild(new LineBreakInline())
.AppendChild(new LiteralInline("normal"))
.AppendChild(new LineBreakInline())
.AppendChild(new LiteralInline("paragraph")),
});
}
[Test]
public void CodeBlock()
AssertSyntax("This is a\nnormal\nparagraph", new ParagraphBlock()
{
AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }");
AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }\n\ntext after two newlines");
AssertNormalizeNoTrim("````\npublic void HelloWorld();\n{\n}\n````\n\ntext after two newlines");
AssertNormalizeNoTrim("````csharp\npublic void HelloWorld();\n{\n}\n````");
AssertNormalizeNoTrim("````csharp hideNewKeyword=true\npublic void HelloWorld();\n{\n}\n````");
}
Inline = new ContainerInline()
.AppendChild(new LiteralInline("This is a"))
.AppendChild(new LineBreakInline())
.AppendChild(new LiteralInline("normal"))
.AppendChild(new LineBreakInline())
.AppendChild(new LiteralInline("paragraph")),
});
}
[Test]
public void Heading()
{
AssertNormalizeNoTrim("# Heading");
AssertNormalizeNoTrim("## Heading");
AssertNormalizeNoTrim("### Heading");
AssertNormalizeNoTrim("#### Heading");
AssertNormalizeNoTrim("##### Heading");
AssertNormalizeNoTrim("###### Heading");
AssertNormalizeNoTrim("###### Heading\n\ntext after two newlines");
AssertNormalizeNoTrim("# Heading\nAnd Text1\n\nAndText2", options: new NormalizeOptions() { EmptyLineAfterHeading = false });
[Test]
public void CodeBlock()
{
AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }");
AssertNormalizeNoTrim(" public void HelloWorld();\n {\n }\n\ntext after two newlines");
AssertNormalizeNoTrim("````\npublic void HelloWorld();\n{\n}\n````\n\ntext after two newlines");
AssertNormalizeNoTrim("````csharp\npublic void HelloWorld();\n{\n}\n````");
AssertNormalizeNoTrim("````csharp hideNewKeyword=true\npublic void HelloWorld();\n{\n}\n````");
}
AssertNormalizeNoTrim("Heading\n=======\n\ntext after two newlines", "# Heading\n\ntext after two newlines");
}
[Test]
public void Heading()
{
AssertNormalizeNoTrim("# Heading");
AssertNormalizeNoTrim("## Heading");
AssertNormalizeNoTrim("### Heading");
AssertNormalizeNoTrim("#### Heading");
AssertNormalizeNoTrim("##### Heading");
AssertNormalizeNoTrim("###### Heading");
AssertNormalizeNoTrim("###### Heading\n\ntext after two newlines");
AssertNormalizeNoTrim("# Heading\nAnd Text1\n\nAndText2", options: new NormalizeOptions() { EmptyLineAfterHeading = false });
[Test]
public void Backslash()
{
AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline");
AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line");
}
AssertNormalizeNoTrim("Heading\n=======\n\ntext after two newlines", "# Heading\n\ntext after two newlines");
}
[Test]
public void HtmlBlock()
{
/*AssertNormalizeNoTrim(@"<div id=""foo"" class=""bar
baz"">
[Test]
public void Backslash()
{
AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline");
AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line");
}
[Test]
public void HtmlBlock()
{
/*AssertNormalizeNoTrim(@"<div id=""foo"" class=""bar
baz"">
</ div >");*/ // TODO: Bug: Throws Exception during emit
}
}
[Test]
public void Paragraph()
{
AssertNormalizeNoTrim("This is a plain paragraph");
AssertNormalizeNoTrim(@"This
[Test]
public void Paragraph()
{
AssertNormalizeNoTrim("This is a plain paragraph");
AssertNormalizeNoTrim(@"This
is
a
plain
paragraph");
}
}
[Test]
public void ParagraphMulti()
{
AssertNormalizeNoTrim(@"line1
[Test]
public void ParagraphMulti()
{
AssertNormalizeNoTrim(@"line1
line2
line3");
}
}
[Test]
public void ListUnordered()
{
AssertNormalizeNoTrim(@"- a
[Test]
public void ListUnordered()
{
AssertNormalizeNoTrim(@"- a
- b
- c");
}
}
[Test]
public void ListUnorderedLoose()
{
AssertNormalizeNoTrim(@"- a
[Test]
public void ListUnorderedLoose()
{
AssertNormalizeNoTrim(@"- a
- b
- c");
}
}
[Test]
public void ListOrderedLooseAndCodeBlock()
{
AssertNormalizeNoTrim(@"1. ```
[Test]
public void ListOrderedLooseAndCodeBlock()
{
AssertNormalizeNoTrim(@"1. ```
foo
```
bar");
}
}
[Test, Ignore("Not sure this is the correct normalize for this one. Need to check the specs")]
public void ListUnorderedLooseTop()
{
AssertNormalizeNoTrim(@"* foo
[Test, Ignore("Not sure this is the correct normalize for this one. Need to check the specs")]
public void ListUnorderedLooseTop()
{
AssertNormalizeNoTrim(@"* foo
* bar
baz", options: new NormalizeOptions() { ListItemCharacter = '*' });
}
}
[Test]
public void ListUnorderedLooseMultiParagraph()
{
AssertNormalizeNoTrim(
[Test]
public void ListUnorderedLooseMultiParagraph()
{
AssertNormalizeNoTrim(
@"- a
And another paragraph a
@@ -185,22 +192,22 @@ line3");
And another paragraph b
- c");
}
}
[Test]
public void ListOrdered()
{
AssertNormalizeNoTrim(@"1. a
[Test]
public void ListOrdered()
{
AssertNormalizeNoTrim(@"1. a
2. b
3. c");
}
}
[Test]
public void ListOrderedAndIntended()
{
AssertNormalizeNoTrim(@"1. a
[Test]
public void ListOrderedAndIntended()
{
AssertNormalizeNoTrim(@"1. a
2. b
- foo
- bar
@@ -218,169 +225,169 @@ line3");
- Bar
11. c
12. c");
}
}
[Test]
public void HeaderAndParagraph()
{
AssertNormalizeNoTrim(@"# heading
[Test]
public void HeaderAndParagraph()
{
AssertNormalizeNoTrim(@"# heading
paragraph
paragraph2 without newlines");
}
}
[Test]
public void QuoteBlock()
{
AssertNormalizeNoTrim(@"> test1
[Test]
public void QuoteBlock()
{
AssertNormalizeNoTrim(@"> test1
>
> test2");
AssertNormalizeNoTrim(@"> test1
AssertNormalizeNoTrim(@"> test1
This is a continuation
> test2",
@"> test1
@"> test1
> This is a continuation
> test2"
);
AssertNormalizeNoTrim(@"> test1
AssertNormalizeNoTrim(@"> test1
> -foobar
asdf
> test2
> -foobar sen.");
}
}
[Test]
public void ThematicBreak()
{
AssertNormalizeNoTrim("***\n");
[Test]
public void ThematicBreak()
{
AssertNormalizeNoTrim("***\n");
AssertNormalizeNoTrim("* * *\n", "***\n");
}
AssertNormalizeNoTrim("* * *\n", "***\n");
}
[Test]
public void AutolinkInline()
{
AssertNormalizeNoTrim("This has a <auto.link.com>");
}
[Test]
public void AutolinkInline()
{
AssertNormalizeNoTrim("This has a <auto.link.com>");
}
[Test]
public void CodeInline()
{
AssertNormalizeNoTrim("This has a ` ` in it");
AssertNormalizeNoTrim("This has a `HelloWorld()` in it");
AssertNormalizeNoTrim(@"This has a ``Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``` Hello`World() ``` in it", @"This has a ``Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``Hello`World()` `` in it");
AssertNormalizeNoTrim(@"This has a ```` ``Hello```World()` ```` in it");
AssertNormalizeNoTrim(@"This has a `` `Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``` ``Hello`World()` ``` in it");
}
[Test]
public void CodeInline()
{
AssertNormalizeNoTrim("This has a ` ` in it");
AssertNormalizeNoTrim("This has a `HelloWorld()` in it");
AssertNormalizeNoTrim(@"This has a ``Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``` Hello`World() ``` in it", @"This has a ``Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``Hello`World()` `` in it");
AssertNormalizeNoTrim(@"This has a ```` ``Hello```World()` ```` in it");
AssertNormalizeNoTrim(@"This has a `` `Hello`World()`` in it");
AssertNormalizeNoTrim(@"This has a ``` ``Hello`World()` ``` in it");
}
[Test]
public void EmphasisInline()
{
AssertNormalizeNoTrim("This is a plain **paragraph**");
AssertNormalizeNoTrim("This is a plain *paragraph*");
AssertNormalizeNoTrim("This is a plain _paragraph_");
AssertNormalizeNoTrim("This is a plain __paragraph__");
AssertNormalizeNoTrim("This is a pl*ai*n **paragraph**");
}
[Test]
public void EmphasisInline()
{
AssertNormalizeNoTrim("This is a plain **paragraph**");
AssertNormalizeNoTrim("This is a plain *paragraph*");
AssertNormalizeNoTrim("This is a plain _paragraph_");
AssertNormalizeNoTrim("This is a plain __paragraph__");
AssertNormalizeNoTrim("This is a pl*ai*n **paragraph**");
}
[Test]
public void LineBreakInline()
{
AssertNormalizeNoTrim("normal\nline break");
AssertNormalizeNoTrim("hard \nline break");
AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline");
AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line");
}
[Test]
public void LineBreakInline()
{
AssertNormalizeNoTrim("normal\nline break");
AssertNormalizeNoTrim("hard \nline break");
AssertNormalizeNoTrim("This is a hardline \nAnd this is another hardline\\\nThis is standard newline");
AssertNormalizeNoTrim("This is a line\nWith another line\nAnd a last line");
}
[Test]
public void LinkInline()
{
AssertNormalizeNoTrim("This is a [link](http://company.com)");
AssertNormalizeNoTrim("This is an ![image](http://company.com)");
[Test]
public void LinkInline()
{
AssertNormalizeNoTrim("This is a [link](http://company.com)");
AssertNormalizeNoTrim("This is an ![image](http://company.com)");
AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy Company"")");
AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy \"" Company"")");
}
AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy Company"")");
AssertNormalizeNoTrim(@"This is a [link](http://company.com ""Crazy \"" Company"")");
}
[Test]
public void LinkReferenceDefinition()
{
// Full link
AssertNormalizeNoTrim("This is a [link][MyLink]\n\n[MyLink]: http://company.com");
[Test]
public void LinkReferenceDefinition()
{
// Full link
AssertNormalizeNoTrim("This is a [link][MyLink]\n\n[MyLink]: http://company.com");
AssertNormalizeNoTrim("[MyLink]: http://company.com\nThis is a [link][MyLink]",
"This is a [link][MyLink]\n\n[MyLink]: http://company.com");
AssertNormalizeNoTrim("[MyLink]: http://company.com\nThis is a [link][MyLink]",
"This is a [link][MyLink]\n\n[MyLink]: http://company.com");
AssertNormalizeNoTrim("This is a [link][MyLink] a normal link [link](http://google.com) and another def link [link2][MyLink2]\n\n[MyLink]: http://company.com\n[MyLink2]: http://company2.com");
AssertNormalizeNoTrim("This is a [link][MyLink] a normal link [link](http://google.com) and another def link [link2][MyLink2]\n\n[MyLink]: http://company.com\n[MyLink2]: http://company2.com");
// Collapsed link
AssertNormalizeNoTrim("This is a [link][]\n\n[link]: http://company.com");
// Collapsed link
AssertNormalizeNoTrim("This is a [link][]\n\n[link]: http://company.com");
// Shortcut link
AssertNormalizeNoTrim("This is a [link]\n\n[link]: http://company.com");
}
// Shortcut link
AssertNormalizeNoTrim("This is a [link]\n\n[link]: http://company.com");
}
[Test]
public void EscapeInline()
{
AssertNormalizeNoTrim("This is an escape \\* with another \\[");
}
[Test]
public void EscapeInline()
{
AssertNormalizeNoTrim("This is an escape \\* with another \\[");
}
[Test]
public void HtmlEntityInline()
{
AssertNormalizeNoTrim("This is a &auml; blank");
}
[Test]
public void HtmlEntityInline()
{
AssertNormalizeNoTrim("This is a &auml; blank");
}
[Test]
public void HtmlInline()
{
AssertNormalizeNoTrim("foo <hr/> bar");
AssertNormalizeNoTrim(@"foo <hr foo=""bar""/> bar");
}
[Test]
public void HtmlInline()
{
AssertNormalizeNoTrim("foo <hr/> bar");
AssertNormalizeNoTrim(@"foo <hr foo=""bar""/> bar");
}
[Test]
public void SpaceBetweenNodes()
{
AssertNormalizeNoTrim("# Hello World\nFoobar is a better bar.",
"# Hello World\n\nFoobar is a better bar.");
}
[Test]
public void SpaceBetweenNodes()
{
AssertNormalizeNoTrim("# Hello World\nFoobar is a better bar.",
"# Hello World\n\nFoobar is a better bar.");
}
[Test]
public void SpaceBetweenNodesEvenForHeadlines()
{
AssertNormalizeNoTrim("# Hello World\n## Chapter 1\nFoobar is a better bar.",
"# Hello World\n\n## Chapter 1\n\nFoobar is a better bar.");
}
[Test]
public void SpaceBetweenNodesEvenForHeadlines()
{
AssertNormalizeNoTrim("# Hello World\n## Chapter 1\nFoobar is a better bar.",
"# Hello World\n\n## Chapter 1\n\nFoobar is a better bar.");
}
[Test]
public void SpaceRemoveAtStartAndEnd()
{
AssertNormalizeNoTrim("\n\n# Hello World\n## Chapter 1\nFoobar is a better bar.\n\n",
"# Hello World\n\n## Chapter 1\n\nFoobar is a better bar.");
}
[Test]
public void SpaceRemoveAtStartAndEnd()
{
AssertNormalizeNoTrim("\n\n# Hello World\n## Chapter 1\nFoobar is a better bar.\n\n",
"# Hello World\n\n## Chapter 1\n\nFoobar is a better bar.");
}
[Test]
public void SpaceShortenBetweenNodes()
{
AssertNormalizeNoTrim("# Hello World\n\n\n\nFoobar is a better bar.",
"# Hello World\n\nFoobar is a better bar.");
}
[Test]
public void SpaceShortenBetweenNodes()
{
AssertNormalizeNoTrim("# Hello World\n\n\n\nFoobar is a better bar.",
"# Hello World\n\nFoobar is a better bar.");
}
[Test]
public void BiggerSample()
{
var input = @"# Heading 1
[Test]
public void BiggerSample()
{
var input = @"# Heading 1
This is a paragraph
@@ -400,100 +407,99 @@ This is a code block
line 2 of indented
This is a last line";
AssertNormalizeNoTrim(input);
}
AssertNormalizeNoTrim(input);
}
[Test]
public void TaskLists()
[Test]
public void TaskLists()
{
AssertNormalizeNoTrim("- [X] This is done");
AssertNormalizeNoTrim("- [x] This is done",
"- [X] This is done");
AssertNormalizeNoTrim("- [ ] This is not done");
// ignore
AssertNormalizeNoTrim("[x] This is not a task list");
AssertNormalizeNoTrim("[ ] This is not a task list");
}
[Test]
public void JiraLinks()
{
AssertNormalizeNoTrim("FOO-1234");
AssertNormalizeNoTrim("AB-1");
AssertNormalizeNoTrim("**Hello World AB-1**");
}
[Test]
public void AutoLinks()
{
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from [http://example.com/foo](http://example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from [www.example.com/foo](http://www.example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from ftp://example.com", "Hello from [ftp://example.com](ftp://example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from [hello@example.com](mailto:hello@example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from http://example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from http://www.example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from mailto:hello@example.com", new NormalizeOptions() { ExpandAutoLinks = false, });
}
private static void AssertSyntax(string expected, MarkdownObject syntax)
{
var writer = new StringWriter();
var normalizer = new NormalizeRenderer(writer);
var document = new MarkdownDocument();
if (syntax is Block)
{
AssertNormalizeNoTrim("- [X] This is done");
AssertNormalizeNoTrim("- [x] This is done",
"- [X] This is done");
AssertNormalizeNoTrim("- [ ] This is not done");
// ignore
AssertNormalizeNoTrim("[x] This is not a task list");
AssertNormalizeNoTrim("[ ] This is not a task list");
document.Add(syntax as Block);
}
[Test]
public void JiraLinks()
else
{
AssertNormalizeNoTrim("FOO-1234");
AssertNormalizeNoTrim("AB-1");
AssertNormalizeNoTrim("**Hello World AB-1**");
throw new InvalidOperationException();
}
normalizer.Render(document);
[Test]
public void AutoLinks()
var actual = writer.ToString();
Assert.AreEqual(expected, actual);
}
public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, string context = null)
{
foreach (var pipeline in TestParser.GetPipeline(extensions))
{
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from [http://example.com/foo](http://example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from [www.example.com/foo](http://www.example.com/foo)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from ftp://example.com", "Hello from [ftp://example.com](ftp://example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from [hello@example.com](mailto:hello@example.com)", new NormalizeOptions() { ExpandAutoLinks = true, });
AssertNormalizeNoTrim("Hello from http://example.com/foo", "Hello from http://example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from www.example.com/foo", "Hello from http://www.example.com/foo", new NormalizeOptions() { ExpandAutoLinks = false, });
AssertNormalizeNoTrim("Hello from mailto:hello@example.com", "Hello from mailto:hello@example.com", new NormalizeOptions() { ExpandAutoLinks = false, });
}
private static void AssertSyntax(string expected, MarkdownObject syntax)
{
var writer = new StringWriter();
var normalizer = new NormalizeRenderer(writer);
var document = new MarkdownDocument();
if (syntax is Block)
{
document.Add(syntax as Block);
}
else
{
throw new InvalidOperationException();
}
normalizer.Render(document);
var actual = writer.ToString();
Assert.AreEqual(expected, actual);
}
public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, string context = null)
{
foreach (var pipeline in TestParser.GetPipeline(extensions))
{
AssertNormalize(inputText, expectedOutputText, trim: false, pipeline: pipeline.Value, context: context);
}
}
public static void AssertNormalizeNoTrim(string input, string expected = null, NormalizeOptions options = null)
=> AssertNormalize(input, expected, false, options);
public static void AssertNormalize(string input, string expected = null, bool trim = true, NormalizeOptions options = null, MarkdownPipeline pipeline = null, string context = null)
{
expected = expected ?? input;
input = NormText(input, trim);
expected = NormText(expected, trim);
pipeline = pipeline ?? new MarkdownPipelineBuilder()
.UseAutoLinks()
.UseJiraLinks(new Extensions.JiraLinks.JiraLinkOptions("https://jira.example.com"))
.UseTaskLists()
.Build();
var result = Markdown.Normalize(input, options, pipeline: pipeline);
result = NormText(result, trim);
TestParser.PrintAssertExpected(input, result, expected, context);
}
private static string NormText(string text, bool trim)
{
if (trim)
{
text = text.Trim();
}
return text.Replace("\r\n", "\n").Replace('\r', '\n');
AssertNormalize(inputText, expectedOutputText, trim: false, pipeline: pipeline.Value, context: context);
}
}
public static void AssertNormalizeNoTrim(string input, string expected = null, NormalizeOptions options = null)
=> AssertNormalize(input, expected, false, options);
public static void AssertNormalize(string input, string expected = null, bool trim = true, NormalizeOptions options = null, MarkdownPipeline pipeline = null, string context = null)
{
expected = expected ?? input;
input = NormText(input, trim);
expected = NormText(expected, trim);
pipeline = pipeline ?? new MarkdownPipelineBuilder()
.UseAutoLinks()
.UseJiraLinks(new Extensions.JiraLinks.JiraLinkOptions("https://jira.example.com"))
.UseTaskLists()
.Build();
var result = Markdown.Normalize(input, options, pipeline: pipeline);
result = NormText(result, trim);
TestParser.PrintAssertExpected(input, result, expected, context);
}
private static string NormText(string text, bool trim)
{
if (trim)
{
text = text.Trim();
}
return text.Replace("\r\n", "\n").Replace('\r', '\n');
}
}

View File

@@ -1,43 +1,41 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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 NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestOrderedList
{
[TestFixture]
public class TestOrderedList
[Test]
public void TestReplace()
{
[Test]
public void TestReplace()
var list = new OrderedList<ITest>
{
var list = new OrderedList<ITest>
{
new A(),
new B(),
new C(),
};
new A(),
new B(),
new C(),
};
// Replacing B with D. Order should now be A, D, B.
var result = list.Replace<B>(new D());
Assert.That(result, Is.True);
Assert.That(list.Count, Is.EqualTo(3));
Assert.That(list[0], Is.InstanceOf<A>());
Assert.That(list[1], Is.InstanceOf<D>());
Assert.That(list[2], Is.InstanceOf<C>());
// Replacing B with D. Order should now be A, D, B.
var result = list.Replace<B>(new D());
Assert.That(result, Is.True);
Assert.That(list.Count, Is.EqualTo(3));
Assert.That(list[0], Is.InstanceOf<A>());
Assert.That(list[1], Is.InstanceOf<D>());
Assert.That(list[2], Is.InstanceOf<C>());
// Replacing B again should fail, as it's no longer in the list.
Assert.That(list.Replace<B>(new D()), Is.False);
}
#region Test fixtures
private interface ITest { }
private class A : ITest { }
private class B : ITest { }
private class C : ITest { }
private class D : ITest { }
#endregion
// Replacing B again should fail, as it's no longer in the list.
Assert.That(list.Replace<B>(new D()), Is.False);
}
#region Test fixtures
private interface ITest { }
private class A : ITest { }
private class B : ITest { }
private class C : ITest { }
private class D : ITest { }
#endregion
}

View File

@@ -1,191 +1,196 @@
// 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Markdig.Extensions.JiraLinks;
using Markdig.Renderers.Roundtrip;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestParser
{
public class TestParser
[Test]
public void EnsureSpecsAreUpToDate()
{
[Test]
public void EnsureSpecsAreUpToDate()
// In CI, SpecFileGen is guaranteed to run
if (IsContinuousIntegration)
return;
var specsFilePaths = Directory.GetDirectories(TestsDirectory)
.Where(dir => dir.EndsWith("Specs"))
.SelectMany(dir => Directory.GetFiles(dir)
.Where(file => file.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
.Where(file => file.IndexOf("readme", StringComparison.OrdinalIgnoreCase) == -1))
.ToArray();
var specsMarkdown = new string[specsFilePaths.Length];
var specsSyntaxTrees = new MarkdownDocument[specsFilePaths.Length];
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < specsFilePaths.Length; i++)
{
// In CI, SpecFileGen is guaranteed to run
if (IsContinuousIntegration)
return;
var specsFilePaths = Directory.GetDirectories(TestsDirectory)
.Where(dir => dir.EndsWith("Specs"))
.SelectMany(dir => Directory.GetFiles(dir)
.Where(file => file.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
.Where(file => file.IndexOf("readme", StringComparison.OrdinalIgnoreCase) == -1))
.ToArray();
var specsMarkdown = new string[specsFilePaths.Length];
var specsSyntaxTrees = new MarkdownDocument[specsFilePaths.Length];
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
for (int i = 0; i < specsFilePaths.Length; i++)
{
string markdown = specsMarkdown[i] = File.ReadAllText(specsFilePaths[i]);
specsSyntaxTrees[i] = Markdown.Parse(markdown, pipeline);
}
foreach (var specFilePath in specsFilePaths)
{
string testFilePath = Path.ChangeExtension(specFilePath, ".generated.cs");
Assert.True(File.Exists(testFilePath),
"A new specification file has been added. Add the spec to the list in SpecFileGen and regenerate the tests.");
DateTime specTime = File.GetLastWriteTimeUtc(specFilePath);
DateTime testTime = File.GetLastWriteTimeUtc(testFilePath);
// If file creation times aren't preserved by git, add some leeway
// If specs have come from git, assume that they were regenerated since CI would fail otherwise
testTime = testTime.AddMinutes(3);
// This might not catch a changed spec every time, but should at least sometimes. Otherwise CI will catch it
// This could also trigger, if a user has modified the spec file but reverted the change - can't think of a good workaround
Assert.Less(specTime, testTime,
$"{Path.GetFileName(specFilePath)} has been modified. Run SpecFileGen to regenerate the tests. " +
"If you have modified a specification file, but reverted all changes, ignore this error or revert the 'changed' timestamp metadata on the file.");
}
TestDescendantsOrder.TestSchemas(specsSyntaxTrees);
string markdown = specsMarkdown[i] = File.ReadAllText(specsFilePaths[i]);
specsSyntaxTrees[i] = Markdown.Parse(markdown, pipeline);
}
public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, bool plainText = false, string context = null)
foreach (var specFilePath in specsFilePaths)
{
context ??= string.Empty;
if (!string.IsNullOrEmpty(context))
{
context += "\n";
}
foreach (var pipeline in GetPipeline(extensions))
{
TestSpec(inputText, expectedOutputText, pipeline.Value, plainText, context: context + $"Pipeline configured with extensions: {pipeline.Key}");
}
string testFilePath = Path.ChangeExtension(specFilePath, ".generated.cs");
Assert.True(File.Exists(testFilePath),
"A new specification file has been added. Add the spec to the list in SpecFileGen and regenerate the tests.");
DateTime specTime = File.GetLastWriteTimeUtc(specFilePath);
DateTime testTime = File.GetLastWriteTimeUtc(testFilePath);
// If file creation times aren't preserved by git, add some leeway
// If specs have come from git, assume that they were regenerated since CI would fail otherwise
testTime = testTime.AddMinutes(3);
// This might not catch a changed spec every time, but should at least sometimes. Otherwise CI will catch it
// This could also trigger, if a user has modified the spec file but reverted the change - can't think of a good workaround
Assert.Less(specTime, testTime,
$"{Path.GetFileName(specFilePath)} has been modified. Run SpecFileGen to regenerate the tests. " +
"If you have modified a specification file, but reverted all changes, ignore this error or revert the 'changed' timestamp metadata on the file.");
}
public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline, bool plainText = false, string context = null)
TestDescendantsOrder.TestSchemas(specsSyntaxTrees);
}
[Test]
public void ParseEmptyDocumentWithTrackTriviaEnabled()
{
var document = Markdown.Parse("", trackTrivia: true);
using var sw = new StringWriter();
new RoundtripRenderer(sw).Render(document);
Assert.AreEqual("", sw.ToString());
}
public static void TestSpec(string inputText, string expectedOutputText, string extensions = null, bool plainText = false, string context = null)
{
context ??= string.Empty;
if (!string.IsNullOrEmpty(context))
{
// Uncomment this line to get more debug information for process inlines.
//pipeline.DebugLog = Console.Out;
var result = plainText ? Markdown.ToPlainText(inputText, pipeline) : Markdown.ToHtml(inputText, pipeline);
result = Compact(result);
expectedOutputText = Compact(expectedOutputText);
PrintAssertExpected(inputText, result, expectedOutputText, context);
context += "\n";
}
public static void PrintAssertExpected(string source, string result, string expected, string context = null)
foreach (var pipeline in GetPipeline(extensions))
{
if (expected != result)
TestSpec(inputText, expectedOutputText, pipeline.Value, plainText, context: context + $"Pipeline configured with extensions: {pipeline.Key}");
}
}
public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline, bool plainText = false, string context = null)
{
// Uncomment this line to get more debug information for process inlines.
//pipeline.DebugLog = Console.Out;
var result = plainText ? Markdown.ToPlainText(inputText, pipeline) : Markdown.ToHtml(inputText, pipeline);
result = Compact(result);
expectedOutputText = Compact(expectedOutputText);
PrintAssertExpected(inputText, result, expectedOutputText, context);
}
public static void PrintAssertExpected(string source, string result, string expected, string context = null)
{
if (expected != result)
{
if (context != null)
{
if (context != null)
{
Console.WriteLine(context);
}
Console.WriteLine("```````````````````Source");
Console.WriteLine(DisplaySpaceAndTabs(source));
Console.WriteLine("```````````````````Result");
Console.WriteLine(DisplaySpaceAndTabs(result));
Console.WriteLine("```````````````````Expected");
Console.WriteLine(DisplaySpaceAndTabs(expected));
Console.WriteLine("```````````````````");
Console.WriteLine();
TextAssert.AreEqual(expected, result);
Console.WriteLine(context);
}
Console.WriteLine("```````````````````Source");
Console.WriteLine(DisplaySpaceAndTabs(source));
Console.WriteLine("```````````````````Result");
Console.WriteLine(DisplaySpaceAndTabs(result));
Console.WriteLine("```````````````````Expected");
Console.WriteLine(DisplaySpaceAndTabs(expected));
Console.WriteLine("```````````````````");
Console.WriteLine();
TextAssert.AreEqual(expected, result);
}
}
public static IEnumerable<KeyValuePair<string, MarkdownPipeline>> GetPipeline(string extensionsGroupText)
{
// For the standard case, we make sure that both the CommmonMark core and Extra/Advanced are CommonMark compliant!
if (string.IsNullOrEmpty(extensionsGroupText))
{
yield return new KeyValuePair<string, MarkdownPipeline>("default", new MarkdownPipelineBuilder().Build());
//yield return new KeyValuePair<string, MarkdownPipeline>("default-trivia", new MarkdownPipelineBuilder().EnableTrackTrivia().Build());
yield return new KeyValuePair<string, MarkdownPipeline>("advanced", new MarkdownPipelineBuilder() // Use similar to advanced extension without auto-identifier
.UseAbbreviations()
//.UseAutoIdentifiers()
.UseCitations()
.UseCustomContainers()
.UseDefinitionLists()
.UseEmphasisExtras()
.UseFigures()
.UseFooters()
.UseFootnotes()
.UseGridTables()
.UseMathematics()
.UseMediaLinks()
.UsePipeTables()
.UseListExtras()
.UseGenericAttributes().Build());
yield break;
}
public static IEnumerable<KeyValuePair<string, MarkdownPipeline>> GetPipeline(string extensionsGroupText)
var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var extensionsText in extensionGroups)
{
// For the standard case, we make sure that both the CommmonMark core and Extra/Advanced are CommonMark compliant!
if (string.IsNullOrEmpty(extensionsGroupText))
var builder = new MarkdownPipelineBuilder();
builder.DebugLog = Console.Out;
if (extensionsText == "jiralinks")
{
yield return new KeyValuePair<string, MarkdownPipeline>("default", new MarkdownPipelineBuilder().Build());
//yield return new KeyValuePair<string, MarkdownPipeline>("default-trivia", new MarkdownPipelineBuilder().EnableTrackTrivia().Build());
yield return new KeyValuePair<string, MarkdownPipeline>("advanced", new MarkdownPipelineBuilder() // Use similar to advanced extension without auto-identifier
.UseAbbreviations()
//.UseAutoIdentifiers()
.UseCitations()
.UseCustomContainers()
.UseDefinitionLists()
.UseEmphasisExtras()
.UseFigures()
.UseFooters()
.UseFootnotes()
.UseGridTables()
.UseMathematics()
.UseMediaLinks()
.UsePipeTables()
.UseListExtras()
.UseGenericAttributes().Build());
yield break;
builder.UseJiraLinks(new JiraLinkOptions("http://your.company.abc"));
}
var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var extensionsText in extensionGroups)
else
{
var builder = new MarkdownPipelineBuilder();
builder.DebugLog = Console.Out;
if (extensionsText == "jiralinks")
{
builder.UseJiraLinks(new JiraLinkOptions("http://your.company.abc"));
}
else
{
builder = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText);
}
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, builder.Build());
builder = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText);
}
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, builder.Build());
}
}
public static string DisplaySpaceAndTabs(string text)
public static string DisplaySpaceAndTabs(string text)
{
// Output special characters to check correctly the results
return text.Replace('\t', '→').Replace(' ', '·');
}
private static string Compact(string html)
{
// Normalize the output to make it compatible with CommonMark specs
html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim();
html = Regex.Replace(html, @"\s+</li>", "</li>");
html = Regex.Replace(html, @"<li>\s+", "<li>");
html = html.Normalize(NormalizationForm.FormKD);
return html;
}
public static readonly bool IsContinuousIntegration = Environment.GetEnvironmentVariable("CI") != null;
public static readonly string TestsDirectory = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(typeof(TestParser).Assembly.Location), "../../.."));
static TestParser()
{
const string RunningInsideVisualStudioPath = "\\src\\.vs\\markdig\\";
int index = TestsDirectory.IndexOf(RunningInsideVisualStudioPath);
if (index != -1)
{
// Output special characters to check correctly the results
return text.Replace('\t', '→').Replace(' ', '·');
}
private static string Compact(string html)
{
// Normalize the output to make it compatible with CommonMark specs
html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim();
html = Regex.Replace(html, @"\s+</li>", "</li>");
html = Regex.Replace(html, @"<li>\s+", "<li>");
html = html.Normalize(NormalizationForm.FormKD);
return html;
}
public static readonly bool IsContinuousIntegration = Environment.GetEnvironmentVariable("CI") != null;
public static readonly string TestsDirectory = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(typeof(TestParser).Assembly.Location), "../../.."));
static TestParser()
{
const string RunningInsideVisualStudioPath = "\\src\\.vs\\markdig\\";
int index = TestsDirectory.IndexOf(RunningInsideVisualStudioPath);
if (index != -1)
{
TestsDirectory = TestsDirectory.Substring(0, index) + "\\src\\Markdig.Tests";
}
TestsDirectory = TestsDirectory.Substring(0, index) + "\\src\\Markdig.Tests";
}
}
}

View File

@@ -1,26 +1,21 @@
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using NUnit.Framework;
using System.Linq;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public sealed class TestPipeTable
{
[TestFixture]
public sealed class TestPipeTable
[TestCase("| S | T |\r\n|---|---| \r\n| G | H |")]
[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)]
public void TestTableBug(string markdown, int tableCount = 1)
{
[TestCase("| S | T |\r\n|---|---| \r\n| G | H |")]
[TestCase("| S | T |\r\n|---|---|\t\r\n| G | H |")]
[TestCase("| S | T |\r\n|---|---|\v\r\n| G | H |")]
[TestCase("| S | T |\r\n|---|---|\f\r\n| G | H |")]
[TestCase("| S | T |\r\n|---|---|\f\v\t \r\n| G | H |")]
[TestCase("| S | \r\n|---|\r\n| G |\r\n\r\n| D | D |\r\n| ---| ---| \r\n| V | V |", 2)]
public void TestTableBug(string markdown, int tableCount = 1)
{
MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
MarkdownDocument document = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build());
Table[] tables = document.Descendants().OfType<Table>().ToArray();
Table[] tables = document.Descendants().OfType<Table>().ToArray();
Assert.AreEqual(tableCount, tables.Length);
}
Assert.AreEqual(tableCount, tables.Length);
}
}

View File

@@ -1,44 +1,42 @@
using NUnit.Framework;
namespace Markdig.Tests;
namespace Markdig.Tests
[TestFixture]
public class TestPlainText
{
[TestFixture]
public class TestPlainText
[Test]
[TestCase(/* markdownText: */ "foo bar", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "foo\nbar", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "*foo\nbar*", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "[foo\nbar](http://example.com)", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "<http://foo.bar.baz>", /* expected: */ "http://foo.bar.baz\n")]
[TestCase(/* markdownText: */ "# foo bar", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "# foo\nbar", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "> foo", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "> foo\nbar\n> baz", /* expected: */ "foo\nbar\nbaz\n")]
[TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "`foo\nbar`", /* expected: */ "foo bar\n")] // new line within codespan is treated as whitespace (Example317)
[TestCase(/* markdownText: */ "```\nfoo bar\n```", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "- foo\n- bar\n- baz", /* expected: */ "foo\nbar\nbaz\n")]
[TestCase(/* markdownText: */ "- foo<baz", /* expected: */ "foo<baz\n")]
[TestCase(/* markdownText: */ "- foo&lt;baz", /* expected: */ "foo<baz\n")]
[TestCase(/* markdownText: */ "## foo `bar::baz >`", /* expected: */ "foo bar::baz >\n")]
public void TestPlainEnsureNewLine(string markdownText, string expected)
{
[Test]
[TestCase(/* markdownText: */ "foo bar", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "foo\nbar", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "*foo\nbar*", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "[foo\nbar](http://example.com)", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "<http://foo.bar.baz>", /* expected: */ "http://foo.bar.baz\n")]
[TestCase(/* markdownText: */ "# foo bar", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "# foo\nbar", /* expected: */ "foo\nbar\n")]
[TestCase(/* markdownText: */ "> foo", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "> foo\nbar\n> baz", /* expected: */ "foo\nbar\nbaz\n")]
[TestCase(/* markdownText: */ "`foo`", /* expected: */ "foo\n")]
[TestCase(/* markdownText: */ "`foo\nbar`", /* expected: */ "foo bar\n")] // new line within codespan is treated as whitespace (Example317)
[TestCase(/* markdownText: */ "```\nfoo bar\n```", /* expected: */ "foo bar\n")]
[TestCase(/* markdownText: */ "- foo\n- bar\n- baz", /* expected: */ "foo\nbar\nbaz\n")]
[TestCase(/* markdownText: */ "- foo<baz", /* expected: */ "foo<baz\n")]
[TestCase(/* markdownText: */ "- foo&lt;baz", /* expected: */ "foo<baz\n")]
[TestCase(/* markdownText: */ "## foo `bar::baz >`", /* expected: */ "foo bar::baz >\n")]
public void TestPlainEnsureNewLine(string markdownText, string expected)
{
var actual = Markdown.ToPlainText(markdownText);
Assert.AreEqual(expected, actual);
}
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")]
public void TestPlainWithExtensions(string markdownText, string expected, string extensions)
{
TestParser.TestSpec(markdownText, expected, extensions, plainText: true);
}
[Test]
[TestCase(/* markdownText: */ ":::\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers|advanced")]
[TestCase(/* markdownText: */ ":::bar\nfoo\n:::", /* expected: */ "foo\n", /*extensions*/ "customcontainers+attributes|advanced")]
[TestCase(/* markdownText: */ "| Header1 | Header2 | Header3 |\n|--|--|--|\nt**es**t|value2|value3", /* expected: */ "Header1 Header2 Header3 test value2 value3","pipetables")]
public void TestPlainWithExtensions(string markdownText, string expected, string extensions)
{
TestParser.TestSpec(markdownText, expected, extensions, plainText: true);
}
public static void TestSpec(string markdownText, string expected, string extensions, string context = null)
{
TestParser.TestSpec(markdownText, expected, extensions, plainText: true, context: context);
}
public static void TestSpec(string markdownText, string expected, string extensions, string context = null)
{
TestParser.TestSpec(markdownText, expected, extensions, plainText: true, context: context);
}
}

View File

@@ -1,97 +1,94 @@
// 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;
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestPlayParser
{
[TestFixture]
public class TestPlayParser
[Test]
public void TestBugWithEmphasisAndTable()
{
[Test]
public void TestBugWithEmphasisAndTable()
{
TestParser.TestSpec("**basics | 8:00**", "<p><strong>basics | 8:00</strong></p>", "advanced");
}
TestParser.TestSpec("**basics | 8:00**", "<p><strong>basics | 8:00</strong></p>", "advanced");
}
[Test]
public void TestLinksWithCarriageReturn()
{
var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com\r\n[link-2]: https://example.com";
var result = Markdown.ToHtml(text).TrimEnd();
Assert.AreEqual("<p><a href=\"https://example.com\">Link 1</a>, <a href=\"https://example.com\">link 2</a>.</p>", result);
}
[Test]
public void TestLinksWithCarriageReturn()
{
var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com\r\n[link-2]: https://example.com";
var result = Markdown.ToHtml(text).TrimEnd();
Assert.AreEqual("<p><a href=\"https://example.com\">Link 1</a>, <a href=\"https://example.com\">link 2</a>.</p>", result);
}
[Test]
public void TestLinksWithTitleAndCarriageReturn()
{
var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com \"title 1\" \r\n[link-2]: https://example.com \"title 2\"";
var result = Markdown.ToHtml(text).TrimEnd();
Assert.AreEqual("<p><a href=\"https://example.com\" title=\"title 1\">Link 1</a>, <a href=\"https://example.com\" title=\"title 2\">link 2</a>.</p>", result);
}
[Test]
public void TestLinksWithTitleAndCarriageReturn()
{
var text = "[Link 1][link-1], [link 2][link-2].\r\n\r\n[link-1]: https://example.com \"title 1\" \r\n[link-2]: https://example.com \"title 2\"";
var result = Markdown.ToHtml(text).TrimEnd();
Assert.AreEqual("<p><a href=\"https://example.com\" title=\"title 1\">Link 1</a>, <a href=\"https://example.com\" title=\"title 2\">link 2</a>.</p>", result);
}
[Test]
public void TestLink()
{
var doc = Markdown.Parse("There is a ![link](/yoyo)");
var link = doc.Descendants<ParagraphBlock>().SelectMany(x => x.Inline.Descendants<LinkInline>()).FirstOrDefault(l => l.IsImage);
Assert.AreEqual("/yoyo", link?.Url);
}
[Test]
public void TestLink()
{
var doc = Markdown.Parse("There is a ![link](/yoyo)");
var link = doc.Descendants<ParagraphBlock>().SelectMany(x => x.Inline.Descendants<LinkInline>()).FirstOrDefault(l => l.IsImage);
Assert.AreEqual("/yoyo", link?.Url);
}
[Test]
public void TestListBug2()
{
TestParser.TestSpec("10.\t*test* test\n\n11.\t__test__ test\n\n", @"<ol start=""10"">
[Test]
public void TestListBug2()
{
TestParser.TestSpec("10.\t*test* test\n\n11.\t__test__ test\n\n", @"<ol start=""10"">
<li><p><em>test</em> test</p>
</li>
<li><p><strong>test</strong> test</p>
</li>
</ol>
");
}
}
[Test]
public void TestSimple()
{
var text = @" *[HTML]: Hypertext Markup Language
[Test]
public void TestSimple()
{
var text = @" *[HTML]: Hypertext Markup Language
Later in a text we are using HTML and it becomes an abbr tag HTML
";
// var reader = new StringReader(@"> > toto tata
//> titi toto
//");
// var reader = new StringReader(@"> > toto tata
//> titi toto
//");
//var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtras());
var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviations().Build());
//File.WriteAllText("test.html", result, Encoding.UTF8);
//Console.WriteLine(result);
}
//var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtras());
var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviations().Build());
//File.WriteAllText("test.html", result, Encoding.UTF8);
//Console.WriteLine(result);
}
[Test]
public void TestEmptyLiteral()
{
var text = @"> *some text*
[Test]
public void TestEmptyLiteral()
{
var text = @"> *some text*
> some other text";
var doc = Markdown.Parse(text);
var doc = Markdown.Parse(text);
Assert.True(doc.Descendants<LiteralInline>().All(x => !x.Content.IsEmpty),
"There should not have any empty literals");
}
Assert.True(doc.Descendants<LiteralInline>().All(x => !x.Content.IsEmpty),
"There should not have any empty literals");
}
[Test]
public void TestSelfPipeline1()
{
var text = @" <!--markdig:pipetables-->
[Test]
public void TestSelfPipeline1()
{
var text = @" <!--markdig:pipetables-->
a | b
- | -
0 | 1
";
TestParser.TestSpec(text, @"<!--markdig:pipetables-->
TestParser.TestSpec(text, @"<!--markdig:pipetables-->
<table>
<thead>
<tr>
@@ -107,17 +104,17 @@ a | b
</tbody>
</table>
", "self");
}
}
[Test]
public void TestListBug()
{
// TODO: Add this test back to the CommonMark specs
var text = @"- item1
[Test]
public void TestListBug()
{
// TODO: Add this test back to the CommonMark specs
var text = @"- item1
- item2
- item3
- item4";
TestParser.TestSpec(text, @"<ul>
TestParser.TestSpec(text, @"<ul>
<li>item1
<ul>
<li>item2
@@ -130,13 +127,13 @@ a | b
</ul></li>
</ul>
");
}
}
[Test]
public void TestHtmlBug()
{
TestParser.TestSpec(@" # header1
[Test]
public void TestHtmlBug()
{
TestParser.TestSpec(@" # header1
<pre class='copy'>
blabla
@@ -148,43 +145,43 @@ blabla
blabla
</pre>
<h1>header2</h1>");
}
}
[Test]
public void TestHtmlh4Bug()
{
TestParser.TestSpec(@"<h4>foobar</h4>", @"<h4>foobar</h4>");
}
[Test]
public void TestHtmlh4Bug()
{
TestParser.TestSpec(@"<h4>foobar</h4>", @"<h4>foobar</h4>");
}
[Test]
public void TestStandardUriEscape()
{
TestParser.TestSpec(@"![你好](你好.png)", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
}
[Test]
public void TestStandardUriEscape()
{
TestParser.TestSpec(@"![你好](你好.png)", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
}
[Test]
public void TestBugAdvanced()
{
TestParser.TestSpec(@"`https://{domain}/callbacks`
[Test]
public void TestBugAdvanced()
{
TestParser.TestSpec(@"`https://{domain}/callbacks`
#### HEADING
Paragraph
", "<p><code>https://{domain}/callbacks</code></p>\n<h4 id=\"heading\">HEADING</h4>\n<p>Paragraph</p>", "advanced");
}
}
[Test]
public void TestBugEmphAttribute()
{
// https://github.com/lunet-io/markdig/issues/108
TestParser.TestSpec(@"*test*{name=value}", "<p><em name=\"value\">test</em></p>", "advanced");
}
[Test]
public void TestBugEmphAttribute()
{
// https://github.com/lunet-io/markdig/issues/108
TestParser.TestSpec(@"*test*{name=value}", "<p><em name=\"value\">test</em></p>", "advanced");
}
[Test]
public void TestBugPipeTables()
{
// https://github.com/lunet-io/markdig/issues/73
TestParser.TestSpec(@"| abc | def |
[Test]
public void TestBugPipeTables()
{
// https://github.com/lunet-io/markdig/issues/73
TestParser.TestSpec(@"| abc | def |
| --- | --- |
| 1 | ~3 |
", @"<table>
@@ -201,12 +198,12 @@ Paragraph
</tr>
</tbody>
</table>", "advanced");
}
}
[Test]
public void TestGridTableWithCustomAttributes() {
[Test]
public void TestGridTableWithCustomAttributes() {
var input = @"
var input = @"
{.table}
+---+---+
| a | b |
@@ -215,7 +212,7 @@ Paragraph
+---+---+
";
var expected = @"<table class=""table"">
var expected = @"<table class=""table"">
<col style=""width:50%"" />
<col style=""width:50%"" />
<thead>
@@ -232,76 +229,75 @@ Paragraph
</tbody>
</table>
";
TestParser.TestSpec(input, expected, "advanced");
}
[Test]
public void TestSamePipelineAllExtensions()
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
// Reuse the same pipeline
var result1 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline);
var result2 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline);
Assert.AreEqual("<p>This is a <cite>citation</cite></p>", result1.Trim());
Assert.AreEqual(result1, result2);
}
// Test for emoji and smileys
// var text = @" This is a test with a :) and a :angry: smiley";
// Test for definition lists:
//
// var text = @"
//Term 1
//: This is a definition item
// With a paragraph
// > This is a block quote
// - This is a list
// - item2
// ```java
// Test
// ```
// And a lazy line
//: This ia another definition item
//Term2
//Term3 *with some inline*
//: This is another definition for term2
//";
// Test for grid table
// var text = @"
//+-----------------------------------+--------------------------------------+
//| - this is a list | > We have a blockquote
//| - this is a second item |
//| |
//| ``` |
//| Yes |
//| ``` |
//+===================================+======================================+
//| This is a second line |
//+-----------------------------------+--------------------------------------+
//:::spoiler {#yessss}
//This is a spoiler
//:::
///| we have mult | paragraph |
///| we have a new colspan with a long line
///| and lots of text
//";
TestParser.TestSpec(input, expected, "advanced");
}
[Test]
public void TestSamePipelineAllExtensions()
{
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
// Reuse the same pipeline
var result1 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline);
var result2 = Markdown.ToHtml("This is a \"\"citation\"\"", pipeline);
Assert.AreEqual("<p>This is a <cite>citation</cite></p>", result1.Trim());
Assert.AreEqual(result1, result2);
}
// Test for emoji and smileys
// var text = @" This is a test with a :) and a :angry: smiley";
// Test for definition lists:
//
// var text = @"
//Term 1
//: This is a definition item
// With a paragraph
// > This is a block quote
// - This is a list
// - item2
// ```java
// Test
// ```
// And a lazy line
//: This ia another definition item
//Term2
//Term3 *with some inline*
//: This is another definition for term2
//";
// Test for grid table
// var text = @"
//+-----------------------------------+--------------------------------------+
//| - this is a list | > We have a blockquote
//| - this is a second item |
//| |
//| ``` |
//| Yes |
//| ``` |
//+===================================+======================================+
//| This is a second line |
//+-----------------------------------+--------------------------------------+
//:::spoiler {#yessss}
//This is a spoiler
//:::
///| we have mult | paragraph |
///| we have a new colspan with a long line
///| and lots of text
//";
}

View File

@@ -1,19 +1,18 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// 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.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestPragmaLines
{
[TestFixture]
public class TestPragmaLines
[Test]
public void TestFindClosest()
{
[Test]
public void TestFindClosest()
{
var doc = Markdown.Parse(
var doc = Markdown.Parse(
"test1\n" + // 0
"\n" + // 1
"test2\n" + // 2
@@ -37,27 +36,27 @@ namespace Markdig.Tests
"- item3\n" + // 20
"\n" + // 21
"This is a last paragraph\n" // 22
, new MarkdownPipelineBuilder().UsePragmaLines().Build());
, new MarkdownPipelineBuilder().UsePragmaLines().Build());
foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22})
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
Assert.AreEqual(22, doc.FindClosestLine(23));
Assert.AreEqual(10, doc.FindClosestLine(11));
Assert.AreEqual(10, doc.FindClosestLine(12));
Assert.AreEqual(10, doc.FindClosestLine(13));
Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next
Assert.AreEqual(18, doc.FindClosestLine(15));
Assert.AreEqual(18, doc.FindClosestLine(16));
foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22})
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
[Test]
public void TestFindClosest1()
{
var text =
Assert.AreEqual(22, doc.FindClosestLine(23));
Assert.AreEqual(10, doc.FindClosestLine(11));
Assert.AreEqual(10, doc.FindClosestLine(12));
Assert.AreEqual(10, doc.FindClosestLine(13));
Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next
Assert.AreEqual(18, doc.FindClosestLine(15));
Assert.AreEqual(18, doc.FindClosestLine(16));
}
[Test]
public void TestFindClosest1()
{
var text =
"- item1\n" + // 0
" - item11\n" + // 1
" - item12\n" + // 2
@@ -66,15 +65,14 @@ namespace Markdig.Tests
" - item131\n" + // 5
" - item1311\n"; // 6
var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build();
var doc = Markdown.Parse(text, pipeline);
var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build();
var doc = Markdown.Parse(text, pipeline);
for (int exact = 0; exact < 7; exact++)
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
Assert.AreEqual(6, doc.FindClosestLine(50));
for (int exact = 0; exact < 7; exact++)
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
Assert.AreEqual(6, doc.FindClosestLine(50));
}
}

View File

@@ -1,57 +1,54 @@
using NUnit.Framework;
namespace Markdig.Tests;
namespace Markdig.Tests
[TestFixture]
public class TestReferralLinks
{
[TestFixture]
public class TestReferralLinks
[Test]
public void TestLinksWithNoFollowRel()
{
[Test]
public void TestLinksWithNoFollowRel()
{
var markdown = "[world](http://example.com)";
var expected = "nofollow";
var markdown = "[world](http://example.com)";
var expected = "nofollow";
#pragma warning disable 0618
var pipeline = new MarkdownPipelineBuilder()
.UseNoFollowLinks()
var pipeline = new MarkdownPipelineBuilder()
.UseNoFollowLinks()
#pragma warning restore 0618
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
[Test]
[TestCase(new[] { "nofollow" }, "nofollow")]
[TestCase(new[] { "noopener" }, "noopener")]
[TestCase(new[] { "nofollow", "noopener"}, "nofollow noopener")]
public void TestLinksWithCustomRel(string[] rels, string expected)
{
var markdown = "[world](http://example.com)";
[Test]
[TestCase(new[] { "nofollow" }, "nofollow")]
[TestCase(new[] { "noopener" }, "noopener")]
[TestCase(new[] { "nofollow", "noopener"}, "nofollow noopener")]
public void TestLinksWithCustomRel(string[] rels, string expected)
{
var markdown = "[world](http://example.com)";
var pipeline = new MarkdownPipelineBuilder()
.UseReferralLinks(rels)
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
var pipeline = new MarkdownPipelineBuilder()
.UseReferralLinks(rels)
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
[Test]
[TestCase(new[] { "noopener" }, "noopener")]
[TestCase(new[] { "nofollow" }, "nofollow")]
[TestCase(new[] { "nofollow", "noopener" }, "nofollow noopener")]
public void TestAutoLinksWithCustomRel(string[] rels, string expected)
{
var markdown = "http://example.com";
[Test]
[TestCase(new[] { "noopener" }, "noopener")]
[TestCase(new[] { "nofollow" }, "nofollow")]
[TestCase(new[] { "nofollow", "noopener" }, "nofollow noopener")]
public void TestAutoLinksWithCustomRel(string[] rels, string expected)
{
var markdown = "http://example.com";
var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks()
.UseReferralLinks(rels)
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks()
.UseReferralLinks(rels)
.Build();
var html = Markdown.ToHtml(markdown, pipeline);
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
Assert.That(html, Contains.Substring($"rel=\"{expected}\""));
}
}

View File

@@ -1,48 +1,44 @@
using System;
using System.IO;
using Markdig.Parsers;
using Markdig.Renderers;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestRelativeUrlReplacement
{
public class TestRelativeUrlReplacement
[Test]
public void ReplacesRelativeLinks()
{
[Test]
public void ReplacesRelativeLinks()
{
TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg");
TestSpec("https://example.com", "Link: [hello](relative.jpg)", "https://example.com/relative.jpg");
TestSpec("https://example.com/", "Link: [hello](/relative.jpg?a=b)", "https://example.com/relative.jpg?a=b");
TestSpec("https://example.com/", "Link: [hello](relative.jpg#x)", "https://example.com/relative.jpg#x");
TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg");
TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg");
TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg");
}
TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg");
TestSpec("https://example.com", "Link: [hello](relative.jpg)", "https://example.com/relative.jpg");
TestSpec("https://example.com/", "Link: [hello](/relative.jpg?a=b)", "https://example.com/relative.jpg?a=b");
TestSpec("https://example.com/", "Link: [hello](relative.jpg#x)", "https://example.com/relative.jpg#x");
TestSpec(null, "Link: [hello](relative.jpg)", "relative.jpg");
TestSpec(null, "Link: [hello](/relative.jpg)", "/relative.jpg");
TestSpec("https://example.com", "Link: [hello](/relative.jpg)", "https://example.com/relative.jpg");
}
[Test]
public void ReplacesRelativeImageSources()
{
TestSpec("https://example.com", "Image: ![alt text](/image.jpg)", "https://example.com/image.jpg");
TestSpec("https://example.com", "Image: ![alt text](image.jpg \"title\")", "https://example.com/image.jpg");
TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg");
}
[Test]
public void ReplacesRelativeImageSources()
{
TestSpec("https://example.com", "Image: ![alt text](/image.jpg)", "https://example.com/image.jpg");
TestSpec("https://example.com", "Image: ![alt text](image.jpg \"title\")", "https://example.com/image.jpg");
TestSpec(null, "Image: ![alt text](/image.jpg)", "/image.jpg");
}
public static void TestSpec(string baseUrl, string markdown, string expectedLink)
{
var pipeline = new MarkdownPipelineBuilder().Build();
public static void TestSpec(string baseUrl, string markdown, string expectedLink)
{
var pipeline = new MarkdownPipelineBuilder().Build();
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
if (baseUrl != null)
renderer.BaseUrl = new Uri(baseUrl);
pipeline.Setup(renderer);
var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
if (baseUrl != null)
renderer.BaseUrl = new Uri(baseUrl);
pipeline.Setup(renderer);
var document = MarkdownParser.Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
var document = MarkdownParser.Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\""));
}
Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\""));
}
}

View File

@@ -1,30 +1,29 @@
using Markdig.Renderers.Roundtrip;
using Markdig.Syntax;
using NUnit.Framework;
using System.IO;
namespace Markdig.Tests
namespace Markdig.Tests;
internal static class TestRoundtrip
{
internal static class TestRoundtrip
internal static void TestSpec(string markdownText, string expected, string extensions, string context = null)
{
internal static void TestSpec(string markdownText, string expected, string extensions, string context = null)
{
RoundTrip(markdownText, context);
}
RoundTrip(markdownText, context);
}
internal static void RoundTrip(string markdown, string context = null)
{
var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder.EnableTrackTrivia();
MarkdownPipeline pipeline = pipelineBuilder.Build();
MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline);
var sw = new StringWriter();
var nr = new RoundtripRenderer(sw);
internal static void RoundTrip(string markdown, string context = null)
{
var pipelineBuilder = new MarkdownPipelineBuilder();
pipelineBuilder.EnableTrackTrivia();
pipelineBuilder.UseYamlFrontMatter();
MarkdownPipeline pipeline = pipelineBuilder.Build();
MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline);
var sw = new StringWriter();
var nr = new RoundtripRenderer(sw);
pipeline.Setup(nr);
nr.Write(markdownDocument);
nr.Write(markdownDocument);
var result = sw.ToString();
TestParser.PrintAssertExpected("", result, markdown, context);
}
var result = sw.ToString();
TestParser.PrintAssertExpected("", result, markdown, context);
}
}

View File

@@ -1,36 +1,34 @@
using Markdig.Extensions.SmartyPants;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestSmartyPants
{
public class TestSmartyPants
[Test]
public void MappingCanBeReconfigured()
{
[Test]
public void MappingCanBeReconfigured()
{
SmartyPantOptions options = new SmartyPantOptions();
options.Mapping[SmartyPantType.LeftAngleQuote] = "foo";
options.Mapping[SmartyPantType.RightAngleQuote] = "bar";
var options = new SmartyPantOptions();
options.Mapping[SmartyPantType.LeftAngleQuote] = "foo";
options.Mapping[SmartyPantType.RightAngleQuote] = "bar";
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants(options)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants(options)
.Build();
TestParser.TestSpec("<<test>>", "<p>footestbar</p>", pipeline);
}
TestParser.TestSpec("<<test>>", "<p>footestbar</p>", pipeline);
}
[Test]
public void MappingCanBeReconfigured_HandlesRemovedMappings()
{
SmartyPantOptions options = new SmartyPantOptions();
options.Mapping.Remove(SmartyPantType.LeftAngleQuote);
options.Mapping.Remove(SmartyPantType.RightAngleQuote);
[Test]
public void MappingCanBeReconfigured_HandlesRemovedMappings()
{
var options = new SmartyPantOptions();
options.Mapping.Remove(SmartyPantType.LeftAngleQuote);
options.Mapping.Remove(SmartyPantType.RightAngleQuote);
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants(options)
.Build();
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants(options)
.Build();
TestParser.TestSpec("<<test>>", "<p>&laquo;test&raquo;</p>", pipeline);
}
TestParser.TestSpec("<<test>>", "<p>&laquo;test&raquo;</p>", pipeline);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,185 +1,242 @@
using NUnit.Framework;
using System.Collections;
using System.Text;
using Markdig.Helpers;
using System;
namespace Markdig.Tests
namespace Markdig.Tests;
[TestFixture]
public class TestStringSliceList
{
[TestFixture]
public class TestStringSliceList
// TODO: Add tests for StringSlice
// TODO: Add more tests for StringLineGroup
[Test]
public void TestStringLineGroupSimple()
{
// TODO: Add tests for StringSlice
// TODO: Add more tests for StringLineGroup
[Test]
public void TestStringLineGroupSimple()
var text = new StringLineGroup(4)
{
var text = new StringLineGroup(4)
{
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed),
new StringSlice("F")
};
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed),
new StringSlice("F")
};
var iterator = text.ToCharIterator();
Assert.AreEqual("ABC\nE\nF".Length, iterator.End - iterator.Start + 1);
var iterator = text.ToCharIterator();
Assert.AreEqual("ABC\nE\nF".Length, iterator.End - iterator.Start + 1);
var chars = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nE\nF", chars.ToString());
var chars = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nE\nF", chars.ToString());
TextAssert.AreEqual("ABC\nE\nF", text.ToString());
}
TextAssert.AreEqual("ABC\nE\nF", text.ToString());
}
[Test]
public void TestStringLineGroupWithSlices()
[Test]
public void TestStringLineGroupWithSlices()
{
var text = new StringLineGroup(4)
{
var text = new StringLineGroup(4)
{
new StringSlice("XABC", NewLine.LineFeed) { Start = 1},
new StringSlice("YYE", NewLine.LineFeed) { Start = 2},
new StringSlice("ZZZF") { Start = 3 }
};
new StringSlice("XABC", NewLine.LineFeed) { Start = 1},
new StringSlice("YYE", NewLine.LineFeed) { Start = 2},
new StringSlice("ZZZF") { Start = 3 }
};
var chars = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nE\nF", chars.ToString());
}
var chars = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nE\nF", chars.ToString());
}
private static string ToString(StringLineGroup.Iterator text)
private static string ToString(StringLineGroup.Iterator text)
{
var chars = new StringBuilder();
while (text.CurrentChar != '\0')
{
var chars = new StringBuilder();
while (text.CurrentChar != '\0')
{
chars.Append(text.CurrentChar);
text.NextChar();
}
return chars.ToString();
}
[Test]
public void TestStringLineGroupSaveAndRestore()
{
var text = new StringLineGroup(4)
{
new StringSlice("ABCD", NewLine.LineFeed),
new StringSlice("EF"),
}.ToCharIterator();
Assert.AreEqual('A', text.CurrentChar);
Assert.AreEqual(0, text.SliceIndex);
text.NextChar(); // B
text.NextChar(); // C
text.NextChar(); // D
text.NextChar(); // \n
chars.Append(text.CurrentChar);
text.NextChar();
Assert.AreEqual('E', text.CurrentChar);
Assert.AreEqual(1, text.SliceIndex);
}
return chars.ToString();
}
[Test]
public void TestSkipWhitespaces()
[Test]
public void TestStringLineGroupSaveAndRestore()
{
var text = new StringLineGroup(4)
{
var text = new StringLineGroup(" ABC").ToCharIterator();
Assert.False(text.TrimStart());
Assert.AreEqual('A', text.CurrentChar);
new StringSlice("ABCD", NewLine.LineFeed),
new StringSlice("EF"),
}.ToCharIterator();
text = new StringLineGroup(" ").ToCharIterator();
Assert.True(text.TrimStart());
Assert.AreEqual('\0', text.CurrentChar);
Assert.AreEqual('A', text.CurrentChar);
Assert.AreEqual(0, text.SliceIndex);
var slice = new StringSlice(" ABC");
Assert.False(slice.TrimStart());
text.NextChar(); // B
slice = new StringSlice(" ");
Assert.True(slice.TrimStart());
}
text.NextChar(); // C
text.NextChar(); // D
text.NextChar(); // \n
text.NextChar();
Assert.AreEqual('E', text.CurrentChar);
Assert.AreEqual(1, text.SliceIndex);
}
[Test]
public void TestStringLineGroupWithModifiedStart()
[Test]
public void TestSkipWhitespaces()
{
var text = new StringLineGroup(" ABC").ToCharIterator();
Assert.False(text.TrimStart());
Assert.AreEqual('A', text.CurrentChar);
text = new StringLineGroup(" ").ToCharIterator();
Assert.True(text.TrimStart());
Assert.AreEqual('\0', text.CurrentChar);
var slice = new StringSlice(" ABC");
Assert.False(slice.TrimStart());
slice = new StringSlice(" ");
Assert.True(slice.TrimStart());
}
[Test]
public void TestStringLineGroupWithModifiedStart()
{
var line1 = new StringSlice(" ABC", NewLine.LineFeed);
line1.NextChar();
line1.NextChar();
var line2 = new StringSlice(" DEF ");
line2.Trim();
var text = new StringLineGroup(4) {line1, line2};
var result = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nDEF", result);
}
[Test]
public void TestStringLineGroupWithTrim()
{
var line1 = new StringSlice(" ABC ", NewLine.LineFeed);
line1.NextChar();
line1.NextChar();
var line2 = new StringSlice(" DEF ");
var text = new StringLineGroup(4) { line1, line2}.ToCharIterator();
text.TrimStart();
var result = ToString(text);
TextAssert.AreEqual("ABC \n DEF ", result);
}
[Test]
public void TestStringLineGroupIteratorPeekChar()
{
var iterator = new StringLineGroup(4)
{
var line1 = new StringSlice(" ABC", NewLine.LineFeed);
line1.NextChar();
line1.NextChar();
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed),
new StringSlice("F")
}.ToCharIterator();
var line2 = new StringSlice(" DEF ");
line2.Trim();
Assert.AreEqual('A', iterator.CurrentChar);
Assert.AreEqual('A', iterator.PeekChar(0));
Assert.AreEqual('B', iterator.PeekChar());
Assert.AreEqual('B', iterator.PeekChar(1));
Assert.AreEqual('C', iterator.PeekChar(2));
Assert.AreEqual('\n', iterator.PeekChar(3));
Assert.AreEqual('E', iterator.PeekChar(4));
Assert.AreEqual('\n', iterator.PeekChar(5));
Assert.AreEqual('F', iterator.PeekChar(6));
Assert.AreEqual('\0', iterator.PeekChar(7)); // There is no \n appended to the last line
Assert.AreEqual('\0', iterator.PeekChar(8));
Assert.AreEqual('\0', iterator.PeekChar(100));
var text = new StringLineGroup(4) {line1, line2};
Assert.Throws<ArgumentOutOfRangeException>(() => iterator.PeekChar(-1));
}
var result = ToString(text.ToCharIterator());
TextAssert.AreEqual("ABC\nDEF", result);
}
[Test]
public void TestStringLineGroupWithTrim()
[Test]
public void TestIteratorSkipChar()
{
var lineGroup = new StringLineGroup(4)
{
var line1 = new StringSlice(" ABC ", NewLine.LineFeed);
line1.NextChar();
line1.NextChar();
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed)
};
var line2 = new StringSlice(" DEF ");
Test(lineGroup.ToCharIterator());
var text = new StringLineGroup(4) { line1, line2}.ToCharIterator();
text.TrimStart();
Test(new StringSlice("ABC\nE\n"));
var result = ToString(text);
TextAssert.AreEqual("ABC \n DEF ", result);
}
Test(new StringSlice("Foo\nABC\nE\n", 4, 9));
[Test]
public void TestStringLineGroupIteratorPeekChar()
static void Test<T>(T iterator) where T : ICharIterator
{
var iterator = new StringLineGroup(4)
{
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed),
new StringSlice("F")
}.ToCharIterator();
Assert.AreEqual('A', iterator.CurrentChar);
Assert.AreEqual('A', iterator.PeekChar(0));
Assert.AreEqual('B', iterator.PeekChar());
Assert.AreEqual('B', iterator.PeekChar(1));
Assert.AreEqual('C', iterator.PeekChar(2));
Assert.AreEqual('\n', iterator.PeekChar(3));
Assert.AreEqual('E', iterator.PeekChar(4));
Assert.AreEqual('\n', iterator.PeekChar(5));
Assert.AreEqual('F', iterator.PeekChar(6));
Assert.AreEqual('\0', iterator.PeekChar(7)); // There is no \n appended to the last line
Assert.AreEqual('\0', iterator.PeekChar(8));
Assert.AreEqual('\0', iterator.PeekChar(100));
Assert.Throws<ArgumentOutOfRangeException>(() => iterator.PeekChar(-1));
}
[Test]
public void TestIteratorSkipChar()
{
var lineGroup = new StringLineGroup(4)
{
new StringSlice("ABC", NewLine.LineFeed),
new StringSlice("E", NewLine.LineFeed)
};
Test(lineGroup.ToCharIterator());
Test(new StringSlice("ABC\nE\n"));
Test(new StringSlice("Foo\nABC\nE\n", 4, 9));
static void Test<T>(T iterator) where T : ICharIterator
{
Assert.AreEqual('A', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('B', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('C', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('E', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
}
Assert.AreEqual('A', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('B', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('C', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('E', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\n', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
Assert.AreEqual('\0', iterator.CurrentChar); iterator.SkipChar();
}
}
}
[Test]
public void TestStringLineGroupCharIteratorAtCapacity()
{
string str = "ABCDEFGHI";
var text = new StringLineGroup(1)
{
// Will store the following line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
};
var iterator = text.ToCharIterator();
var chars = ToString(iterator);
TextAssert.AreEqual("ABC\r\n", chars.ToString());
TextAssert.AreEqual("ABC", text.ToString());
}
[Test]
public void TestStringLineGroupCharIteratorForcingIncreaseCapacity()
{
string str = "ABCDEFGHI";
var text = new StringLineGroup(1)
{
// Will store the following line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 2 },
// Will force increase capacity to 2 and store the line at capacity
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 3, End = 3 },
};
var iterator = text.ToCharIterator();
var chars = ToString(iterator);
TextAssert.AreEqual("ABC\r\nD\r\n", chars.ToString());
TextAssert.AreEqual("ABC\r\nD", text.ToString());
}
[Test]
public void TestStringLineGroup_EnumeratorReturnsRealLines()
{
string str = "A\r\n";
var text = new StringLineGroup(4)
{
new StringSlice(str, NewLine.CarriageReturnLineFeed) { Start = 0, End = 0 }
};
var enumerator = ((IEnumerable)text).GetEnumerator();
Assert.True(enumerator.MoveNext());
StringLine currentLine = (StringLine)enumerator.Current;
TextAssert.AreEqual("A", currentLine.ToString());
Assert.False(enumerator.MoveNext());
var nonBoxedEnumerator = text.GetEnumerator();
Assert.True(nonBoxedEnumerator.MoveNext());
currentLine = (StringLine)nonBoxedEnumerator.Current;
TextAssert.AreEqual("A", currentLine.ToString());
Assert.False(nonBoxedEnumerator.MoveNext());
}
}

View File

@@ -1,102 +1,98 @@
using System;
using System.Linq;
using Markdig.Helpers;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
public class TestTransformedStringCache
{
public class TestTransformedStringCache
[Test]
public void GetRunsTransformationCallback()
{
[Test]
public void GetRunsTransformationCallback()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
var cache = new TransformedStringCache(static s => "callback-" + s);
Assert.AreEqual("callback-foo", cache.Get("foo"));
Assert.AreEqual("callback-bar", cache.Get("bar"));
Assert.AreEqual("callback-baz", cache.Get("baz"));
Assert.AreEqual("callback-foo", cache.Get("foo"));
Assert.AreEqual("callback-bar", cache.Get("bar"));
Assert.AreEqual("callback-baz", cache.Get("baz"));
}
[Test]
public void CachesTransformedInstance()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string transformedBar = cache.Get("bar");
Assert.AreSame(transformedBar, cache.Get("bar"));
string transformedFoo = cache.Get("foo".AsSpan());
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(cache.Get("baz"), cache.Get("baz".AsSpan()));
Assert.AreSame(transformedBar, cache.Get("bar"));
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(transformedBar, cache.Get("bar".AsSpan()));
Assert.AreSame(transformedFoo, cache.Get("foo".AsSpan()));
}
[Test]
public void DoesNotCacheEmptyInputs()
{
var cache = new TransformedStringCache(static s => new string('a', 4));
string cached = cache.Get("");
string cached2 = cache.Get("");
string cached3 = cache.Get(ReadOnlySpan<char>.Empty);
Assert.AreEqual("aaaa", cached);
Assert.AreEqual(cached, cached2);
Assert.AreEqual(cached, cached3);
Assert.AreNotSame(cached, cached2);
Assert.AreNotSame(cached, cached3);
Assert.AreNotSame(cached2, cached3);
}
[Test]
[TestCase(TransformedStringCache.InputLengthLimit, true)]
[TestCase(TransformedStringCache.InputLengthLimit + 1, false)]
public void DoesNotCacheLongInputs(int length, bool shouldBeCached)
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string input = new string('a', length);
string cached = cache.Get(input);
string cached2 = cache.Get(input);
Assert.AreEqual("callback-" + input, cached);
Assert.AreEqual(cached, cached2);
if (shouldBeCached)
{
Assert.AreSame(cached, cached2);
}
[Test]
public void CachesTransformedInstance()
else
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string transformedBar = cache.Get("bar");
Assert.AreSame(transformedBar, cache.Get("bar"));
string transformedFoo = cache.Get("foo".AsSpan());
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(cache.Get("baz"), cache.Get("baz".AsSpan()));
Assert.AreSame(transformedBar, cache.Get("bar"));
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(transformedBar, cache.Get("bar".AsSpan()));
Assert.AreSame(transformedFoo, cache.Get("foo".AsSpan()));
}
[Test]
public void DoesNotCacheEmptyInputs()
{
var cache = new TransformedStringCache(static s => new string('a', 4));
string cached = cache.Get("");
string cached2 = cache.Get("");
string cached3 = cache.Get(ReadOnlySpan<char>.Empty);
Assert.AreEqual("aaaa", cached);
Assert.AreEqual(cached, cached2);
Assert.AreEqual(cached, cached3);
Assert.AreNotSame(cached, cached2);
Assert.AreNotSame(cached, cached3);
Assert.AreNotSame(cached2, cached3);
}
[Test]
[TestCase(TransformedStringCache.InputLengthLimit, true)]
[TestCase(TransformedStringCache.InputLengthLimit + 1, false)]
public void DoesNotCacheLongInputs(int length, bool shouldBeCached)
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string input = new string('a', length);
string cached = cache.Get(input);
string cached2 = cache.Get(input);
Assert.AreEqual("callback-" + input, cached);
Assert.AreEqual(cached, cached2);
if (shouldBeCached)
{
Assert.AreSame(cached, cached2);
}
else
{
Assert.AreNotSame(cached, cached2);
}
}
[Test]
public void CachesAtMostNEntriesPerCharacter()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
int limit = TransformedStringCache.MaxEntriesPerCharacter;
string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray();
string[] cachedAs = a.Select(a => cache.Get(a)).ToArray();
for (int i = 0; i < limit; i++)
{
Assert.AreSame(cachedAs[i], cache.Get(a[i]));
}
Assert.AreNotSame(cachedAs[limit], cache.Get(a[limit]));
Assert.AreSame(cache.Get("b1"), cache.Get("b1"));
}
}
[Test]
public void CachesAtMostNEntriesPerCharacter()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
int limit = TransformedStringCache.MaxEntriesPerCharacter;
string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray();
string[] cachedAs = a.Select(a => cache.Get(a)).ToArray();
for (int i = 0; i < limit; i++)
{
Assert.AreSame(cachedAs[i], cache.Get(a[i]));
}
Assert.AreNotSame(cachedAs[limit], cache.Get(a[limit]));
Assert.AreSame(cache.Get("b1"), cache.Get("b1"));
}
}

View File

@@ -0,0 +1,113 @@
using Markdig.Extensions.Yaml;
using Markdig.Renderers;
using Markdig.Syntax;
namespace Markdig.Tests;
public class TestYamlFrontMatterExtension
{
[TestCaseSource(nameof(TestCases))]
public void ProperYamlFrontMatterRenderersAdded(IMarkdownObjectRenderer[] objectRenderers, bool hasYamlFrontMatterHtmlRenderer, bool hasYamlFrontMatterRoundtripRenderer)
{
var builder = new MarkdownPipelineBuilder();
builder.Extensions.Add(new YamlFrontMatterExtension());
var markdownRenderer = new DummyRenderer();
markdownRenderer.ObjectRenderers.AddRange(objectRenderers);
builder.Build().Setup(markdownRenderer);
Assert.That(markdownRenderer.ObjectRenderers.Contains<YamlFrontMatterHtmlRenderer>(), Is.EqualTo(hasYamlFrontMatterHtmlRenderer));
Assert.That(markdownRenderer.ObjectRenderers.Contains<YamlFrontMatterRoundtripRenderer>(), Is.EqualTo(hasYamlFrontMatterRoundtripRenderer));
}
[Test]
public void AllowYamlFrontMatterInMiddleOfDocument()
{
var pipeline = new MarkdownPipelineBuilder()
.Use(new YamlFrontMatterExtension { AllowInMiddleOfDocument = true })
.Build();
TestParser.TestSpec(
"This is a text1\n---\nthis: is a frontmatter\n---\nThis is a text2",
"<p>This is a text1</p>\n<p>This is a text2</p>",
pipeline);
}
private static IEnumerable<TestCaseData> TestCases()
{
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
}, false, false) {TestName = "No ObjectRenderers"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer()
}, true, false) {TestName = "Html CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Roundtrip.CodeBlockRenderer()
}, false, true) {TestName = "Roundtrip CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer()
}, true, true) {TestName = "Html/Roundtrip CodeBlock"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer(),
new YamlFrontMatterHtmlRenderer()
}, true, true) {TestName = "Html/Roundtrip CodeBlock, Yaml Html"};
yield return new TestCaseData(new IMarkdownObjectRenderer[]
{
new Markdig.Renderers.Html.CodeBlockRenderer(),
new Markdig.Renderers.Roundtrip.CodeBlockRenderer(),
new YamlFrontMatterRoundtripRenderer()
}, true, true) { TestName = "Html/Roundtrip CodeBlock, Yaml Roundtrip" };
}
private class DummyRenderer : IMarkdownRenderer
{
public DummyRenderer()
{
ObjectRenderers = new ObjectRendererCollection();
}
#pragma warning disable CS0067 // ObjectWriteBefore/ObjectWriteAfter is never used
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
#pragma warning restore CS0067
public ObjectRendererCollection ObjectRenderers { get; }
public object Render(MarkdownObject markdownObject)
{
return null;
}
}
[TestCase("---\nkey1: value1\nkey2: value2\n---\n\n# Content\n")]
[TestCase("---\nkey1: value1\nkey2: value2\nkey3: value3\nkey4: value4\nkey5: value5\nkey6: value6\nkey7: value7\nkey8: value8\n---\n\n# Content\n")]
public void FrontMatterBlockLinesCharIterator(string value)
{
var builder = new MarkdownPipelineBuilder();
builder.Extensions.Add(new YamlFrontMatterExtension());
var markdownDocument = Markdown.Parse(value, builder.Build());
var yamlBlocks = markdownDocument.Descendants<YamlFrontMatterBlock>();
Assert.True(yamlBlocks.Any());
foreach (var yamlBlock in yamlBlocks)
{
var iterator = yamlBlock.Lines.ToCharIterator();
while(iterator.CurrentChar != '\0')
{
iterator.NextChar();
}
}
Assert.Pass("No exception parsing and iterating through YAML front matter block lines");
}
}

View File

@@ -1,119 +1,115 @@
// 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;
using System.Globalization;
using System.IO;
using NUnit.Framework;
namespace Markdig.Tests
namespace Markdig.Tests;
/// <summary>
/// Pretty text assert from https://gist.github.com/Haacked/1610603
/// Modified version to only print +-10 characters around the first diff
/// </summary>
public static class TextAssert
{
/// <summary>
/// Pretty text assert from https://gist.github.com/Haacked/1610603
/// Modified version to only print +-10 characters around the first diff
/// </summary>
public static class TextAssert
public enum DiffStyle
{
public enum DiffStyle
Full,
Minimal
}
public static void AreEqual(string expectedValue, string actualValue)
{
AreEqual(expectedValue, actualValue, DiffStyle.Full, Console.Out);
}
public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle)
{
AreEqual(expectedValue, actualValue, diffStyle, Console.Out);
}
public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle, TextWriter output)
{
if (actualValue == null || expectedValue == null)
{
Full,
Minimal
}
public static void AreEqual(string expectedValue, string actualValue)
{
AreEqual(expectedValue, actualValue, DiffStyle.Full, Console.Out);
}
public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle)
{
AreEqual(expectedValue, actualValue, diffStyle, Console.Out);
}
public static void AreEqual(string expectedValue, string actualValue, DiffStyle diffStyle, TextWriter output)
{
if (actualValue == null || expectedValue == null)
{
Assert.AreEqual(expectedValue, actualValue);
return;
}
if (actualValue.Equals(expectedValue, StringComparison.Ordinal))
{
return;
}
Console.WriteLine();
output.WriteLine("Index Expected Actual");
output.WriteLine("----------------------------");
int maxLen = Math.Max(actualValue.Length, expectedValue.Length);
int minLen = Math.Min(actualValue.Length, expectedValue.Length);
if (diffStyle != DiffStyle.Minimal)
{
int startDifferAt = 0;
for (int i = 0; i < maxLen; i++)
{
if (i >= minLen || actualValue[i] != expectedValue[i])
{
startDifferAt = i;
break;
}
}
var endDifferAt = Math.Min(startDifferAt + 10, maxLen);
startDifferAt = Math.Max(startDifferAt - 10, 0);
bool isFirstDiff = true;
for (int i = startDifferAt; i < endDifferAt; i++)
{
if (i >= minLen || actualValue[i] != expectedValue[i])
{
output.WriteLine("{0,-3} {1,-3} {2,-4} {3,-3} {4,-4} {5,-3}",
i < minLen && actualValue[i] == expectedValue[i] ? " " : isFirstDiff ? ">>>": "***",
// put a mark beside a differing row
i, // the index
i < expectedValue.Length ? ((int) expectedValue[i]).ToString() : "",
// character decimal value
i < expectedValue.Length ? expectedValue[i].ToSafeString() : "", // character safe string
i < actualValue.Length ? ((int) actualValue[i]).ToString() : "", // character decimal value
i < actualValue.Length ? actualValue[i].ToSafeString() : "" // character safe string
);
isFirstDiff = false;
}
}
//output.WriteLine();
}
Assert.AreEqual(expectedValue, actualValue);
return;
}
private static string ToSafeString(this char c)
if (actualValue.Equals(expectedValue, StringComparison.Ordinal))
{
if (char.IsControl(c) || char.IsWhiteSpace(c))
return;
}
Console.WriteLine();
output.WriteLine("Index Expected Actual");
output.WriteLine("----------------------------");
int maxLen = Math.Max(actualValue.Length, expectedValue.Length);
int minLen = Math.Min(actualValue.Length, expectedValue.Length);
if (diffStyle != DiffStyle.Minimal)
{
int startDifferAt = 0;
for (int i = 0; i < maxLen; i++)
{
switch (c)
if (i >= minLen || actualValue[i] != expectedValue[i])
{
case '\b':
return @"\b";
case '\r':
return @"\r";
case '\n':
return @"\n";
case '\t':
return @"\t";
case '\a':
return @"\a";
case '\v':
return @"\v";
case '\f':
return @"\f";
default:
return $"\\u{(int) c:X};";
startDifferAt = i;
break;
}
}
return c.ToString(CultureInfo.InvariantCulture);
var endDifferAt = Math.Min(startDifferAt + 10, maxLen);
startDifferAt = Math.Max(startDifferAt - 10, 0);
bool isFirstDiff = true;
for (int i = startDifferAt; i < endDifferAt; i++)
{
if (i >= minLen || actualValue[i] != expectedValue[i])
{
output.WriteLine("{0,-3} {1,-3} {2,-4} {3,-3} {4,-4} {5,-3}",
i < minLen && actualValue[i] == expectedValue[i] ? " " : isFirstDiff ? ">>>": "***",
// put a mark beside a differing row
i, // the index
i < expectedValue.Length ? ((int) expectedValue[i]).ToString() : "",
// character decimal value
i < expectedValue.Length ? expectedValue[i].ToSafeString() : "", // character safe string
i < actualValue.Length ? ((int) actualValue[i]).ToString() : "", // character decimal value
i < actualValue.Length ? actualValue[i].ToSafeString() : "" // character safe string
);
isFirstDiff = false;
}
}
//output.WriteLine();
}
Assert.AreEqual(expectedValue, actualValue);
}
private static string ToSafeString(this char c)
{
if (char.IsControl(c) || char.IsWhiteSpace(c))
{
switch (c)
{
case '\b':
return @"\b";
case '\r':
return @"\r";
case '\n':
return @"\n";
case '\t':
return @"\t";
case '\a':
return @"\a";
case '\v':
return @"\v";
case '\f':
return @"\f";
default:
return $"\\u{(int) c:X};";
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
}

View File

@@ -1,55 +1,54 @@
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
namespace Markdig.WebApp
namespace Markdig.WebApp;
public class ApiController : Controller
{
public class ApiController : Controller
[HttpGet()]
[Route("")]
public string Empty()
{
[HttpGet()]
[Route("")]
public string Empty()
return string.Empty;
}
// GET api/to_html?text=xxx&extension=advanced
[Route("api/to_html")]
[HttpGet()]
public object Get([FromQuery] string text, [FromQuery] string extension)
{
try
{
return string.Empty;
if (text == null)
{
text = string.Empty;
}
if (text.Length > 1000)
{
text = text.Substring(0, 1000);
}
var pipeline = new MarkdownPipelineBuilder().Configure(extension).Build();
var result = Markdown.ToHtml(text, pipeline);
return new {name = "markdig", html = result, version = Markdown.Version};
}
// GET api/to_html?text=xxx&extension=advanced
[Route("api/to_html")]
[HttpGet()]
public object Get([FromQuery] string text, [FromQuery] string extension)
catch (Exception ex)
{
try
{
if (text == null)
{
text = string.Empty;
}
if (text.Length > 1000)
{
text = text.Substring(0, 1000);
}
var pipeline = new MarkdownPipelineBuilder().Configure(extension).Build();
var result = Markdown.ToHtml(text, pipeline);
return new {name = "markdig", html = result, version = Markdown.Version};
}
catch (Exception ex)
{
return new { name = "markdig", html = "exception: " + GetPrettyMessageFromException(ex), version = Markdown.Version };
}
}
private static string GetPrettyMessageFromException(Exception exception)
{
var builder = new StringBuilder();
while (exception != null)
{
builder.Append(exception.Message);
exception = exception.InnerException;
}
return builder.ToString();
return new { name = "markdig", html = "exception: " + GetPrettyMessageFromException(ex), version = Markdown.Version };
}
}
private static string GetPrettyMessageFromException(Exception exception)
{
var builder = new StringBuilder();
while (exception != null)
{
builder.Append(exception.Message);
exception = exception.InnerException;
}
return builder.ToString();
}
}

View File

@@ -1,9 +1,10 @@
<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>
<OutputType>Exe</OutputType>
<PackageId>Markdig.WebApp</PackageId>
<ApplicationInsightsResourceId>/subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/Markdig.WebApp</ApplicationInsightsResourceId>
@@ -13,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,22 +1,16 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
namespace Markdig.WebApp;
namespace Markdig.WebApp
public class Program
{
public class Program
public static void Main(string[] args)
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

View File

@@ -1,60 +1,52 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Markdig.WebApp;
namespace Markdig.WebApp
public class Startup
{
public class Startup
public Startup(IWebHostEnvironment env)
{
public Startup(IWebHostEnvironment env)
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
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.AddEnvironmentVariables();
Configuration = builder.Build();
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
public IConfigurationRoot Configuration { get; }
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
services.AddControllers();
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
app.UseDeveloperExceptionPage();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
endpoints.MapControllers();
});
}
}

View File

@@ -7,36 +7,35 @@ using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
namespace Markdig.Extensions.Abbreviations
namespace Markdig.Extensions.Abbreviations;
/// <summary>
/// An abbreviation object stored at the document level. See extension methods in <see cref="AbbreviationHelper"/>.
/// </summary>
/// <seealso cref="LeafBlock" />
[DebuggerDisplay("Abbr {Label} => {Text}")]
public class Abbreviation : LeafBlock
{
/// <summary>
/// An abbreviation object stored at the document level. See extension methods in <see cref="AbbreviationHelper"/>.
/// Initializes a new instance of the <see cref="Abbreviation"/> class.
/// </summary>
/// <seealso cref="LeafBlock" />
[DebuggerDisplay("Abbr {Label} => {Text}")]
public class Abbreviation : LeafBlock
/// <param name="parser">The parser used to create this block.</param>
public Abbreviation(BlockParser parser) : base(parser)
{
/// <summary>
/// Initializes a new instance of the <see cref="Abbreviation"/> class.
/// </summary>
/// <param name="parser">The parser used to create this block.</param>
public Abbreviation(BlockParser parser) : base(parser)
{
}
/// <summary>
/// Gets or sets the label.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// The text associated to this label.
/// </summary>
public StringSlice Text;
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
}
/// <summary>
/// Gets or sets the label.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// The text associated to this label.
/// </summary>
public StringSlice Text;
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
}

View File

@@ -4,26 +4,25 @@
using Markdig.Renderers;
namespace Markdig.Extensions.Abbreviations
{
/// <summary>
/// Extension to allow abbreviations.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class AbbreviationExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.BlockParsers.AddIfNotAlready<AbbreviationParser>();
}
namespace Markdig.Extensions.Abbreviations;
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
/// <summary>
/// Extension to allow abbreviations.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class AbbreviationExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.BlockParsers.AddIfNotAlready<AbbreviationParser>();
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains<HtmlAbbreviationRenderer>())
{
if (renderer is HtmlRenderer htmlRenderer && !htmlRenderer.ObjectRenderers.Contains<HtmlAbbreviationRenderer>())
{
// Must be inserted before CodeBlockRenderer
htmlRenderer.ObjectRenderers.Insert(0, new HtmlAbbreviationRenderer());
}
// Must be inserted before CodeBlockRenderer
htmlRenderer.ObjectRenderers.Insert(0, new HtmlAbbreviationRenderer());
}
}
}

View File

@@ -2,42 +2,40 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Syntax;
namespace Markdig.Extensions.Abbreviations
namespace Markdig.Extensions.Abbreviations;
/// <summary>
/// Extension methods for <see cref="Abbreviation"/>.
/// </summary>
public static class AbbreviationHelper
{
/// <summary>
/// Extension methods for <see cref="Abbreviation"/>.
/// </summary>
public static class AbbreviationHelper
private static readonly object DocumentKey = typeof (Abbreviation);
public static bool HasAbbreviations(this MarkdownDocument document)
{
private static readonly object DocumentKey = typeof (Abbreviation);
return document.GetAbbreviations() != null;
}
public static bool HasAbbreviations(this MarkdownDocument document)
public static void AddAbbreviation(this MarkdownDocument document, string label, Abbreviation abbr)
{
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
if (label is null) ThrowHelper.ArgumentNullException_label();
if (abbr is null) ThrowHelper.ArgumentNullException(nameof(abbr));
var map = document.GetAbbreviations();
if (map is null)
{
return document.GetAbbreviations() != null;
map = new Dictionary<string, Abbreviation>();
document.SetData(DocumentKey, map);
}
map[label] = abbr;
}
public static void AddAbbreviation(this MarkdownDocument document, string label, Abbreviation abbr)
{
if (document is null) ThrowHelper.ArgumentNullException(nameof(document));
if (label is null) ThrowHelper.ArgumentNullException_label();
if (abbr is null) ThrowHelper.ArgumentNullException(nameof(abbr));
var map = document.GetAbbreviations();
if (map is null)
{
map = new Dictionary<string, Abbreviation>();
document.SetData(DocumentKey, map);
}
map[label] = abbr;
}
public static Dictionary<string, Abbreviation>? GetAbbreviations(this MarkdownDocument document)
{
return document.GetData(DocumentKey) as Dictionary<string, Abbreviation>;
}
public static Dictionary<string, Abbreviation>? GetAbbreviations(this MarkdownDocument document)
{
return document.GetData(DocumentKey) as Dictionary<string, Abbreviation>;
}
}

View File

@@ -5,24 +5,23 @@
using System.Diagnostics;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.Abbreviations
namespace Markdig.Extensions.Abbreviations;
/// <summary>
/// The inline abbreviation.
/// </summary>
/// <seealso cref="LeafInline" />
[DebuggerDisplay("{Abbreviation}")]
public class AbbreviationInline : LeafInline
{
/// <summary>
/// The inline abbreviation.
/// Initializes a new instance of the <see cref="AbbreviationInline"/> class.
/// </summary>
/// <seealso cref="LeafInline" />
[DebuggerDisplay("{Abbreviation}")]
public class AbbreviationInline : LeafInline
/// <param name="abbreviation">The abbreviation.</param>
public AbbreviationInline(Abbreviation abbreviation)
{
/// <summary>
/// Initializes a new instance of the <see cref="AbbreviationInline"/> class.
/// </summary>
/// <param name="abbreviation">The abbreviation.</param>
public AbbreviationInline(Abbreviation abbreviation)
{
Abbreviation = abbreviation;
}
public Abbreviation Abbreviation { get; set; }
Abbreviation = abbreviation;
}
public Abbreviation Abbreviation { get; set; }
}

View File

@@ -2,225 +2,222 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.Abbreviations
namespace Markdig.Extensions.Abbreviations;
/// <summary>
/// A block parser for abbreviations.
/// </summary>
/// <seealso cref="BlockParser" />
public class AbbreviationParser : BlockParser
{
/// <summary>
/// A block parser for abbreviations.
/// Initializes a new instance of the <see cref="AbbreviationParser"/> class.
/// </summary>
/// <seealso cref="BlockParser" />
public class AbbreviationParser : BlockParser
public AbbreviationParser()
{
/// <summary>
/// Initializes a new instance of the <see cref="AbbreviationParser"/> class.
/// </summary>
public AbbreviationParser()
OpeningCharacters = ['*'];
}
public override BlockState TryOpen(BlockProcessor processor)
{
if (processor.IsCodeIndent)
{
OpeningCharacters = new[] { '*' };
return BlockState.None;
}
public override BlockState TryOpen(BlockProcessor processor)
// A link must be of the form *[Some Text]: An abbreviation
var slice = processor.Line;
var startPosition = slice.Start;
var c = slice.NextChar();
if (c != '[')
{
if (processor.IsCodeIndent)
{
return BlockState.None;
}
// A link must be of the form *[Some Text]: An abbreviation
var slice = processor.Line;
var startPosition = slice.Start;
var c = slice.NextChar();
if (c != '[')
{
return BlockState.None;
}
if (!LinkHelper.TryParseLabel(ref slice, out string? label, out SourceSpan labelSpan))
{
return BlockState.None;
}
c = slice.CurrentChar;
if (c != ':')
{
return BlockState.None;
}
slice.SkipChar();
slice.Trim();
var abbr = new Abbreviation(this)
{
Label = label,
Text = slice,
Span = new SourceSpan(startPosition, slice.End),
Line = processor.LineIndex,
Column = processor.Column,
LabelSpan = labelSpan,
};
if (!processor.Document.HasAbbreviations())
{
processor.Document.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
}
processor.Document.AddAbbreviation(abbr.Label, abbr);
return BlockState.BreakDiscard;
return BlockState.None;
}
private void DocumentOnProcessInlinesBegin(InlineProcessor inlineProcessor, Inline? inline)
if (!LinkHelper.TryParseLabel(ref slice, out string? label, out SourceSpan labelSpan))
{
inlineProcessor.Document.ProcessInlinesBegin -= DocumentOnProcessInlinesBegin;
return BlockState.None;
}
var abbreviations = inlineProcessor.Document.GetAbbreviations();
// Should not happen, but another extension could decide to remove them, so...
if (abbreviations is null)
c = slice.CurrentChar;
if (c != ':')
{
return BlockState.None;
}
slice.SkipChar();
slice.Trim();
var abbr = new Abbreviation(this)
{
Label = label,
Text = slice,
Span = new SourceSpan(startPosition, slice.End),
Line = processor.LineIndex,
Column = processor.Column,
LabelSpan = labelSpan,
};
if (!processor.Document.HasAbbreviations())
{
processor.Document.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
}
processor.Document.AddAbbreviation(abbr.Label, abbr);
return BlockState.BreakDiscard;
}
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)
{
return;
}
// Build a text matcher from the abbreviations labels
var prefixTree = new CompactPrefixTree<Abbreviation>(abbreviations);
inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) =>
{
var literal = (LiteralInline)processor.Inline!;
var originalLiteral = literal;
ContainerInline? container = null;
// This is slow, but we don't have much the choice
var content = literal.Content;
var text = content.Text;
for (int i = content.Start; i <= content.End; i++)
{
return;
}
// Build a text matcher from the abbreviations labels
var prefixTree = new CompactPrefixTree<Abbreviation>(abbreviations);
inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) =>
{
var literal = (LiteralInline)processor.Inline!;
var originalLiteral = literal;
ContainerInline? container = null;
// This is slow, but we don't have much the choice
var content = literal.Content;
var text = content.Text;
for (int i = content.Start; i <= content.End; i++)
// Abbreviation must be a whole word == start at the start of a line or after a whitespace
if (i != 0)
{
// Abbreviation must be a whole word == start at the start of a line or after a whitespace
if (i != 0)
for (i = i - 1; i <= content.End; i++)
{
for (i = i - 1; i <= content.End; i++)
if (text[i].IsWhitespace())
{
if (text[i].IsWhitespace())
{
i++;
goto ValidAbbreviationStart;
}
i++;
goto ValidAbbreviationStart;
}
break;
}
ValidAbbreviationStart:;
if (prefixTree.TryMatchLongest(text.AsSpan(i, content.End - i + 1), out KeyValuePair<string, Abbreviation> abbreviationMatch))
{
var match = abbreviationMatch.Key;
if (!IsValidAbbreviationEnding(match, content, i))
{
continue;
}
var indexAfterMatch = i + match.Length;
// If we don't have a container, create a new one
if (container is null)
{
container = literal.Parent ??
new ContainerInline
{
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
}
var abbrInline = new AbbreviationInline(abbreviationMatch.Value)
{
Span =
{
Start = processor.GetSourcePosition(i, out int line, out int column),
},
Line = line,
Column = column
};
abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1;
// Append the previous literal
if (i > content.Start && literal.Parent is null)
{
container.AppendChild(literal);
}
literal.Span.End = abbrInline.Span.Start - 1;
// Truncate it before the abbreviation
literal.Content.End = i - 1;
// Append the abbreviation
container.AppendChild(abbrInline);
// If this is the end of the string, clear the literal and exit
if (content.End == indexAfterMatch - 1)
{
literal = null;
break;
}
// Process the remaining literal
literal = new LiteralInline()
{
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
Line = line,
Column = column + match.Length,
};
content.Start = indexAfterMatch;
literal.Content = content;
i = indexAfterMatch - 1;
}
break;
}
if (container != null)
ValidAbbreviationStart:;
if (prefixTree.TryMatchLongest(text.AsSpan(i, content.End - i + 1), out KeyValuePair<string, Abbreviation> abbreviationMatch))
{
if (literal != null)
var match = abbreviationMatch.Key;
if (!IsValidAbbreviationEnding(match, content, i))
{
continue;
}
var indexAfterMatch = i + match.Length;
// If we don't have a container, create a new one
if (container is null)
{
container = literal.Parent ??
new ContainerInline
{
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
}
var abbrInline = new AbbreviationInline(abbreviationMatch.Value)
{
Span =
{
Start = processor.GetSourcePosition(i, out int line, out int column),
},
Line = line,
Column = column
};
abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1;
// Append the previous literal
if (i > content.Start && literal.Parent is null)
{
container.AppendChild(literal);
}
processor.Inline = container;
}
};
}
private static bool IsValidAbbreviationEnding(string match, StringSlice content, int matchIndex)
{
// This will check if the next char at the end of the StringSlice is whitespace, punctuation or \0.
var contentNew = content;
contentNew.End = content.End + 1;
int index = matchIndex + match.Length;
while (index <= contentNew.End)
{
var c = contentNew.PeekCharAbsolute(index);
if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation()))
{
return false;
}
literal.Span.End = abbrInline.Span.Start - 1;
// Truncate it before the abbreviation
literal.Content.End = i - 1;
if (c.IsAlphaNumeric())
{
return false;
}
if (c.IsWhitespace())
{
break;
// Append the abbreviation
container.AppendChild(abbrInline);
// If this is the end of the string, clear the literal and exit
if (content.End == indexAfterMatch - 1)
{
literal = null;
break;
}
// Process the remaining literal
literal = new LiteralInline()
{
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
Line = line,
Column = column + match.Length,
};
content.Start = indexAfterMatch;
literal.Content = content;
i = indexAfterMatch - 1;
}
index++;
}
return true;
if (container != null)
{
if (literal != null)
{
container.AppendChild(literal);
}
processor.Inline = container;
}
};
}
private static bool IsValidAbbreviationEnding(string match, StringSlice content, int matchIndex)
{
// This will check if the next char at the end of the StringSlice is whitespace, punctuation or \0.
var contentNew = content;
contentNew.End = content.End + 1;
int index = matchIndex + match.Length;
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;
}
index++;
}
return true;
}
}

View File

@@ -5,27 +5,26 @@
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Abbreviations
namespace Markdig.Extensions.Abbreviations;
/// <summary>
/// A HTML renderer for a <see cref="AbbreviationInline"/>.
/// </summary>
/// <seealso cref="HtmlObjectRenderer{CustomContainer}" />
public class HtmlAbbreviationRenderer : HtmlObjectRenderer<AbbreviationInline>
{
/// <summary>
/// A HTML renderer for a <see cref="AbbreviationInline"/>.
/// </summary>
/// <seealso cref="HtmlObjectRenderer{CustomContainer}" />
public class HtmlAbbreviationRenderer : HtmlObjectRenderer<AbbreviationInline>
protected override void Write(HtmlRenderer renderer, AbbreviationInline obj)
{
protected override void Write(HtmlRenderer renderer, AbbreviationInline obj)
// <abbr title="Hyper Text Markup Language">HTML</abbr>
var abbr = obj.Abbreviation;
if (renderer.EnableHtmlForInline)
{
// <abbr title="Hyper Text Markup Language">HTML</abbr>
var abbr = obj.Abbreviation;
if (renderer.EnableHtmlForInline)
{
renderer.Write("<abbr").WriteAttributes(obj).Write(" title=\"").WriteEscape(ref abbr.Text).Write("\">");
}
renderer.Write(abbr.Label);
if (renderer.EnableHtmlForInline)
{
renderer.Write("</abbr>");
}
renderer.Write("<abbr").WriteAttributes(obj).Write(" title=\"").WriteEscape(ref abbr.Text).Write("\">");
}
renderer.Write(abbr.Label);
if (renderer.EnableHtmlForInline)
{
renderer.Write("</abbr>");
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Syntax;
namespace Markdig.Extensions.Alerts;
/// <summary>
/// A block representing an alert quote block.
/// </summary>
public class AlertBlock : QuoteBlock
{
/// <summary>
/// Creates a new instance of this block.
/// </summary>
/// <param name="kind"></param>
public AlertBlock(StringSlice kind) : base(null)
{
Kind = kind;
}
/// <summary>
/// Gets or sets the kind of the alert block (e.g `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`)
/// </summary>
public StringSlice Kind { get; set; }
/// <summary>
/// Gets or sets the trivia space after the kind.
/// </summary>
public StringSlice TriviaSpaceAfterKind { get; set; }
}

View File

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

View File

@@ -0,0 +1,44 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Alerts;
/// <summary>
/// Extension for adding alerts to a Markdown pipeline.
/// </summary>
public class AlertExtension : IMarkdownExtension
{
/// <summary>
/// Gets or sets the delegate to render the kind of the alert.
/// </summary>
public Action<HtmlRenderer, StringSlice>? RenderKind { get; set; }
/// <inheritdoc />
public void Setup(MarkdownPipelineBuilder pipeline)
{
var inlineParser = pipeline.InlineParsers.Find<AlertInlineParser>();
if (inlineParser == null)
{
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new AlertInlineParser());
}
}
/// <inheritdoc />
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
var blockRenderer = renderer.ObjectRenderers.FindExact<AlertBlockRenderer>();
if (blockRenderer == null)
{
renderer.ObjectRenderers.InsertBefore<QuoteBlockRenderer>(new AlertBlockRenderer()
{
RenderKind = RenderKind ?? AlertBlockRenderer.DefaultRenderKind
});
}
}
}

View File

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

View File

@@ -2,8 +2,6 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers;
@@ -11,206 +9,210 @@ using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.AutoIdentifiers
namespace Markdig.Extensions.AutoIdentifiers;
/// <summary>
/// The auto-identifier extension
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class AutoIdentifierExtension : IMarkdownExtension
{
private const string AutoIdentifierKey = "AutoIdentifier";
private static readonly StripRendererCache _rendererCache = new();
private readonly AutoIdentifierOptions _options;
/// <summary>
/// The auto-identifier extension
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class AutoIdentifierExtension : IMarkdownExtension
/// <param name="options">The options.</param>
public AutoIdentifierExtension(AutoIdentifierOptions options)
{
private const string AutoIdentifierKey = "AutoIdentifier";
private readonly AutoIdentifierOptions options;
private readonly StripRendererCache rendererCache = new StripRendererCache();
_options = options;
}
/// <summary>
/// Initializes a new instance of the <see cref="AutoIdentifierExtension"/> class.
/// </summary>
/// <param name="options">The options.</param>
public AutoIdentifierExtension(AutoIdentifierOptions options)
public void Setup(MarkdownPipelineBuilder pipeline)
{
var headingBlockParser = pipeline.BlockParsers.Find<HeadingBlockParser>();
if (headingBlockParser != null)
{
this.options = options;
// Install a hook on the HeadingBlockParser when a HeadingBlock is actually processed
headingBlockParser.Closed -= HeadingBlockParser_Closed;
headingBlockParser.Closed += HeadingBlockParser_Closed;
}
var paragraphBlockParser = pipeline.BlockParsers.FindExact<ParagraphBlockParser>();
if (paragraphBlockParser != null)
{
// Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading
paragraphBlockParser.Closed -= HeadingBlockParser_Closed;
paragraphBlockParser.Closed += HeadingBlockParser_Closed;
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}
/// <summary>
/// Process on a new <see cref="HeadingBlock"/>
/// </summary>
/// <param name="processor">The processor.</param>
/// <param name="block">The heading block.</param>
private void HeadingBlockParser_Closed(BlockProcessor processor, Block block)
{
// We may have a ParagraphBlock here as we have a hook on the ParagraphBlockParser
if (!(block is HeadingBlock headingBlock))
{
return;
}
public void Setup(MarkdownPipelineBuilder pipeline)
// If the AutoLink options is set, we register a LinkReferenceDefinition at the document level
if ((_options & AutoIdentifierOptions.AutoLink) != 0)
{
var headingBlockParser = pipeline.BlockParsers.Find<HeadingBlockParser>();
if (headingBlockParser != null)
var headingLine = headingBlock.Lines.Lines[0];
var text = headingLine.ToString();
var linkRef = new HeadingLinkReferenceDefinition(headingBlock)
{
// Install a hook on the HeadingBlockParser when a HeadingBlock is actually processed
headingBlockParser.Closed -= HeadingBlockParser_Closed;
headingBlockParser.Closed += HeadingBlockParser_Closed;
}
var paragraphBlockParser = pipeline.BlockParsers.FindExact<ParagraphBlockParser>();
if (paragraphBlockParser != null)
{
// Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading
paragraphBlockParser.Closed -= HeadingBlockParser_Closed;
paragraphBlockParser.Closed += HeadingBlockParser_Closed;
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}
/// <summary>
/// Process on a new <see cref="HeadingBlock"/>
/// </summary>
/// <param name="processor">The processor.</param>
/// <param name="block">The heading block.</param>
private void HeadingBlockParser_Closed(BlockProcessor processor, Block block)
{
// We may have a ParagraphBlock here as we have a hook on the ParagraphBlockParser
if (!(block is HeadingBlock headingBlock))
{
return;
}
// If the AutoLink options is set, we register a LinkReferenceDefinition at the document level
if ((options & AutoIdentifierOptions.AutoLink) != 0)
{
var headingLine = headingBlock.Lines.Lines[0];
var text = headingLine.ToString();
var linkRef = new HeadingLinkReferenceDefinition(headingBlock)
{
CreateLinkInline = CreateLinkInlineForHeading
};
var doc = processor.Document;
var dictionary = doc.GetData(this) as Dictionary<string, HeadingLinkReferenceDefinition>;
if (dictionary is null)
{
dictionary = new Dictionary<string, HeadingLinkReferenceDefinition>();
doc.SetData(this, dictionary);
doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
}
dictionary[text] = linkRef;
}
// Then we register after inline have been processed to actually generate the proper #id
headingBlock.ProcessInlinesEnd += HeadingBlock_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)
{
// Here we make sure that auto-identifiers will not override an existing link definition
// defined in the document
// If it is the case, we skip the auto identifier for the Heading
if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef))
{
doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true);
}
}
// Once we are done, we don't need to keep the intermediate dictionary around
doc.RemoveData(this);
}
/// <summary>
/// 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)
{
var headingRef = (HeadingLinkReferenceDefinition) linkRef;
return new LinkInline()
{
// Use GetDynamicUrl to allow late binding of the Url (as a link may occur before the heading is declared and
// the inlines of the heading are actually processed by HeadingBlock_ProcessInlinesEnd)
GetDynamicUrl = () => HtmlHelper.Unescape("#" + headingRef.Heading.GetAttributes().Id),
Title = HtmlHelper.Unescape(linkRef.Title),
CreateLinkInline = CreateLinkInlineForHeading
};
var doc = processor.Document;
var dictionary = doc.GetData(this) as Dictionary<string, HeadingLinkReferenceDefinition>;
if (dictionary is null)
{
dictionary = new Dictionary<string, HeadingLinkReferenceDefinition>();
doc.SetData(this, dictionary);
doc.ProcessInlinesBegin += DocumentOnProcessInlinesBegin;
}
dictionary[text] = linkRef;
}
/// <summary>
/// Process the inlines of the heading to create a unique identifier
/// </summary>
/// <param name="processor">The processor.</param>
/// <param name="inline">The inline.</param>
private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? inline)
// Then we register after inline have been processed to actually generate the proper #id
headingBlock.ProcessInlinesEnd += HeadingBlock_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)
{
var identifiers = processor.Document.GetData(AutoIdentifierKey) as HashSet<string>;
if (identifiers is null)
// Here we make sure that auto-identifiers will not override an existing link definition
// defined in the document
// If it is the case, we skip the auto identifier for the Heading
if (!doc.TryGetLinkReferenceDefinition(keyPair.Key, out var linkDef))
{
identifiers = new HashSet<string>();
processor.Document.SetData(AutoIdentifierKey, identifiers);
doc.SetLinkReferenceDefinition(keyPair.Key, keyPair.Value, true);
}
}
// Once we are done, we don't need to keep the intermediate dictionary around
doc.RemoveData(this);
}
var headingBlock = (HeadingBlock) processor.Block!;
if (headingBlock.Inline is null)
{
return;
}
/// <summary>
/// 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)
{
var headingRef = (HeadingLinkReferenceDefinition) linkRef;
return new LinkInline()
{
// Use GetDynamicUrl to allow late binding of the Url (as a link may occur before the heading is declared and
// the inlines of the heading are actually processed by HeadingBlock_ProcessInlinesEnd)
GetDynamicUrl = () => HtmlHelper.Unescape("#" + headingRef.Heading.GetAttributes().Id),
Title = HtmlHelper.Unescape(linkRef.Title),
};
}
// If id is already set, don't try to modify it
var attributes = processor.Block!.GetAttributes();
if (attributes.Id != null)
{
return;
}
// Use internally a HtmlRenderer to strip links from a heading
var stripRenderer = rendererCache.Get();
stripRenderer.Render(headingBlock.Inline);
var headingText = stripRenderer.Writer.ToString()!;
rendererCache.Release(stripRenderer);
// Urilize the link
headingText = (options & AutoIdentifierOptions.GitHub) != 0
? LinkHelper.UrilizeAsGfm(headingText)
: LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
// If the heading is empty, use the word "section" instead
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
// Add a trailing -1, -2, -3...etc. in case of collision
var headingId = baseHeadingId;
if (!identifiers.Add(headingId))
{
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
headingBuffer.Append(baseHeadingId);
headingBuffer.Append('-');
uint index = 0;
do
{
index++;
headingBuffer.Append(index);
headingId = headingBuffer.AsSpan().ToString();
headingBuffer.Length = baseHeadingId.Length + 1;
}
while (!identifiers.Add(headingId));
headingBuffer.Dispose();
}
attributes.Id = headingId;
/// <summary>
/// Process the inlines of the heading to create a unique identifier
/// </summary>
/// <param name="processor">The processor.</param>
/// <param name="inline">The inline.</param>
private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? inline)
{
var identifiers = processor.Document.GetData(AutoIdentifierKey) as HashSet<string>;
if (identifiers is null)
{
identifiers = new HashSet<string>();
processor.Document.SetData(AutoIdentifierKey, identifiers);
}
private sealed class StripRendererCache : ObjectCache<HtmlRenderer>
var headingBlock = (HeadingBlock) processor.Block!;
if (headingBlock.Inline is null)
{
protected override HtmlRenderer NewInstance()
{
var headingWriter = new StringWriter();
var stripRenderer = new HtmlRenderer(headingWriter)
{
// Set to false both to avoid having any HTML tags in the output
EnableHtmlForInline = false,
EnableHtmlEscape = false
};
return stripRenderer;
}
return;
}
protected override void Reset(HtmlRenderer instance)
// If id is already set, don't try to modify it
var attributes = processor.Block!.GetAttributes();
if (attributes.Id != null)
{
return;
}
// Use internally a HtmlRenderer to strip links from a heading
var stripRenderer = _rendererCache.Get();
stripRenderer.Render(headingBlock.Inline);
ReadOnlySpan<char> rawHeadingText = ((FastStringWriter)stripRenderer.Writer).AsSpan();
// Urilize the link
string headingText = (_options & AutoIdentifierOptions.GitHub) != 0
? LinkHelper.UrilizeAsGfm(rawHeadingText)
: LinkHelper.Urilize(rawHeadingText, (_options & AutoIdentifierOptions.AllowOnlyAscii) != 0);
_rendererCache.Release(stripRenderer);
// If the heading is empty, use the word "section" instead
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
// Add a trailing -1, -2, -3...etc. in case of collision
var headingId = baseHeadingId;
if (!identifiers.Add(headingId))
{
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
headingBuffer.Append(baseHeadingId);
headingBuffer.Append('-');
uint index = 0;
do
{
instance.Reset();
index++;
headingBuffer.Append(index);
headingId = headingBuffer.AsSpan().ToString();
headingBuffer.Length = baseHeadingId.Length + 1;
}
while (!identifiers.Add(headingId));
headingBuffer.Dispose();
}
attributes.Id = headingId;
}
private sealed class StripRendererCache : ObjectCache<HtmlRenderer>
{
protected override HtmlRenderer NewInstance()
{
var headingWriter = new FastStringWriter();
var stripRenderer = new HtmlRenderer(headingWriter)
{
// Set to false both to avoid having any HTML tags in the output
EnableHtmlForInline = false,
EnableHtmlEscape = false
};
return stripRenderer;
}
protected override void Reset(HtmlRenderer instance)
{
instance.ResetInternal();
((FastStringWriter)instance.Writer).Reset();
}
}
}

View File

@@ -2,39 +2,36 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
namespace Markdig.Extensions.AutoIdentifiers;
namespace Markdig.Extensions.AutoIdentifiers
/// <summary>
/// Options for the <see cref="AutoIdentifierExtension"/>.
/// </summary>
[Flags]
public enum AutoIdentifierOptions
{
/// <summary>
/// Options for the <see cref="AutoIdentifierExtension"/>.
/// No options: does not apply any additional formatting and/or transformations.
/// </summary>
[Flags]
public enum AutoIdentifierOptions
{
/// <summary>
/// No options: does not apply any additional formatting and/or transformations.
/// </summary>
None = 0,
None = 0,
/// <summary>
/// Default (<see cref="AutoLink"/>)
/// </summary>
Default = AutoLink | AllowOnlyAscii,
/// <summary>
/// Default (<see cref="AutoLink"/>)
/// </summary>
Default = AutoLink | AllowOnlyAscii,
/// <summary>
/// Allows to link to a header by using the same text as the header for the link label. Default is <c>true</c>
/// </summary>
AutoLink = 1,
/// <summary>
/// Allows to link to a header by using the same text as the header for the link label. Default is <c>true</c>
/// </summary>
AutoLink = 1,
/// <summary>
/// Allows only ASCII characters in the url (HTML 5 allows to have UTF8 characters). Default is <c>true</c>
/// </summary>
AllowOnlyAscii = 2,
/// <summary>
/// Allows only ASCII characters in the url (HTML 5 allows to have UTF8 characters). Default is <c>true</c>
/// </summary>
AllowOnlyAscii = 2,
/// <summary>
/// Renders auto identifiers like GitHub.
/// </summary>
GitHub = 4,
}
/// <summary>
/// Renders auto identifiers like GitHub.
/// </summary>
GitHub = 4,
}

View File

@@ -4,22 +4,21 @@
using Markdig.Syntax;
namespace Markdig.Extensions.AutoIdentifiers
{
/// <summary>
/// A link reference definition to a <see cref="HeadingBlock"/> stored at the <see cref="MarkdownDocument"/> level.
/// </summary>
/// <seealso cref="LinkReferenceDefinition" />
public class HeadingLinkReferenceDefinition : LinkReferenceDefinition
{
public HeadingLinkReferenceDefinition(HeadingBlock headling)
{
Heading = headling;
}
namespace Markdig.Extensions.AutoIdentifiers;
/// <summary>
/// Gets or sets the heading related to this link reference definition.
/// </summary>
public HeadingBlock Heading { get; set; }
/// <summary>
/// A link reference definition to a <see cref="HeadingBlock"/> stored at the <see cref="MarkdownDocument"/> level.
/// </summary>
/// <seealso cref="LinkReferenceDefinition" />
public class HeadingLinkReferenceDefinition : LinkReferenceDefinition
{
public HeadingLinkReferenceDefinition(HeadingBlock headling)
{
Heading = headling;
}
/// <summary>
/// Gets or sets the heading related to this link reference definition.
/// </summary>
public HeadingBlock Heading { get; set; }
}

View File

@@ -5,32 +5,26 @@
using Markdig.Renderers;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.AutoLinks
namespace Markdig.Extensions.AutoLinks;
/// <summary>
/// Extension to automatically create <see cref="LinkInline"/> when a link url http: or mailto: is found.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class AutoLinkExtension(AutoLinkOptions? options) : IMarkdownExtension
{
/// <summary>
/// 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 readonly AutoLinkOptions Options = options ?? new AutoLinkOptions();
public void Setup(MarkdownPipelineBuilder pipeline)
{
public readonly AutoLinkOptions Options;
public AutoLinkExtension(AutoLinkOptions? options)
{
Options = options ?? new AutoLinkOptions();
}
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<AutoLinkParser>())
{
// Insert the parser before any other parsers
pipeline.InlineParsers.Insert(0, new AutoLinkParser(Options));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
if (!pipeline.InlineParsers.Contains<AutoLinkParser>())
{
// Insert the parser before any other parsers
pipeline.InlineParsers.Insert(0, new AutoLinkParser(Options));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}
}

View File

@@ -2,25 +2,24 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
namespace Markdig.Extensions.AutoLinks
namespace Markdig.Extensions.AutoLinks;
public class AutoLinkOptions
{
public class AutoLinkOptions
public AutoLinkOptions()
{
public AutoLinkOptions()
{
ValidPreviousCharacters = "*_~(";
}
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; }
ValidPreviousCharacters = "*_~(";
}
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; }
}

View File

@@ -2,281 +2,278 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.AutoLinks
namespace Markdig.Extensions.AutoLinks;
/// <summary>
/// The inline parser used to for autolinks.
/// </summary>
/// <seealso cref="InlineParser" />
public class AutoLinkParser : InlineParser
{
/// <summary>
/// The inline parser used to for autolinks.
/// Initializes a new instance of the <see cref="AutoLinkParser"/> class.
/// </summary>
/// <seealso cref="InlineParser" />
public class AutoLinkParser : InlineParser
public AutoLinkParser(AutoLinkOptions options)
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLinkParser"/> class.
/// </summary>
public AutoLinkParser(AutoLinkOptions options)
Options = options ?? throw new ArgumentNullException(nameof(options));
OpeningCharacters =
[
'h', // for http:// and https://
'f', // for ftp://
'm', // for mailto:
't', // for tel:
'w', // for www.
];
_listOfCharCache = new ListOfCharCache();
}
public readonly AutoLinkOptions Options;
private readonly ListOfCharCache _listOfCharCache;
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)
{
Options = options ?? throw new ArgumentNullException(nameof(options));
OpeningCharacters = new char[]
{
'h', // for http:// and https://
'f', // for ftp://
'm', // for mailto:
't', // for tel:
'w', // for www.
};
_listOfCharCache = new ListOfCharCache();
return false;
}
public readonly AutoLinkOptions Options;
var startPosition = slice.Start;
int domainOffset = 0;
private readonly ListOfCharCache _listOfCharCache;
public override bool Match(InlineProcessor processor, ref StringSlice slice)
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
// Previous char must be a whitespace or a punctuation
var previousChar = slice.PeekCharExtra(-1);
if (!previousChar.IsWhiteSpaceOrZero() && Options.ValidPreviousCharacters.IndexOf(previousChar) == -1)
case 'h':
if (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 't':
if (!slice.MatchLowercase("el:", 1))
{
return false;
}
domainOffset = 4;
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1)) // We won't match http:/www. or /www.xxx
{
return false;
}
domainOffset = 4; // www.
break;
}
List<char> pendingEmphasis = _listOfCharCache.Get();
try
{
// Check that an autolink is possible in the current context
if (!IsAutoLinkValidInCurrentContext(processor, pendingEmphasis))
{
return false;
}
var startPosition = slice.Start;
int domainOffset = 0;
// Parse URL
if (!LinkHelper.TryParseUrl(ref slice, out string? link, out _, true))
{
return false;
}
var c = slice.CurrentChar;
// Precheck URL
// 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 (slice.MatchLowercase("ttp://", 1))
{
domainOffset = 7; // http://
}
else if (slice.MatchLowercase("ttps://", 1))
{
domainOffset = 8; // https://
}
else return false;
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
domainOffset = 6; // ftp://
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
case 'f':
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 't':
if (!slice.MatchLowercase("el:", 1))
if (string.Equals(link, "tel", StringComparison.OrdinalIgnoreCase))
{
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))
{
// 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,
};
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;
}
finally
{
_listOfCharCache.Release(pendingEmphasis);
}
}
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List<char> pendingEmphasis)
{
// Case where there is a pending HtmlInline <a>
var currentInline = processor.Inline;
while (currentInline != null)
{
if (currentInline is HtmlInline htmlInline)
{
// If we have a </a> we don't expect nested <a>
if (htmlInline.Tag.StartsWith("</a", StringComparison.OrdinalIgnoreCase))
{
break;
}
// If there is a pending <a>, we can't allow a link
if (htmlInline.Tag.StartsWith("<a", StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
// Check previous sibling and parents in the tree
currentInline = currentInline.PreviousSibling ?? currentInline.Parent;
}
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, List<char> pendingEmphasis)
// Check that we don't have any pending brackets opened (where we could have a possible markdown link)
// NOTE: This assume that [ and ] are used for links, otherwise autolink will not work properly
currentInline = processor.Inline;
int countBrackets = 0;
while (currentInline != null)
{
// Case where there is a pending HtmlInline <a>
var currentInline = processor.Inline;
while (currentInline != null)
if (currentInline is LinkDelimiterInline linkDelimiterInline && linkDelimiterInline.IsActive)
{
if (currentInline is HtmlInline htmlInline)
if (linkDelimiterInline.Type == DelimiterType.Open)
{
// If we have a </a> we don't expect nested <a>
if (htmlInline.Tag.StartsWith("</a", StringComparison.OrdinalIgnoreCase))
{
break;
}
// If there is a pending <a>, we can't allow a link
if (htmlInline.Tag.StartsWith("<a", StringComparison.OrdinalIgnoreCase))
{
return false;
}
countBrackets++;
}
else if (linkDelimiterInline.Type == DelimiterType.Close)
{
countBrackets--;
}
// Check previous sibling and parents in the tree
currentInline = currentInline.PreviousSibling ?? currentInline.Parent;
}
// Check that we don't have any pending brackets opened (where we could have a possible markdown link)
// NOTE: This assume that [ and ] are used for links, otherwise autolink will not work properly
currentInline = processor.Inline;
int countBrackets = 0;
while (currentInline != null)
else
{
if (currentInline is LinkDelimiterInline linkDelimiterInline && linkDelimiterInline.IsActive)
// Record all pending characters for emphasis
if (currentInline is EmphasisDelimiterInline emphasisDelimiter)
{
if (linkDelimiterInline.Type == DelimiterType.Open)
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
{
countBrackets++;
}
else if (linkDelimiterInline.Type == DelimiterType.Close)
{
countBrackets--;
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
}
}
else
{
// Record all pending characters for emphasis
if (currentInline is EmphasisDelimiterInline emphasisDelimiter)
{
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
{
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
}
}
}
currentInline = currentInline.Parent;
}
return countBrackets <= 0;
currentInline = currentInline.Parent;
}
private sealed class ListOfCharCache : DefaultObjectCache<List<char>>
return countBrackets <= 0;
}
private sealed class ListOfCharCache : DefaultObjectCache<List<char>>
{
protected override void Reset(List<char> instance)
{
protected override void Reset(List<char> instance)
{
instance.Clear();
}
instance.Clear();
}
}
}

View File

@@ -7,57 +7,56 @@ using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.Bootstrap
namespace Markdig.Extensions.Bootstrap;
/// <summary>
/// Extension for tagging some HTML elements with bootstrap classes.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class BootstrapExtension : IMarkdownExtension
{
/// <summary>
/// Extension for tagging some HTML elements with bootstrap classes.
/// </summary>
/// <seealso cref="IMarkdownExtension" />
public class BootstrapExtension : IMarkdownExtension
public void Setup(MarkdownPipelineBuilder pipeline)
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Make sure we don't have a delegate twice
pipeline.DocumentProcessed -= PipelineOnDocumentProcessed;
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
}
// Make sure we don't have a delegate twice
pipeline.DocumentProcessed -= PipelineOnDocumentProcessed;
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
{
foreach (var node in document.Descendants())
{
foreach (var node in document.Descendants())
if (node.IsInline)
{
if (node.IsInline)
if (node.IsContainerInline && node is LinkInline link && link.IsImage)
{
if (node.IsContainerInline && node is LinkInline link && link.IsImage)
{
link.GetAttributes().AddClass("img-fluid");
}
link.GetAttributes().AddClass("img-fluid");
}
else if (node.IsContainerBlock)
}
else if (node.IsContainerBlock)
{
if (node is Tables.Table)
{
if (node is Tables.Table)
{
node.GetAttributes().AddClass("table");
}
else if (node is QuoteBlock)
{
node.GetAttributes().AddClass("blockquote");
}
else if (node is Figures.Figure)
{
node.GetAttributes().AddClass("figure");
}
node.GetAttributes().AddClass("table");
}
else
else if (node is QuoteBlock)
{
if (node is Figures.FigureCaption)
{
node.GetAttributes().AddClass("figure-caption");
}
node.GetAttributes().AddClass("blockquote");
}
else if (node is Figures.Figure)
{
node.GetAttributes().AddClass("figure");
}
}
else
{
if (node is Figures.FigureCaption)
{
node.GetAttributes().AddClass("figure-caption");
}
}
}

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