mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-04 05:44:50 +00:00
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a54f06540 | ||
|
|
3f305a25a8 | ||
|
|
5a210223b6 | ||
|
|
a7786d934d | ||
|
|
cb3c1f1505 | ||
|
|
50b33b8512 | ||
|
|
80790b5038 | ||
|
|
311c28ae60 | ||
|
|
9906a0554f | ||
|
|
7e92f1881d | ||
|
|
41911806a0 | ||
|
|
610f1519b0 | ||
|
|
29aa56b4f1 | ||
|
|
5004ecedb7 | ||
|
|
ba06a796dc | ||
|
|
b1d6f34976 | ||
|
|
28f4236a57 | ||
|
|
0739a82735 | ||
|
|
3ac9f2e788 | ||
|
|
d9607cc687 | ||
|
|
f0573ef9e2 | ||
|
|
017a3bd7e2 | ||
|
|
d6f9a1055c | ||
|
|
2612e9d565 | ||
|
|
1076d7af10 | ||
|
|
850cecfa6c | ||
|
|
bfdb03bd78 | ||
|
|
56b6e329d5 | ||
|
|
006e384d1a | ||
|
|
9cd9b683a5 | ||
|
|
e01e2c3d2b | ||
|
|
97ce0da564 | ||
|
|
26c5ce59b5 | ||
|
|
d2d94ecf39 | ||
|
|
0ec82bb81d | ||
|
|
ad9e941cb0 | ||
|
|
27dab6ca6d | ||
|
|
762196f03b | ||
|
|
26a565fa45 | ||
|
|
4369db1a43 | ||
|
|
d83f1f87cc | ||
|
|
14027f4be3 | ||
|
|
8bcfb53607 | ||
|
|
700bb21e03 | ||
|
|
1bc9d9083c | ||
|
|
1d7bb021a7 | ||
|
|
3305a74b06 | ||
|
|
6312bc0515 | ||
|
|
b595383281 | ||
|
|
bf85964543 | ||
|
|
d5b020b784 | ||
|
|
8ab0467e78 | ||
|
|
74fe3be7c7 | ||
|
|
b47ae8d9ba | ||
|
|
a926e1a326 | ||
|
|
6048406f52 | ||
|
|
1edf14c742 | ||
|
|
d4bb7c0a51 | ||
|
|
ba4e463517 | ||
|
|
2d0dae4ebb | ||
|
|
1a9d923c44 | ||
|
|
013ec0bf80 | ||
|
|
f82e874cb7 | ||
|
|
976fc569fa | ||
|
|
5aab21f0bf | ||
|
|
1df8344576 | ||
|
|
9eb84cf95e | ||
|
|
47d45b5577 | ||
|
|
ebcc286df0 | ||
|
|
f11d8037ac | ||
|
|
f668b3fe38 | ||
|
|
5ca4e31332 | ||
|
|
a8fbe79e30 | ||
|
|
bdb09b9820 | ||
|
|
79348ebea8 | ||
|
|
b366c1a5e3 | ||
|
|
a5e53b014f | ||
|
|
c2f21e21c8 | ||
|
|
81b8dce7a8 | ||
|
|
e6223f86d8 | ||
|
|
2054c16662 | ||
|
|
95641e562f | ||
|
|
e4953931c7 | ||
|
|
93b5b7a091 | ||
|
|
4f52e893ee | ||
|
|
23ede077a1 | ||
|
|
d5e6f17683 | ||
|
|
4d5980a485 | ||
|
|
19dd902519 | ||
|
|
f046a46275 | ||
|
|
25e9eafa8b | ||
|
|
f4ff981008 | ||
|
|
a1b48aff89 | ||
|
|
0aa34caa82 | ||
|
|
98ce9b1a06 | ||
|
|
ab157b21ea | ||
|
|
53c72d3031 | ||
|
|
165e2f97d0 | ||
|
|
6c577059ad | ||
|
|
0ea4dc769b | ||
|
|
a9b626e810 | ||
|
|
b90d6f9769 | ||
|
|
f0ea008c46 | ||
|
|
c43d5ccd63 | ||
|
|
d6d7b398e4 | ||
|
|
e1032e5094 | ||
|
|
a32ac298c5 | ||
|
|
2e6ab670cb | ||
|
|
72b7fce48c | ||
|
|
fa281f1ca1 | ||
|
|
1b92311aeb | ||
|
|
a593212f03 | ||
|
|
c4ec928953 | ||
|
|
e7df7fabeb | ||
|
|
1c187f2d81 | ||
|
|
da66cf90c3 | ||
|
|
0e8bd7407f | ||
|
|
d70f14addb | ||
|
|
e7b9eea3a5 | ||
|
|
97af9d822d | ||
|
|
cf7a09ab76 | ||
|
|
e755627421 | ||
|
|
f9be64a988 | ||
|
|
d65431e6cc | ||
|
|
86fb962fdb | ||
|
|
d003837b27 | ||
|
|
fd813e3c5a | ||
|
|
a3691c4423 | ||
|
|
9506f22025 | ||
|
|
06ae907949 | ||
|
|
555523b2af | ||
|
|
3b9772f772 | ||
|
|
891c80f48c | ||
|
|
105b09e1ec | ||
|
|
9fe7596a23 | ||
|
|
82af7cadc5 | ||
|
|
800c81bb9a | ||
|
|
5653a4f7ee | ||
|
|
4f14ffe63b | ||
|
|
d57acefe56 | ||
|
|
fb61e5a8da | ||
|
|
264516bfdb | ||
|
|
6d8f8996d5 | ||
|
|
ba8557d3bf | ||
|
|
d18fd0b957 | ||
|
|
fba96774f4 | ||
|
|
0b2764ea62 | ||
|
|
64d81ed47b | ||
|
|
9a9742888b | ||
|
|
5400b30a90 | ||
|
|
673f4a4beb | ||
|
|
05a27649aa | ||
|
|
26c6b12dea | ||
|
|
9a18ca222f | ||
|
|
354f31b870 | ||
|
|
c6c2f58ec0 | ||
|
|
4a146f00f6 | ||
|
|
2c10ac59d3 | ||
|
|
9a98fb9453 | ||
|
|
1c7f843a4c | ||
|
|
fdaf301bd6 | ||
|
|
5a9f2f3afe | ||
|
|
6a1b30761a | ||
|
|
8f8b08fad6 | ||
|
|
312b63a4c8 | ||
|
|
1598b538ab | ||
|
|
16ce46a741 | ||
|
|
4456598228 | ||
|
|
2a937c63b9 | ||
|
|
191bc940c7 | ||
|
|
05673758e3 | ||
|
|
cff2b9a8ca | ||
|
|
586095a475 | ||
|
|
eae2082a1e | ||
|
|
4576548df3 | ||
|
|
266e0c8bfd | ||
|
|
f5c07dbab5 | ||
|
|
c8a28a1ad7 | ||
|
|
72cc454314 | ||
|
|
7fe2c1f939 | ||
|
|
087e7a68b6 | ||
|
|
abeabf15a1 | ||
|
|
f9bfcaab7b | ||
|
|
afa0182f02 |
46
appveyor.yml
Normal file
46
appveyor.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
version: 10.0.{build}
|
||||
image: Visual Studio 2017
|
||||
configuration: Release
|
||||
install:
|
||||
- ps: >-
|
||||
cd src
|
||||
|
||||
nuget restore Markdig.sln
|
||||
|
||||
$env:MARKDIG_BUILD_NUMBER = ([int]$env:APPVEYOR_BUILD_NUMBER).ToString("000")
|
||||
|
||||
$env:MARKDIG_VERSION_SUFFIX = ""
|
||||
|
||||
$env:appveyor_nuget_push = 'false'
|
||||
|
||||
if(-Not $env:APPVEYOR_PULL_REQUEST_NUMBER) {
|
||||
if($env:appveyor_repo_tag -eq 'True') {
|
||||
if($env:appveyor_repo_tag_name -match '^v[0-9]') {
|
||||
$env:appveyor_nuget_push = 'true'
|
||||
$env:MARKDIG_VERSION_SUFFIX = ""
|
||||
}
|
||||
if($env:appveyor_repo_tag_name -eq 'latest') {
|
||||
$env:appveyor_nuget_push = 'true'
|
||||
$env:MARKDIG_VERSION_SUFFIX = "pre$env:MARKDIG_BUILD_NUMBER"
|
||||
}
|
||||
}
|
||||
}
|
||||
build:
|
||||
project: src/Markdig.sln
|
||||
verbosity: minimal
|
||||
before_package:
|
||||
- cmd: >-
|
||||
msbuild /t:pack /p:VersionSuffix="%MARKDIG_VERSION_SUFFIX%" /p:Configuration=Release Markdig/Markdig.csproj
|
||||
|
||||
msbuild /t:Clean Markdig/Markdig.csproj
|
||||
|
||||
msbuild /t:pack /p:VersionSuffix="%MARKDIG_VERSION_SUFFIX%" /p:Configuration=Release;SignAssembly=true Markdig/Markdig.csproj
|
||||
artifacts:
|
||||
- path: src\Markdig\Bin\Release\*.nupkg
|
||||
name: Markdig Nugets
|
||||
deploy:
|
||||
- provider: NuGet
|
||||
api_key:
|
||||
secure: 7cthHh+wYWZjhqxaxR6QObRaRnstvFkQOY7MkxIsC5kpQEBlKZXuinf0IybbYxJt
|
||||
on:
|
||||
appveyor_nuget_push: true
|
||||
68
readme.md
68
readme.md
@@ -1,8 +1,8 @@
|
||||
# Markdig [](https://ci.appveyor.com/project/xoofx/markdig) [](https://www.nuget.org/packages/Markdig/)
|
||||
# Markdig [](https://ci.appveyor.com/project/xoofx/markdig) [](https://www.nuget.org/packages/Markdig/) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
|
||||
|
||||
<img align="right" width="160px" height="160px" src="img/markdig.png">
|
||||
|
||||
Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
Markdig is a fast, powerful, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
|
||||
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
|
||||
|
||||
@@ -14,46 +14,51 @@ You can **try Markdig online** and compare it to other implementations on [babel
|
||||
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
|
||||
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
|
||||
- Converter to **HTML**
|
||||
- Passing more than **600+ tests** from the latest [CommonMark specs](http://spec.commonmark.org/)
|
||||
- Passing more than **600+ tests** from the latest [CommonMark specs (0.27)](http://spec.commonmark.org/)
|
||||
- Includes all the core elements of CommonMark:
|
||||
- including **GFM fenced code blocks**.
|
||||
- **Extensible** architecture
|
||||
- Even the core Markdown/CommonMark parsing is pluggable, so it allows to disable builtin Markdown/Commonmark parsing (e.g [Disable HTML parsing](https://github.com/lunet-io/markdig/blob/7964bd0160d4c18e4155127a4c863d61ebd8944a/src/Markdig/MarkdownExtensions.cs#L306)) or change behaviour (e.g change matching `#` of a headers with `@`)
|
||||
- Built-in with **20+ extensions**, including:
|
||||
- 2 kind of tables:
|
||||
- **Pipe tables** (inspired from Github tables and [PanDoc - Pipe Tables](http://pandoc.org/README.html#extension-pipe_tables))
|
||||
- **Grid tables** (inspired from [Pandoc - Grid Tables](http://pandoc.org/README.html#extension-grid_tables))
|
||||
- **Extra emphasis** (inspired from [Pandoc - Emphasis](http://pandoc.org/README.html#strikeout) and [Markdown-it](https://markdown-it.github.io/))
|
||||
- [**Pipe tables**](src/Markdig.Tests/Specs/PipeTableSpecs.md) (inspired from Github tables and [PanDoc - Pipe Tables](http://pandoc.org/README.html#extension-pipe_tables))
|
||||
- [**Grid tables**](src/Markdig.Tests/Specs/GridTableSpecs.md) (inspired from [Pandoc - Grid Tables](http://pandoc.org/README.html#extension-grid_tables))
|
||||
- [**Extra emphasis**](src/Markdig.Tests/Specs/EmphasisExtraSpecs.md) (inspired from [Pandoc - Emphasis](http://pandoc.org/README.html#strikeout) and [Markdown-it](https://markdown-it.github.io/))
|
||||
- strike through `~~`,
|
||||
- Subscript `~`
|
||||
- Superscript `^`
|
||||
- Inserted `++`
|
||||
- Marked `==`
|
||||
- **Special attributes** or attached HTML attributes (inspired from [PHP Markdown Extra - Special Attributes](https://michelf.ca/projects/php-markdown/extra/#spe-attr))
|
||||
- **Definition lists** (inspired from [PHP Markdown Extra - Definitions Lists](https://michelf.ca/projects/php-markdown/extra/#def-list))
|
||||
- **Footnotes** (inspired from [PHP Markdown Extra - Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes))
|
||||
- **Auto-identifiers** for headings (similar to [Pandoc - Auto Identifiers](http://pandoc.org/README.html#extension-auto_identifiers))
|
||||
- **Task Lists** inspired from [Github Task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments).
|
||||
- **Extra bullet lists**, supporting alpha bullet `a.` `b.` and roman bullet (`i`, `ii`...etc.)
|
||||
- **Media support** for media url (youtube, vimeo, mp4...etc.) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/embedded-audio-and-video/441))
|
||||
- **Abbreviations** (inspired from [PHP Markdown Extra - Abbreviations](https://michelf.ca/projects/php-markdown/extra/#abbr))
|
||||
- **Citation** text by enclosing `""...""` (inspired by this [CommonMark discussion ](https://talk.commonmark.org/t/referencing-creative-works-with-cite/892))
|
||||
- **Custom containers** similar to fenced code block `:::` for generating a proper `<div>...</div>` instead (inspired by this [CommonMark discussion ](https://talk.commonmark.org/t/custom-container-for-block-and-inline/2051))
|
||||
- **Figures** (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/image-tag-should-expand-to-figure-when-used-with-title/265/5))
|
||||
- **Footers** (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/syntax-for-footer/2070))
|
||||
- **Mathematics**/Latex extension by enclosing `$$` for block and `$` for inline math (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/mathematics-extension/457/31))
|
||||
- **Soft lines as hard lines**
|
||||
- **Emoji** support (inspired from [Markdown-it](https://markdown-it.github.io/))
|
||||
- **SmartyPants** (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
|
||||
- **Bootstrap** class (to output bootstrap class)
|
||||
- [**Special attributes**](src/Markdig.Tests/Specs/GenericAttributesSpecs.md) or attached HTML attributes (inspired from [PHP Markdown Extra - Special Attributes](https://michelf.ca/projects/php-markdown/extra/#spe-attr))
|
||||
- [**Definition lists**](src/Markdig.Tests/Specs/DefinitionListSpecs.md) (inspired from [PHP Markdown Extra - Definitions Lists](https://michelf.ca/projects/php-markdown/extra/#def-list))
|
||||
- [**Footnotes**](src/Markdig.Tests/Specs/FootnotesSpecs.md) (inspired from [PHP Markdown Extra - Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes))
|
||||
- [**Auto-identifiers**](src/Markdig.Tests/Specs/AutoIdentifierSpecs.md) for headings (similar to [Pandoc - Auto Identifiers](http://pandoc.org/README.html#extension-auto_identifiers))
|
||||
- [**Auto-links**](src/Markdig.Tests/Specs/AutoLinks.md) generates links if a text starts with `http://` or `https://` or `ftp://` or `mailto:` or `www.xxx.yyy`
|
||||
- [**Task Lists**](src/Markdig.Tests/Specs/TaskListSpecs.md) inspired from [Github Task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments).
|
||||
- [**Extra bullet lists**](src/Markdig.Tests/Specs/ListExtraSpecs.md), supporting alpha bullet `a.` `b.` and roman bullet (`i`, `ii`...etc.)
|
||||
- [**Media support**](src/Markdig.Tests/Specs/MediaSpecs.md) for media url (youtube, vimeo, mp4...etc.) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/embedded-audio-and-video/441))
|
||||
- [**Abbreviations**](src/Markdig.Tests/Specs/AbbreviationSpecs.md) (inspired from [PHP Markdown Extra - Abbreviations](https://michelf.ca/projects/php-markdown/extra/#abbr))
|
||||
- [**Citation**](src/Markdig.Tests/Specs/FigureFooterAndCiteSpecs.md) text by enclosing `""...""` (inspired by this [CommonMark discussion ](https://talk.commonmark.org/t/referencing-creative-works-with-cite/892))
|
||||
- [**Custom containers**](src/Markdig.Tests/Specs/CustomContainerSpecs.md) similar to fenced code block `:::` for generating a proper `<div>...</div>` instead (inspired by this [CommonMark discussion ](https://talk.commonmark.org/t/custom-container-for-block-and-inline/2051))
|
||||
- [**Figures**](src/Markdig.Tests/Specs/FigureFooterAndCiteSpecs.md) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/image-tag-should-expand-to-figure-when-used-with-title/265/5))
|
||||
- [**Footers**](src/Markdig.Tests/Specs/FigureFooterAndCiteSpecs.md) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/syntax-for-footer/2070))
|
||||
- [**Mathematics**](src/Markdig.Tests/Specs/MathSpecs.md)/Latex extension by enclosing `$$` for block and `$` for inline math (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/mathematics-extension/457/31))
|
||||
- [**Soft lines as hard lines**](src/Markdig.Tests/Specs/HardlineBreakSpecs.md)
|
||||
- [**Emoji**](src/Markdig.Tests/Specs/EmojiSpecs.md) support (inspired from [Markdown-it](https://markdown-it.github.io/))
|
||||
- [**SmartyPants**](src/Markdig.Tests/Specs/SmartyPantsSpecs.md) (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
|
||||
- [**Bootstrap**](src/Markdig.Tests/Specs/BootstrapSpecs.md) class (to output bootstrap class)
|
||||
- [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports only for [`mermaid` diagrams](https://knsv.github.io/mermaid/))
|
||||
- [**YAML frontmatter**](src/Markdig.Tests/Specs/YamlSpecs.md) to parse without evaluating the frontmatter and to discard it from the HTML output (typically used for previewing without the frontmatter in MarkdownEditor)
|
||||
- [**JIRA links**](src/Markdig.Tests/Specs/JiraLinks.md) to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker)
|
||||
- Compatible with .NET 3.5, 4.0+ and .NET Core (`netstandard1.1+`)
|
||||
|
||||
## Documentation
|
||||
|
||||
> The repository is under construction. There will be a dedicated website and proper documentation at some point!
|
||||
|
||||
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
|
||||
While there is not yet a dedicated documentation, you can find from the [specs documentation](src/Markdig.Tests/Specs/readme.md) how to use these extensions.
|
||||
|
||||
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
|
||||
|
||||
## Download
|
||||
|
||||
@@ -70,7 +75,7 @@ var result = Markdown.ToHtml("This is a text with some *emphasis*");
|
||||
Console.WriteLine(result); // prints: <p>This is a text with some <em>emphasis</em></p>
|
||||
```
|
||||
|
||||
In order to activate most of all advanced extensions (except Emoji, SoftLine as HarLine and SmartyPants)
|
||||
In order to activate most of all advanced extensions (except Emoji, SoftLine as HardLine, JiraLinks and SmartyPants)
|
||||
|
||||
```csharp
|
||||
// Configure the pipeline with all advanced extensions active
|
||||
@@ -80,6 +85,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)
|
||||
|
||||
## Build
|
||||
|
||||
In order to build Markdig, you need to install [.NET Core RTM](https://www.microsoft.com/net/core)
|
||||
|
||||
## License
|
||||
|
||||
This software is released under the [BSD-Clause 2 license](https://github.com/lunet-io/markdig/blob/master/license.txt).
|
||||
@@ -111,7 +120,7 @@ This is an early preview of the benchmarking against various implementations:
|
||||
### Analysis of the results:
|
||||
|
||||
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
|
||||
- **Among the best in CPU**, Extremelly competitive and often faster than other implementations (not feature wise equivalent)
|
||||
- **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:
|
||||
@@ -169,6 +178,12 @@ WarmupCount=2 TargetCount=10
|
||||
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
If you are using this library and find it useful for your project, please consider a donation for it!
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to the fantastic work done by [John Mac Farlane](http://johnmacfarlane.net/) for the CommonMark specs and all the people involved in making Markdown a better standard!
|
||||
@@ -179,6 +194,7 @@ Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/Bench
|
||||
|
||||
Some decoding part (e.g HTML [EntityHelper.cs](https://github.com/lunet-io/markdig/blob/master/src/Markdig/Helpers/EntityHelper.cs)) have been re-used from [CommonMark.NET](https://github.com/Knagis/CommonMark.NET)
|
||||
|
||||
Thanks to the work done by @clarkd on the JIRA Link extension (https://github.com/clarkd/MarkdigJiraLinker), now included with this project!
|
||||
## Author
|
||||
|
||||
Alexandre MUTEL aka [xoofx](http://xoofx.com)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Markdig.Benchmarks</RootNamespace>
|
||||
<AssemblyName>Markdig.Benchmarks</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<TargetFrameworkProfile />
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition="'$(NuGetPackageRoot)' == ''">
|
||||
<NuGetPackageRoot>$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
</PropertyGroup>
|
||||
<ImportGroup>
|
||||
<Import Project="$(NuGetPackageRoot)\Microsoft.Diagnostics.Tracing.TraceEvent\1.0.41\build\Microsoft.Diagnostics.Tracing.TraceEvent.targets" Condition="Exists('$(NuGetPackageRoot)\Microsoft.Diagnostics.Tracing.TraceEvent\1.0.41\build\Microsoft.Diagnostics.Tracing.TraceEvent.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -131,7 +131,7 @@ namespace Testamina.Markdig.Benchmarks
|
||||
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(new Job { Mode = Mode.Throughput, LaunchCount = 2, WarmupCount = 2, TargetCount = 10 });
|
||||
//config.Add(gcDiagnoser);
|
||||
|
||||
//var config = DefaultConfig.Instance;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/></startup></configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/></startup></configuration>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"win-x64": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net451": {
|
||||
"net46": {
|
||||
"compilationOptions": {
|
||||
"define": [
|
||||
"CLASSIC"
|
||||
@@ -19,9 +19,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"BenchmarkDotNet": "0.9.6",
|
||||
"BenchmarkDotNet.Diagnostics.Windows": "0.9.6",
|
||||
"CommonMark.NET": "0.11.0",
|
||||
"BenchmarkDotNet": "0.10.6",
|
||||
"BenchmarkDotNet.Diagnostics.Windows": "0.10.6",
|
||||
"CommonMark.NET": "0.15.1",
|
||||
"MarkdownSharp": "1.13.0.0",
|
||||
"Microsoft.Diagnostics.Runtime": "0.8.31-beta",
|
||||
"Microsoft.Diagnostics.Tracing.TraceEvent": "1.0.41.0"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@@ -38,10 +39,6 @@
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Markdig">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\Markdig\Bin\$(Configuration)\net40\Markdig.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
@@ -59,9 +56,12 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Specs.tt</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Specs\TestEmphasisPlus.cs" />
|
||||
<Compile Include="TestHtmlAttributes.cs" />
|
||||
<Compile Include="TestHtmlHelper.cs" />
|
||||
<Compile Include="TestLineReader.cs" />
|
||||
<Compile Include="TestLinkHelper.cs" />
|
||||
<Compile Include="TestOrderedList.cs" />
|
||||
<Compile Include="TestPragmaLines.cs" />
|
||||
<Compile Include="TestSourcePosition.cs" />
|
||||
<Compile Include="TestStringSliceList.cs" />
|
||||
@@ -72,6 +72,8 @@
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="project.json" />
|
||||
<None Include="Specs\JiraLinks.md" />
|
||||
<None Include="Specs\AutoLinks.md" />
|
||||
<None Include="Specs\AutoIdentifierSpecs.md" />
|
||||
<None Include="Specs\AbbreviationSpecs.md" />
|
||||
<None Include="Specs\FigureFooterAndCiteSpecs.md" />
|
||||
@@ -84,6 +86,10 @@
|
||||
<None Include="Specs\GridTableSpecs.md" />
|
||||
<None Include="Specs\HardlineBreakSpecs.md" />
|
||||
<None Include="Specs\BootstrapSpecs.md" />
|
||||
<None Include="Specs\DiagramsSpecs.md" />
|
||||
<None Include="Specs\NoHtmlSpecs.md" />
|
||||
<None Include="Specs\readme.md" />
|
||||
<None Include="Specs\YamlSpecs.md" />
|
||||
<None Include="Specs\TaskListSpecs.md" />
|
||||
<None Include="Specs\SmartyPantsSpecs.md" />
|
||||
<None Include="Specs\MediaSpecs.md" />
|
||||
@@ -99,6 +105,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Markdig\Markdig.csproj">
|
||||
<Project>{8a58a7e2-627c-4f41-933f-5ac92adfab48}</Project>
|
||||
<Name>Markdig</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
||||
@@ -45,3 +45,56 @@ This is a 😃 HTML document
|
||||
.
|
||||
<p>This is a <abbr title="Hypertext Markup Language">😃 HTML</abbr> document</p>
|
||||
````````````````````````````````
|
||||
|
||||
Abbreviations may be similar:
|
||||
|
||||
```````````````````````````````` example
|
||||
*[1A]: First
|
||||
*[1A1]: Second
|
||||
*[1A2]: Third
|
||||
|
||||
We can abbreviate 1A, 1A1 and 1A2!
|
||||
.
|
||||
<p>We can abbreviate <abbr title="First">1A</abbr>, <abbr title="Second">1A1</abbr> and <abbr title="Third">1A2</abbr>!</p>
|
||||
````````````````````````````````
|
||||
|
||||
Abbreviations should match whole word only:
|
||||
|
||||
```````````````````````````````` example
|
||||
*[1A]: First
|
||||
|
||||
We should not abbreviate 1.1A or 11A!
|
||||
.
|
||||
<p>We should not abbreviate 1.1A or 11A!</p>
|
||||
````````````````````````````````
|
||||
|
||||
Abbreviations should match whole word only, even if the word is the entire content:
|
||||
|
||||
```````````````````````````````` example
|
||||
*[1A]: First
|
||||
|
||||
1.1A
|
||||
.
|
||||
<p>1.1A</p>
|
||||
````````````````````````````````
|
||||
|
||||
Abbreviations should match whole word only, even if there is another glossary term:
|
||||
|
||||
```````````````````````````````` example
|
||||
*[SCO]: First
|
||||
*[SCOM]: Second
|
||||
|
||||
SCOM
|
||||
.
|
||||
<p><abbr title="Second">SCOM</abbr></p>
|
||||
````````````````````````````````
|
||||
|
||||
Abbreviations should only match when surrounded by whitespace:
|
||||
|
||||
```````````````````````````````` example
|
||||
*[PR]: Pull Request
|
||||
|
||||
PRAA
|
||||
.
|
||||
<p>PRAA</p>
|
||||
````````````````````````````````
|
||||
78
src/Markdig.Tests/Specs/AutoLinks.md
Normal file
78
src/Markdig.Tests/Specs/AutoLinks.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Extensions
|
||||
|
||||
This section describes the different extensions supported:
|
||||
|
||||
## AutoLinks
|
||||
|
||||
Autolinks will format as a HTML link any string that starts by:
|
||||
|
||||
- `http://` or `https://`
|
||||
- `ftp://`
|
||||
- `mailto:`
|
||||
- `www.`
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a http://www.google.com URL and https://www.google.com
|
||||
This is a ftp://test.com
|
||||
And a mailto:email@toto.com
|
||||
And a plain www.google.com
|
||||
.
|
||||
<p>This is a <a href="http://www.google.com">http://www.google.com</a> URL and <a href="https://www.google.com">https://www.google.com</a>
|
||||
This is a <a href="ftp://test.com">ftp://test.com</a>
|
||||
And a <a href="mailto:email@toto.com">mailto:email@toto.com</a>
|
||||
And a plain <a href="http://www.google.com">www.google.com</a></p>
|
||||
````````````````````````````````
|
||||
|
||||
But incomplete links will not be matched:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a http:/www.google.com URL and https:/www.google.com
|
||||
This is not a ftp:/test.com
|
||||
And not a mailto:emailtoto.com
|
||||
And not a plain www. or a www.x
|
||||
.
|
||||
<p>This is not a http:/www.google.com URL and https:/www.google.com
|
||||
This is not a ftp:/test.com
|
||||
And not a mailto:emailtoto.com
|
||||
And not a plain www. or a www.x</p>
|
||||
````````````````````````````````
|
||||
|
||||
Previous character must be a punctuation or a valid space (tab, space, new line):
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a nhttp://www.google.com URL but this is (https://www.google.com)
|
||||
.
|
||||
<p>This is not a nhttp://www.google.com URL but this is (<a href="https://www.google.com">https://www.google.com</a>)</p>
|
||||
````````````````````````````````
|
||||
|
||||
An autolink should not interfere with an `<a>` HTML inline:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is an HTML <a href="http://www.google.com">http://www.google.com</a> link
|
||||
.
|
||||
<p>This is an HTML <a href="http://www.google.com">http://www.google.com</a> link</p>
|
||||
````````````````````````````````
|
||||
or even within emphasis:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is an HTML <a href="http://www.google.com"> **http://www.google.com** </a> link
|
||||
.
|
||||
<p>This is an HTML <a href="http://www.google.com"> <strong>http://www.google.com</strong> </a> link</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
An autolink should not interfere with a markdown link:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is an HTML [http://www.google.com](http://www.google.com) link
|
||||
.
|
||||
<p>This is an HTML <a href="http://www.google.com">http://www.google.com</a> link</p>
|
||||
````````````````````````````````
|
||||
|
||||
A link embraced by pending emphasis should let the emphasis takes precedence if characters are placed at the end of the matched link:
|
||||
|
||||
```````````````````````````````` example
|
||||
Check **http://www.a.com** or __http://www.b.com__
|
||||
.
|
||||
<p>Check <strong><a href="http://www.a.com">http://www.a.com</a></strong> or <strong><a href="http://www.b.com">http://www.b.com</a></strong></p>
|
||||
````````````````````````````````
|
||||
@@ -105,3 +105,28 @@ Term 1
|
||||
<pre><code>: Not valid
|
||||
</code></pre>
|
||||
````````````````````````````````
|
||||
|
||||
Definition lists can be nested inside list items
|
||||
|
||||
```````````````````````````````` example
|
||||
1. First
|
||||
|
||||
2. Second
|
||||
|
||||
Term 1
|
||||
: Definition
|
||||
|
||||
Term 2
|
||||
: Second Definition
|
||||
.
|
||||
<ol>
|
||||
<li><p>First</p></li>
|
||||
<li><p>Second</p>
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd>Definition</dd>
|
||||
<dt>Term 2</dt>
|
||||
<dd>Second Definition</dd>
|
||||
</dl></li>
|
||||
</ol>
|
||||
````````````````````````````````
|
||||
26
src/Markdig.Tests/Specs/DiagramsSpecs.md
Normal file
26
src/Markdig.Tests/Specs/DiagramsSpecs.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Extensions
|
||||
|
||||
Adds support for diagrams extension:
|
||||
|
||||
## Mermaid diagrams
|
||||
|
||||
Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
|
||||
|
||||
```````````````````````````````` example
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
```
|
||||
.
|
||||
<div class="mermaid">graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
TODO: Add other text diagram languages
|
||||
@@ -11,3 +11,29 @@ This is a test with a :) and a :angry: smiley
|
||||
.
|
||||
<p>This is a test with a 😃 and a 😠 smiley</p>
|
||||
````````````````````````````````
|
||||
|
||||
An emoji needs to be preceded by a space:
|
||||
|
||||
```````````````````````````````` example
|
||||
These are not:) an emoji with a:) x:angry:x
|
||||
.
|
||||
<p>These are not:) an emoji with a:) x:angry:x</p>
|
||||
````````````````````````````````
|
||||
|
||||
Emoji can be followed by close ponctuation (or any other characters):
|
||||
|
||||
```````````````````````````````` example
|
||||
We all need :), it makes us :muscle:. (and :ok_hand:).
|
||||
.
|
||||
<p>We all need 😃, it makes us 💪. (and 👌).</p>
|
||||
````````````````````````````````
|
||||
|
||||
Sentences can end with Emoji:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a sentance :ok_hand:
|
||||
and keeps going to the next line :)
|
||||
.
|
||||
<p>This is a sentance 👌
|
||||
and keeps going to the next line 😃</p>
|
||||
````````````````````````````````
|
||||
@@ -21,6 +21,17 @@ H~2~O is a liquid. 2^10^ is 1024
|
||||
.
|
||||
<p>H<sub>2</sub>O is a liquid. 2<sup>10</sup> is 1024</p>
|
||||
````````````````````````````````
|
||||
|
||||
Certain punctuation characters are exempted from the rule forbidding them within inline delimiters
|
||||
|
||||
```````````````````````````````` example
|
||||
One quintillionth can be expressed as 10^-18^
|
||||
|
||||
Daggers^†^ and double-daggers^‡^ can be used to denote notes.
|
||||
.
|
||||
<p>One quintillionth can be expressed as 10<sup>-18</sup></p>
|
||||
<p>Daggers<sup>†</sup> and double-daggers<sup>‡</sup> can be used to denote notes.</p>
|
||||
````````````````````````````````
|
||||
|
||||
## Inserted
|
||||
|
||||
@@ -41,3 +52,16 @@ Marked text can be used to specify that a text has been marked in a document. T
|
||||
.
|
||||
<p><mark>Marked text</mark></p>
|
||||
````````````````````````````````
|
||||
## Emphasis on Html Entities
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
This is text MyBrand ^®^ and MyTrademark ^™^
|
||||
This is text MyBrand^®^ and MyTrademark^™^
|
||||
This is text MyBrand~®~ and MyCopyright^©^
|
||||
.
|
||||
<p>This is text MyBrand <sup>®</sup> and MyTrademark <sup>TM</sup>
|
||||
This is text MyBrand<sup>®</sup> and MyTrademark<sup>TM</sup>
|
||||
This is text MyBrand<sub>®</sub> and MyCopyright<sup>©</sup></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -61,3 +61,60 @@ multi-paragraph list items.<a href="#fnref:3" class="footnote-back-ref">↩<
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
Check with mulitple consecutive footnotes:
|
||||
|
||||
```````````````````````````````` example
|
||||
Here is a footnote[^1]. And another one[^2]. And a third one[^3]. And a fourth[^4].
|
||||
|
||||
[^1]: Footnote 1 text
|
||||
|
||||
[^2]: Footnote 2 text
|
||||
|
||||
a
|
||||
|
||||
[^3]: Footnote 3 text
|
||||
|
||||
[^4]: Footnote 4 text
|
||||
.
|
||||
<p>Here is a footnote<a id="fnref:1" href="#fn:1" class="footnote-ref"><sup>1</sup></a>. And another one<a id="fnref:2" href="#fn:2" class="footnote-ref"><sup>2</sup></a>. And a third one<a id="fnref:3" href="#fn:3" class="footnote-ref"><sup>3</sup></a>. And a fourth<a id="fnref:4" href="#fn:4" class="footnote-ref"><sup>4</sup></a>.</p>
|
||||
<p>a</p>
|
||||
<div class="footnotes">
|
||||
<hr />
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>Footnote 1 text<a href="#fnref:1" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:2">
|
||||
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:3">
|
||||
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:4">
|
||||
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">↩</a></p></li>
|
||||
</ol>
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
Another test with consecutive footnotes without a blank line separator:
|
||||
|
||||
```````````````````````````````` example
|
||||
Here is a footnote[^1]. And another one[^2]. And a third one[^3]. And a fourth[^4].
|
||||
|
||||
[^1]: Footnote 1 text
|
||||
[^2]: Footnote 2 text
|
||||
[^3]: Footnote 3 text
|
||||
[^4]: Footnote 4 text
|
||||
.
|
||||
<p>Here is a footnote<a id="fnref:1" href="#fn:1" class="footnote-ref"><sup>1</sup></a>. And another one<a id="fnref:2" href="#fn:2" class="footnote-ref"><sup>2</sup></a>. And a third one<a id="fnref:3" href="#fn:3" class="footnote-ref"><sup>3</sup></a>. And a fourth<a id="fnref:4" href="#fn:4" class="footnote-ref"><sup>4</sup></a>.</p>
|
||||
<div class="footnotes">
|
||||
<hr />
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>Footnote 1 text<a href="#fnref:1" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:2">
|
||||
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:3">
|
||||
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">↩</a></p></li>
|
||||
<li id="fn:4">
|
||||
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">↩</a></p></li>
|
||||
</ol>
|
||||
</div>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -27,13 +27,13 @@ The following shows that attributes is attached to the current block or the prev
|
||||
This is a heading{#heading-link2}
|
||||
-----------------
|
||||
|
||||
This is a paragraph with an attached attributes {#myparagraph attached-bool-property}
|
||||
This is a paragraph with an attached attributes {#myparagraph attached-bool-property attached-bool-property2}
|
||||
.
|
||||
<h1 id="heading-link">This is a heading with an an attribute</h1>
|
||||
<h1 id="heading-link2">This is a heading</h1>
|
||||
<p><a href="http://google.com" id="a-link" class="myclass" data-lang="fr" data-value="This is a value">This is a link</a></p>
|
||||
<h2 id="heading-link2">This is a heading</h2>
|
||||
<p id="myparagraph" attached-bool-property>This is a paragraph with an attached attributes </p>
|
||||
<p id="myparagraph" attached-bool-property attached-bool-property2>This is a paragraph with an attached attributes </p>
|
||||
````````````````````````````````
|
||||
|
||||
The following shows that attributes can be attached to the next block if they are used inside a single line just preceding the block (and preceded by a blank line or beginning of a block container):
|
||||
|
||||
@@ -70,8 +70,8 @@ A regular row can continue a previous regular row when column separator `|` are
|
||||
+---------+---------+---------+
|
||||
| Col1 | Col2 | Col3 |
|
||||
| Col1a | Col2a | Col3a |
|
||||
| Col12 | Col3b |
|
||||
| Col123 |
|
||||
| Col1b | Col3b |
|
||||
| Col1c |
|
||||
.
|
||||
<table>
|
||||
<col style="width:33.33%">
|
||||
@@ -87,11 +87,11 @@ Col2a</td>
|
||||
Col3a</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">Col12</td>
|
||||
<td></td>
|
||||
<td colspan="2">Col1b</td>
|
||||
<td>Col3b</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">Col123</td>
|
||||
<td colspan="3">Col1c</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -184,3 +184,103 @@ Alignment might be specified on the first row using the character `:`:
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
A grid table may have cells spanning both columns and rows:
|
||||
|
||||
```````````````````````````````` example
|
||||
+---+---+---+
|
||||
| AAAAA | B |
|
||||
+---+---+ B +
|
||||
| D | E | B |
|
||||
+ D +---+---+
|
||||
| D | CCCCC |
|
||||
+---+---+---+
|
||||
.
|
||||
<table>
|
||||
<col style="width:33.33%">
|
||||
<col style="width:33.33%">
|
||||
<col style="width:33.33%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2">AAAAA</td>
|
||||
<td rowspan="2">B
|
||||
B
|
||||
B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">D
|
||||
D
|
||||
D</td>
|
||||
<td>E</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">CCCCC</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
A grid table may have cells with both colspan and rowspan:
|
||||
|
||||
```````````````````````````````` example
|
||||
+---+---+---+
|
||||
| AAAAA | B |
|
||||
+ AAAAA +---+
|
||||
| AAAAA | C |
|
||||
+---+---+---+
|
||||
| D | E | F |
|
||||
+---+---+---+
|
||||
.
|
||||
<table>
|
||||
<col style="width:33.33%">
|
||||
<col style="width:33.33%">
|
||||
<col style="width:33.33%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2" rowspan="2">AAAAA
|
||||
AAAAA
|
||||
AAAAA</td>
|
||||
<td>B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>D</td>
|
||||
<td>E</td>
|
||||
<td>F</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
A grid table may not have irregularly shaped cells:
|
||||
|
||||
```````````````````````````````` example
|
||||
+---+---+---+
|
||||
| AAAAA | B |
|
||||
+ A +---+ B +
|
||||
| A | C | B |
|
||||
+---+---+---+
|
||||
| DDDDD | E |
|
||||
+---+---+---+
|
||||
.
|
||||
<p>+---+---+---+
|
||||
| AAAAA | B |
|
||||
+ A +---+ B +
|
||||
| A | C | B |
|
||||
+---+---+---+
|
||||
| DDDDD | E |
|
||||
+---+---+---+</p>
|
||||
````````````````````````````````
|
||||
|
||||
An empty `+` on a line should result in a simple empty list output:
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
+
|
||||
.
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
77
src/Markdig.Tests/Specs/JiraLinks.md
Normal file
77
src/Markdig.Tests/Specs/JiraLinks.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Jira Links
|
||||
|
||||
The JiraLinks extension will automatically add links to JIRA issue items within your markdown, e.g. XX-1234. For this to happen, you must configure the extension when adding to the pipeline, e.g.
|
||||
|
||||
```
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseJiraLinks(new JiraLinkOptions("http://your.company.abc"))
|
||||
.Build();
|
||||
```
|
||||
|
||||
The rules for detecting a link are:
|
||||
|
||||
- The project key must be composed of onre or more capitalised ASCII letter `[A-Z]+`
|
||||
- A single hypen `-` must separate the project key and issue number.
|
||||
- The issue number is composed of 1 or more digits `[0, 9]+`
|
||||
- The reference must be preceeded by either `(` or whitespace or EOF.
|
||||
- The reference must be followed by either `)` or whitespace or EOF.
|
||||
|
||||
The following are valid examples:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a ABCD-123 issue
|
||||
.
|
||||
<p>This is a <a href="http://your.company.abc/browse/ABCD-123" target="blank">ABCD-123</a> issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a KIRA-1 issue
|
||||
.
|
||||
<p>This is a <a href="http://your.company.abc/browse/KIRA-1" target="blank">KIRA-1</a> issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a Z-1 issue
|
||||
.
|
||||
<p>This is a <a href="http://your.company.abc/browse/Z-1" target="blank">Z-1</a> issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
These are also valid links with `(` and `)`:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a (ABCD-123) issue
|
||||
.
|
||||
<p>This is a (<a href="http://your.company.abc/browse/ABCD-123" target="blank">ABCD-123</a>) issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a (KIRA-1) issue
|
||||
.
|
||||
<p>This is a (<a href="http://your.company.abc/browse/KIRA-1" target="blank">KIRA-1</a>) issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a (Z-1) issue
|
||||
.
|
||||
<p>This is a (<a href="http://your.company.abc/browse/Z-1" target="blank">Z-1</a>) issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
These are not valid links:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not aJIRA-123 issue
|
||||
.
|
||||
<p>This is not aJIRA-123 issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not JIRA-123a issue
|
||||
.
|
||||
<p>This is not JIRA-123a issue</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not JIRA- issue
|
||||
.
|
||||
<p>This is not JIRA- issue</p>
|
||||
````````````````````````````````
|
||||
@@ -38,7 +38,7 @@ Like for numbered list, a list can start with a different letter
|
||||
b. First item
|
||||
c. Second item
|
||||
.
|
||||
<ol type="a" start="b">
|
||||
<ol type="a" start="2">
|
||||
<li>First item</li>
|
||||
<li>Second item</li>
|
||||
</ol>
|
||||
@@ -100,8 +100,26 @@ Like for numbered list, a list can start with a different letter
|
||||
ii. First item
|
||||
iii. Second item
|
||||
.
|
||||
<ol type="i" start="ii">
|
||||
<ol type="i" start="2">
|
||||
<li>First item</li>
|
||||
<li>Second item</li>
|
||||
</ol>
|
||||
````````````````````````````````
|
||||
|
||||
Lists can be restarted, specifying the start point.
|
||||
|
||||
```````````````````````````````` example
|
||||
1. First item
|
||||
|
||||
Some text
|
||||
|
||||
2. Second item
|
||||
.
|
||||
<ol>
|
||||
<li>First item</li>
|
||||
</ol>
|
||||
<p>Some text</p>
|
||||
<ol start="2">
|
||||
<li>Second item</li>
|
||||
</ol>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -20,12 +20,47 @@ This is a $$math block$$
|
||||
<p>This is a <span class="math">math block</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
The opening `$` and closing `$` is following the rules of the emphasis delimiter `_`:
|
||||
Newlines inside an inline math are not allowed:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a $ math block $
|
||||
This is not a $$math
|
||||
block$$ and? this is a $$math block$$
|
||||
.
|
||||
<p>This is not a $ math block $</p>
|
||||
<p>This is not a $$math
|
||||
block$$ and? this is a <span class="math">math block</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is not a $math
|
||||
block$ and? this is a $math block$
|
||||
.
|
||||
<p>This is not a $math
|
||||
block$ and? this is a <span class="math">math block</span></p>
|
||||
````````````````````````````````
|
||||
An opening `$` can be followed by a space if the closing is also preceded by a space `$`:
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $ math block $
|
||||
.
|
||||
<p>This is a <span class="math">math block</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $ math block $ after
|
||||
.
|
||||
<p>This is a <span class="math">math block</span> after</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $$ math block $$ after
|
||||
.
|
||||
<p>This is a <span class="math">math block</span> after</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a not $ math block$ because there is not a whitespace before the closing
|
||||
.
|
||||
<p>This is a not $ math block$ because there is not a whitespace before the closing</p>
|
||||
````````````````````````````````
|
||||
|
||||
For the opening `$` it requires a space or a punctuation before (but cannot be used within a word):
|
||||
@@ -68,6 +103,13 @@ This is a $$$math block$$$
|
||||
<p>This is a <span class="math">$math block$</span></p>
|
||||
````````````````````````````````
|
||||
|
||||
Regular text can come both before and after the math inline
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a $math block$ with text on both sides.
|
||||
.
|
||||
<p>This is a <span class="math">math block</span> with text on both sides.</p>
|
||||
````````````````````````````````
|
||||
A mathematic block takes precedence over standard emphasis `*` `_`:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -75,6 +117,13 @@ This is *a $math* block$
|
||||
.
|
||||
<p>This is *a <span class="math">math* block</span></p>
|
||||
````````````````````````````````
|
||||
An opening $$ at the beginning of a line should not be interpreted as a Math block:
|
||||
|
||||
```````````````````````````````` example
|
||||
$$ math $$ starting at a line
|
||||
.
|
||||
<p><span class="math">math</span> starting at a line</p>
|
||||
````````````````````````````````
|
||||
|
||||
## Math Block
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ Allows to embed audio/video links to popular website:
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
.
|
||||
<p><iframe src="https://www.youtube.com/embed/mswPy5bt3TQ" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
|
||||
<p><iframe src="https://player.vimeo.com/video/8607834" width="500" height="281" frameborder="0" allowfullscreen></iframe></p>
|
||||
<p><video width="500" height="281" controls><source type="video/mp4" src="https://sample.com/video.mp4"></source></video></p>
|
||||
````````````````````````````````
|
||||
27
src/Markdig.Tests/Specs/NoHtmlSpecs.md
Normal file
27
src/Markdig.Tests/Specs/NoHtmlSpecs.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Extensions
|
||||
|
||||
## NoHTML
|
||||
|
||||
The extension DisableHtml allows to disable the parsing of HTML:
|
||||
|
||||
For inline HTML:
|
||||
|
||||
```````````````````````````````` example
|
||||
this is some text</td></tr>
|
||||
.
|
||||
<p>this is some text</td></tr></p>
|
||||
````````````````````````````````
|
||||
|
||||
For Block HTML:
|
||||
|
||||
```````````````````````````````` example
|
||||
<div>
|
||||
this is some text
|
||||
</div>
|
||||
.
|
||||
<p><div>
|
||||
this is some text
|
||||
</div></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ c no d
|
||||
c no d</p>
|
||||
````````````````````````````````
|
||||
|
||||
The number of columns in the first row determine the number of columns for the whole table. Any extra columns delimiter `|` for sub-sequent lines are converted to literal strings instead:
|
||||
If a row contains more column than the header row, it will still be added as a column:
|
||||
|
||||
```````````````````````````````` example
|
||||
a | b
|
||||
@@ -136,19 +136,24 @@ a | b
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1 | 2</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>4</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -345,7 +350,8 @@ The first row is considered as a **header row** if it is separated from the regu
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
The text alignment is defined by default to be left.
|
||||
The text alignment is defined by default to be center for header and left for cells. If the left alignment is applied, it will force the column heading to be left aligned.
|
||||
There is no way to define a different alignment for heading and cells (apart from the default).
|
||||
The text alignment can be changed by using the character `:` with the header column separator:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -357,19 +363,19 @@ The text alignment can be changed by using the character `:` with the header col
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th style="text-align: left;">a</th>
|
||||
<th style="text-align: center;">b</th>
|
||||
<th style="text-align: right;">c</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td style="text-align: left;">0</td>
|
||||
<td style="text-align: center;">1</td>
|
||||
<td style="text-align: right;">2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td style="text-align: left;">3</td>
|
||||
<td style="text-align: center;">4</td>
|
||||
<td style="text-align: right;">5</td>
|
||||
</tr>
|
||||
@@ -377,6 +383,31 @@ The text alignment can be changed by using the character `:` with the header col
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
Test alignment with starting and ending pipes:
|
||||
|
||||
```````````````````````````````` example
|
||||
| abc | def | ghi |
|
||||
|:---:|-----|----:|
|
||||
| 1 | 2 | 3 |
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: center;">abc</th>
|
||||
<th>def</th>
|
||||
<th style="text-align: right;">ghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: center;">1</td>
|
||||
<td>2</td>
|
||||
<td style="text-align: right;">3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
The following example shows a non matching header column separator:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -482,3 +513,70 @@ a | b
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
** Tests **
|
||||
|
||||
Tests trailing spaces after pipes
|
||||
|
||||
```````````````````````````````` example
|
||||
| abc | def |
|
||||
|---|---|
|
||||
| cde| ddd|
|
||||
| eee| fff|
|
||||
| fff | fffff |
|
||||
|gggg | ffff |
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>cde</td>
|
||||
<td>ddd</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>eee</td>
|
||||
<td>fff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>fff</td>
|
||||
<td>fffff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gggg</td>
|
||||
<td>ffff</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
** Normalized columns count **
|
||||
|
||||
The tables are normalized to the maximum number of columns found in a table
|
||||
|
||||
|
||||
```````````````````````````````` example
|
||||
a | b
|
||||
-- | -
|
||||
0 | 1 | 2
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
@@ -83,29 +83,6 @@ They are' not matching 'quotes
|
||||
.
|
||||
<p>They are' not matching 'quotes</p>
|
||||
````````````````````````````````
|
||||
|
||||
Double quotes using ``` `` ``` are working if they match another `''` pair, and there is no other double quotes on the line (otherwise they would be parsed as a code span):
|
||||
|
||||
```````````````````````````````` example
|
||||
This is ``a double quote''
|
||||
.
|
||||
<p>This is “a double quote”</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is ``a code span''``
|
||||
.
|
||||
<p>This is <code>a code span''</code></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
hello ``there```
|
||||
test
|
||||
.
|
||||
<p>hello “there”`
|
||||
test</p>
|
||||
````````````````````````````````
|
||||
|
||||
An emphasis starting inside left/right quotes will span over the right quote:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -133,3 +110,36 @@ This is a en ellipsis...
|
||||
.
|
||||
<p>This is a en ellipsis…</p>
|
||||
````````````````````````````````
|
||||
|
||||
Check that a smartypants are not breaking pipetable parsing:
|
||||
|
||||
```````````````````````````````` example
|
||||
a | b
|
||||
-- | --
|
||||
0 | 1
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
Check quotes and dash:
|
||||
|
||||
```````````````````````````````` example
|
||||
A "quote" with a ---
|
||||
.
|
||||
<p>A “quote” with a —</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,25 +38,30 @@ SOFTWARE.
|
||||
<#@ import namespace="System.CodeDom.Compiler" #>
|
||||
<#@ output extension=".cs" #><#
|
||||
var specFiles = new KeyValuePair<string, string>[] {
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics"),
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/791b1c121f16d3d7e80837c6f52917e57bbb2f61/spec.txt", string.Empty), // 0.27
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak|advanced+hardlinebreak"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis|advanced+emojis"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("BootstrapSpecs.md"), "bootstrap+pipetables+figures+attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "medialinks"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "smartypants"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "medialinks|advanced+medialinks"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "pipetables+smartypants|advanced+smartypants"), // Check with smartypants to make sure that it doesn't break pipetables
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("DiagramsSpecs.md"), "diagrams|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("NoHtmlSpecs.md"), "nohtml"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("YamlSpecs.md"), "yaml"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoLinks.md"), "autolinks|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("JiraLinks.md"), "jiralinks"),
|
||||
};
|
||||
var emptyLines = false;
|
||||
var displayEmptyLines = false;
|
||||
|
||||
25
src/Markdig.Tests/Specs/TestEmphasisPlus.cs
Normal file
25
src/Markdig.Tests/Specs/TestEmphasisPlus.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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 NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestEmphasisPlus
|
||||
{
|
||||
[Test]
|
||||
public void StrongNormal()
|
||||
{
|
||||
TestParser.TestSpec("***Strong emphasis*** normal", "<p><strong><em>Strong emphasis</em></strong> normal</p>", "");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalStrongNormal()
|
||||
{
|
||||
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <strong><em>Strong emphasis</em></strong> normal</p>", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Markdig.Tests/Specs/YamlSpecs.md
Normal file
44
src/Markdig.Tests/Specs/YamlSpecs.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Extensions
|
||||
|
||||
Adds support for YAML frontmatter parsing:
|
||||
|
||||
## YAML frontmatter discard
|
||||
|
||||
If a frontmatter is present, it will not be rendered:
|
||||
|
||||
```````````````````````````````` example
|
||||
---
|
||||
this: is a frontmatter
|
||||
---
|
||||
This is a text
|
||||
.
|
||||
<p>This is a text</p>
|
||||
````````````````````````````````
|
||||
|
||||
But if a frontmatter doesn't happen on the first line, it will be parse as regular Markdown content
|
||||
|
||||
```````````````````````````````` example
|
||||
This is a text1
|
||||
---
|
||||
this: is a frontmatter
|
||||
---
|
||||
This is a text2
|
||||
.
|
||||
<h2>This is a text1</h2>
|
||||
<h2>this: is a frontmatter</h2>
|
||||
<p>This is a text2</p>
|
||||
````````````````````````````````
|
||||
|
||||
It expects an exact 3 dashes `---`:
|
||||
|
||||
```````````````````````````````` example
|
||||
----
|
||||
this: is a frontmatter
|
||||
----
|
||||
This is a text
|
||||
.
|
||||
<hr />
|
||||
<h2>this: is a frontmatter</h2>
|
||||
<p>This is a text</p>
|
||||
````````````````````````````````
|
||||
|
||||
36
src/Markdig.Tests/Specs/readme.md
Normal file
36
src/Markdig.Tests/Specs/readme.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Documentation Tests/Specs
|
||||
|
||||
You will find from the following links the supported extensions in markdig and their usage:
|
||||
|
||||
- 2 kind of tables:
|
||||
- [**Pipe tables**](PipeTableSpecs.md)
|
||||
- [**Grid tables**](GridTableSpecs.md)
|
||||
- [**Extra emphasis**](EmphasisExtraSpecs.md)
|
||||
- strike through `~~`,
|
||||
- Subscript `~`
|
||||
- Superscript `^`
|
||||
- Inserted `++`
|
||||
- Marked `==`
|
||||
- [**Special attributes**](GenericAttributesSpecs.md)
|
||||
- [**Definition lists**](DefinitionListSpecs.md)
|
||||
- [**Footnotes**](FootnotesSpecs.md)
|
||||
- [**Auto-identifiers**](AutoIdentifierSpecs.md)
|
||||
- [**Auto-links**](AutoLinks.md)
|
||||
- [**Task Lists**](TaskListSpecs.md)
|
||||
- [**Extra bullet lists**](ListExtraSpecs.md)
|
||||
- [**Media support**](MediaSpecs.md)
|
||||
- [**Abbreviations**](AbbreviationSpecs.md)
|
||||
- [**Citation**](FigureFooterAndCiteSpecs.md)
|
||||
- [**Custom containers**](CustomContainerSpecs.md)
|
||||
- [**Figures**](FigureFooterAndCiteSpecs.md)
|
||||
- [**Footers**](FigureFooterAndCiteSpecs.md)
|
||||
- [**Mathematics**](MathSpecs.md)/Latex extension by enclosing `$$` for block and `$` for inline math (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/mathematics-extension/457/31))
|
||||
- [**Soft lines as hard lines**](HardlineBreakSpecs.md)
|
||||
- [**Emoji**](EmojiSpecs.md)
|
||||
- [**SmartyPants**](SmartyPantsSpecs.md)
|
||||
- [**Bootstrap**](BootstrapSpecs.md)
|
||||
- [**Diagrams**](DiagramsSpecs.md)
|
||||
- [**YAML frontmatter**](YamlSpecs.md)
|
||||
- [**JIRA links**](JiraLinks.md)
|
||||
|
||||
> Notice that the links above are not yet the final documentation but are "specification" files used for testing the correctness of markdig for each extension
|
||||
96
src/Markdig.Tests/TestHtmlAttributes.cs
Normal file
96
src/Markdig.Tests/TestHtmlAttributes.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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.Renderers.Html;
|
||||
using NUnit.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class TestHtmlAttributes
|
||||
{
|
||||
[Test]
|
||||
public void TestAddClass()
|
||||
{
|
||||
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("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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCopyTo()
|
||||
{
|
||||
var from = new HtmlAttributes();
|
||||
from.AddClass("test");
|
||||
from.AddProperty("key1", "1");
|
||||
|
||||
var to = new HtmlAttributes();
|
||||
from.CopyTo(to);
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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.Security;
|
||||
using NUnit.Framework;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
@@ -30,6 +32,18 @@ namespace Markdig.Tests
|
||||
Assert.AreEqual(')', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("http://google.com.")]
|
||||
[TestCase("http://google.com. ")]
|
||||
public void TestUrlTrailingFullStop(string uri)
|
||||
{
|
||||
var text = new StringSlice(uri);
|
||||
string link;
|
||||
Assert.True(LinkHelper.TryParseUrl(ref text, out link));
|
||||
Assert.AreEqual("http://google.com", link);
|
||||
Assert.AreEqual('.', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlNestedParenthesis()
|
||||
{
|
||||
@@ -297,5 +311,103 @@ namespace Markdig.Tests
|
||||
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("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", "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("一二三四五", "一二三四五")]
|
||||
[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));
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/Markdig.Tests/TestOrderedList.cs
Normal file
43
src/Markdig.Tests/TestOrderedList.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestOrderedList
|
||||
{
|
||||
[Test]
|
||||
public void TestReplace()
|
||||
{
|
||||
var list = new OrderedList<ITest>
|
||||
{
|
||||
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 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
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Markdig.Extensions.JiraLinks;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
@@ -15,25 +16,30 @@ namespace Markdig.Tests
|
||||
foreach (var pipeline in GetPipeline(extensions))
|
||||
{
|
||||
Console.WriteLine($"Pipeline configured with extensions: {pipeline.Key}");
|
||||
// Uncomment this line to get more debug information for process inlines.
|
||||
//pipeline.DebugLog = Console.Out;
|
||||
var result = Markdown.ToHtml(inputText, pipeline.Value);
|
||||
|
||||
result = Compact(result);
|
||||
expectedOutputText = Compact(expectedOutputText);
|
||||
|
||||
Console.WriteLine("```````````````````Source");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(inputText));
|
||||
Console.WriteLine("```````````````````Result");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(result));
|
||||
Console.WriteLine("```````````````````Expected");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(expectedOutputText));
|
||||
Console.WriteLine("```````````````````");
|
||||
Console.WriteLine();
|
||||
TextAssert.AreEqual(expectedOutputText, result);
|
||||
TestSpec(inputText, expectedOutputText, pipeline.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline)
|
||||
{
|
||||
// Uncomment this line to get more debug information for process inlines.
|
||||
//pipeline.DebugLog = Console.Out;
|
||||
var result = Markdown.ToHtml(inputText, pipeline);
|
||||
|
||||
result = Compact(result);
|
||||
expectedOutputText = Compact(expectedOutputText);
|
||||
|
||||
Console.WriteLine("```````````````````Source");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(inputText));
|
||||
Console.WriteLine("```````````````````Result");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(result));
|
||||
Console.WriteLine("```````````````````Expected");
|
||||
Console.WriteLine(DisplaySpaceAndTabs(expectedOutputText));
|
||||
Console.WriteLine("```````````````````");
|
||||
Console.WriteLine();
|
||||
TextAssert.AreEqual(expectedOutputText, result);
|
||||
}
|
||||
|
||||
private 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!
|
||||
@@ -64,8 +70,17 @@ namespace Markdig.Tests
|
||||
var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var extensionsText in extensionGroups)
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().Configure(extensionsText);
|
||||
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, pipeline.Build());
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
@@ -27,6 +30,44 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyLiteral()
|
||||
{
|
||||
var text = @"> *some text*
|
||||
> some other text";
|
||||
var doc = Markdown.Parse(text);
|
||||
|
||||
Assert.True(doc.Descendants().OfType<LiteralInline>().All(x => !x.Content.IsEmpty),
|
||||
"There should not have any empty literals");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelfPipeline1()
|
||||
{
|
||||
var text = @" <!--markdig:pipetables-->
|
||||
|
||||
a | b
|
||||
- | -
|
||||
0 | 1
|
||||
";
|
||||
TestParser.TestSpec(text, @"<!--markdig:pipetables-->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
", "self");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestListBug()
|
||||
{
|
||||
@@ -68,10 +109,21 @@ blabla
|
||||
<h1>header2</h1>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlh4Bug()
|
||||
{
|
||||
TestParser.TestSpec(@"<h4>foobar</h4>", @"<h4>foobar</h4>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStandardUriEscape()
|
||||
{
|
||||
TestParser.TestSpec(@"", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestBugAdvancaed()
|
||||
public void TestBugAdvanced()
|
||||
{
|
||||
TestParser.TestSpec(@"`https://{domain}/callbacks`
|
||||
#### HEADING
|
||||
@@ -79,6 +131,69 @@ 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 TestBugPipeTables()
|
||||
{
|
||||
// https://github.com/lunet-io/markdig/issues/73
|
||||
TestParser.TestSpec(@"| abc | def |
|
||||
| --- | --- |
|
||||
| 1 | ~3 |
|
||||
", @"<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>~3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>", "advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGridTableWithCustomAttributes() {
|
||||
|
||||
var input = @"
|
||||
{.table}
|
||||
+---+---+
|
||||
| a | b |
|
||||
+===+===+
|
||||
| 1 | 2 |
|
||||
+---+---+
|
||||
";
|
||||
|
||||
var expected = @"<table class=""table"">
|
||||
<col style=""width:50%"">
|
||||
<col style=""width:50%"">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, "advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePipelineAllExtensions()
|
||||
{
|
||||
|
||||
@@ -88,9 +88,9 @@ literal ( 0, 5) 5-11
|
||||
Check("01*2**3*", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
literal ( 0, 0) 0-1
|
||||
emphasis ( 0, 2) 2-4
|
||||
emphasis ( 0, 2) 2-7
|
||||
literal ( 0, 3) 3-3
|
||||
emphasis ( 0, 5) 5-7
|
||||
literal ( 0, 4) 4-5
|
||||
literal ( 0, 6) 6-6
|
||||
");
|
||||
}
|
||||
@@ -293,10 +293,6 @@ literal ( 0, 9) 9-9
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestThematicBreak()
|
||||
{
|
||||
@@ -373,12 +369,13 @@ literal ( 0, 2) 2-3
|
||||
[Test]
|
||||
public void TestHtmlEntityInline()
|
||||
{
|
||||
// 01234567
|
||||
Check("0 1", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
// 01 23456789
|
||||
Check("0\n 1", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-0
|
||||
htmlentity ( 0, 1) 1-6
|
||||
literal ( 0, 7) 7-7
|
||||
linebreak ( 0, 1) 1-1
|
||||
htmlentity ( 1, 0) 2-7
|
||||
literal ( 1, 6) 8-9
|
||||
");
|
||||
}
|
||||
|
||||
@@ -658,7 +655,21 @@ literal ( 4, 2) 13-13
|
||||
Check("0\n\n 0\n 1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 7-13
|
||||
code ( 2, 0) 3-13
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCodeAfterList()
|
||||
{
|
||||
// 0 1 2 3 4 5
|
||||
// 012345678901234567 8 901234567890123456 789012345678901234 56789
|
||||
Check("1) Some list item\n\n some code\n more code\n", @"
|
||||
list ( 0, 0) 0-53
|
||||
listitem ( 0, 0) 0-53
|
||||
paragraph ( 0, 3) 3-16
|
||||
literal ( 0, 3) 3-16
|
||||
code ( 2, 0) 19-53
|
||||
");
|
||||
}
|
||||
|
||||
@@ -669,7 +680,7 @@ code ( 2, 4) 7-13
|
||||
Check("0\n\n\t0\n\t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 4-7
|
||||
code ( 2, 0) 3-7
|
||||
");
|
||||
}
|
||||
|
||||
@@ -680,7 +691,7 @@ code ( 2, 4) 4-7
|
||||
Check("0\n\n \t0\n \t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 5-9
|
||||
code ( 2, 0) 3-9
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"NUnit": "3.2.0"
|
||||
"NUnit": "3.2.0",
|
||||
"NUnit3TestAdapter": "3.2.0"
|
||||
}
|
||||
}
|
||||
36
src/Markdig.WebApp/Markdig.WebApp.csproj
Normal file
36
src/Markdig.WebApp/Markdig.WebApp.csproj
Normal file
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp1.0</TargetFramework>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<AssemblyName>Markdig.WebApp</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<PackageId>Markdig.WebApp</PackageId>
|
||||
<RuntimeFrameworkVersion>1.0.4</RuntimeFrameworkVersion>
|
||||
<PackageTargetFallback>$(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8</PackageTargetFallback>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Views">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Markdig\Markdig.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>3cad9801-9976-46be-baca-f6d0d21fdc00</ProjectGuid>
|
||||
<RootNamespace>Markdig.WebApp</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.0.0-rc2-3002702",
|
||||
"type": "platform"
|
||||
},
|
||||
"Microsoft.ApplicationInsights.AspNetCore": "1.0.0-rc2-final",
|
||||
"Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final",
|
||||
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final",
|
||||
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Logging": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final",
|
||||
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-final",
|
||||
"Markdig": "0.2.1"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
|
||||
"version": "1.0.0-preview1-final",
|
||||
"imports": "portable-net45+win8+dnxcore50"
|
||||
}
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"imports": [
|
||||
"dotnet5.6",
|
||||
"dnxcore50",
|
||||
"portable-net45+win8"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true,
|
||||
"preserveCompilationContext": true
|
||||
},
|
||||
|
||||
"runtimeOptions": {
|
||||
"gcServer": true
|
||||
},
|
||||
|
||||
"publishOptions": {
|
||||
"include": [
|
||||
"wwwroot",
|
||||
"Views",
|
||||
"appsettings.json",
|
||||
"web.config"
|
||||
]
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace Markdig.Extensions.Abbreviations
|
||||
pipeline.BlockParsers.AddIfNotAlready<AbbreviationParser>();
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null && !htmlRenderer.ObjectRenderers.Contains<HtmlAbbreviationRenderer>())
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Markdig.Extensions.Abbreviations
|
||||
/// </summary>
|
||||
public AbbreviationParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'*'};
|
||||
OpeningCharacters = new[] { '*' };
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -90,7 +90,7 @@ namespace Markdig.Extensions.Abbreviations
|
||||
|
||||
inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) =>
|
||||
{
|
||||
var literal = (LiteralInline) processor.Inline;
|
||||
var literal = (LiteralInline)processor.Inline;
|
||||
var originalLiteral = literal;
|
||||
|
||||
ContainerInline container = null;
|
||||
@@ -101,20 +101,9 @@ namespace Markdig.Extensions.Abbreviations
|
||||
for (int i = content.Start; i < content.End; i++)
|
||||
{
|
||||
string match;
|
||||
if (matcher.TryMatch(text, i, content.End - i + 1, out match))
|
||||
if (matcher.TryMatch(text, i, content.End - i + 1, out match) && IsValidAbbreviation(match, content, i))
|
||||
{
|
||||
// The word matched must be embraced by punctuation or whitespace or \0.
|
||||
var c = content.PeekCharAbsolute(i - 1);
|
||||
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var indexAfterMatch = i + match.Length;
|
||||
c = content.PeekCharAbsolute(indexAfterMatch);
|
||||
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We should have a match, but in case...
|
||||
Abbreviation abbr;
|
||||
@@ -126,12 +115,13 @@ namespace Markdig.Extensions.Abbreviations
|
||||
// If we don't have a container, create a new one
|
||||
if (container == null)
|
||||
{
|
||||
container = new ContainerInline()
|
||||
{
|
||||
Span = originalLiteral.Span,
|
||||
Line = originalLiteral.Line,
|
||||
Column = originalLiteral.Column,
|
||||
};
|
||||
container = literal.Parent ??
|
||||
new ContainerInline
|
||||
{
|
||||
Span = originalLiteral.Span,
|
||||
Line = originalLiteral.Line,
|
||||
Column = originalLiteral.Column,
|
||||
};
|
||||
}
|
||||
|
||||
int line;
|
||||
@@ -150,13 +140,18 @@ namespace Markdig.Extensions.Abbreviations
|
||||
// Append the previous literal
|
||||
if (i > content.Start)
|
||||
{
|
||||
container.AppendChild(literal);
|
||||
if (literal.Parent == null)
|
||||
{
|
||||
container.AppendChild(literal);
|
||||
}
|
||||
|
||||
literal.Span.End = abbrInline.Span.Start - 1;
|
||||
// Truncate it before the abbreviation
|
||||
literal.Content.End = i - 1;
|
||||
}
|
||||
|
||||
literal.Span.End = abbrInline.Span.Start - 1;
|
||||
// Truncate it before the abbreviation
|
||||
literal.Content.End = i - 1;
|
||||
|
||||
|
||||
// Appned the abbreviation
|
||||
container.AppendChild(abbrInline);
|
||||
|
||||
@@ -192,5 +187,55 @@ namespace Markdig.Extensions.Abbreviations
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsValidAbbreviation(string match, StringSlice content, int matchIndex)
|
||||
{
|
||||
// The word matched must be embraced by punctuation or whitespace or \0.
|
||||
var index = matchIndex - 1;
|
||||
while (index >= content.Start)
|
||||
{
|
||||
var c = content.PeekCharAbsolute(index);
|
||||
if (!(c == '\0' || c.IsWhitespace() || c.IsAsciiPunctuation()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c.IsAlphaNumeric())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!c.IsAsciiPunctuation() || c.IsWhitespace())
|
||||
{
|
||||
break;
|
||||
}
|
||||
index--;
|
||||
}
|
||||
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ namespace Markdig.Extensions.AutoIdentifiers
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -132,6 +132,13 @@ namespace Markdig.Extensions.AutoIdentifiers
|
||||
return;
|
||||
}
|
||||
|
||||
// If id is already set, don't try to modify it
|
||||
var attributes = processor.Block.GetAttributes();
|
||||
if (attributes.Id != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a HtmlRenderer with
|
||||
stripRenderer.Render(headingBlock.Inline);
|
||||
var headingText = headingWriter.ToString();
|
||||
@@ -152,7 +159,7 @@ namespace Markdig.Extensions.AutoIdentifiers
|
||||
headingBuffer.Length = 0;
|
||||
}
|
||||
|
||||
processor.Block.GetAttributes().Id = headingId;
|
||||
attributes.Id = headingId;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs
Normal file
29
src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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.Renderers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.AutoLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to automatically create <see cref="LinkInline"/> when a link url http: or mailto: is found.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class AutoLinkExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
if (!pipeline.InlineParsers.Contains<AutoLinkParser>())
|
||||
{
|
||||
// Insert the parser before any other parsers
|
||||
pipeline.InlineParsers.Insert(0, new AutoLinkParser());
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs
Normal file
239
src/Markdig/Extensions/AutoLinks/AutoLinkParser.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
// 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 Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.AutoLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// The inline parser used to for autolinks.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.InlineParser" />
|
||||
public class AutoLinkParser : InlineParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoLinkParser"/> class.
|
||||
/// </summary>
|
||||
public AutoLinkParser()
|
||||
{
|
||||
OpeningCharacters = new char[]
|
||||
{
|
||||
'h', // for http:// and https://
|
||||
'f', // for ftp://
|
||||
'm', // for mailto:
|
||||
'w', // for www.
|
||||
};
|
||||
}
|
||||
|
||||
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.IsAsciiPunctuation() && !previousChar.IsWhiteSpaceOrZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
List<char> pendingEmphasis;
|
||||
// Check that an autolink is possible in the current context
|
||||
if (!IsAutoLinkValidInCurrentContext(processor, out pendingEmphasis))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startPosition = slice.Start;
|
||||
|
||||
var c = slice.CurrentChar;
|
||||
// Precheck URL
|
||||
switch (c)
|
||||
{
|
||||
case 'h':
|
||||
if (!slice.MatchLowercase("ttp://", 1) && !slice.MatchLowercase("ttps://", 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
if (!slice.MatchLowercase("tp://", 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
if (!slice.MatchLowercase("ailto:", 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
if (!slice.MatchLowercase("ww.", 1) || previousChar == '/') // We won't match http:/www. or /www.xxx
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse URL
|
||||
string link;
|
||||
if (!LinkHelper.TryParseUrl(ref slice, out link))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// If we have any pending emphasis, remove any pending emphasis characters from the end of the link
|
||||
if (pendingEmphasis != null)
|
||||
{
|
||||
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 'm':
|
||||
if (string.Equals(link, "mailto:", StringComparison.OrdinalIgnoreCase) || !link.Contains("@"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
// We require at least two .
|
||||
if (link.Length <= "www.x.y".Length || link.IndexOf(".", 4, StringComparison.Ordinal) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int line;
|
||||
int column;
|
||||
var inline = new LinkInline()
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(startPosition, out line, out column),
|
||||
},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Url = c == 'w' ? "http://" + link : link,
|
||||
IsClosed = true,
|
||||
};
|
||||
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, startPosition + link.Length - 1),
|
||||
IsClosed = true
|
||||
});
|
||||
processor.Inline = inline;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsAutoLinkValidInCurrentContext(InlineProcessor processor, out List<char> pendingEmphasis)
|
||||
{
|
||||
pendingEmphasis = null;
|
||||
|
||||
// Case where there is a pending HtmlInline <a>
|
||||
var currentInline = processor.Inline;
|
||||
while (currentInline != null)
|
||||
{
|
||||
var htmlInline = currentInline as HtmlInline;
|
||||
if (htmlInline != null)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
var linkDelimiterInline = currentInline as LinkDelimiterInline;
|
||||
if (linkDelimiterInline != null && linkDelimiterInline.IsActive)
|
||||
{
|
||||
if (linkDelimiterInline.Type == DelimiterType.Open)
|
||||
{
|
||||
countBrackets++;
|
||||
}
|
||||
else if (linkDelimiterInline.Type == DelimiterType.Close)
|
||||
{
|
||||
countBrackets--;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Record all pending characters for emphasis
|
||||
var emphasisDelimiter = currentInline as EmphasisDelimiterInline;
|
||||
if (emphasisDelimiter != null)
|
||||
{
|
||||
if (pendingEmphasis == null)
|
||||
{
|
||||
// Not optimized for GC, but we don't expect this case much
|
||||
pendingEmphasis = new List<char>();
|
||||
}
|
||||
if (!pendingEmphasis.Contains(emphasisDelimiter.DelimiterChar))
|
||||
{
|
||||
pendingEmphasis.Add(emphasisDelimiter.DelimiterChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
currentInline = currentInline.Parent;
|
||||
}
|
||||
|
||||
return countBrackets <= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Markdig.Extensions.Bootstrap
|
||||
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Markdig.Extensions.Citations
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Markdig.Extensions.CustomContainers
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -51,8 +51,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
}
|
||||
|
||||
var previousParent = paragraphBlock.Parent;
|
||||
var indexOfParagraph = previousParent.IndexOf(paragraphBlock);
|
||||
var currentDefinitionList = indexOfParagraph - 1 >= 0 ? previousParent[indexOfParagraph - 1] as DefinitionList : null;
|
||||
var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent);
|
||||
|
||||
processor.Discard(paragraphBlock);
|
||||
|
||||
@@ -103,6 +102,19 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
private static DefinitionList GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent)
|
||||
{
|
||||
var index = previousParent.IndexOf(paragraphBlock) - 1;
|
||||
if (index < 0) return null;
|
||||
var lastBlock = previousParent[index];
|
||||
if (lastBlock is BlankLineBlock)
|
||||
{
|
||||
lastBlock = previousParent[index - 1];
|
||||
previousParent.RemoveAt(index);
|
||||
}
|
||||
return lastBlock as DefinitionList;
|
||||
}
|
||||
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var definitionItem = (DefinitionItem)block;
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
{
|
||||
if (!hasOpendd)
|
||||
{
|
||||
renderer.Write("<dd>");
|
||||
renderer.Write("<dd").WriteAttributes(definitionItem).Write(">");
|
||||
countdd = 0;
|
||||
hasOpendd = true;
|
||||
}
|
||||
|
||||
31
src/Markdig/Extensions/Diagrams/DiagramExtension.cs
Normal file
31
src/Markdig/Extensions/Diagrams/DiagramExtension.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.Diagrams
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to allow diagrams.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class DiagramExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var codeRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>();
|
||||
// TODO: Add other well known diagram languages
|
||||
codeRenderer.BlocksAsDiv.Add("mermaid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Markdig.Extensions.Emoji
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Markdig.Extensions.Emoji
|
||||
/// </summary>
|
||||
public Dictionary<string, string> SmileyToEmoji { get; }
|
||||
|
||||
public override void Initialize(InlineProcessor processor)
|
||||
public override void Initialize()
|
||||
{
|
||||
var firstChars = new HashSet<char>();
|
||||
var textToMatch = new HashSet<string>();
|
||||
@@ -67,18 +67,28 @@ namespace Markdig.Extensions.Emoji
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
string match;
|
||||
|
||||
// Previous char must be a space
|
||||
if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to match an existing emoji
|
||||
var startPosition = slice.Start;
|
||||
if (!textMatchHelper.TryMatch(slice.Text, slice.Start, slice.Length, out match))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a smiley, we decode it to emoji
|
||||
string emoji;
|
||||
if (!SmileyToEmoji.TryGetValue(match, out emoji))
|
||||
{
|
||||
emoji = match;
|
||||
}
|
||||
|
||||
// Decode the eomji to unicode
|
||||
string unicode;
|
||||
if (!EmojiToUnicode.TryGetValue(emoji, out unicode))
|
||||
{
|
||||
@@ -981,7 +991,16 @@ namespace Markdig.Extensions.Emoji
|
||||
{":large_orange_diamond:", "🔶"},
|
||||
{":large_blue_diamond:", "🔷"},
|
||||
{":small_orange_diamond:", "🔸"},
|
||||
{":small_blue_diamond:", "🔹"}
|
||||
{":small_blue_diamond:", "🔹"},
|
||||
|
||||
// Custom additions
|
||||
{ ":custom_arrow_left:", "←"},
|
||||
{ ":custom_arrow_right:", "→"},
|
||||
{ ":custom_arrow_left_right:", "↔"},
|
||||
|
||||
{ ":custom_arrow_left_strong:", "⇐"},
|
||||
{ ":custom_arrow_right_strong:", "⇒"},
|
||||
{ ":custom_arrow_left_right_strong:", "⇔"},
|
||||
};
|
||||
|
||||
SmileyToEmojiDefault = new Dictionary<string, string>()
|
||||
@@ -1051,6 +1070,15 @@ namespace Markdig.Extensions.Emoji
|
||||
{":-$", ":unamused:"},
|
||||
{";)", ":wink:"},
|
||||
{";-)", ":wink:"},
|
||||
|
||||
// Custom arrows
|
||||
{"<-", ":custom_arrow_left:" },
|
||||
{"->", ":custom_arrow_rigth:" },
|
||||
{"<->", ":custom_arrow_left_rigth:" },
|
||||
|
||||
{"<=", ":custom_arrow_left_strong:" },
|
||||
{"=>", ":custom_arrow_rigth_strong:" },
|
||||
{"<=>", ":custom_arrow_left_rigth_strong:" },
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace Markdig.Extensions.EmphasisExtras
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Markdig.Extensions.Figures
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Markdig.Extensions.Footers
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Markdig.Extensions.Footnotes
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -26,9 +26,14 @@ namespace Markdig.Extensions.Footnotes
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
return TryOpen(processor, false);
|
||||
}
|
||||
|
||||
private BlockState TryOpen(BlockProcessor processor, bool isContinue)
|
||||
{
|
||||
// We expect footnote to appear only at document level and not indented more than a code indent block
|
||||
if (processor.IsCodeIndent || processor.CurrentContainer.GetType() != typeof(MarkdownDocument) )
|
||||
if (processor.IsCodeIndent || (!isContinue && processor.CurrentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(processor.CurrentContainer is Footnote)))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
@@ -42,7 +47,7 @@ namespace Markdig.Extensions.Footnotes
|
||||
processor.GoToColumn(saved);
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
|
||||
// Advance the column
|
||||
int deltaColumn = processor.Start - start;
|
||||
processor.Column = processor.Column + deltaColumn;
|
||||
@@ -88,9 +93,23 @@ namespace Markdig.Extensions.Footnotes
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
if (footnote.IsLastLineEmpty && processor.Column == 0)
|
||||
if (processor.Column == 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
if (footnote.IsLastLineEmpty)
|
||||
{
|
||||
// Close the current footnote
|
||||
processor.Close(footnote);
|
||||
|
||||
// Parse any opening footnote
|
||||
return TryOpen(processor);
|
||||
}
|
||||
|
||||
// Make sure that consecutive footnotes without a blanklines are parsed correctly
|
||||
if (TryOpen(processor, true) == BlockState.Continue)
|
||||
{
|
||||
processor.Close(footnote);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
footnote.IsLastLineEmpty = false;
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,12 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
{
|
||||
objectToAttach = parent[indexOfParagraph + 1];
|
||||
// We can remove the paragraph as it is empty
|
||||
parent.RemoveAt(indexOfParagraph);
|
||||
paragraph.RemoveAfterProcessInlines = true;
|
||||
}
|
||||
}
|
||||
|
||||
var currentHtmlAttributes = objectToAttach.GetAttributes();
|
||||
attributes.CopyTo(currentHtmlAttributes);
|
||||
attributes.CopyTo(currentHtmlAttributes, true, false);
|
||||
|
||||
// Update the position of the attributes
|
||||
int line;
|
||||
@@ -171,9 +171,10 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
|
||||
// Skip any whitespaces
|
||||
line.TrimStart();
|
||||
c = line.CurrentChar;
|
||||
|
||||
// Handle boolean properties that are not followed by =
|
||||
if ((hasSpace && (line.CurrentChar == '.' || line.CurrentChar == '#' || IsStartAttributeName(line.CurrentChar))) || line.CurrentChar == '}')
|
||||
if ((hasSpace && (c == '.' || c == '#' || IsStartAttributeName(c))) || c == '}')
|
||||
{
|
||||
if (properties == null)
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Markdig.Extensions.Hardlines
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
31
src/Markdig/Extensions/JiraLinks/JiraLink.cs
Normal file
31
src/Markdig/Extensions/JiraLinks/JiraLink.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
using System.Diagnostics;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Model for a JIRA link item
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{ProjectKey}-{Issue}")]
|
||||
public class JiraLink : LinkInline
|
||||
{
|
||||
public JiraLink()
|
||||
{
|
||||
IsClosed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JIRA Project Key
|
||||
/// </summary>
|
||||
public StringSlice ProjectKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JIRA Issue Number
|
||||
/// </summary>
|
||||
public StringSlice Issue { get; set; }
|
||||
}
|
||||
}
|
||||
37
src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs
Normal file
37
src/Markdig/Extensions/JiraLinks/JiraLinkExtension.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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.Parsers.Inlines;
|
||||
using Markdig.Renderers;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple inline parser extension for Markdig to find, and
|
||||
/// automatically add links to JIRA issue numbers.
|
||||
/// </summary>
|
||||
public class JiraLinkExtension : IMarkdownExtension
|
||||
{
|
||||
private readonly JiraLinkOptions _options;
|
||||
|
||||
public JiraLinkExtension(JiraLinkOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
if (!pipeline.InlineParsers.Contains<JiraLinkInlineParser>())
|
||||
{
|
||||
// Insert the parser before the link inline parser
|
||||
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new JiraLinkInlineParser(_options));
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
// Nothing to setup, JiraLinks used a normal LinkInlineRenderer
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
115
src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs
Normal file
115
src/Markdig/Extensions/JiraLinks/JiraLinkInlineParser.cs
Normal file
@@ -0,0 +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.Text;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds and replaces JIRA links inline
|
||||
/// </summary>
|
||||
public class JiraLinkInlineParser : InlineParser
|
||||
{
|
||||
private readonly JiraLinkOptions _options;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public JiraLinkInlineParser(JiraLinkOptions options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_baseUrl = _options.GetUrl();
|
||||
//look for uppercase chars at the start (for the project key)
|
||||
OpeningCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Allow preceding whitespace or `(`
|
||||
var pc = slice.PeekCharExtra(-1);
|
||||
if (!pc.IsWhiteSpaceOrZero() && pc != '(')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = slice.CurrentChar;
|
||||
|
||||
var startKey = slice.Start;
|
||||
var endKey = slice.Start;
|
||||
|
||||
//read as many uppercase characters as required - project key
|
||||
while (current.IsAlphaUpper())
|
||||
{
|
||||
endKey = slice.Start;
|
||||
current = slice.NextChar();
|
||||
}
|
||||
|
||||
//require a '-' between key and issue number
|
||||
if (!current.Equals('-'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = slice.NextChar(); // skip -
|
||||
|
||||
//read as many numbers as required - issue number
|
||||
if (!current.IsDigit())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startIssue = slice.Start;
|
||||
var endIssue = slice.Start;
|
||||
|
||||
while (current.IsDigit())
|
||||
{
|
||||
endIssue = slice.Start;
|
||||
current = slice.NextChar();
|
||||
}
|
||||
|
||||
if (!current.IsWhiteSpaceOrZero() && current != ')') //can be followed only by a whitespace or `)`
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int line;
|
||||
int column;
|
||||
|
||||
var jiraLink = new JiraLink() //create the link at the relevant position
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(slice.Start, out line, out column)
|
||||
},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Issue = new StringSlice(slice.Text, startIssue, endIssue),
|
||||
ProjectKey = new StringSlice(slice.Text, startKey, endKey),
|
||||
};
|
||||
jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey);
|
||||
|
||||
// Builds the Url
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(_baseUrl).Append('/').Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
|
||||
jiraLink.Url = builder.ToString();
|
||||
|
||||
// Builds the Label
|
||||
builder.Length = 0;
|
||||
builder.Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
|
||||
jiraLink.AppendChild(new LiteralInline(builder.ToString()));
|
||||
|
||||
if (_options.OpenInNewWindow)
|
||||
{
|
||||
jiraLink.GetAttributes().AddProperty("target", "blank");
|
||||
}
|
||||
|
||||
processor.Inline = jiraLink;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
57
src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs
Normal file
57
src/Markdig/Extensions/JiraLinks/JiraLinkOptions.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
using System.Text;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Available options for replacing JIRA links
|
||||
/// </summary>
|
||||
public class JiraLinkOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The base Url (e.g. `https://mycompany.atlassian.net`)
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The base path after the base url (default is `/browse`)
|
||||
/// </summary>
|
||||
public string BasePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the link open in a new window when clicked
|
||||
/// </summary>
|
||||
public bool OpenInNewWindow { get; set; }
|
||||
|
||||
public JiraLinkOptions(string baseUrl)
|
||||
{
|
||||
OpenInNewWindow = true; //default
|
||||
BaseUrl = baseUrl;
|
||||
BasePath = "/browse";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full url composed of the <see cref="BaseUrl"/> and <see cref="BasePath"/> with no trailing `/`
|
||||
/// </summary>
|
||||
public virtual string GetUrl()
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
var baseUrl = BaseUrl;
|
||||
if (baseUrl != null)
|
||||
{
|
||||
url.Append(baseUrl.TrimEnd('/'));
|
||||
}
|
||||
|
||||
url.Append("/");
|
||||
|
||||
if (BasePath != null)
|
||||
{
|
||||
url.Append(BasePath.Trim('/'));
|
||||
}
|
||||
|
||||
return url.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ namespace Markdig.Extensions.ListExtras
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using Markdig.Parsers;
|
||||
|
||||
namespace Markdig.Extensions.ListExtras
|
||||
{
|
||||
using System;
|
||||
|
||||
/// <summary>
|
||||
/// Parser that adds supports for parsing alpha/roman list items (e.g: `a)` or `a.` or `ii.` or `II.`)
|
||||
/// </summary>
|
||||
@@ -52,7 +54,7 @@ namespace Markdig.Extensions.ListExtras
|
||||
c = state.NextChar();
|
||||
}
|
||||
|
||||
result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1);
|
||||
result.OrderedStart = CharHelper.RomanToArabic(state.Line.Text.Substring(startChar, endChar - startChar + 1)).ToString();
|
||||
result.BulletType = isRomanLow ? 'i' : 'I';
|
||||
result.DefaultOrderedStart = isRomanLow ? "i" : "I";
|
||||
}
|
||||
@@ -60,7 +62,7 @@ namespace Markdig.Extensions.ListExtras
|
||||
{
|
||||
// otherwise we expect a regular alpha lettered list with a single character.
|
||||
var isUpper = c.IsAlphaUpper();
|
||||
result.OrderedStart = c.ToString();
|
||||
result.OrderedStart = (Char.ToUpperInvariant(c) - 64).ToString();
|
||||
result.BulletType = isUpper ? 'A' : 'a';
|
||||
result.DefaultOrderedStart = isUpper ? "A" : "a";
|
||||
state.NextChar();
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Mathematics
|
||||
{
|
||||
/// <summary>
|
||||
/// The block parser for a <see cref="MathBlock"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.FencedBlockParserBase{Markdig.Extensions.Mathematics.MathBlock}" />
|
||||
/// <seealso cref="MathBlock" />
|
||||
public class MathBlockParser : FencedBlockParserBase<MathBlock>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -22,6 +25,8 @@ namespace Markdig.Extensions.Mathematics
|
||||
MinimumMatchCount = 2;
|
||||
MaximumMatchCount = 2;
|
||||
|
||||
InfoParser = NoInfoParser;
|
||||
|
||||
DefaultClass = "math";
|
||||
|
||||
// We don't need a prefix
|
||||
@@ -39,5 +44,19 @@ namespace Markdig.Extensions.Mathematics
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private static bool NoInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced)
|
||||
{
|
||||
var c = line.CurrentChar;
|
||||
for (int i = line.Start; i <= line.End; i++)
|
||||
{
|
||||
c = line.Text[i];
|
||||
if (!c.IsSpaceOrTab())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace Markdig.Extensions.Mathematics
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Markdig.Extensions.Mathematics
|
||||
/// An inline parser for <see cref="MathInline"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.InlineParser" />
|
||||
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
|
||||
/// <seealso cref="IPostInlineProcessor" />
|
||||
public class MathInlineParser : InlineParser
|
||||
{
|
||||
/// <summary>
|
||||
@@ -50,11 +50,18 @@ namespace Markdig.Extensions.Mathematics
|
||||
c = slice.NextChar();
|
||||
}
|
||||
|
||||
bool canOpen;
|
||||
bool canClose;
|
||||
// Check that opening $/$$ is correct, using the same heuristics than for emphasis delimiters
|
||||
CharHelper.CheckOpenCloseDelimiter(pc, c, false, out canOpen, out canClose);
|
||||
if (!canOpen)
|
||||
bool openPrevIsPunctuation;
|
||||
bool openPrevIsWhiteSpace;
|
||||
bool openNextIsPunctuation;
|
||||
bool openNextIsWhiteSpace;
|
||||
bool openNextIsDigit = c.IsDigit();
|
||||
pc.CheckUnicodeCategory(out openPrevIsWhiteSpace, out openPrevIsPunctuation);
|
||||
c.CheckUnicodeCategory(out openNextIsWhiteSpace, out openNextIsPunctuation);
|
||||
|
||||
// Check that opening $/$$ is correct, using the different heuristics than for emphasis delimiters
|
||||
// If a $/$$ is not preceded by a whitespace or punctuation, or followed by a digit
|
||||
// this is a not a math block
|
||||
if ((!openPrevIsWhiteSpace && !openPrevIsPunctuation) || openNextIsDigit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -62,24 +69,60 @@ namespace Markdig.Extensions.Mathematics
|
||||
bool isMatching = false;
|
||||
int closeDollars = 0;
|
||||
|
||||
// Eat any leading spaces
|
||||
while (c.IsSpaceOrTab())
|
||||
{
|
||||
c = slice.NextChar();
|
||||
}
|
||||
|
||||
var start = slice.Start;
|
||||
var end = 0;
|
||||
|
||||
pc = match;
|
||||
var lastWhiteSpace = -1;
|
||||
while (c != '\0')
|
||||
{
|
||||
// Don't allow newline in an inline math expression
|
||||
if (c == '\r' || c == '\n')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't process sticks if we have a '\' as a previous char
|
||||
if (pc != '\\' )
|
||||
{
|
||||
while (c == match)
|
||||
// Record continous whitespaces at the end
|
||||
if (c.IsSpaceOrTab())
|
||||
{
|
||||
closeDollars++;
|
||||
c = slice.NextChar();
|
||||
if (lastWhiteSpace < 0)
|
||||
{
|
||||
lastWhiteSpace = slice.Start;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool hasClosingDollars = c == match;
|
||||
if (hasClosingDollars)
|
||||
{
|
||||
while (c == match)
|
||||
{
|
||||
closeDollars++;
|
||||
c = slice.NextChar();
|
||||
}
|
||||
}
|
||||
|
||||
if (closeDollars >= openDollars)
|
||||
{
|
||||
break;
|
||||
if (closeDollars >= openDollars)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lastWhiteSpace = -1;
|
||||
if (hasClosingDollars)
|
||||
{
|
||||
pc = match;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pc = match;
|
||||
}
|
||||
|
||||
if (closeDollars > 0)
|
||||
@@ -95,13 +138,30 @@ namespace Markdig.Extensions.Mathematics
|
||||
|
||||
if (closeDollars >= openDollars)
|
||||
{
|
||||
// Check that closing $/$$ is correct
|
||||
CharHelper.CheckOpenCloseDelimiter(pc, c, false, out canOpen, out canClose);
|
||||
if (!canClose || c.IsDigit())
|
||||
bool closePrevIsPunctuation;
|
||||
bool closePrevIsWhiteSpace;
|
||||
bool closeNextIsPunctuation;
|
||||
bool closeNextIsWhiteSpace;
|
||||
pc.CheckUnicodeCategory(out closePrevIsWhiteSpace, out closePrevIsPunctuation);
|
||||
c.CheckUnicodeCategory(out closeNextIsWhiteSpace, out closeNextIsPunctuation);
|
||||
|
||||
// A closing $/$$ should be followed by at least a punctuation or a whitespace
|
||||
// and if the character after an openning $/$$ was a whitespace, it should be
|
||||
// a whitespace as well for the character preceding the closing of $/$$
|
||||
if ((!closeNextIsPunctuation && !closeNextIsWhiteSpace) || (openNextIsWhiteSpace != closePrevIsWhiteSpace))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (closePrevIsWhiteSpace && lastWhiteSpace > 0)
|
||||
{
|
||||
end = lastWhiteSpace + openDollars - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
end = slice.Start - 1;
|
||||
}
|
||||
|
||||
// Create a new MathInline
|
||||
int line;
|
||||
int column;
|
||||
@@ -116,7 +176,7 @@ namespace Markdig.Extensions.Mathematics
|
||||
};
|
||||
inline.Content.Start = start;
|
||||
// We substract the end to the number of opening $ to keep inside the block the additionals $
|
||||
inline.Content.End = inline.Content.End - openDollars;
|
||||
inline.Content.End = end - openDollars;
|
||||
|
||||
// Add the default class if necessary
|
||||
if (DefaultClass != null)
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Markdig.Extensions.MediaLinks
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
@@ -115,7 +115,6 @@ namespace Markdig.Extensions.MediaLinks
|
||||
renderer.WriteAttributes(htmlAttributes);
|
||||
|
||||
renderer.Write($"><source type=\"{mimeType}\" src=\"{linkInline.Url}\"></source></{tagType}>");
|
||||
renderer.Write("</iframe>");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
34
src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs
Normal file
34
src/Markdig/Extensions/NoRefLinks/NoFollowLinksExtension.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.NoRefLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to automatically render rel=nofollow to all links in an HTML output.
|
||||
/// </summary>
|
||||
public class NoFollowLinksExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var linkRenderer = renderer.ObjectRenderers.Find<LinkInlineRenderer>();
|
||||
if (linkRenderer != null)
|
||||
{
|
||||
linkRenderer.AutoRelNoFollow = true;
|
||||
}
|
||||
|
||||
var autolinkRenderer = renderer.ObjectRenderers.Find<AutolinkInlineRenderer>();
|
||||
if (autolinkRenderer != null)
|
||||
{
|
||||
autolinkRenderer.AutoRelNoFollow = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Renderers;
|
||||
|
||||
namespace Markdig.Extensions.NonAsciiNoEscape
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension that will disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE.
|
||||
/// </summary>
|
||||
public class NonAsciiNoEscapeExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
htmlRenderer.UseNonAsciiNoEscape = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Markdig.Extensions.PragmaLines
|
||||
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
99
src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs
Normal file
99
src/Markdig/Extensions/SelfPipeline/SelfPipelineExtension.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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 Markdig.Renderers;
|
||||
|
||||
namespace Markdig.Extensions.SelfPipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to enable SelfPipeline, to configure a Markdown parsing/convertion to HTML automatically
|
||||
/// from an embedded special tag in the input text <code><!--markdig:extensions--></code> where extensions is a string
|
||||
/// that specifies the extensions to use for the pipeline as exposed by <see cref="MarkdownExtensions.Configure"/> extension method
|
||||
/// on the <see cref="MarkdownPipelineBuilder"/>. This extension will invalidate all other extensions and will override them.
|
||||
/// </summary>
|
||||
public sealed class SelfPipelineExtension : IMarkdownExtension
|
||||
{
|
||||
public const string DefaultTag = "markdig";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SelfPipelineExtension"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tag">The matching start tag.</param>
|
||||
/// <param name="defaultExtensions">The default extensions.</param>
|
||||
/// <exception cref="System.ArgumentException">Tag cannot contain `<` or `>` characters</exception>
|
||||
public SelfPipelineExtension(string tag = null, string defaultExtensions = null)
|
||||
{
|
||||
tag = tag?.Trim();
|
||||
tag = string.IsNullOrEmpty(tag) ? DefaultTag : tag;
|
||||
if (tag.IndexOfAny(new []{'<', '>'}) >= 0)
|
||||
{
|
||||
throw new ArgumentException("Tag cannot contain `<` or `>` characters", nameof(tag));
|
||||
}
|
||||
|
||||
if (defaultExtensions != null)
|
||||
{
|
||||
// Check that this default pipeline is supported
|
||||
// Will throw an ArgumentInvalidException if not
|
||||
new MarkdownPipelineBuilder().Configure(defaultExtensions);
|
||||
}
|
||||
DefaultExtensions = defaultExtensions;
|
||||
SelfPipelineHintTagStart = "<!--" + tag + ":";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default pipeline to configure if no tag was found in the input text. Default is <c>null</c> (core pipeline).
|
||||
/// </summary>
|
||||
public string DefaultExtensions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the self pipeline hint tag start that will be matched.
|
||||
/// </summary>
|
||||
public string SelfPipelineHintTagStart { get; }
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
// Make sure that this pipeline has only one extension (itself)
|
||||
if (pipeline.Extensions.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The SelfPipeline extension cannot be configured with other extensions");
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pipeline automatically configured from an input markdown based on the presence of the configuration tag.
|
||||
/// </summary>
|
||||
/// <param name="inputText">The input text.</param>
|
||||
/// <returns>The pipeline configured from the input</returns>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
public MarkdownPipeline CreatePipelineFromInput(string inputText)
|
||||
{
|
||||
if (inputText == null) throw new ArgumentNullException(nameof(inputText));
|
||||
|
||||
var builder = new MarkdownPipelineBuilder();
|
||||
string defaultConfig = DefaultExtensions;
|
||||
var indexOfSelfPipeline = inputText.IndexOf(SelfPipelineHintTagStart, StringComparison.OrdinalIgnoreCase);
|
||||
if (indexOfSelfPipeline >= 0)
|
||||
{
|
||||
var optionStart = indexOfSelfPipeline + SelfPipelineHintTagStart.Length;
|
||||
var endOfTag = inputText.IndexOf("-->", optionStart, StringComparison.OrdinalIgnoreCase);
|
||||
if (endOfTag >= 0)
|
||||
{
|
||||
defaultConfig = inputText.Substring(optionStart, endOfTag - optionStart).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(defaultConfig))
|
||||
{
|
||||
builder.Configure(defaultConfig);
|
||||
}
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,14 +28,14 @@ namespace Markdig.Extensions.SmartyPants
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
if (!pipeline.InlineParsers.Contains<SmaryPantsInlineParser>())
|
||||
if (!pipeline.InlineParsers.Contains<SmartyPantsInlineParser>())
|
||||
{
|
||||
// Insert the parser after the code span parser
|
||||
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(new SmaryPantsInlineParser());
|
||||
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(new SmartyPantsInlineParser());
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.SmartyPants
|
||||
@@ -11,14 +12,14 @@ namespace Markdig.Extensions.SmartyPants
|
||||
/// <summary>
|
||||
/// The inline parser for SmartyPants.
|
||||
/// </summary>
|
||||
public class SmaryPantsInlineParser : InlineParser
|
||||
public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SmaryPantsInlineParser"/> class.
|
||||
/// Initializes a new instance of the <see cref="SmartyPantsInlineParser"/> class.
|
||||
/// </summary>
|
||||
public SmaryPantsInlineParser()
|
||||
public SmartyPantsInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'`', '\'', '"', '<', '>', '.', '-'};
|
||||
OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'};
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
@@ -30,6 +31,8 @@ namespace Markdig.Extensions.SmartyPants
|
||||
// " “ ” “ ” 'left-double-quote', 'right-double-quote'
|
||||
// << >> « » « » 'left-angle-quote', 'right-angle-quote'
|
||||
// ... … … 'ellipsis'
|
||||
|
||||
// Special case: – and — are handle as a PostProcess step to avoid conflicts with pipetables header separator row
|
||||
// -- – – 'ndash'
|
||||
// --- — — 'mdash'
|
||||
|
||||
@@ -44,13 +47,6 @@ namespace Markdig.Extensions.SmartyPants
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case '`':
|
||||
if (slice.PeekChar(1) == '`')
|
||||
{
|
||||
slice.NextChar();
|
||||
type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines
|
||||
}
|
||||
break;
|
||||
case '\'':
|
||||
type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines
|
||||
if (slice.PeekChar(1) == '\'')
|
||||
@@ -83,12 +79,9 @@ namespace Markdig.Extensions.SmartyPants
|
||||
case '-':
|
||||
if (slice.NextChar() == '-')
|
||||
{
|
||||
type = SmartyPantType.Dash2;
|
||||
if (slice.PeekChar(1) == '-')
|
||||
{
|
||||
slice.NextChar();
|
||||
type = SmartyPantType.Dash3;
|
||||
}
|
||||
var quotePants = GetOrCreateState(processor);
|
||||
quotePants.HasDash = true;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -178,11 +171,8 @@ namespace Markdig.Extensions.SmartyPants
|
||||
// We will check in a post-process step for balanaced open/close quotes
|
||||
if (postProcess)
|
||||
{
|
||||
var quotePants = processor.ParserStates[Index] as List<SmartyPant>;
|
||||
if (quotePants == null)
|
||||
{
|
||||
processor.ParserStates[Index] = quotePants = new List<SmartyPant>();
|
||||
}
|
||||
var quotePants = GetOrCreateState(processor);
|
||||
|
||||
// Register only if we don't have yet any quotes
|
||||
if (quotePants.Count == 0)
|
||||
{
|
||||
@@ -195,11 +185,21 @@ namespace Markdig.Extensions.SmartyPants
|
||||
return true;
|
||||
}
|
||||
|
||||
private ListSmartyPants GetOrCreateState(InlineProcessor processor)
|
||||
{
|
||||
var quotePants = processor.ParserStates[Index] as ListSmartyPants;
|
||||
if (quotePants == null)
|
||||
{
|
||||
processor.ParserStates[Index] = quotePants = new ListSmartyPants();
|
||||
}
|
||||
return quotePants;
|
||||
}
|
||||
|
||||
private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline)
|
||||
{
|
||||
processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd;
|
||||
|
||||
var pants = (List<SmartyPant>) processor.ParserStates[Index];
|
||||
var pants = (ListSmartyPants) processor.ParserStates[Index];
|
||||
|
||||
// We only change quote into left or right quotes if we find proper balancing
|
||||
var previousIndices = new int[3] {-1, -1, -1};
|
||||
@@ -289,5 +289,92 @@ namespace Markdig.Extensions.SmartyPants
|
||||
|
||||
pants.Clear();
|
||||
}
|
||||
|
||||
bool IPostInlineProcessor.PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex,
|
||||
bool isFinalProcessing)
|
||||
{
|
||||
// Don't try to process anything if there are no dash
|
||||
var quotePants = state.ParserStates[Index] as ListSmartyPants;
|
||||
if (quotePants == null || !quotePants.HasDash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var child = root;
|
||||
var pendingContainers = new Stack<Inline>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
while (child != null)
|
||||
{
|
||||
var next = child.NextSibling;
|
||||
|
||||
if (child is LiteralInline)
|
||||
{
|
||||
var literal = (LiteralInline) child;
|
||||
|
||||
var startIndex = 0;
|
||||
|
||||
var indexOfDash = literal.Content.IndexOf("--", startIndex);
|
||||
if (indexOfDash >= 0)
|
||||
{
|
||||
var type = SmartyPantType.Dash2;
|
||||
if (literal.Content.PeekCharAbsolute(indexOfDash + 2) == '-')
|
||||
{
|
||||
type = SmartyPantType.Dash3;
|
||||
}
|
||||
var nextContent = literal.Content;
|
||||
var originalSpan = literal.Span;
|
||||
literal.Span.End -= literal.Content.End - indexOfDash + 1;
|
||||
literal.Content.End = indexOfDash - 1;
|
||||
nextContent.Start = indexOfDash + (type == SmartyPantType.Dash2 ? 2 : 3);
|
||||
|
||||
var pant = new SmartyPant()
|
||||
{
|
||||
Span = new SourceSpan(literal.Content.End + 1, nextContent.Start - 1),
|
||||
Line = literal.Line,
|
||||
Column = literal.Column,
|
||||
OpeningCharacter = '-',
|
||||
Type = type
|
||||
};
|
||||
literal.InsertAfter(pant);
|
||||
|
||||
var postLiteral = new LiteralInline()
|
||||
{
|
||||
Span = new SourceSpan(pant.Span.End + 1, originalSpan.End),
|
||||
Line = literal.Line,
|
||||
Column = literal.Column,
|
||||
Content = nextContent
|
||||
};
|
||||
pant.InsertAfter(postLiteral);
|
||||
|
||||
// Use the pending literal to proceed further
|
||||
next = postLiteral;
|
||||
}
|
||||
}
|
||||
else if (child is ContainerInline)
|
||||
{
|
||||
pendingContainers.Push(((ContainerInline)child).FirstChild);
|
||||
}
|
||||
|
||||
child = next;
|
||||
}
|
||||
if (pendingContainers.Count > 0)
|
||||
{
|
||||
child = pendingContainers.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private class ListSmartyPants : List<SmartyPant>
|
||||
{
|
||||
public bool HasDash { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null && !htmlRenderer.ObjectRenderers.Contains<HtmlTableRenderer>())
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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.Collections.Generic;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
@@ -10,7 +13,7 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
public GridTableParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'+'};
|
||||
OpeningCharacters = new[] { '+' };
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
@@ -22,65 +25,51 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
|
||||
var line = processor.Line;
|
||||
|
||||
// A grid table must start with a line like this:
|
||||
// + ------------- + ------------ + ---------------------------------------- +
|
||||
// Spaces are optional
|
||||
|
||||
GridTableState tableState = null;
|
||||
|
||||
// Match the first row that should be of the minimal form: +---------------
|
||||
var c = line.CurrentChar;
|
||||
var startPosition = processor.Start;
|
||||
while (true)
|
||||
var lineStart = line.Start;
|
||||
while (c == '+')
|
||||
{
|
||||
if (c == '+')
|
||||
var columnStart = line.Start;
|
||||
line.NextChar();
|
||||
line.TrimStart();
|
||||
|
||||
// if we have reached the end of the line, exit
|
||||
c = line.CurrentChar;
|
||||
if (c == 0)
|
||||
{
|
||||
var startCharacter = line.Start;
|
||||
line.NextChar();
|
||||
if (line.IsEmptyOrWhitespace())
|
||||
{
|
||||
if (tableState == null)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
TableColumnAlign align;
|
||||
if (TableHelper.ParseColumnHeader(ref line, '-', out align))
|
||||
{
|
||||
if (tableState == null)
|
||||
{
|
||||
tableState = new GridTableState()
|
||||
{
|
||||
Start = processor.Start,
|
||||
ExpectRow = true,
|
||||
};
|
||||
}
|
||||
tableState.AddColumn(startCharacter - startPosition, line.Start - 1 - startPosition, align);
|
||||
|
||||
c = line.CurrentChar;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we have any other characters, this is an invalid line
|
||||
return BlockState.None;
|
||||
// Parse a column alignment
|
||||
TableColumnAlign? columnAlign;
|
||||
if (!TableHelper.ParseColumnHeader(ref line, '-', out columnAlign))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
tableState = tableState ?? new GridTableState { Start = processor.Start, ExpectRow = true };
|
||||
tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign);
|
||||
|
||||
c = line.CurrentChar;
|
||||
}
|
||||
|
||||
if (c != 0 || tableState == null)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
// Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid)
|
||||
tableState.AddLine(ref processor.Line);
|
||||
|
||||
// Create the grid table
|
||||
var table = new Table(this);
|
||||
|
||||
table.SetData(typeof(GridTableState), tableState);
|
||||
|
||||
|
||||
// Calculate the total width of all columns
|
||||
int totalWidth = 0;
|
||||
foreach (var columnSlice in tableState.ColumnSlices)
|
||||
{
|
||||
totalWidth += columnSlice.End - columnSlice.Start;
|
||||
totalWidth += columnSlice.End - columnSlice.Start - 1;
|
||||
}
|
||||
|
||||
// Store the column width and alignment
|
||||
@@ -89,8 +78,8 @@ namespace Markdig.Extensions.Tables
|
||||
var columnDefinition = new TableColumnDefinition
|
||||
{
|
||||
// Column width proportional to the total width
|
||||
Width = (float)(columnSlice.End - columnSlice.Start) * 100.0f / totalWidth,
|
||||
Alignment = columnSlice.Align,
|
||||
Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth,
|
||||
Alignment = columnSlice.Align
|
||||
};
|
||||
table.ColumnDefinitions.Add(columnDefinition);
|
||||
}
|
||||
@@ -102,253 +91,276 @@ namespace Markdig.Extensions.Tables
|
||||
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var gridTable = (Table) block;
|
||||
var gridTable = (Table)block;
|
||||
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
|
||||
|
||||
// We expect to start at the same
|
||||
//if (processor.Start == tableState.Start)
|
||||
tableState.AddLine(ref processor.Line);
|
||||
if (processor.CurrentChar == '+')
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
|
||||
columnSlice.CurrentColumnSpan = 0;
|
||||
}
|
||||
|
||||
if (processor.CurrentChar == '+')
|
||||
{
|
||||
var result = ParseRowSeparator(processor, tableState, gridTable);
|
||||
if (result != BlockState.None)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else if (processor.CurrentChar == '|')
|
||||
{
|
||||
var line = processor.Line;
|
||||
|
||||
// | ------------- | ------------ | ---------------------------------------- |
|
||||
// Calculate the colspan for the new row
|
||||
int columnIndex = -1;
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (line.PeekCharExtra(columnSlice.Start) == '|')
|
||||
{
|
||||
columnIndex++;
|
||||
}
|
||||
if (columnIndex >= 0)
|
||||
{
|
||||
columns[columnIndex].CurrentColumnSpan++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the colspan of the current row is the same than the previous row
|
||||
bool continueRow = true;
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
|
||||
{
|
||||
continueRow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the current row doesn't continue the previous row (col span are different)
|
||||
// Close the previous row
|
||||
if (!continueRow)
|
||||
{
|
||||
TerminateLastRow(processor, tableState, gridTable, false);
|
||||
}
|
||||
|
||||
for (int i = 0; i < columns.Count;)
|
||||
{
|
||||
var column = columns[i];
|
||||
var nextColumnIndex = i + column.CurrentColumnSpan;
|
||||
// If the span is 0, we exit
|
||||
if (nextColumnIndex == i)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
|
||||
|
||||
var sliceForCell = line;
|
||||
sliceForCell.Start = line.Start + column.Start + 1;
|
||||
if (nextColumn != null)
|
||||
{
|
||||
sliceForCell.End = line.Start + nextColumn.Start - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var columnEnd = columns[columns.Count - 1].End;
|
||||
// If there is a `|` exactly at the expected end of the table row, we cut the line
|
||||
// otherwise we allow to have the last cell of a row to be open for longer cell content
|
||||
if (line.PeekCharExtra(columnEnd + 1) == '|')
|
||||
{
|
||||
sliceForCell.End = line.Start + columnEnd;
|
||||
}
|
||||
}
|
||||
sliceForCell.TrimEnd();
|
||||
|
||||
// Process the content of the cell
|
||||
column.BlockProcessor.LineIndex = processor.LineIndex;
|
||||
column.BlockProcessor.ProcessLine(sliceForCell);
|
||||
|
||||
// Go to next column
|
||||
i = nextColumnIndex;
|
||||
}
|
||||
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
return HandleNewRow(processor, tableState, gridTable);
|
||||
}
|
||||
|
||||
TerminateLastRow(processor, tableState, gridTable, true);
|
||||
|
||||
// If we don't have a row, it means that only the header was valid
|
||||
// So we need to remove the grid table, and create a ParagraphBlock
|
||||
// with the 2 slices
|
||||
if (gridTable.Count == 0)
|
||||
if (processor.CurrentChar == '|')
|
||||
{
|
||||
var parser = processor.Parsers.FindExact<ParagraphBlockParser>();
|
||||
// Discard the grid table
|
||||
var parent = gridTable.Parent;
|
||||
processor.Discard(gridTable);
|
||||
var paragraphBlock = new ParagraphBlock(parser)
|
||||
{
|
||||
Lines = tableState.Lines,
|
||||
};
|
||||
parent.Add(paragraphBlock);
|
||||
processor.Open(paragraphBlock);
|
||||
return HandleContents(processor, tableState, gridTable);
|
||||
}
|
||||
TerminateCurrentRow(processor, tableState, gridTable, true);
|
||||
// If the table is not valid we need to remove the grid table,
|
||||
// and create a ParagraphBlock with the slices
|
||||
if (!gridTable.IsValid())
|
||||
{
|
||||
Undo(processor, tableState, gridTable);
|
||||
}
|
||||
|
||||
return BlockState.Break;
|
||||
}
|
||||
|
||||
private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
bool isHeaderRow, hasRowSpan;
|
||||
var columns = tableState.ColumnSlices;
|
||||
SetRowSpanState(columns, processor.Line, out isHeaderRow, out hasRowSpan);
|
||||
SetColumnSpanState(columns, processor.Line);
|
||||
TerminateCurrentRow(processor, tableState, gridTable, false);
|
||||
if (isHeaderRow)
|
||||
{
|
||||
for (int i = 0; i < gridTable.Count; i++)
|
||||
{
|
||||
var row = (TableRow)gridTable[i];
|
||||
row.IsHeader = true;
|
||||
}
|
||||
}
|
||||
tableState.StartRowGroup = gridTable.Count;
|
||||
if (hasRowSpan)
|
||||
{
|
||||
HandleContents(processor, tableState, gridTable);
|
||||
}
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
|
||||
{
|
||||
var lineStart = line.Start;
|
||||
isHeaderRow = line.PeekChar(1) == '=' || line.PeekChar(2) == '=';
|
||||
hasRowSpan = false;
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
line.Start = lineStart + columnSlice.Start + 1;
|
||||
line.End = lineStart + columnSlice.End - 1;
|
||||
line.Trim();
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line))
|
||||
{
|
||||
hasRowSpan = true;
|
||||
columnSlice.CurrentCell.RowSpan++;
|
||||
columnSlice.CurrentCell.AllowClose = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
columnSlice.CurrentCell.AllowClose = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRowSeperator(StringSlice slice)
|
||||
{
|
||||
while (slice.Length > 0)
|
||||
{
|
||||
if (slice.CurrentChar != '-' && slice.CurrentChar != '=' && slice.CurrentChar != ':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
slice.NextChar();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow)
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
TableRow currentRow = null;
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
if (currentRow == null)
|
||||
{
|
||||
currentRow = new TableRow();
|
||||
}
|
||||
// If this cell does not already belong to a row
|
||||
if (columnSlice.CurrentCell.Parent == null)
|
||||
{
|
||||
currentRow.Add(columnSlice.CurrentCell);
|
||||
}
|
||||
// If the cell is not going to span through to the next row
|
||||
if (columnSlice.CurrentCell.AllowClose)
|
||||
{
|
||||
columnSlice.BlockProcessor.Close(columnSlice.CurrentCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Renew the block parser processor (or reset it for the last row)
|
||||
if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell == null || columnSlice.CurrentCell.AllowClose))
|
||||
{
|
||||
columnSlice.BlockProcessor.ReleaseChild();
|
||||
columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild();
|
||||
}
|
||||
|
||||
// Create or erase the cell
|
||||
if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose))
|
||||
{
|
||||
// We don't need the cell anymore if we have a last row
|
||||
// Or the cell has a columnspan == 0
|
||||
// And the cell does not have to be kept open to span rows
|
||||
columnSlice.CurrentCell = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow != null && currentRow.Count > 0)
|
||||
{
|
||||
gridTable.Add(currentRow);
|
||||
}
|
||||
}
|
||||
|
||||
private BlockState HandleContents(BlockProcessor processor, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
var isRowLine = processor.CurrentChar == '+';
|
||||
var columns = tableState.ColumnSlices;
|
||||
var line = processor.Line;
|
||||
SetColumnSpanState(columns, line);
|
||||
if (!isRowLine && !CanContinueRow(columns))
|
||||
{
|
||||
TerminateCurrentRow(processor, tableState, gridTable, false);
|
||||
}
|
||||
for (int i = 0; i < columns.Count;)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
var nextColumnIndex = i + columnSlice.CurrentColumnSpan;
|
||||
// If the span is 0, we exit
|
||||
if (nextColumnIndex == i)
|
||||
{
|
||||
break;
|
||||
}
|
||||
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
|
||||
|
||||
var sliceForCell = line;
|
||||
sliceForCell.Start = line.Start + columnSlice.Start + 1;
|
||||
if (nextColumn != null)
|
||||
{
|
||||
sliceForCell.End = line.Start + nextColumn.Start - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var columnEnd = columns[columns.Count - 1].End;
|
||||
var columnEndChar = line.PeekCharExtra(columnEnd);
|
||||
// If there is a `|` (or a `+` in the case that we are dealing with a row line
|
||||
// with spanned contents) exactly at the expected end of the table row, we cut the line
|
||||
// otherwise we allow to have the last cell of a row to be open for longer cell content
|
||||
if (columnEndChar == '|' || (isRowLine && columnEndChar == '+'))
|
||||
{
|
||||
sliceForCell.End = line.Start + columnEnd - 1;
|
||||
}
|
||||
else if (line.PeekCharExtra(line.End) == '|')
|
||||
{
|
||||
sliceForCell.End = line.End - 1;
|
||||
}
|
||||
}
|
||||
sliceForCell.TrimEnd();
|
||||
|
||||
if (!isRowLine || !IsRowSeperator(sliceForCell))
|
||||
{
|
||||
if (columnSlice.CurrentCell == null)
|
||||
{
|
||||
columnSlice.CurrentCell = new TableCell(this)
|
||||
{
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan,
|
||||
ColumnIndex = i
|
||||
};
|
||||
|
||||
if (columnSlice.BlockProcessor == null)
|
||||
{
|
||||
columnSlice.BlockProcessor = processor.CreateChild();
|
||||
}
|
||||
|
||||
// Ensure that the BlockParser is aware that the TableCell is the top-level container
|
||||
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
|
||||
}
|
||||
// Process the content of the cell
|
||||
columnSlice.BlockProcessor.LineIndex = processor.LineIndex;
|
||||
columnSlice.BlockProcessor.ProcessLine(sliceForCell);
|
||||
}
|
||||
|
||||
// Go to next column
|
||||
i = nextColumnIndex;
|
||||
}
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
private static void SetColumnSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line)
|
||||
{
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
|
||||
columnSlice.CurrentColumnSpan = 0;
|
||||
}
|
||||
// | ------------- | ------------ | ---------------------------------------- |
|
||||
// Calculate the colspan for the new row
|
||||
int columnIndex = -1;
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
var peek = line.PeekChar(columnSlice.Start);
|
||||
if (peek == '|' || peek == '+')
|
||||
{
|
||||
columnIndex = i;
|
||||
}
|
||||
if (columnIndex >= 0)
|
||||
{
|
||||
columns[columnIndex].CurrentColumnSpan++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanContinueRow(List<GridTableState.ColumnSlice> columns)
|
||||
{
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Undo(BlockProcessor processor, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
var parser = processor.Parsers.FindExact<ParagraphBlockParser>();
|
||||
// Discard the grid table
|
||||
var parent = gridTable.Parent;
|
||||
processor.Discard(gridTable);
|
||||
var paragraphBlock = new ParagraphBlock(parser)
|
||||
{
|
||||
Lines = tableState.Lines,
|
||||
};
|
||||
parent.Add(paragraphBlock);
|
||||
processor.Open(paragraphBlock);
|
||||
}
|
||||
|
||||
public override bool Close(BlockProcessor processor, Block block)
|
||||
{
|
||||
// Work only on Table, not on TableCell
|
||||
var gridTable = block as Table;
|
||||
if (gridTable != null)
|
||||
{
|
||||
var tableState = (GridTableState) block.GetData(typeof (GridTableState));
|
||||
TerminateLastRow(processor, tableState, gridTable, true);
|
||||
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
|
||||
TerminateCurrentRow(processor, tableState, gridTable, true);
|
||||
if (!gridTable.IsValid())
|
||||
{
|
||||
Undo(processor, tableState, gridTable);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private BlockState ParseRowSeparator(BlockProcessor state, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
// A grid table must start with a line like this:
|
||||
// + ------------- + ------------ + ---------------------------------------- +
|
||||
// Spaces are optional
|
||||
|
||||
var line = state.Line;
|
||||
var c = line.CurrentChar;
|
||||
bool isFirst = true;
|
||||
var delimiterChar = '\0';
|
||||
while (true)
|
||||
{
|
||||
if (c == '+')
|
||||
{
|
||||
line.NextChar();
|
||||
if (line.IsEmptyOrWhitespace())
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
TableColumnAlign align;
|
||||
if (TableHelper.ParseColumnHeaderDetect(ref line, ref delimiterChar, out align))
|
||||
{
|
||||
isFirst = false;
|
||||
c = line.CurrentChar;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have any other characters, this is an invalid line
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
// If we have an header row
|
||||
var isHeader = delimiterChar == '=';
|
||||
|
||||
// Terminate the current row
|
||||
TerminateLastRow(state, tableState, gridTable, false);
|
||||
|
||||
// If we had a header row separator, we can mark all rows since last row separator
|
||||
// to be header rows
|
||||
if (isHeader)
|
||||
{
|
||||
for (int i = tableState.StartRowGroup; i < gridTable.Count; i++)
|
||||
{
|
||||
var row = (TableRow) gridTable[i];
|
||||
row.IsHeader = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Makr the next start row group continue on the next row
|
||||
tableState.StartRowGroup = gridTable.Count;
|
||||
|
||||
// We don't keep the line
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
private void TerminateLastRow(BlockProcessor state, GridTableState tableState, Table gridTable, bool isLastRow)
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
TableRow currentRow = null;
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
if (currentRow == null)
|
||||
{
|
||||
currentRow = new TableRow();
|
||||
}
|
||||
currentRow.Add(columnSlice.CurrentCell);
|
||||
columnSlice.BlockProcessor.Close(columnSlice.CurrentCell);
|
||||
}
|
||||
|
||||
// Renew the block parser processor (or reset it for the last row)
|
||||
if (columnSlice.BlockProcessor != null)
|
||||
{
|
||||
columnSlice.BlockProcessor.ReleaseChild();
|
||||
columnSlice.BlockProcessor = isLastRow ? null : state.CreateChild();
|
||||
}
|
||||
|
||||
// Create or erase the cell
|
||||
if (isLastRow || columnSlice.CurrentColumnSpan == 0)
|
||||
{
|
||||
// We don't need the cell anymore if we have a last row
|
||||
// Or the cell has a columnspan == 0
|
||||
columnSlice.CurrentCell = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Else we can create a new cell
|
||||
columnSlice.CurrentCell = new TableCell(this)
|
||||
{
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan
|
||||
};
|
||||
|
||||
if (columnSlice.BlockProcessor == null)
|
||||
{
|
||||
columnSlice.BlockProcessor = state.CreateChild();
|
||||
}
|
||||
|
||||
// Ensure that the BlockParser is aware that the TableCell is the top-level container
|
||||
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow != null)
|
||||
{
|
||||
gridTable.Add(currentRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Markdig.Extensions.Tables
|
||||
Lines.Add(line);
|
||||
}
|
||||
|
||||
public void AddColumn(int start, int end, TableColumnAlign align)
|
||||
public void AddColumn(int start, int end, TableColumnAlign? align)
|
||||
{
|
||||
if (ColumnSlices == null)
|
||||
{
|
||||
@@ -60,7 +60,7 @@ namespace Markdig.Extensions.Tables
|
||||
|
||||
public int End { get; set; }
|
||||
|
||||
public TableColumnAlign Align { get; set; }
|
||||
public TableColumnAlign? Align { get; set; }
|
||||
|
||||
public int CurrentColumnSpan { get; set; }
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
@@ -38,7 +39,9 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
foreach (var tableColumnDefinition in table.ColumnDefinitions)
|
||||
{
|
||||
renderer.WriteLine($"<col style=\"width:{Math.Round(tableColumnDefinition.Width*100)/100}%\">");
|
||||
var width = Math.Round(tableColumnDefinition.Width*100)/100;
|
||||
var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width);
|
||||
renderer.WriteLine($"<col style=\"width:{widthValue}%\">");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,17 +81,31 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
|
||||
}
|
||||
|
||||
if (table.ColumnDefinitions != null && i < table.ColumnDefinitions.Count)
|
||||
if (cell.RowSpan != 1)
|
||||
{
|
||||
switch (table.ColumnDefinitions[i].Alignment)
|
||||
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
|
||||
}
|
||||
if (table.ColumnDefinitions != null)
|
||||
{
|
||||
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
|
||||
? i
|
||||
: cell.ColumnIndex;
|
||||
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
|
||||
var alignment = table.ColumnDefinitions[columnIndex].Alignment;
|
||||
if (alignment.HasValue)
|
||||
{
|
||||
case TableColumnAlign.Center:
|
||||
renderer.Write(" style=\"text-align: center;\"");
|
||||
break;
|
||||
case TableColumnAlign.Right:
|
||||
renderer.Write(" style=\"text-align: right;\"");
|
||||
break;
|
||||
switch (alignment)
|
||||
{
|
||||
case TableColumnAlign.Center:
|
||||
renderer.Write(" style=\"text-align: center;\"");
|
||||
break;
|
||||
case TableColumnAlign.Right:
|
||||
renderer.Write(" style=\"text-align: right;\"");
|
||||
break;
|
||||
case TableColumnAlign.Left:
|
||||
renderer.Write(" style=\"text-align: left;\"");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.WriteAttributes(cell);
|
||||
@@ -101,7 +118,7 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
renderer.Write(cell);
|
||||
renderer.ImplicitParagraph = previousImplicitParagraph;
|
||||
|
||||
|
||||
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
|
||||
}
|
||||
renderer.WriteLine("</tr>");
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace Markdig.Extensions.Tables
|
||||
/// The delimiter used to separate the columns of a pipe table.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Syntax.Inlines.DelimiterInline" />
|
||||
public class PiprTableDelimiterInline : DelimiterInline
|
||||
public class PipeTableDelimiterInline : DelimiterInline
|
||||
{
|
||||
public PiprTableDelimiterInline(InlineParser parser) : base(parser)
|
||||
public PipeTableDelimiterInline(InlineParser parser) : base(parser)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -34,13 +34,14 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
pipeline.BlockParsers.Insert(0, new PipeTableBlockParser());
|
||||
}
|
||||
var lineBreakParser = pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
|
||||
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
|
||||
{
|
||||
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(Options));
|
||||
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(lineBreakParser, Options));
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null && !htmlRenderer.ObjectRenderers.Contains<HtmlTableRenderer>())
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// 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.Diagnostics;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Parsers.Inlines;
|
||||
@@ -15,17 +18,20 @@ namespace Markdig.Extensions.Tables
|
||||
/// The inline parser used to transform a <see cref="ParagraphBlock"/> into a <see cref="Table"/> at inline parsing time.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.InlineParser" />
|
||||
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
|
||||
public class PipeTableParser : InlineParser, IDelimiterProcessor
|
||||
/// <seealso cref="IPostInlineProcessor" />
|
||||
public class PipeTableParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
private LineBreakInlineParser lineBreakParser;
|
||||
private readonly LineBreakInlineParser lineBreakParser;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipeTableParser" /> class.
|
||||
/// </summary>
|
||||
/// <param name="lineBreakParser">The linebreak parser to use</param>
|
||||
/// <param name="options">The options.</param>
|
||||
public PipeTableParser(PipeTableOptions options = null)
|
||||
public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions options = null)
|
||||
{
|
||||
if (lineBreakParser == null) throw new ArgumentNullException(nameof(lineBreakParser));
|
||||
this.lineBreakParser = lineBreakParser;
|
||||
OpeningCharacters = new[] { '|', '\n' };
|
||||
Options = options ?? new PipeTableOptions();
|
||||
}
|
||||
@@ -35,12 +41,6 @@ namespace Markdig.Extensions.Tables
|
||||
/// </summary>
|
||||
public PipeTableOptions Options { get; }
|
||||
|
||||
public override void Initialize(InlineProcessor processor)
|
||||
{
|
||||
// We are using the linebreak parser
|
||||
lineBreakParser = processor.Parsers.Find<LineBreakInlineParser>() ?? new LineBreakInlineParser();
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Only working on Paragraph block
|
||||
@@ -96,11 +96,12 @@ namespace Markdig.Extensions.Tables
|
||||
if (!isFirstLineEmpty)
|
||||
{
|
||||
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
|
||||
tableState.EndOfLines.Add(processor.Inline);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processor.Inline = new PiprTableDelimiterInline(this)
|
||||
processor.Inline = new PipeTableDelimiterInline(this)
|
||||
{
|
||||
Span = new SourceSpan(position, position),
|
||||
Line = globalLineIndex,
|
||||
@@ -123,7 +124,7 @@ namespace Markdig.Extensions.Tables
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProcessDelimiters(InlineProcessor state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing)
|
||||
public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
|
||||
{
|
||||
var container = root as ContainerInline;
|
||||
var tableState = state.ParserStates[Index] as TableState;
|
||||
@@ -137,16 +138,16 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
|
||||
var child = container.LastChild;
|
||||
List<PiprTableDelimiterInline> delimitersToRemove = null;
|
||||
List<PipeTableDelimiterInline> delimitersToRemove = null;
|
||||
|
||||
while (child != null)
|
||||
{
|
||||
var pipeDelimiter = child as PiprTableDelimiterInline;
|
||||
var pipeDelimiter = child as PipeTableDelimiterInline;
|
||||
if (pipeDelimiter != null)
|
||||
{
|
||||
if (delimitersToRemove == null)
|
||||
{
|
||||
delimitersToRemove = new List<PiprTableDelimiterInline>();
|
||||
delimitersToRemove = new List<PipeTableDelimiterInline>();
|
||||
}
|
||||
delimitersToRemove.Add(pipeDelimiter);
|
||||
}
|
||||
@@ -168,8 +169,7 @@ namespace Markdig.Extensions.Tables
|
||||
for (int i = 0; i < delimitersToRemove.Count; i++)
|
||||
{
|
||||
var pipeDelimiter = delimitersToRemove[i];
|
||||
var literalInline = new LiteralInline() {Content = new StringSlice("|"), IsClosed = true};
|
||||
pipeDelimiter.ReplaceBy(literalInline);
|
||||
pipeDelimiter.ReplaceByLiteral();
|
||||
|
||||
// Check that the pipe that is being removed is not going to make a line without pipe delimiters
|
||||
var tableDelimiters = tableState.ColumnAndLineDelimiters;
|
||||
@@ -177,12 +177,12 @@ namespace Markdig.Extensions.Tables
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PiprTableDelimiterInline;
|
||||
leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PipeTableDelimiterInline;
|
||||
}
|
||||
else if (i + 1 == delimitersToRemove.Count)
|
||||
{
|
||||
rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count &&
|
||||
tableDelimiters[delimiterIndex + 1] is PiprTableDelimiterInline;
|
||||
tableDelimiters[delimiterIndex + 1] is PipeTableDelimiterInline;
|
||||
}
|
||||
// Remove this delimiter from the table processor
|
||||
tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter);
|
||||
@@ -198,14 +198,18 @@ namespace Markdig.Extensions.Tables
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove previous state
|
||||
state.ParserStates[Index] = null;
|
||||
|
||||
// Continue
|
||||
if (tableState == null || container == null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Detect the header row
|
||||
var delimiters = tableState.ColumnAndLineDelimiters;
|
||||
delimiters.Add(null);
|
||||
// TODO: we could optimize this by merging FindHeaderRow and the cell loop
|
||||
var aligns = FindHeaderRow(delimiters);
|
||||
|
||||
if (Options.RequireHeaderSeparator && aligns == null)
|
||||
@@ -223,153 +227,219 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
|
||||
state.BlockNew = table;
|
||||
TableRow firstRow = null;
|
||||
int maxColumn = 0;
|
||||
var cells = tableState.Cells;
|
||||
cells.Clear();
|
||||
|
||||
Inline column = container.FirstChild;
|
||||
if (column is PiprTableDelimiterInline)
|
||||
//delimiters[0].DumpTo(state.DebugLog);
|
||||
|
||||
// delimiters contain a list of `|` and `\n` delimiters
|
||||
// The `|` delimiters are created as child containers.
|
||||
// So the following:
|
||||
// | a | b \n
|
||||
// | d | e \n
|
||||
//
|
||||
// Will generate a tree of the following node:
|
||||
// |
|
||||
// a
|
||||
// |
|
||||
// b
|
||||
// \n
|
||||
// |
|
||||
// d
|
||||
// |
|
||||
// e
|
||||
// \n
|
||||
// When parsing delimiters, we need to recover whether a row is of the following form:
|
||||
// 0) | a | b | \n
|
||||
// 1) | a | b \n
|
||||
// 2) a | b \n
|
||||
// 3) a | b | \n
|
||||
|
||||
// If the last element is not a line break, add a line break to homogenize parsing in the next loop
|
||||
var lastElement = delimiters[delimiters.Count - 1];
|
||||
if (!(lastElement is LineBreakInline))
|
||||
{
|
||||
column = ((PiprTableDelimiterInline)column).FirstChild;
|
||||
while (true)
|
||||
{
|
||||
if (lastElement is ContainerInline)
|
||||
{
|
||||
var nextElement = ((ContainerInline) lastElement).LastChild;
|
||||
if (nextElement != null)
|
||||
{
|
||||
lastElement = nextElement;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var endOfTable = new LineBreakInline();
|
||||
// If the last element is a container, we have to add the EOL to its child
|
||||
// otherwise only next sibling
|
||||
if (lastElement is ContainerInline)
|
||||
{
|
||||
((ContainerInline)lastElement).AppendChild(endOfTable);
|
||||
}
|
||||
else
|
||||
{
|
||||
lastElement.InsertAfter(endOfTable);
|
||||
}
|
||||
delimiters.Add(endOfTable);
|
||||
tableState.EndOfLines.Add(endOfTable);
|
||||
}
|
||||
|
||||
// TODO: This is not accurate for the table
|
||||
table.Span.Start = column.Span.Start;
|
||||
table.Span.End = column.Span.End;
|
||||
table.Line = column.Line;
|
||||
table.Column = column.Column;
|
||||
|
||||
int lastIndex = 0;
|
||||
// Cell loop
|
||||
// Reconstruct the table from the delimiters
|
||||
TableRow row = null;
|
||||
TableRow firstRow = null;
|
||||
for (int i = 0; i < delimiters.Count; i++)
|
||||
{
|
||||
var delimiter = delimiters[i];
|
||||
if (delimiter == null || IsLine(delimiter))
|
||||
var pipeSeparator = delimiter as PipeTableDelimiterInline;
|
||||
var isLine = delimiter is LineBreakInline;
|
||||
|
||||
if (row == null)
|
||||
{
|
||||
var beforeDelimiter = delimiter?.PreviousSibling;
|
||||
var nextLineColumn = delimiter?.NextSibling;
|
||||
|
||||
TableRow row = null;
|
||||
|
||||
for (int j = lastIndex; j <= i; j++)
|
||||
{
|
||||
var columnSeparator = delimiters[j];
|
||||
var pipeSeparator = columnSeparator as PiprTableDelimiterInline;
|
||||
|
||||
var endOfColumn = columnSeparator?.PreviousSibling;
|
||||
|
||||
// This is the first column empty
|
||||
if (j == lastIndex && pipeSeparator != null && endOfColumn == null)
|
||||
{
|
||||
columnSeparator.Remove();
|
||||
column = pipeSeparator.FirstChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pipeSeparator != null && IsTrailingColumnDelimiter(pipeSeparator))
|
||||
{
|
||||
TrimEnd(endOfColumn);
|
||||
columnSeparator.Remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
var cellContainer = new ContainerInline();
|
||||
var item = column;
|
||||
var isFirstItem = true;
|
||||
TrimStart(item);
|
||||
while (item != null && !IsLine(item) && !(item is PiprTableDelimiterInline))
|
||||
{
|
||||
var nextSibling = item.NextSibling;
|
||||
item.Remove();
|
||||
cellContainer.AppendChild(item);
|
||||
if (isFirstItem)
|
||||
{
|
||||
cellContainer.Line = item.Line;
|
||||
cellContainer.Column = item.Column;
|
||||
cellContainer.Span.Start = item.Span.Start;
|
||||
isFirstItem = false;
|
||||
}
|
||||
cellContainer.Span.End = item.Span.End;
|
||||
item = nextSibling;
|
||||
}
|
||||
|
||||
var tableParagraph = new ParagraphBlock()
|
||||
{
|
||||
Span = cellContainer.Span,
|
||||
Line = cellContainer.Line,
|
||||
Column = cellContainer.Column,
|
||||
Inline = cellContainer
|
||||
};
|
||||
|
||||
var tableCell = new TableCell()
|
||||
{
|
||||
Span = cellContainer.Span,
|
||||
Line = cellContainer.Line,
|
||||
Column = cellContainer.Column,
|
||||
};
|
||||
|
||||
tableCell.Add(tableParagraph);
|
||||
|
||||
if (row == null)
|
||||
{
|
||||
row = new TableRow()
|
||||
{
|
||||
Span = cellContainer.Span,
|
||||
Line = cellContainer.Line,
|
||||
Column = cellContainer.Column,
|
||||
};
|
||||
}
|
||||
row.Add(tableCell);
|
||||
cells.Add(tableCell);
|
||||
|
||||
// If we have reached the end, we can add remaining delimiters as pure child of the current cell
|
||||
if (row.Count == maxColumn && columnSeparator is PiprTableDelimiterInline)
|
||||
{
|
||||
columnSeparator.Remove();
|
||||
tableParagraph.Inline.AppendChild(columnSeparator);
|
||||
break;
|
||||
}
|
||||
TrimEnd(endOfColumn);
|
||||
|
||||
//TrimEnd(previousSibling);
|
||||
if (columnSeparator != null)
|
||||
{
|
||||
if (pipeSeparator != null)
|
||||
{
|
||||
column = pipeSeparator.FirstChild;
|
||||
}
|
||||
columnSeparator.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (row != null)
|
||||
{
|
||||
table.Add(row);
|
||||
}
|
||||
|
||||
TrimEnd(beforeDelimiter);
|
||||
|
||||
if (delimiter != null)
|
||||
{
|
||||
delimiter.Remove();
|
||||
}
|
||||
|
||||
if (nextLineColumn != null)
|
||||
{
|
||||
column = nextLineColumn;
|
||||
}
|
||||
|
||||
row = new TableRow();
|
||||
if (firstRow == null)
|
||||
{
|
||||
firstRow = row;
|
||||
maxColumn = firstRow.Count;
|
||||
}
|
||||
|
||||
lastIndex = i + 1;
|
||||
// If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like:
|
||||
// 0) | a | b | \n
|
||||
// 1) | a | b \n
|
||||
if (pipeSeparator != null && (delimiter.PreviousSibling == null || delimiter.PreviousSibling is LineBreakInline))
|
||||
{
|
||||
delimiter.Remove();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`)
|
||||
// So we iterate back to the first pipe or line break
|
||||
// x
|
||||
// 1) | a | b \n
|
||||
// 2) a | b \n
|
||||
Inline endOfCell = null;
|
||||
Inline beginOfCell = null;
|
||||
var cellContentIt = delimiter;
|
||||
while (true)
|
||||
{
|
||||
cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent;
|
||||
|
||||
if (cellContentIt == null || cellContentIt is LineBreakInline)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation)
|
||||
if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent == null ))
|
||||
{
|
||||
beginOfCell = ((ContainerInline)cellContentIt).FirstChild;
|
||||
if (endOfCell == null)
|
||||
{
|
||||
endOfCell = beginOfCell;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
beginOfCell = cellContentIt;
|
||||
if (endOfCell == null)
|
||||
{
|
||||
endOfCell = beginOfCell;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If the current deilimiter is a pipe `|` OR
|
||||
// the beginOfCell/endOfCell are not null and
|
||||
// either they are :
|
||||
// - different
|
||||
// - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal.
|
||||
// Then we can add a cell to the current row
|
||||
if (!isLine || (beginOfCell != null && endOfCell != null && ( beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline && ((LiteralInline)beginOfCell).Content.IsEmptyOrWhitespace())))))
|
||||
{
|
||||
if (!isLine)
|
||||
{
|
||||
// If the delimiter is a pipe, we need to remove it from the tree
|
||||
// so that previous loop looking for a parent will not go further on subsequent cells
|
||||
delimiter.Remove();
|
||||
}
|
||||
|
||||
// We trim whitespace at the beginning and ending of the cell
|
||||
TrimStart(beginOfCell);
|
||||
TrimEnd(endOfCell);
|
||||
|
||||
var cellContainer = new ContainerInline();
|
||||
|
||||
// Copy elements from beginOfCell on the first level
|
||||
var cellIt = beginOfCell;
|
||||
while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline))
|
||||
{
|
||||
var nextSibling = cellIt.NextSibling;
|
||||
cellIt.Remove();
|
||||
if (cellContainer.Span.IsEmpty)
|
||||
{
|
||||
cellContainer.Line = cellIt.Line;
|
||||
cellContainer.Column = cellIt.Column;
|
||||
cellContainer.Span = cellIt.Span;
|
||||
}
|
||||
cellContainer.AppendChild(cellIt);
|
||||
cellContainer.Span.End = cellIt.Span.End;
|
||||
cellIt = nextSibling;
|
||||
}
|
||||
|
||||
// Create the cell and add it to the pending row
|
||||
var tableParagraph = new ParagraphBlock()
|
||||
{
|
||||
Span = cellContainer.Span,
|
||||
Line = cellContainer.Line,
|
||||
Column = cellContainer.Column,
|
||||
Inline = cellContainer
|
||||
};
|
||||
|
||||
var tableCell = new TableCell()
|
||||
{
|
||||
Span = cellContainer.Span,
|
||||
Line = cellContainer.Line,
|
||||
Column = cellContainer.Column,
|
||||
};
|
||||
|
||||
tableCell.Add(tableParagraph);
|
||||
if (row.Span.IsEmpty)
|
||||
{
|
||||
row.Span = cellContainer.Span;
|
||||
row.Line = cellContainer.Line;
|
||||
row.Column = cellContainer.Column;
|
||||
}
|
||||
row.Add(tableCell);
|
||||
cells.Add(tableCell);
|
||||
}
|
||||
|
||||
// If we have a new line, we can add the row
|
||||
if (isLine)
|
||||
{
|
||||
Debug.Assert(row != null);
|
||||
if (table.Span.IsEmpty)
|
||||
{
|
||||
table.Span = row.Span;
|
||||
table.Line = row.Line;
|
||||
table.Column = row.Column;
|
||||
}
|
||||
table.Add(row);
|
||||
row = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Once we are done with the cells, we can remove all end of lines in the table tree
|
||||
foreach (var endOfLine in tableState.EndOfLines)
|
||||
{
|
||||
endOfLine.Remove();
|
||||
}
|
||||
|
||||
// If we have a header row, we can remove it
|
||||
// TODO: we could optimize this by merging FindHeaderRow and the previous loop
|
||||
if (aligns != null)
|
||||
{
|
||||
table.RemoveAt(1);
|
||||
@@ -379,28 +449,23 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
|
||||
// Perform delimiter processor that are coming after this processor
|
||||
var delimiterProcessors = state.Parsers.DelimiterProcessors;
|
||||
for (int i = 0; i < delimiterProcessors.Length; i++)
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (delimiterProcessors[i] == this)
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
var paragraph = (ParagraphBlock) cell[0];
|
||||
|
||||
state.ProcessDelimiters(i + 1, paragraph.Inline, null, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
var paragraph = (ParagraphBlock) cell[0];
|
||||
state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true);
|
||||
}
|
||||
|
||||
// Clear cells when we are done
|
||||
cells.Clear();
|
||||
|
||||
// Normalize the table
|
||||
table.Normalize();
|
||||
|
||||
// We don't want to continue procesing delimiters, as we are already processing them here
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ParseHeaderString(Inline inline, out TableColumnAlign align)
|
||||
private static bool ParseHeaderString(Inline inline, out TableColumnAlign? align)
|
||||
{
|
||||
align = 0;
|
||||
var literal = inline as LiteralInline;
|
||||
@@ -429,60 +494,62 @@ namespace Markdig.Extensions.Tables
|
||||
List<TableColumnDefinition> aligns = null;
|
||||
for (int i = 0; i < delimiters.Count; i++)
|
||||
{
|
||||
if (delimiters[i] != null && IsLine(delimiters[i]))
|
||||
if (!IsLine(delimiters[i]))
|
||||
{
|
||||
// The last delimiter is always null,
|
||||
for (int j = i + 1; j < delimiters.Count - 1; j++)
|
||||
{
|
||||
var delimiter = delimiters[j];
|
||||
var nextDelimiter = delimiters[j + 1];
|
||||
|
||||
var columnDelimiter = delimiter as PiprTableDelimiterInline;
|
||||
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the left side of a `|` delimiter
|
||||
TableColumnAlign align = TableColumnAlign.Left;
|
||||
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Create aligns until we may have a header row
|
||||
if (aligns == null)
|
||||
{
|
||||
aligns = new List<TableColumnDefinition>();
|
||||
}
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
|
||||
// If this is the last delimiter, we need to check the right side of the `|` delimiter
|
||||
if (nextDelimiter == null)
|
||||
{
|
||||
var nextSibling = columnDelimiter != null
|
||||
? columnDelimiter.FirstChild
|
||||
: delimiter.NextSibling;
|
||||
|
||||
if (!ParseHeaderString(nextSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
isValidRow = true;
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are on a Line delimiter, exit
|
||||
if (IsLine(delimiter))
|
||||
{
|
||||
isValidRow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The last delimiter is always null,
|
||||
for (int j = i + 1; j < delimiters.Count; j++)
|
||||
{
|
||||
var delimiter = delimiters[j];
|
||||
var nextDelimiter = j + 1 < delimiters.Count ? delimiters[j + 1] : null;
|
||||
|
||||
var columnDelimiter = delimiter as PipeTableDelimiterInline;
|
||||
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the left side of a `|` delimiter
|
||||
TableColumnAlign? align = null;
|
||||
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Create aligns until we may have a header row
|
||||
if (aligns == null)
|
||||
{
|
||||
aligns = new List<TableColumnDefinition>();
|
||||
}
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
|
||||
// If this is the last delimiter, we need to check the right side of the `|` delimiter
|
||||
if (nextDelimiter == null)
|
||||
{
|
||||
var nextSibling = columnDelimiter != null
|
||||
? columnDelimiter.FirstChild
|
||||
: delimiter.NextSibling;
|
||||
|
||||
if (!ParseHeaderString(nextSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
isValidRow = true;
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are on a Line delimiter, exit
|
||||
if (IsLine(delimiter))
|
||||
{
|
||||
isValidRow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return isValidRow ? aligns : null;
|
||||
@@ -517,21 +584,6 @@ namespace Markdig.Extensions.Tables
|
||||
return previous == null || IsLine(previous);
|
||||
}
|
||||
|
||||
private static bool IsTrailingColumnDelimiter(PiprTableDelimiterInline inline)
|
||||
{
|
||||
var child = inline.FirstChild;
|
||||
var literal = child as LiteralInline;
|
||||
if (literal != null)
|
||||
{
|
||||
if (!literal.Content.IsEmptyOrWhitespace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
child = child.NextSibling;
|
||||
}
|
||||
return child == null || IsLine(child);
|
||||
}
|
||||
|
||||
private static void TrimStart(Inline inline)
|
||||
{
|
||||
while (inline is ContainerInline && !(inline is DelimiterInline))
|
||||
@@ -560,6 +612,7 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
ColumnAndLineDelimiters = new List<Inline>();
|
||||
Cells = new List<TableCell>();
|
||||
EndOfLines = new List<Inline>();
|
||||
}
|
||||
|
||||
public bool IsInvalidTable { get; set; }
|
||||
@@ -571,6 +624,8 @@ namespace Markdig.Extensions.Tables
|
||||
public List<Inline> ColumnAndLineDelimiters { get; }
|
||||
|
||||
public List<TableCell> Cells { get; }
|
||||
|
||||
public List<Inline> EndOfLines { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,68 @@ namespace Markdig.Extensions.Tables
|
||||
/// Gets or sets the column alignments. May be null.
|
||||
/// </summary>
|
||||
public List<TableColumnDefinition> ColumnDefinitions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the table structure is valid.
|
||||
/// </summary>
|
||||
/// <returns><c>True</c> if the table has rows and the number of cells per row is correct, other wise <c>false</c>.</returns>
|
||||
public bool IsValid()
|
||||
{
|
||||
// A table with no rows is not valid.
|
||||
if (Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var columnCount = ColumnDefinitions.Count;
|
||||
var rows = new int[Count];
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
var row = (TableRow)this[i];
|
||||
for (int j = 0; j < row.Count; j++)
|
||||
{
|
||||
var cell = (TableCell)row[j];
|
||||
rows[i] += cell.ColumnSpan;
|
||||
var rowSpan = cell.RowSpan - 1;
|
||||
while (rowSpan > 0)
|
||||
{
|
||||
rows[i + rowSpan] += cell.ColumnSpan;
|
||||
rowSpan--;
|
||||
}
|
||||
}
|
||||
if (rows[i] > columnCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the number of columns of this table by taking the maximum columns and appending empty cells.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
var maxColumn = 0;
|
||||
for (int i = 0; i < this.Count; i++)
|
||||
{
|
||||
var row = this[i] as TableRow;
|
||||
if (row != null && row.Count > maxColumn)
|
||||
{
|
||||
maxColumn = row.Count;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.Count; i++)
|
||||
{
|
||||
var row = this[i] as TableRow;
|
||||
if (row != null)
|
||||
{
|
||||
for (int j = row.Count; j < maxColumn; j++)
|
||||
{
|
||||
row.Add(new TableCell());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,30 @@ namespace Markdig.Extensions.Tables
|
||||
/// <param name="parser">The parser used to create this block.</param>
|
||||
public TableCell(BlockParser parser) : base(parser)
|
||||
{
|
||||
AllowClose = true;
|
||||
ColumnSpan = 1;
|
||||
ColumnIndex = -1;
|
||||
RowSpan = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the column to which this cell belongs.
|
||||
/// </summary>
|
||||
public int ColumnIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the column span this cell is covering. Default is 1.
|
||||
/// </summary>
|
||||
public int ColumnSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the row span this cell is covering. Default is 1.
|
||||
/// </summary>
|
||||
public int RowSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this cell can be closed.
|
||||
/// </summary>
|
||||
public bool AllowClose { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ namespace Markdig.Extensions.Tables
|
||||
/// <summary>
|
||||
/// Gets or sets the column alignment.
|
||||
/// </summary>
|
||||
public TableColumnAlign Alignment { get; set; }
|
||||
public TableColumnAlign? Alignment { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace Markdig.Extensions.Tables
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successfull
|
||||
/// </returns>
|
||||
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign align)
|
||||
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align)
|
||||
{
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace Markdig.Extensions.Tables
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successfull
|
||||
/// </returns>
|
||||
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign align)
|
||||
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align)
|
||||
{
|
||||
delimiterChar = '\0';
|
||||
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
|
||||
@@ -49,11 +49,10 @@ namespace Markdig.Extensions.Tables
|
||||
/// <returns>
|
||||
/// <c>true</c> if parsing was successfull
|
||||
/// </returns>
|
||||
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign align)
|
||||
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align)
|
||||
{
|
||||
align = TableColumnAlign.Left;
|
||||
align = null;
|
||||
|
||||
// Work on a copy of the slice
|
||||
slice.TrimStart();
|
||||
var c = slice.CurrentChar;
|
||||
bool hasLeft = false;
|
||||
@@ -87,6 +86,7 @@ namespace Markdig.Extensions.Tables
|
||||
count++;
|
||||
}
|
||||
|
||||
// We expect at least one `-` delimiter char
|
||||
if (count == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -104,7 +104,7 @@ namespace Markdig.Extensions.Tables
|
||||
|
||||
align = hasLeft && hasRight
|
||||
? TableColumnAlign.Center
|
||||
: hasRight ? TableColumnAlign.Right : TableColumnAlign.Left;
|
||||
: hasRight ? TableColumnAlign.Right : hasLeft ? TableColumnAlign.Left : (TableColumnAlign?) null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Markdig.Extensions.TaskLists
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
|
||||
31
src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs
Normal file
31
src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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.Parsers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Yaml
|
||||
{
|
||||
/// <summary>
|
||||
/// A YAML frontmatter block.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Syntax.CodeBlock" />
|
||||
public class YamlFrontMatterBlock : CodeBlock, IFencedBlock
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="YamlFrontMatterBlock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parser">The parser.</param>
|
||||
public YamlFrontMatterBlock(BlockParser parser) : base(parser)
|
||||
{
|
||||
}
|
||||
|
||||
public string Info { get; set; }
|
||||
|
||||
public string Arguments { get; set; }
|
||||
|
||||
public int FencedCharCount { get; set; }
|
||||
|
||||
public char FencedChar { get; set; }
|
||||
}
|
||||
}
|
||||
33
src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs
Normal file
33
src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs
Normal 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.Parsers;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.Yaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to discard a YAML frontmatter at the beginning of a Markdown document.
|
||||
/// </summary>
|
||||
public class YamlFrontMatterExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
if (!pipeline.BlockParsers.Contains<YamlFrontMatterParser>())
|
||||
{
|
||||
// Insert the YAML parser before the thematic break parser, as it is also triggered on a --- dash
|
||||
pipeline.BlockParsers.InsertBefore<ThematicBreakParser>(new YamlFrontMatterParser());
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (!renderer.ObjectRenderers.Contains<YamlFrontMatterRenderer>())
|
||||
{
|
||||
renderer.ObjectRenderers.InsertBefore<CodeBlockRenderer>(new YamlFrontMatterRenderer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs
Normal file
44
src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs
Normal 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.Parsers;
|
||||
|
||||
namespace Markdig.Extensions.Yaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Block parser for a YAML frontmatter.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.FencedBlockParserBase{YamlFrontMatterBlock}" />
|
||||
public class YamlFrontMatterParser : FencedBlockParserBase<YamlFrontMatterBlock>
|
||||
{
|
||||
// We reuse a FencedCodeBlock parser to grab a frontmatter, only active if it happens on the first line of the document.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FencedCodeBlockParser"/> class.
|
||||
/// </summary>
|
||||
public YamlFrontMatterParser()
|
||||
{
|
||||
OpeningCharacters = new[] { '-' };
|
||||
InfoPrefix = null;
|
||||
// We expect only 3 --- at the beginning of the file no more, no less
|
||||
MinimumMatchCount = 3;
|
||||
MaximumMatchCount = 3;
|
||||
}
|
||||
|
||||
protected override YamlFrontMatterBlock CreateFencedBlock(BlockProcessor processor)
|
||||
{
|
||||
return new YamlFrontMatterBlock(this);
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
// Only accept a frontmatter at the beginning of the file
|
||||
if (processor.LineIndex != 0)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
return base.TryOpen(processor);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Markdig/Extensions/Yaml/YamlFrontMatterRenderer.cs
Normal file
19
src/Markdig/Extensions/Yaml/YamlFrontMatterRenderer.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.Yaml
|
||||
{
|
||||
/// <summary>
|
||||
/// Empty renderer for a <see cref="YamlFrontMatterBlock"/>
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{YamlFrontMatterBlock}" />
|
||||
public class YamlFrontMatterRenderer : HtmlObjectRenderer<YamlFrontMatterBlock>
|
||||
{
|
||||
protected override void Write(HtmlRenderer renderer, YamlFrontMatterBlock obj)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for handling characters.
|
||||
/// </summary>
|
||||
@@ -18,6 +20,11 @@ namespace Markdig.Helpers
|
||||
|
||||
public const string ZeroSafeString = "\uFFFD";
|
||||
|
||||
// We don't support LCDM
|
||||
private static IDictionary<char, int> romanMap = new Dictionary<char, int> { { 'I', 1 }, { 'V', 5 }, { 'X', 10 } };
|
||||
|
||||
private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' };
|
||||
|
||||
public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose)
|
||||
{
|
||||
// A left-flanking delimiter run is a delimiter run that is
|
||||
@@ -32,8 +39,11 @@ namespace Markdig.Helpers
|
||||
pc.CheckUnicodeCategory(out prevIsWhiteSpace, out prevIsPunctuation);
|
||||
c.CheckUnicodeCategory(out nextIsWhiteSpace, out nextIsPunctuation);
|
||||
|
||||
var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc);
|
||||
var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c);
|
||||
|
||||
canOpen = !nextIsWhiteSpace &&
|
||||
(!nextIsPunctuation || prevIsWhiteSpace || prevIsPunctuation);
|
||||
((!nextIsPunctuation || nextIsExcepted) || prevIsWhiteSpace || prevIsPunctuation);
|
||||
|
||||
|
||||
// A right-flanking delimiter run is a delimiter run that is
|
||||
@@ -42,7 +52,7 @@ namespace Markdig.Helpers
|
||||
// or a punctuation character.
|
||||
// For purposes of this definition, the beginning and the end of the line count as Unicode whitespace.
|
||||
canClose = !prevIsWhiteSpace &&
|
||||
(!prevIsPunctuation || nextIsWhiteSpace || nextIsPunctuation);
|
||||
((!prevIsPunctuation || prevIsExcepted) || nextIsWhiteSpace || nextIsPunctuation);
|
||||
|
||||
if (!enableWithinWord)
|
||||
{
|
||||
@@ -80,6 +90,26 @@ namespace Markdig.Helpers
|
||||
return c == 'I' || c == 'V' || c == 'X';
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public static int RomanToArabic(string text)
|
||||
{
|
||||
int result = 0;
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
var character = Char.ToUpperInvariant(text[i]);
|
||||
var candidate = romanMap[character];
|
||||
if (i + 1 < text.Length && candidate < romanMap[Char.ToUpperInvariant(text[i + 1])])
|
||||
{
|
||||
result -= candidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += candidate;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public static int AddTab(int column)
|
||||
{
|
||||
@@ -132,6 +162,9 @@ namespace Markdig.Helpers
|
||||
return IsWhitespace(c) || IsZero(c);
|
||||
}
|
||||
|
||||
// Note that we are not considering the character & as a punctuation in HTML
|
||||
// as it is used for HTML entities, print unicode, so we assume that when we have a `&`
|
||||
// it is more likely followed by a valid HTML Entity that represents a non punctuation
|
||||
public static void CheckUnicodeCategory(this char c, out bool space, out bool punctuation)
|
||||
{
|
||||
// Credits: code from CommonMark.NET
|
||||
@@ -140,7 +173,7 @@ namespace Markdig.Helpers
|
||||
if (c <= 'ÿ')
|
||||
{
|
||||
space = c == '\0' || c == ' ' || (c >= '\t' && c <= '\r') || c == '\u00a0' || c == '\u0085';
|
||||
punctuation = c == '\0' || (c >= 33 && c <= 47) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126);
|
||||
punctuation = c == '\0' || (c >= 33 && c <= 47 && c != 38) || (c >= 58 && c <= 64) || (c >= 91 && c <= 96) || (c >= 123 && c <= 126);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
1304
src/Markdig/Helpers/CharNormalizer.cs
Normal file
1304
src/Markdig/Helpers/CharNormalizer.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -115,35 +115,44 @@ namespace Markdig.Helpers
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
|
||||
public unsafe int IndexOfOpeningCharacter(string text, int start, int end)
|
||||
public int IndexOfOpeningCharacter(string text, int start, int end)
|
||||
{
|
||||
var maxChar = isOpeningCharacter.Length;
|
||||
#if SUPPORT_UNSAFE
|
||||
unsafe
|
||||
#endif
|
||||
{
|
||||
#if SUPPORT_FIXED_STRING
|
||||
fixed (char* pText = text)
|
||||
#else
|
||||
var pText = text;
|
||||
var pText = text;
|
||||
#endif
|
||||
#if SUPPORT_UNSAFE
|
||||
fixed (bool* openingChars = isOpeningCharacter)
|
||||
#else
|
||||
var openingChars = isOpeningCharacter;
|
||||
#endif
|
||||
fixed (bool* openingChars = isOpeningCharacter)
|
||||
{
|
||||
if (nonAsciiMap == null)
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
if (nonAsciiMap == null)
|
||||
{
|
||||
var c = pText[i];
|
||||
if (c < maxChar && openingChars[c])
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
return i;
|
||||
var c = pText[i];
|
||||
if (c < maxChar && openingChars[c])
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = start; i <= end; i++)
|
||||
else
|
||||
{
|
||||
var c = pText[i];
|
||||
if ((c < maxChar && openingChars[c]) || nonAsciiMap.ContainsKey(c))
|
||||
for (int i = start; i <= end; i++)
|
||||
{
|
||||
return i;
|
||||
var c = pText[i];
|
||||
if ((c < maxChar && openingChars[c]) || nonAsciiMap.ContainsKey(c))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +617,7 @@ namespace Markdig.Helpers
|
||||
{
|
||||
// expect a letter and 1-31 letters or digits
|
||||
c = s[pos + 1];
|
||||
if ((c < 'A' || c > 'Z') && (c < 'a' && c > 'z'))
|
||||
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')))
|
||||
return 0;
|
||||
|
||||
for (i = pos + 2; i < lastPos; i++)
|
||||
|
||||
@@ -31,6 +31,12 @@ namespace Markdig.Helpers
|
||||
/// <returns>The next character. `\0` is end of the iteration.</returns>
|
||||
char NextChar();
|
||||
|
||||
/// <summary>
|
||||
/// Peeks at the next character, without incrementing the <see cref="Start"/> position.
|
||||
/// </summary>
|
||||
/// <returns>The next character. `\0` is end of the iteration.</returns>
|
||||
char PeekChar();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is empty.
|
||||
/// </summary>
|
||||
|
||||
@@ -20,55 +20,59 @@ namespace Markdig.Helpers
|
||||
|
||||
public static string Urilize(string headingText, bool allowOnlyAscii)
|
||||
{
|
||||
#if SUPPORT_NORMALIZE
|
||||
// Normalzie the string if we don't allow UTF8
|
||||
if (allowOnlyAscii)
|
||||
{
|
||||
headingText = headingText.Normalize(NormalizationForm.FormD);
|
||||
}
|
||||
#endif
|
||||
|
||||
var headingBuffer = StringBuilderCache.Local();
|
||||
bool hasLetter = false;
|
||||
bool previousIsSpace = false;
|
||||
for (int i = 0; i < headingText.Length; i++)
|
||||
{
|
||||
var c = headingText[i];
|
||||
if (char.IsLetter(c))
|
||||
var normalized = allowOnlyAscii ? CharNormalizer.ConvertToAscii(c) : null;
|
||||
for (int j = 0; j < (normalized?.Length ?? 1); j++)
|
||||
{
|
||||
#if SUPPORT_NORMALIZE
|
||||
if (allowOnlyAscii && (c < ' ' || c >= 127))
|
||||
if (normalized != null)
|
||||
{
|
||||
continue;
|
||||
c = normalized[j];
|
||||
}
|
||||
#endif
|
||||
c = char.IsUpper(c) ? char.ToLowerInvariant(c) : c;
|
||||
headingBuffer.Append(c);
|
||||
hasLetter = true;
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (hasLetter)
|
||||
{
|
||||
if (IsReservedPunctuation(c))
|
||||
|
||||
if (char.IsLetter(c))
|
||||
{
|
||||
if (previousIsSpace)
|
||||
if (allowOnlyAscii && (c < ' ' || c >= 127))
|
||||
{
|
||||
headingBuffer.Length--;
|
||||
}
|
||||
if (headingBuffer[headingBuffer.Length - 1] != c)
|
||||
{
|
||||
headingBuffer.Append(c);
|
||||
continue;
|
||||
}
|
||||
c = char.IsUpper(c) ? char.ToLowerInvariant(c) : c;
|
||||
headingBuffer.Append(c);
|
||||
hasLetter = true;
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (!previousIsSpace && c.IsWhitespace())
|
||||
else if (hasLetter)
|
||||
{
|
||||
var pc = headingBuffer[headingBuffer.Length - 1];
|
||||
if (!IsReservedPunctuation(pc))
|
||||
if (IsReservedPunctuation(c))
|
||||
{
|
||||
headingBuffer.Append('-');
|
||||
if (previousIsSpace)
|
||||
{
|
||||
headingBuffer.Length--;
|
||||
}
|
||||
if (headingBuffer[headingBuffer.Length - 1] != c)
|
||||
{
|
||||
headingBuffer.Append(c);
|
||||
}
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (c.IsDigit())
|
||||
{
|
||||
headingBuffer.Append(c);
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (!previousIsSpace && c.IsWhitespace())
|
||||
{
|
||||
var pc = headingBuffer[headingBuffer.Length - 1];
|
||||
if (!IsReservedPunctuation(pc))
|
||||
{
|
||||
headingBuffer.Append('-');
|
||||
}
|
||||
previousIsSpace = true;
|
||||
}
|
||||
previousIsSpace = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +116,7 @@ namespace Markdig.Helpers
|
||||
// An absolute URI, for these purposes, consists of a scheme followed by a colon (:)
|
||||
// followed by zero or more characters other than ASCII whitespace and control characters, <, and >.
|
||||
// If the URI includes these characters, they must be percent-encoded (e.g. %20 for a space).
|
||||
// A URI that would end with a full stop (.) is treated instead as ending immediately before the full stop.
|
||||
|
||||
// a scheme is any sequence of 2–32 characters
|
||||
// beginning with an ASCII letter
|
||||
@@ -254,6 +259,7 @@ namespace Markdig.Helpers
|
||||
break;
|
||||
}
|
||||
builder.Append(c);
|
||||
pc = c;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -591,7 +597,13 @@ namespace Markdig.Helpers
|
||||
|
||||
hasEscape = false;
|
||||
|
||||
if (c == '\0' || c.IsSpaceOrTab() || c.IsControl()) // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
|
||||
if (IsEndOfUri(c))
|
||||
{
|
||||
isValid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == '.' && IsEndOfUri(text.PeekChar()))
|
||||
{
|
||||
isValid = true;
|
||||
break;
|
||||
@@ -608,6 +620,11 @@ namespace Markdig.Helpers
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private static bool IsEndOfUri(char c)
|
||||
{
|
||||
return c == '\0' || c.IsSpaceOrTab() || c.IsControl(); // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
|
||||
}
|
||||
|
||||
public static bool TryParseLinkReferenceDefinition<T>(T text, out string label, out string url,
|
||||
out string title) where T : ICharIterator
|
||||
{
|
||||
@@ -644,7 +661,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
text.NextChar(); // Skip ':'
|
||||
|
||||
// Skip any whitespaces before the url
|
||||
// Skip any whitespace before the url
|
||||
text.TrimStart();
|
||||
|
||||
urlSpan.Start = text.Start;
|
||||
|
||||
@@ -102,14 +102,20 @@ namespace Markdig.Helpers
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ReplacyBy<TElement>(T element) where TElement : T
|
||||
/// <summary>
|
||||
/// Replaces <typeparamref name="TElement"/> with <paramref name="replacement"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TElement">Element type to find in the list</typeparam>
|
||||
/// <param name="replacement">Object to replace this element with</param>
|
||||
/// <returns><c>true</c> if a replacement was made; otherwise <c>false</c>.</returns>
|
||||
public bool Replace<TElement>(T replacement) where TElement : T
|
||||
{
|
||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||
for (int i = 0; i < Count; i++)
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
if (this[i] is TElement)
|
||||
{
|
||||
this[i] = element;
|
||||
RemoveAt(i);
|
||||
Insert(i, replacement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user