mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-12 05:44:48 +00:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b83de5934d | ||
|
|
c294d3bfb4 | ||
|
|
a7cdb2351a | ||
|
|
46ef21a3ed | ||
|
|
18c8d0178c | ||
|
|
ebc79dafbd | ||
|
|
a1d2467643 | ||
|
|
8220f0fa56 | ||
|
|
ec385acc7f | ||
|
|
04c1cc62d4 | ||
|
|
abdbd65f60 | ||
|
|
1f32a060da | ||
|
|
699d80c150 | ||
|
|
56bcac7600 | ||
|
|
cab3365104 | ||
|
|
3821bd00fe | ||
|
|
62701fd0f1 | ||
|
|
1be5e60506 | ||
|
|
c9f1512358 | ||
|
|
8f23aed6af | ||
|
|
6a62ae9c69 | ||
|
|
2c3de5688b | ||
|
|
f3c08b4ec4 | ||
|
|
69e3baafe5 | ||
|
|
5844ccc395 | ||
|
|
be9c6fa54b | ||
|
|
d14f277c7b | ||
|
|
593bf08b92 | ||
|
|
85f631f868 | ||
|
|
5ad964bcb6 | ||
|
|
137a404bdc | ||
|
|
5204ec758a | ||
|
|
6f4fb69c62 | ||
|
|
0a1b37c965 | ||
|
|
90bdafb05a | ||
|
|
8cc668ae6d | ||
|
|
1787dc4590 | ||
|
|
0a9cc8fcd7 | ||
|
|
10c06daf5d | ||
|
|
a262e42980 | ||
|
|
4cd3d045d1 | ||
|
|
92357576b1 | ||
|
|
d45f67f8c2 | ||
|
|
2571cdffee | ||
|
|
c31cb6da27 | ||
|
|
5503929d15 | ||
|
|
ca32dda1fe | ||
|
|
12111e0b63 | ||
|
|
bdd46c0fc0 | ||
|
|
daf4c8fe86 | ||
|
|
bd2c2aff9c | ||
|
|
921f75e1f3 | ||
|
|
44a3b85f0b | ||
|
|
f9e827395b | ||
|
|
64a9a80774 | ||
|
|
e10594391d | ||
|
|
60eb03a221 | ||
|
|
c0a0f10af0 | ||
|
|
56e1ed0e25 | ||
|
|
a9f33cbca6 | ||
|
|
6f1d39e1bb | ||
|
|
838f7c5598 | ||
|
|
0f54cc5927 | ||
|
|
12745f70cf | ||
|
|
442737767f | ||
|
|
61ac46e467 | ||
|
|
85550580d5 | ||
|
|
ed69ac5fe0 | ||
|
|
0aec5a5783 | ||
|
|
c1885fe31b | ||
|
|
c2270a2b3a | ||
|
|
3e60515bb3 | ||
|
|
8550c13688 | ||
|
|
3aa65694aa | ||
|
|
52403687db | ||
|
|
3e83409cf4 | ||
|
|
9b051955bd | ||
|
|
35c8126add | ||
|
|
67e1c8ce7f | ||
|
|
6e5fbda8e5 | ||
|
|
90e7ccd80a | ||
|
|
c09b3eedd2 | ||
|
|
9441e8a04b | ||
|
|
301dc70ab4 | ||
|
|
959d6db62f | ||
|
|
38a7410e4b | ||
|
|
b941a58ad0 | ||
|
|
3fa20fa92f | ||
|
|
5dcd4ea4aa | ||
|
|
9c03683913 | ||
|
|
2fed1b3ebf |
BIN
img/BenchmarkCPU.png
Normal file
BIN
img/BenchmarkCPU.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
img/BenchmarkMemory.png
Normal file
BIN
img/BenchmarkMemory.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
img/markdig.png
Normal file
BIN
img/markdig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
131
img/markdig.svg
Normal file
131
img/markdig.svg
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="192"
|
||||
height="192"
|
||||
viewBox="0 0 192 192"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="markdig.svg"
|
||||
inkscape:export-filename="C:\Code\lunet-io\markdig\markdig.png"
|
||||
inkscape:export-xdpi="93.400002"
|
||||
inkscape:export-ydpi="93.400002">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.4374889"
|
||||
inkscape:cx="174.14769"
|
||||
inkscape:cy="93.189838"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(0,-860.36216)">
|
||||
<path
|
||||
style="fill:#000000"
|
||||
d="M 75.009766 60.486328 L 34.648438 122.74414 C 33.793918 123.59769 37.647081 134.96384 37.052734 136.09766 L 4.0234375 177.69727 C 2.4142291 180.39677 3.2900484 182.21846 4.8730469 183.84766 C 5.9214414 184.93636 6.6591287 186.06887 8.3828125 185.67188 C 9.0750612 185.50987 10.104893 185.27338 10.875 184.76758 L 52.806641 151.81445 C 53.912466 151.23195 64.44071 154.77813 65.289062 153.92383 L 126.45312 111.46875 L 75.009766 60.486328 z M 89.632812 84.769531 L 103.77539 98.912109 L 79.027344 123.66016 L 86.238281 130.87109 L 48.621094 139.92383 L 57.435547 102.30859 L 64.884766 109.51758 L 89.632812 84.769531 z "
|
||||
transform="translate(0,860.36216)"
|
||||
id="path4140" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
d="m 111.18463,862.06984 c -1.98231,0 -3.96282,0.78454 -5.54759,2.38445 L 88.200894,881.94537 75.123368,868.82653 c -3.169466,-3.18017 -7.92567,-3.18017 -11.095213,0 -3.169466,3.18017 -3.169466,7.95108 0,11.13109 l 13.077526,13.11885 -11.095212,10.73223 82.031621,81.49227 11.09525,-11.13109 13.87084,13.51554 c 1.57915,1.59105 3.56724,2.38445 5.54759,2.38445 1.98231,0 3.96285,-0.78453 5.54762,-2.38445 3.16947,-3.18017 3.16947,-7.95111 0,-11.13109 l -13.87083,-13.11884 17.43611,-17.09442 c 1.17983,-1.59105 1.98231,-3.5788 1.98231,-5.96329 0,-1.98351 -0.79863,-3.97554 -2.37816,-5.56446 l -70.54053,-70.35856 c -1.57914,-1.59105 -3.56724,-2.38446 -5.54758,-2.38446 z m 15.86949,20.75826 9.50139,9.5291 -4.04453,23.11372 23.04691,-4.05619 9.50138,9.5291 -36.8437,36.95052 -9.50135,-9.5291 21.13082,-21.19197 -23.04672,4.05619 4.04453,-23.11396 -21.131009,21.19198 -9.501383,-9.5291 36.843662,-36.95048 z"
|
||||
id="path4142"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
id="rect4168"
|
||||
mask="url(#a)"
|
||||
ry="0"
|
||||
height="0"
|
||||
width="0"
|
||||
x="141.51523"
|
||||
y="364.10403" />
|
||||
<rect
|
||||
id="rect4184"
|
||||
mask="url(#a)"
|
||||
ry="0"
|
||||
height="0"
|
||||
width="0"
|
||||
x="96.108383"
|
||||
y="352.01443" />
|
||||
<rect
|
||||
id="rect4200"
|
||||
mask="url(#a)"
|
||||
ry="2.1886277"
|
||||
height="0.14590852"
|
||||
width="0.17464182"
|
||||
x="87.014519"
|
||||
y="276.38696" />
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot4797"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
transform="matrix(0.1746417,0,0,0.1459084,499.69318,366.39614)"><flowRegion
|
||||
id="flowRegion4799"><rect
|
||||
id="rect4801"
|
||||
width="972.27185"
|
||||
height="618.71844"
|
||||
x="959.01355"
|
||||
y="-976.15039" /></flowRegion><flowPara
|
||||
id="flowPara4803" /></flowRoot> <g
|
||||
id="g4833"
|
||||
transform="matrix(0.09510056,0,0,0.09061765,496.09965,368.83934)">
|
||||
<rect
|
||||
id="rect4823"
|
||||
height="1"
|
||||
width="1"
|
||||
x="0"
|
||||
y="0"
|
||||
style="fill:#ffffff" />
|
||||
</g>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4886"
|
||||
d="m 111.18463,862.06984 c -1.98231,0 -3.96282,0.78454 -5.54759,2.38445 L 88.200894,881.94537 75.123368,868.82653 c -3.169466,-3.18017 -7.92567,-3.18017 -11.095213,0 -3.169466,3.18017 -3.169466,7.95108 0,11.13109 l 13.077526,13.11885 -11.095212,10.73223 82.031621,81.49227 11.09525,-11.13109 13.87084,13.51554 c 1.57915,1.59105 3.56724,2.38445 5.54759,2.38445 1.98231,0 3.96285,-0.78453 5.54762,-2.38445 3.16947,-3.18017 3.16947,-7.95111 0,-11.13109 l -13.87083,-13.11884 17.43611,-17.09442 c 1.17983,-1.59105 1.98231,-3.5788 1.98231,-5.96329 0,-1.98351 -0.79863,-3.97554 -2.37816,-5.56446 l -70.54053,-70.35856 c -1.57914,-1.59105 -3.56724,-2.38446 -5.54758,-2.38446 z m 15.86949,20.75826 9.50139,9.5291 -4.04453,23.11372 23.04691,-4.05619 9.50138,9.5291 -36.8437,36.95052 -9.50135,-9.5291 21.13082,-21.19197 -23.04672,4.05619 4.04453,-23.11396 -21.131009,21.19198 -9.501383,-9.5291 36.843662,-36.95048 z"
|
||||
style="fill:#000000;fill-opacity:1" />
|
||||
<g
|
||||
transform="translate(234.63786,787.55486)"
|
||||
id="g4170" />
|
||||
<path
|
||||
id="path4225"
|
||||
transform="translate(0,860.36216)"
|
||||
d="M 75.009766 60.486328 L 34.648438 122.74414 C 33.793918 123.59769 37.647081 134.96384 37.052734 136.09766 L 4.0234375 177.69727 C 2.4142291 180.39677 3.2900484 182.21846 4.8730469 183.84766 C 5.9214414 184.93636 6.6591287 186.06887 8.3828125 185.67188 C 9.0750612 185.50987 10.104893 185.27338 10.875 184.76758 L 52.806641 151.81445 C 53.912466 151.23195 64.44071 154.77813 65.289062 153.92383 L 126.45312 111.46875 L 75.009766 60.486328 z M 89.632812 84.769531 L 103.77539 98.912109 L 79.027344 123.66016 L 86.238281 130.87109 L 48.621094 139.92383 L 57.435547 102.30859 L 64.884766 109.51758 L 89.632812 84.769531 z "
|
||||
style="fill:#000000" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
BIN
img/markdig128.png
Normal file
BIN
img/markdig128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
img/markdig64.png
Normal file
BIN
img/markdig64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
121
readme.md
121
readme.md
@@ -1,22 +1,25 @@
|
||||
# Markdig [](https://ci.appveyor.com/project/xoofx/markdig) [](https://www.nuget.org/packages/Markdig/)
|
||||
|
||||
Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
<img align="right" width="160px" height="160px" src="img/markdig.png">
|
||||
|
||||
Markdig is a fast, powerful, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
|
||||
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
|
||||
|
||||
You can **try Markdig online** and compare it to other implems on [babelmark3](http://babelmark.github.io/)
|
||||
You can **try Markdig online** and compare it to other implementations on [babelmark3](https://babelmark.github.io/?text=Hello+**Markdig**!)
|
||||
|
||||
## Features
|
||||
|
||||
- **Very fast parser** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
|
||||
- **Abstract Syntax Tree**
|
||||
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
|
||||
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
|
||||
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
|
||||
- Converter to **HTML**
|
||||
- Passing more than **600+ tests** from the latest [CommonMark specs](http://spec.commonmark.org/)
|
||||
- Includes all the core elements of CommonMark:
|
||||
- including GFM fenced code blocks.
|
||||
- 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 **18+ extensions**, including:
|
||||
- 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))
|
||||
@@ -30,6 +33,7 @@ You can **try Markdig online** and compare it to other implems on [babelmark3](h
|
||||
- **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))
|
||||
@@ -42,8 +46,17 @@ You can **try Markdig online** and compare it to other implems on [babelmark3](h
|
||||
- **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)
|
||||
- **Diagrams** 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** to parse without evaluating the frontmatter and to discard it from the HTML output (typically used for previewing without the frontmatter in MarkdownEditor)
|
||||
- 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/)
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
Markdig is available as a NuGet package: [](https://www.nuget.org/packages/Markdig/)
|
||||
@@ -69,6 +82,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).
|
||||
@@ -78,24 +95,44 @@ This software is released under the [BSD-Clause 2 license](https://github.com/lu
|
||||
|
||||
This is an early preview of the benchmarking against various implementations:
|
||||
|
||||
- Markdig: itself
|
||||
- CommonMarkCpp: [cmark](https://github.com/jgm/cmark), Reference C implementation of CommonMark, no support for extensions
|
||||
- [CommonMark.NET](https://github.com/Knagis/CommonMark.NET): CommonMark implementation for .NET, no support for extensions, port of cmark
|
||||
- [CommonMarkNet (devel)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
|
||||
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) another .NET implementation
|
||||
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
|
||||
- [Moonshine](https://github.com/brandonc/moonshine): popular C Markdown processor
|
||||
**C implementations**:
|
||||
|
||||
Markdig is roughly x100 times faster than MarkdownSharp and extremelly competitive to other implems (that are not feature wise comparable)
|
||||
- [cmark](https://github.com/jgm/cmark) (version: 0.25.0): Reference C implementation of CommonMark, no support for extensions
|
||||
- [Moonshine](https://github.com/brandonc/moonshine) (version: : popular C Markdown processor
|
||||
|
||||
Performance in x86:
|
||||
**.NET implementations**:
|
||||
|
||||
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.5.x): itself
|
||||
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.11.0): CommonMark implementation for .NET, no support for extensions, port of cmark
|
||||
- [CommonMark.NET(pipe_tables)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
|
||||
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) (version: 1.5.0): another .NET implementation
|
||||
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 1.13.0): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
|
||||
- [Marked.NET](https://github.com/T-Alex/MarkedNet) (version: 1.0.5) port of original [marked.js](https://github.com/chjj/marked) project
|
||||
- [Microsoft.DocAsCode.MarkdownLite](https://github.com/dotnet/docfx/tree/dev/src/Microsoft.DocAsCode.MarkdownLite) (version: 2.0.1) used by the [docfx](https://github.com/dotnet/docfx) project
|
||||
|
||||
**JavaScript/V8 implementations**:
|
||||
|
||||
- [Strike.V8](https://github.com/SimonCropp/Strike) (version: 1.5.0) [marked.js](https://github.com/chjj/marked) running in Google V8 (not .NET based)
|
||||
|
||||
### Analysis of the results:
|
||||
|
||||
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
|
||||
- **Among the best in CPU**, Extremely competitive and often faster than other implementations (not feature wise equivalent)
|
||||
- **15% to 30% less allocations** and GC pressure
|
||||
|
||||
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
### Performance for x86:
|
||||
|
||||
```
|
||||
// * Summary *
|
||||
|
||||
BenchmarkDotNet-Dev=v0.9.6.0+
|
||||
BenchmarkDotNet-Dev=v0.9.7.0+
|
||||
OS=Microsoft Windows NT 6.2.9200.0
|
||||
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
|
||||
Processor=Intel(R) Core(TM) i7-4770 CPU 3.40GHz, ProcessorCount=8
|
||||
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
|
||||
HostCLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
|
||||
JitModules=clrjit-v4.6.1080.0
|
||||
@@ -103,26 +140,23 @@ JitModules=clrjit-v4.6.1080.0
|
||||
Type=Program Mode=SingleRun LaunchCount=2
|
||||
WarmupCount=2 TargetCount=10
|
||||
|
||||
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
|
||||
--------------------- |---------- |---------- |------- |------ |------- |------------------- |
|
||||
TestMarkdig | 5.4870 ms | 0.0158 ms | 193.00 | 12.00 | 84.00 | 1,425,192.72 |
|
||||
TestCommonMarkCpp | 4.0134 ms | 0.1008 ms | - | - | 180.00 | 454,859.74 |
|
||||
TestCommonMarkNet | 4.6139 ms | 0.0581 ms | 193.00 | 12.00 | 84.00 | 1,406,367.27 |
|
||||
TestCommonMarkNetNew | 5.5327 ms | 0.0461 ms | 193.00 | 96.00 | 84.00 | 1,738,465.42 |
|
||||
TestMarkdownDeep | 7.5910 ms | 0.1006 ms | 205.00 | 96.00 | 84.00 | 1,758,383.79 |
|
||||
TestMoonshine | 5.8843 ms | 0.1758 ms | - | - | 215.00 | 565,000.73 |
|
||||
|
||||
// * Diagnostic Output - MemoryDiagnoser *
|
||||
|
||||
|
||||
// ***** BenchmarkRunner: End *****
|
||||
Method | Median | StdDev |Scaled | Gen 0 | Gen 1| Gen 2|Bytes Allocated/Op |
|
||||
--------------------------- |------------ |---------- |------ | ------ |------|---------|------------------ |
|
||||
Markdig | 5.5316 ms | 0.0372 ms | 0.71 | 56.00| 21.00| 49.00| 1,285,917.31 |
|
||||
CommonMark.NET(master) | 4.7035 ms | 0.0422 ms | 0.60 | 113.00| 7.00| 49.00| 1,502,404.60 |
|
||||
CommonMark.NET(pipe_tables) | 5.6164 ms | 0.0298 ms | 0.72 | 111.00| 56.00| 49.00| 1,863,128.13 |
|
||||
MarkdownDeep | 7.8193 ms | 0.0334 ms | 1.00 | 120.00| 56.00| 49.00| 1,884,854.85 |
|
||||
cmark | 4.2698 ms | 0.1526 ms | 0.55 | -| -| -| NA |
|
||||
Moonshine | 6.0929 ms | 0.1053 ms | 1.28 | -| -| -| NA |
|
||||
Strike.V8 | 10.5895 ms | 0.0492 ms | 1.35 | -| -| -| NA |
|
||||
Marked.NET | 207.3169 ms | 5.2628 ms | 26.51 | 0.00| 0.00| 0.00| 303,125,228.65 |
|
||||
MarkdownSharp | 675.0185 ms | 2.8447 ms | 86.32 | 40.00| 27.00| 41.00| 2,413,394.17 |
|
||||
Microsoft DocfxMarkdownLite | 166.3357 ms | 0.4529 ms | 21.27 |4,452.00|948.00|11,167.00| 180,218,359.60 |
|
||||
```
|
||||
|
||||
Performance for x64:
|
||||
### Performance for x64:
|
||||
|
||||
```
|
||||
// * Summary *
|
||||
|
||||
BenchmarkDotNet-Dev=v0.9.6.0+
|
||||
OS=Microsoft Windows NT 6.2.9200.0
|
||||
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
|
||||
@@ -135,15 +169,10 @@ WarmupCount=2 TargetCount=10
|
||||
|
||||
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
|
||||
--------------------- |---------- |---------- |------- |------- |------ |------------------- |
|
||||
TestMarkdig | 5.9539 ms | 0.0495 ms | 157.00 | 96.00 | 84.00 | 1,767,834.52 |
|
||||
TestCommonMarkNet | 4.3158 ms | 0.0161 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
|
||||
TestCommonMarkNetNew | 5.3421 ms | 0.0435 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
|
||||
TestMarkdownDeep | 7.4750 ms | 0.0281 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
|
||||
|
||||
// * Diagnostic Output - MemoryDiagnoser *
|
||||
|
||||
|
||||
// ***** BenchmarkRunner: End *****
|
||||
TestMarkdig | 5.5276 ms | 0.0402 ms | 109.00 | 96.00 | 84.00 | 1,537,027.66 |
|
||||
TestCommonMarkNet | 4.4661 ms | 0.1190 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
|
||||
TestCommonMarkNetNew | 5.3151 ms | 0.0815 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
|
||||
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
|
||||
```
|
||||
|
||||
## Credits
|
||||
@@ -152,7 +181,9 @@ Thanks to the fantastic work done by [John Mac Farlane](http://johnmacfarlane.ne
|
||||
|
||||
This project would not have been possible without this huge foundation.
|
||||
|
||||
Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/BenchmarkDotNet) that makes benchmarking so easy to setup!
|
||||
Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/BenchmarkDotNet) that makes benchmarking so easy to setup!
|
||||
|
||||
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)
|
||||
|
||||
## Author
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -59,8 +60,13 @@
|
||||
<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="TestPragmaLines.cs" />
|
||||
<Compile Include="TestSourcePosition.cs" />
|
||||
<Compile Include="TestStringSliceList.cs" />
|
||||
<Compile Include="TestPlayParser.cs" />
|
||||
<Compile Include="TextAssert.cs" />
|
||||
@@ -81,6 +87,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\YamlSpecs.md" />
|
||||
<None Include="Specs\TaskListSpecs.md" />
|
||||
<None Include="Specs\SmartyPantsSpecs.md" />
|
||||
<None Include="Specs\MediaSpecs.md" />
|
||||
<None Include="Specs\MathSpecs.md" />
|
||||
@@ -95,6 +105,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
|
||||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
|
||||
</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,25 @@ 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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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,11 @@ 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 and followed 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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -68,6 +68,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
|
||||
|
||||
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>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -38,18 +38,27 @@ a | b
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
While the following would be considered as a plain paragraph with a list item:
|
||||
The following is also considered as a table, even if the second line starts like a list:
|
||||
|
||||
```````````````````````````````` example
|
||||
a | b
|
||||
- | -
|
||||
0 | 1
|
||||
.
|
||||
<p>a | b</p>
|
||||
<ul>
|
||||
<li>| -
|
||||
0 | 1</li>
|
||||
</ul>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
A pipe table with only one header row is allowed:
|
||||
@@ -113,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
|
||||
@@ -132,7 +141,8 @@ a | b
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1 | 2</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
@@ -205,6 +215,76 @@ Column delimiters `|` at the very beginning of a line or just before a line endi
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
A pipe may be present at both the beginning/ending of each line:
|
||||
|
||||
```````````````````````````````` 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>
|
||||
````````````````````````````````
|
||||
|
||||
Or may be ommitted on one side:
|
||||
|
||||
```````````````````````````````` 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>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` 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>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
|
||||
Single column table can be declared with lines starting only by a column delimiter:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -266,7 +346,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
|
||||
@@ -278,19 +359,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>
|
||||
@@ -298,6 +379,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
|
||||
@@ -403,3 +509,42 @@ 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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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,24 +38,29 @@ 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"), "emphasisextra"),
|
||||
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"), "listextra"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+cites"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "math"),
|
||||
// new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/cfc84164475d3bec8be9482c21a705adc93a54f5/spec.txt", string.Empty), // 0.26 specs
|
||||
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"), "medias"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "smartypants"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers"),
|
||||
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"),
|
||||
};
|
||||
var emptyLines = false;
|
||||
var displayEmptyLines = false;
|
||||
|
||||
29
src/Markdig.Tests/Specs/TaskListSpecs.md
Normal file
29
src/Markdig.Tests/Specs/TaskListSpecs.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Extensions
|
||||
|
||||
Adds support for task lists:
|
||||
|
||||
## TaskLists
|
||||
|
||||
A task list item consist of `[ ]` or `[x]` or `[X]` inside a list item (ordered or unordered)
|
||||
|
||||
```````````````````````````````` example
|
||||
- [ ] Item1
|
||||
- [x] Item2
|
||||
- [ ] Item3
|
||||
- Item4
|
||||
.
|
||||
<ul class="contains-task-list">
|
||||
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item1</li>
|
||||
<li class="task-list-item"><input disabled="disabled" type="checkbox" checked="checked" /> Item2</li>
|
||||
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item3</li>
|
||||
<li>Item4</li>
|
||||
</ul>
|
||||
````````````````````````````````
|
||||
|
||||
A task is not recognized outside a list item:
|
||||
|
||||
```````````````````````````````` example
|
||||
[ ] This is not a task list
|
||||
.
|
||||
<p>[ ] This is not a task list</p>
|
||||
````````````````````````````````
|
||||
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>
|
||||
````````````````````````````````
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/Markdig.Tests/TestLineReader.cs
Normal file
128
src/Markdig.Tests/TestLineReader.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using Markdig.Helpers;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Test for <see cref="LineReader"/>.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TestLineReader
|
||||
{
|
||||
[Test]
|
||||
public void TestEmpty()
|
||||
{
|
||||
var lineReader = new LineReader("");
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinesOnlyLf()
|
||||
{
|
||||
var lineReader = new LineReader("\n\n\n");
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(1, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(2, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinesOnlyCr()
|
||||
{
|
||||
var lineReader = new LineReader("\r\r\r");
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(1, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(2, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinesOnlyCrLf()
|
||||
{
|
||||
var lineReader = new LineReader("\r\n\r\n\r\n");
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(2, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(4, lineReader.SourcePosition);
|
||||
Assert.AreEqual(string.Empty, lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoEndOfLine()
|
||||
{
|
||||
var lineReader = new LineReader("123");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLf()
|
||||
{
|
||||
var lineReader = new LineReader("123\n");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(4, lineReader.SourcePosition);
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLf2()
|
||||
{
|
||||
// When limited == true, we limit the internal buffer exactly after the first new line char '\n'
|
||||
var lineReader = new LineReader("123\n456");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(4, lineReader.SourcePosition);
|
||||
Assert.AreEqual("456", lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCr()
|
||||
{
|
||||
var lineReader = new LineReader("123\r");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(4, lineReader.SourcePosition);
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCr2()
|
||||
{
|
||||
var lineReader = new LineReader("123\r456");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(4, lineReader.SourcePosition);
|
||||
Assert.AreEqual("456", lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCrLf()
|
||||
{
|
||||
// When limited == true, we limit the internal buffer exactly after the first new line char '\r'
|
||||
// and we check that we don't get a new line for `\n`
|
||||
var lineReader = new LineReader("123\r\n");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(5, lineReader.SourcePosition);
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCrLf2()
|
||||
{
|
||||
var lineReader = new LineReader("123\r\n456");
|
||||
Assert.AreEqual("123", lineReader.ReadLine()?.ToString());
|
||||
Assert.AreEqual(5, lineReader.SourcePosition);
|
||||
Assert.AreEqual("456", lineReader.ReadLine()?.ToString());
|
||||
Assert.Null(lineReader.ReadLine()?.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,36 +80,52 @@ namespace Markdig.Tests
|
||||
[Test]
|
||||
public void TestUrlAndTitle()
|
||||
{
|
||||
// 0 1 2 3
|
||||
// 0123456789012345678901234567890123456789
|
||||
var text = new StringSlice(@"(http://google.com 'this is a title')ABC");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual("http://google.com", link);
|
||||
Assert.AreEqual("this is a title", title);
|
||||
Assert.AreEqual(new SourceSpan(1, 17), linkSpan);
|
||||
Assert.AreEqual(new SourceSpan(19, 35), titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlAndTitleEmpty()
|
||||
{
|
||||
// 01234
|
||||
var text = new StringSlice(@"(<>)A");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(new SourceSpan(1, 2), linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlAndTitleEmpty2()
|
||||
{
|
||||
// 012345
|
||||
var text = new StringSlice(@"( <> )A");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(new SourceSpan(2, 3), linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
@@ -117,12 +133,18 @@ namespace Markdig.Tests
|
||||
[Test]
|
||||
public void TestUrlEmptyWithTitleWithMultipleSpaces()
|
||||
{
|
||||
// 0 1 2
|
||||
// 0123456789012345678901234567
|
||||
var text = new StringSlice(@"( <> 'toto' )A");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual("toto", title);
|
||||
Assert.AreEqual(new SourceSpan(4, 5), linkSpan);
|
||||
Assert.AreEqual(new SourceSpan(12, 17), titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
@@ -132,50 +154,67 @@ namespace Markdig.Tests
|
||||
var text = new StringSlice(@"()A");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual(string.Empty, link);
|
||||
Assert.AreEqual(string.Empty, title);
|
||||
Assert.AreEqual(SourceSpan.Empty, linkSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleLines()
|
||||
{
|
||||
var text = new StringSlice(@"(
|
||||
<http://google.com>
|
||||
'toto' )A");
|
||||
// 0 1 2 3
|
||||
// 01 2345678901234567890 1234567890123456789
|
||||
var text = new StringSlice("(\n<http://google.com>\n 'toto' )A");
|
||||
string link;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
|
||||
Assert.AreEqual("http://google.com", link);
|
||||
Assert.AreEqual("toto", title);
|
||||
Assert.AreEqual(new SourceSpan(2, 20), linkSpan);
|
||||
Assert.AreEqual(new SourceSpan(26, 31), titleSpan);
|
||||
Assert.AreEqual('A', text.CurrentChar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLabelSimple()
|
||||
{
|
||||
// 01234
|
||||
var text = new StringSlice("[foo]");
|
||||
string label;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
|
||||
SourceSpan labelSpan;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
|
||||
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
|
||||
Assert.AreEqual("foo", label);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLabelEscape()
|
||||
{
|
||||
// 012345678
|
||||
var text = new StringSlice(@"[fo\[\]o]");
|
||||
string label;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
|
||||
SourceSpan labelSpan;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
|
||||
Assert.AreEqual(new SourceSpan(1, 7), labelSpan);
|
||||
Assert.AreEqual(@"fo[]o", label);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLabelEscape2()
|
||||
{
|
||||
// 0123
|
||||
var text = new StringSlice(@"[\]]");
|
||||
string label;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
|
||||
SourceSpan labelSpan;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
|
||||
Assert.AreEqual(new SourceSpan(1, 2), labelSpan);
|
||||
Assert.AreEqual(@"]", label);
|
||||
}
|
||||
|
||||
@@ -194,23 +233,36 @@ namespace Markdig.Tests
|
||||
[Test]
|
||||
public void TestLabelWhitespaceCollapsedAndTrim()
|
||||
{
|
||||
// 0 1 2 3
|
||||
// 0123456789012345678901234567890123456789
|
||||
var text = new StringSlice(@"[ fo o z ]");
|
||||
string label;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
|
||||
SourceSpan labelSpan;
|
||||
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
|
||||
Assert.AreEqual(new SourceSpan(6, 17), labelSpan);
|
||||
Assert.AreEqual(@"fo o z", label);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestlLinkReferenceDefinitionSimple()
|
||||
{
|
||||
// 0 1 2 3
|
||||
// 0123456789012345678901234567890123456789
|
||||
var text = new StringSlice(@"[foo]: /toto 'title'");
|
||||
string label;
|
||||
string url;
|
||||
string title;
|
||||
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title));
|
||||
SourceSpan labelSpan;
|
||||
SourceSpan urlSpan;
|
||||
SourceSpan titleSpan;
|
||||
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan, out titleSpan));
|
||||
Assert.AreEqual(@"foo", label);
|
||||
Assert.AreEqual(@"/toto", url);
|
||||
Assert.AreEqual(@"title", title);
|
||||
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
|
||||
Assert.AreEqual(new SourceSpan(7, 11), urlSpan);
|
||||
Assert.AreEqual(new SourceSpan(13, 19), titleSpan);
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -245,5 +297,102 @@ 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")]
|
||||
public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult)
|
||||
{
|
||||
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,25 +15,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!
|
||||
@@ -42,20 +47,20 @@ namespace Markdig.Tests
|
||||
yield return new KeyValuePair<string, MarkdownPipeline>("default", new MarkdownPipelineBuilder().Build());
|
||||
|
||||
yield return new KeyValuePair<string, MarkdownPipeline>("advanced", new MarkdownPipelineBuilder() // Use similar to advanced extension without auto-identifier
|
||||
.UseAbbreviation()
|
||||
//.UseAutoIdentifier()
|
||||
.UseCite()
|
||||
.UseCustomContainer()
|
||||
.UseDefinitionList()
|
||||
.UseEmphasisExtra()
|
||||
.UseFigure()
|
||||
.UseFooter()
|
||||
.UseAbbreviations()
|
||||
//.UseAutoIdentifiers()
|
||||
.UseCitations()
|
||||
.UseCustomContainers()
|
||||
.UseDefinitionLists()
|
||||
.UseEmphasisExtras()
|
||||
.UseFigures()
|
||||
.UseFooters()
|
||||
.UseFootnotes()
|
||||
.UseGridTable()
|
||||
.UseMath()
|
||||
.UseMedia()
|
||||
.UsePipeTable()
|
||||
.UseListExtra()
|
||||
.UseGridTables()
|
||||
.UseMathematics()
|
||||
.UseMediaLinks()
|
||||
.UsePipeTables()
|
||||
.UseListExtras()
|
||||
.UseGenericAttributes().Build());
|
||||
|
||||
yield break;
|
||||
@@ -64,12 +69,14 @@ namespace Markdig.Tests
|
||||
var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var extensionsText in extensionGroups)
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().Configure(extensionsText);
|
||||
var builder = new MarkdownPipelineBuilder();
|
||||
builder.DebugLog = Console.Out;
|
||||
var pipeline = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText);
|
||||
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, pipeline.Build());
|
||||
}
|
||||
}
|
||||
|
||||
private static string DisplaySpaceAndTabs(string text)
|
||||
public static string DisplaySpaceAndTabs(string text)
|
||||
{
|
||||
// Output special characters to check correctly the results
|
||||
return text.Replace('\t', '→').Replace(' ', '·');
|
||||
@@ -78,7 +85,7 @@ namespace Markdig.Tests
|
||||
private static string Compact(string html)
|
||||
{
|
||||
// Normalize the output to make it compatible with CommonMark specs
|
||||
html = html.Replace("\r", "").Trim();
|
||||
html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim();
|
||||
html = Regex.Replace(html, @"\s+</li>", "</li>");
|
||||
html = Regex.Replace(html, @"<li>\s+", "<li>");
|
||||
html = html.Normalize(NormalizationForm.FormKD);
|
||||
|
||||
@@ -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
|
||||
@@ -21,12 +24,108 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
|
||||
//> titi toto
|
||||
//");
|
||||
|
||||
//var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtra());
|
||||
var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviation().Build());
|
||||
//var result = Markdown.ToHtml(text, new MarkdownPipeline().UseFootnotes().UseEmphasisExtras());
|
||||
var result = Markdown.ToHtml(text, new MarkdownPipelineBuilder().UseAbbreviations().Build());
|
||||
//File.WriteAllText("test.html", result, Encoding.UTF8);
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyLiteral()
|
||||
{
|
||||
var text = @"> *some text*
|
||||
> 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()
|
||||
{
|
||||
// TODO: Add this test back to the CommonMark specs
|
||||
var text = @"- item1
|
||||
- item2
|
||||
- item3
|
||||
- item4";
|
||||
TestParser.TestSpec(text, @"<ul>
|
||||
<li>item1
|
||||
<ul>
|
||||
<li>item2
|
||||
<ul>
|
||||
<li>item3
|
||||
<ul>
|
||||
<li>item4</li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestHtmlBug()
|
||||
{
|
||||
TestParser.TestSpec(@" # header1
|
||||
|
||||
<pre class='copy'>
|
||||
blabla
|
||||
</pre>
|
||||
|
||||
# header2
|
||||
", @"<h1>header1</h1>
|
||||
<pre class='copy'>
|
||||
blabla
|
||||
</pre>
|
||||
<h1>header2</h1>");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestStandardUriEscape()
|
||||
{
|
||||
TestParser.TestSpec(@"", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestBugAdvancaed()
|
||||
{
|
||||
TestParser.TestSpec(@"`https://{domain}/callbacks`
|
||||
#### HEADING
|
||||
Paragraph
|
||||
", "<p><code>https://{domain}/callbacks</code></p>\n<h4 id=\"heading\">HEADING</h4>\n<p>Paragraph</p>", "advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamePipelineAllExtensions()
|
||||
{
|
||||
|
||||
81
src/Markdig.Tests/TestPragmaLines.cs
Normal file
81
src/Markdig.Tests/TestPragmaLines.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Syntax;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestPragmaLines
|
||||
{
|
||||
[Test]
|
||||
public void TestFindClosest()
|
||||
{
|
||||
var doc = Markdown.Parse(
|
||||
"test1\n" + // 0
|
||||
"\n" + // 1
|
||||
"test2\n" + // 2
|
||||
"\n" + // 3
|
||||
"test3\n" + // 4
|
||||
"\n" + // 5
|
||||
"test4\n" + // 6
|
||||
"\n" + // 7
|
||||
"# Heading\n" + // 8
|
||||
"\n" + // 9
|
||||
"Long para\n" + // 10
|
||||
"on multiple\n" + // 11
|
||||
"lines\n" + // 12
|
||||
"to check that\n" + // 13
|
||||
"lines are\n" + // 14
|
||||
"correctly \n" + // 15
|
||||
"found\n" + // 16
|
||||
"\n" + // 17
|
||||
"- item1\n" + // 18
|
||||
"- item2\n" + // 19
|
||||
"- item3\n" + // 20
|
||||
"\n" + // 21
|
||||
"This is a last paragraph\n" // 22
|
||||
, new MarkdownPipelineBuilder().UsePragmaLines().Build());
|
||||
|
||||
foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22})
|
||||
{
|
||||
Assert.AreEqual(exact, doc.FindClosestLine(exact));
|
||||
}
|
||||
|
||||
Assert.AreEqual(22, doc.FindClosestLine(23));
|
||||
|
||||
Assert.AreEqual(10, doc.FindClosestLine(11));
|
||||
Assert.AreEqual(10, doc.FindClosestLine(12));
|
||||
Assert.AreEqual(10, doc.FindClosestLine(13));
|
||||
Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next
|
||||
Assert.AreEqual(18, doc.FindClosestLine(15));
|
||||
Assert.AreEqual(18, doc.FindClosestLine(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFindClosest1()
|
||||
{
|
||||
var text =
|
||||
"- item1\n" + // 0
|
||||
" - item11\n" + // 1
|
||||
" - item12\n" + // 2
|
||||
" - item121\n" + // 3
|
||||
" - item13\n" + // 4
|
||||
" - item131\n" + // 5
|
||||
" - item1311\n"; // 6
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build();
|
||||
var doc = Markdown.Parse(text, pipeline);
|
||||
|
||||
for (int exact = 0; exact < 7; exact++)
|
||||
{
|
||||
Assert.AreEqual(exact, doc.FindClosestLine(exact));
|
||||
}
|
||||
|
||||
Assert.AreEqual(6, doc.FindClosestLine(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
791
src/Markdig.Tests/TestSourcePosition.cs
Normal file
791
src/Markdig.Tests/TestSourcePosition.cs
Normal file
@@ -0,0 +1,791 @@
|
||||
// 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.Linq;
|
||||
using System.Text;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Test the precise source location of all Markdown elements, including extensions
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TestSourcePosition
|
||||
{
|
||||
[Test]
|
||||
public void TestParagraph()
|
||||
{
|
||||
Check("0123456789", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParagraphAndNewLine()
|
||||
{
|
||||
Check("0123456789\n0123456789", @"
|
||||
paragraph ( 0, 0) 0-20
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
literal ( 1, 0) 11-20
|
||||
");
|
||||
|
||||
Check("0123456789\r\n0123456789", @"
|
||||
paragraph ( 0, 0) 0-21
|
||||
literal ( 0, 0) 0-9
|
||||
linebreak ( 0,10) 10-10
|
||||
literal ( 1, 0) 12-21
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParagraphNewLineAndSpaces()
|
||||
{
|
||||
// 0123 45678
|
||||
Check("012\n 345", @"
|
||||
paragraph ( 0, 0) 0-8
|
||||
literal ( 0, 0) 0-2
|
||||
linebreak ( 0, 3) 3-3
|
||||
literal ( 1, 2) 6-8
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParagraph2()
|
||||
{
|
||||
Check("0123456789\n\n0123456789", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-9
|
||||
paragraph ( 2, 0) 12-21
|
||||
literal ( 2, 0) 12-21
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasis()
|
||||
{
|
||||
Check("012**3456789**", @"
|
||||
paragraph ( 0, 0) 0-13
|
||||
literal ( 0, 0) 0-2
|
||||
emphasis ( 0, 3) 3-13
|
||||
literal ( 0, 5) 5-11
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasis2()
|
||||
{
|
||||
// 01234567
|
||||
Check("01*2**3*", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
literal ( 0, 0) 0-1
|
||||
emphasis ( 0, 2) 2-7
|
||||
literal ( 0, 3) 3-3
|
||||
literal ( 0, 4) 4-5
|
||||
literal ( 0, 6) 6-6
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasis3()
|
||||
{
|
||||
// 0123456789
|
||||
Check("01**2***3*", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-1
|
||||
emphasis ( 0, 2) 2-6
|
||||
literal ( 0, 4) 4-4
|
||||
emphasis ( 0, 7) 7-9
|
||||
literal ( 0, 8) 8-8
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasisFalse()
|
||||
{
|
||||
Check("0123456789**0123", @"
|
||||
paragraph ( 0, 0) 0-15
|
||||
literal ( 0, 0) 0-9
|
||||
literal ( 0,10) 10-11
|
||||
literal ( 0,12) 12-15
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHeading()
|
||||
{
|
||||
// 012345
|
||||
Check("# 2345", @"
|
||||
heading ( 0, 0) 0-5
|
||||
literal ( 0, 2) 2-5
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHeadingWithEmphasis()
|
||||
{
|
||||
// 0123456789
|
||||
Check("# 23**45**", @"
|
||||
heading ( 0, 0) 0-9
|
||||
literal ( 0, 2) 2-3
|
||||
emphasis ( 0, 4) 4-9
|
||||
literal ( 0, 6) 6-7
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCodeSpan()
|
||||
{
|
||||
// 012345678
|
||||
Check("0123`456`", @"
|
||||
paragraph ( 0, 0) 0-8
|
||||
literal ( 0, 0) 0-3
|
||||
code ( 0, 4) 4-8
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLink()
|
||||
{
|
||||
// 0123456789
|
||||
Check("012[45](#)", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-2
|
||||
link ( 0, 3) 3-9
|
||||
literal ( 0, 4) 4-5
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinkParts1()
|
||||
{
|
||||
// 0 1
|
||||
// 01 2 3456789012345
|
||||
var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
|
||||
Assert.NotNull(link);
|
||||
|
||||
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
|
||||
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLinkParts2()
|
||||
{
|
||||
// 0 1
|
||||
// 01 2 34567890123456789
|
||||
var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
|
||||
Assert.NotNull(link);
|
||||
|
||||
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
|
||||
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
|
||||
Assert.AreEqual(new SourceSpan(16, 19), link.TitleSpan);
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestLinkParts3()
|
||||
{
|
||||
// 0 1
|
||||
// 01 2 3456789012345
|
||||
var link = Markdown.Parse("0\n\n01", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
|
||||
Assert.NotNull(link);
|
||||
|
||||
Assert.AreEqual(new SourceSpan(5, 15), link.Span);
|
||||
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
|
||||
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
|
||||
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAutolinkInline()
|
||||
{
|
||||
// 0123456789ABCD
|
||||
Check("01<http://yes>", @"
|
||||
paragraph ( 0, 0) 0-13
|
||||
literal ( 0, 0) 0-1
|
||||
autolink ( 0, 2) 2-13
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFencedCodeBlock()
|
||||
{
|
||||
// 012 3456 78 9ABC
|
||||
Check("01\n```\n3\n```\n", @"
|
||||
paragraph ( 0, 0) 0-1
|
||||
literal ( 0, 0) 0-1
|
||||
fencedcode ( 1, 0) 3-11
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlBlock()
|
||||
{
|
||||
// 012345 67 89ABCDE F 0
|
||||
Check("<div>\n0\n</div>\n\n1", @"
|
||||
html ( 0, 0) 0-13
|
||||
paragraph ( 4, 0) 16-16
|
||||
literal ( 4, 0) 16-16
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlBlock1()
|
||||
{
|
||||
// 0 1
|
||||
// 01 2 345678901 23
|
||||
Check("0\n\n<!--A-->\n1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
html ( 2, 0) 3-10
|
||||
paragraph ( 3, 0) 12-12
|
||||
literal ( 3, 0) 12-12
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlComment()
|
||||
{
|
||||
// 0 1 2
|
||||
// 012345678901 234567890 1234
|
||||
Check("# 012345678\n<!--0-->\n123\n", @"
|
||||
heading ( 0, 0) 0-10
|
||||
literal ( 0, 2) 2-10
|
||||
html ( 1, 0) 12-19
|
||||
paragraph ( 2, 0) 21-23
|
||||
literal ( 2, 0) 21-23
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlInline()
|
||||
{
|
||||
// 0123456789
|
||||
Check("01<b>4</b>", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-1
|
||||
html ( 0, 2) 2-4
|
||||
literal ( 0, 5) 5-5
|
||||
html ( 0, 6) 6-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlInline1()
|
||||
{
|
||||
// 0
|
||||
// 0123456789
|
||||
Check("0<!--A-->1", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-0
|
||||
html ( 0, 1) 1-8
|
||||
literal ( 0, 9) 9-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestThematicBreak()
|
||||
{
|
||||
// 0123 4567
|
||||
Check("---\n---\n", @"
|
||||
thematicbreak ( 0, 0) 0-2
|
||||
thematicbreak ( 1, 0) 4-6
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuoteBlock()
|
||||
{
|
||||
// 0123456
|
||||
Check("> 2345\n", @"
|
||||
quote ( 0, 0) 0-5
|
||||
paragraph ( 0, 2) 2-5
|
||||
literal ( 0, 2) 2-5
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuoteBlockWithLines()
|
||||
{
|
||||
// 01234 56789A
|
||||
Check("> 01\n> 23\n", @"
|
||||
quote ( 0, 0) 0-9
|
||||
paragraph ( 0, 2) 2-9
|
||||
literal ( 0, 2) 2-3
|
||||
linebreak ( 0, 4) 4-4
|
||||
literal ( 1, 3) 8-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuoteBlockWithLazyContinuation()
|
||||
{
|
||||
// 01234 56
|
||||
Check("> 01\n23\n", @"
|
||||
quote ( 0, 0) 0-6
|
||||
paragraph ( 0, 2) 2-6
|
||||
literal ( 0, 2) 2-3
|
||||
linebreak ( 0, 4) 4-4
|
||||
literal ( 1, 0) 5-6
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestListBlock()
|
||||
{
|
||||
// 0123 4567
|
||||
Check("- 0\n- 1\n", @"
|
||||
list ( 0, 0) 0-6
|
||||
listitem ( 0, 0) 0-2
|
||||
paragraph ( 0, 2) 2-2
|
||||
literal ( 0, 2) 2-2
|
||||
listitem ( 1, 0) 4-6
|
||||
paragraph ( 1, 2) 6-6
|
||||
literal ( 1, 2) 6-6
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEscapeInline()
|
||||
{
|
||||
// 0123
|
||||
Check(@"\-\)", @"
|
||||
paragraph ( 0, 0) 0-3
|
||||
literal ( 0, 0) 0-1
|
||||
literal ( 0, 2) 2-3
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHtmlEntityInline()
|
||||
{
|
||||
// 01 23456789
|
||||
Check("0\n 1", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
htmlentity ( 1, 0) 2-7
|
||||
literal ( 1, 6) 8-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAbbreviations()
|
||||
{
|
||||
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @"
|
||||
paragraph ( 2, 0) 38-102
|
||||
container ( 2, 0) 38-102
|
||||
literal ( 2, 0) 38-66
|
||||
abbreviation ( 2,29) 67-70
|
||||
literal ( 2,33) 71-98
|
||||
abbreviation ( 2,61) 99-102
|
||||
", "abbreviations");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCitation()
|
||||
{
|
||||
// 0123 4 567 8
|
||||
Check("01 \"\"23\"\"", @"
|
||||
paragraph ( 0, 0) 0-8
|
||||
literal ( 0, 0) 0-2
|
||||
emphasis ( 0, 3) 3-8
|
||||
literal ( 0, 5) 5-6
|
||||
", "citations");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomContainer()
|
||||
{
|
||||
// 01 2345 678 9ABC DEF
|
||||
Check("0\n:::\n23\n:::\n45\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
customcontainer ( 1, 0) 2-11
|
||||
paragraph ( 2, 0) 6-7
|
||||
literal ( 2, 0) 6-7
|
||||
paragraph ( 4, 0) 13-14
|
||||
literal ( 4, 0) 13-14
|
||||
", "customcontainers");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefinitionList()
|
||||
{
|
||||
// 012 3456789A
|
||||
Check("a0\n: 1234", @"
|
||||
definitionlist ( 0, 0) 0-10
|
||||
definitionitem ( 1, 0) 3-10
|
||||
definitionterm ( 0, 0) 0-1
|
||||
literal ( 0, 0) 0-1
|
||||
paragraph ( 1, 4) 7-10
|
||||
literal ( 1, 4) 7-10
|
||||
", "definitionlists");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefinitionList2()
|
||||
{
|
||||
// 012 3456789AB CDEF01234
|
||||
Check("a0\n: 1234\n: 5678", @"
|
||||
definitionlist ( 0, 0) 0-20
|
||||
definitionitem ( 1, 0) 3-10
|
||||
definitionterm ( 0, 0) 0-1
|
||||
literal ( 0, 0) 0-1
|
||||
paragraph ( 1, 4) 7-10
|
||||
literal ( 1, 4) 7-10
|
||||
definitionitem ( 2, 4) 12-20
|
||||
paragraph ( 2, 5) 17-20
|
||||
literal ( 2, 5) 17-20
|
||||
", "definitionlists");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmoji()
|
||||
{
|
||||
// 01 2345
|
||||
Check("0\n :)\n", @"
|
||||
paragraph ( 0, 0) 0-4
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
emoji ( 1, 1) 3-4
|
||||
", "emojis");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasisExtra()
|
||||
{
|
||||
// 0123456
|
||||
Check("0 ~~1~~", @"
|
||||
paragraph ( 0, 0) 0-6
|
||||
literal ( 0, 0) 0-1
|
||||
emphasis ( 0, 2) 2-6
|
||||
literal ( 0, 4) 4-4
|
||||
", "emphasisextras");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFigures()
|
||||
{
|
||||
// 01 2345 67 89AB
|
||||
Check("0\n^^^\n0\n^^^\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
figure ( 1, 0) 2-10
|
||||
paragraph ( 2, 0) 6-6
|
||||
literal ( 2, 0) 6-6
|
||||
", "figures");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFiguresCaption1()
|
||||
{
|
||||
// 01 234567 89 ABCD
|
||||
Check("0\n^^^ab\n0\n^^^\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
figure ( 1, 0) 2-12
|
||||
figurecaption ( 1, 3) 5-6
|
||||
literal ( 1, 3) 5-6
|
||||
paragraph ( 2, 0) 8-8
|
||||
literal ( 2, 0) 8-8
|
||||
", "figures");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFiguresCaption2()
|
||||
{
|
||||
// 01 2345 67 89ABCD
|
||||
Check("0\n^^^\n0\n^^^ab\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
figure ( 1, 0) 2-12
|
||||
paragraph ( 2, 0) 6-6
|
||||
literal ( 2, 0) 6-6
|
||||
figurecaption ( 3, 3) 11-12
|
||||
literal ( 3, 3) 11-12
|
||||
", "figures");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFooters()
|
||||
{
|
||||
// 01 234567 89ABCD
|
||||
Check("0\n^^ 12\n^^ 34\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
footer ( 1, 0) 2-12
|
||||
paragraph ( 1, 3) 5-12
|
||||
literal ( 1, 3) 5-6
|
||||
linebreak ( 1, 5) 7-7
|
||||
literal ( 2, 3) 11-12
|
||||
", "footers");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestAttributes()
|
||||
{
|
||||
// 0123456789
|
||||
Check("0123{#456}", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
attributes ( 0, 4) 4-9
|
||||
literal ( 0, 0) 0-3
|
||||
", "attributes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAttributesForHeading()
|
||||
{
|
||||
// 0123456789ABC
|
||||
Check("# 01 {#456}", @"
|
||||
heading ( 0, 0) 0-4
|
||||
attributes ( 0, 5) 5-10
|
||||
literal ( 0, 2) 2-3
|
||||
", "attributes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMathematicsInline()
|
||||
{
|
||||
// 01 23456789AB
|
||||
Check("0\n012 $abcd$", @"
|
||||
paragraph ( 0, 0) 0-11
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
literal ( 1, 0) 2-5
|
||||
math ( 1, 4) 6-11
|
||||
attributes ( 0, 0) 0--1
|
||||
", "mathematics");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSmartyPants()
|
||||
{
|
||||
// 01234567
|
||||
// 01 23456789
|
||||
Check("0\n2 <<45>>", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
literal ( 1, 0) 2-3
|
||||
smartypant ( 1, 2) 4-5
|
||||
literal ( 1, 4) 6-7
|
||||
smartypant ( 1, 6) 8-9
|
||||
", "smartypants");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSmartyPantsUnbalanced()
|
||||
{
|
||||
// 012345
|
||||
// 01 234567
|
||||
Check("0\n2 <<45", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
literal ( 0, 0) 0-0
|
||||
linebreak ( 0, 1) 1-1
|
||||
literal ( 1, 0) 2-3
|
||||
literal ( 1, 2) 4-5
|
||||
literal ( 1, 4) 6-7
|
||||
", "smartypants");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPipeTable()
|
||||
{
|
||||
// 0123 4567 89AB
|
||||
Check("a|b\n-|-\n0|1\n", @"
|
||||
table ( 0, 0) 0-10
|
||||
tablerow ( 0, 0) 0-2
|
||||
tablecell ( 0, 0) 0-0
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
tablecell ( 0, 2) 2-2
|
||||
paragraph ( 0, 2) 2-2
|
||||
literal ( 0, 2) 2-2
|
||||
tablerow ( 2, 0) 8-10
|
||||
tablecell ( 2, 0) 8-8
|
||||
paragraph ( 2, 0) 8-8
|
||||
literal ( 2, 0) 8-8
|
||||
tablecell ( 2, 2) 10-10
|
||||
paragraph ( 2, 2) 10-10
|
||||
literal ( 2, 2) 10-10
|
||||
", "pipetables");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPipeTable2()
|
||||
{
|
||||
// 01 2 3456 789A BCD
|
||||
Check("0\n\na|b\n-|-\n0|1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
table ( 2, 0) 3-13
|
||||
tablerow ( 2, 0) 3-5
|
||||
tablecell ( 2, 0) 3-3
|
||||
paragraph ( 2, 0) 3-3
|
||||
literal ( 2, 0) 3-3
|
||||
tablecell ( 2, 2) 5-5
|
||||
paragraph ( 2, 2) 5-5
|
||||
literal ( 2, 2) 5-5
|
||||
tablerow ( 4, 0) 11-13
|
||||
tablecell ( 4, 0) 11-11
|
||||
paragraph ( 4, 0) 11-11
|
||||
literal ( 4, 0) 11-11
|
||||
tablecell ( 4, 2) 13-13
|
||||
paragraph ( 4, 2) 13-13
|
||||
literal ( 4, 2) 13-13
|
||||
", "pipetables");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCode()
|
||||
{
|
||||
// 01 2 345678 9ABCDE
|
||||
Check("0\n\n 0\n 1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
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
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCodeWithTabs()
|
||||
{
|
||||
// 01 2 3 45 6 78
|
||||
Check("0\n\n\t0\n\t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 0) 3-7
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCodeWithMixedTabs()
|
||||
{
|
||||
// 01 2 34 56 78 9
|
||||
Check("0\n\n \t0\n \t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 0) 3-9
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTabsInList()
|
||||
{
|
||||
// 012 34 567 89
|
||||
Check("- \t0\n- \t1\n", @"
|
||||
list ( 0, 0) 0-8
|
||||
listitem ( 0, 0) 0-3
|
||||
paragraph ( 0, 4) 3-3
|
||||
literal ( 0, 4) 3-3
|
||||
listitem ( 1, 0) 5-8
|
||||
paragraph ( 1, 4) 8-8
|
||||
literal ( 1, 4) 8-8
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDocument()
|
||||
{
|
||||
// L0 L0 L1L2 L3 L4 L5L6 L7L8
|
||||
// 0 10 20 30 40 50 60 70 80 90
|
||||
// 012345678901234567890 1 2345678901 2345678901 2345678901 2 345678901234567890123 4 5678901234567890123
|
||||
Check("# This is a document\n\n1) item 1\n2) item 2\n3) item 4\n\nWith an **emphasis**\n\n> and a blockquote\n", @"
|
||||
heading ( 0, 0) 0-19
|
||||
literal ( 0, 2) 2-19
|
||||
list ( 2, 0) 22-51
|
||||
listitem ( 2, 0) 22-30
|
||||
paragraph ( 2, 3) 25-30
|
||||
literal ( 2, 3) 25-30
|
||||
listitem ( 3, 0) 32-40
|
||||
paragraph ( 3, 3) 35-40
|
||||
literal ( 3, 3) 35-40
|
||||
listitem ( 4, 0) 42-51
|
||||
paragraph ( 4, 3) 45-50
|
||||
literal ( 4, 3) 45-50
|
||||
paragraph ( 6, 0) 53-72
|
||||
literal ( 6, 0) 53-60
|
||||
emphasis ( 6, 8) 61-72
|
||||
literal ( 6,10) 63-70
|
||||
quote ( 8, 0) 75-92
|
||||
paragraph ( 8, 2) 77-92
|
||||
literal ( 8, 2) 77-92
|
||||
");
|
||||
}
|
||||
|
||||
private static void Check(string text, string expectedResult, string extensions = null)
|
||||
{
|
||||
var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation();
|
||||
if (extensions != null)
|
||||
{
|
||||
pipelineBuilder.Configure(extensions);
|
||||
}
|
||||
var pipeline = pipelineBuilder.Build();
|
||||
|
||||
var document = Markdown.Parse(text, pipeline);
|
||||
|
||||
var build = new StringBuilder();
|
||||
foreach (var val in document.Descendants())
|
||||
{
|
||||
var name = GetTypeName(val.GetType());
|
||||
build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.Span.Start,2}-{val.Span.End}\n");
|
||||
var attributes = val.TryGetAttributes();
|
||||
if (attributes != null)
|
||||
{
|
||||
build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.Span.Start,2}-{attributes.Span.End}\n");
|
||||
}
|
||||
}
|
||||
var result = build.ToString().Trim();
|
||||
|
||||
expectedResult = expectedResult.Trim();
|
||||
expectedResult = expectedResult.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||
|
||||
if (expectedResult != result)
|
||||
{
|
||||
Console.WriteLine("```````````````````Source");
|
||||
Console.WriteLine(TestParser.DisplaySpaceAndTabs(text));
|
||||
Console.WriteLine("```````````````````Result");
|
||||
Console.WriteLine(result);
|
||||
Console.WriteLine("```````````````````Expected");
|
||||
Console.WriteLine(expectedResult);
|
||||
Console.WriteLine("```````````````````");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
TextAssert.AreEqual(expectedResult, result);
|
||||
}
|
||||
|
||||
private static string GetTypeName(Type type)
|
||||
{
|
||||
return type.Name.ToLowerInvariant()
|
||||
.Replace("block", string.Empty)
|
||||
.Replace("inline", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"NUnit": "3.2.0"
|
||||
"NUnit": "3.2.0",
|
||||
"NUnit3TestAdapter": "3.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.0.0-rc2-3002702",
|
||||
"version": "1.0.0",
|
||||
"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"
|
||||
"Microsoft.ApplicationInsights.AspNetCore": "1.0.0",
|
||||
"Microsoft.AspNetCore.Mvc": "1.0.0",
|
||||
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
|
||||
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
|
||||
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
|
||||
"Microsoft.Extensions.Configuration.Json": "1.0.0",
|
||||
"Microsoft.Extensions.Logging": "1.0.0",
|
||||
"Microsoft.Extensions.Logging.Console": "1.0.0",
|
||||
"Microsoft.Extensions.Logging.Debug": "1.0.0",
|
||||
"Markdig": "0.7.2"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
|
||||
"version": "1.0.0-preview1-final",
|
||||
"version": "1.0.0-preview2-final",
|
||||
"imports": "portable-net45+win8+dnxcore50"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,5 +33,10 @@ namespace Markdig.Extensions.Abbreviations
|
||||
/// The text associated to this label.
|
||||
/// </summary>
|
||||
public StringSlice Text;
|
||||
|
||||
/// <summary>
|
||||
/// The label span
|
||||
/// </summary>
|
||||
public SourceSpan LabelSpan;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.Abbreviations
|
||||
@@ -31,14 +32,16 @@ namespace Markdig.Extensions.Abbreviations
|
||||
|
||||
// A link must be of the form *[Some Text]: An abbreviation
|
||||
var slice = processor.Line;
|
||||
var startPosition = slice.Start;
|
||||
var c = slice.NextChar();
|
||||
if (c != '[')
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
SourceSpan labelSpan;
|
||||
string label;
|
||||
if (!LinkHelper.TryParseLabel(ref slice, out label))
|
||||
if (!LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
@@ -55,7 +58,11 @@ namespace Markdig.Extensions.Abbreviations
|
||||
var abbr = new Abbreviation(this)
|
||||
{
|
||||
Label = label,
|
||||
Text = slice, Line = processor.LineIndex, Column = processor.Column
|
||||
Text = slice,
|
||||
Span = new SourceSpan(startPosition, slice.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column,
|
||||
LabelSpan = labelSpan,
|
||||
};
|
||||
if (!processor.Document.HasAbbreviations())
|
||||
{
|
||||
@@ -84,6 +91,7 @@ namespace Markdig.Extensions.Abbreviations
|
||||
inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) =>
|
||||
{
|
||||
var literal = (LiteralInline) processor.Inline;
|
||||
var originalLiteral = literal;
|
||||
|
||||
ContainerInline container = null;
|
||||
|
||||
@@ -93,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.PeekCharExtra(i - 1);
|
||||
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var indexAfterMatch = i + match.Length;
|
||||
c = content.PeekCharExtra(indexAfterMatch);
|
||||
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// We should have a match, but in case...
|
||||
Abbreviation abbr;
|
||||
@@ -118,15 +115,37 @@ namespace Markdig.Extensions.Abbreviations
|
||||
// If we don't have a container, create a new one
|
||||
if (container == null)
|
||||
{
|
||||
container = new ContainerInline();
|
||||
container = literal.Parent ??
|
||||
new ContainerInline
|
||||
{
|
||||
Span = originalLiteral.Span,
|
||||
Line = originalLiteral.Line,
|
||||
Column = originalLiteral.Column,
|
||||
};
|
||||
}
|
||||
|
||||
var abbrInline = new AbbreviationInline(abbr);
|
||||
int line;
|
||||
int column;
|
||||
var abbrInline = new AbbreviationInline(abbr)
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(i, out line, out column),
|
||||
},
|
||||
Line = line,
|
||||
Column = column
|
||||
};
|
||||
abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1;
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -143,7 +162,12 @@ namespace Markdig.Extensions.Abbreviations
|
||||
}
|
||||
|
||||
// Process the remaining literal
|
||||
literal = new LiteralInline();
|
||||
literal = new LiteralInline()
|
||||
{
|
||||
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
|
||||
Line = line,
|
||||
Column = column + match.Length,
|
||||
};
|
||||
content.Start = indexAfterMatch;
|
||||
literal.Content = content;
|
||||
|
||||
@@ -153,14 +177,47 @@ namespace Markdig.Extensions.Abbreviations
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
processor.Inline = container;
|
||||
// If we have a pending literal, we can add it
|
||||
if (literal != null)
|
||||
{
|
||||
container.AppendChild(literal);
|
||||
}
|
||||
processor.Inline = container;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
break;
|
||||
}
|
||||
index--;
|
||||
}
|
||||
index = matchIndex + match.Length;
|
||||
while (index < content.End)
|
||||
{
|
||||
var c = content.PeekCharAbsolute(index);
|
||||
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!c.IsAsciiPunctuation())
|
||||
{
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace Markdig.Extensions.AutoIdentifiers
|
||||
headingBlockParser.Closed -= HeadingBlockParser_Closed;
|
||||
headingBlockParser.Closed += HeadingBlockParser_Closed;
|
||||
}
|
||||
var paragraphBlockParser = pipeline.BlockParsers.Find<ParagraphBlockParser>();
|
||||
var paragraphBlockParser = pipeline.BlockParsers.FindExact<ParagraphBlockParser>();
|
||||
if (paragraphBlockParser != null)
|
||||
{
|
||||
// Install a hook on the ParagraphBlockParser when a HeadingBlock is actually processed as a Setex heading
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,13 @@ using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.Cites
|
||||
namespace Markdig.Extensions.Citations
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for cite ""...""
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class CiteExtension : IMarkdownExtension
|
||||
public class CitationExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
@@ -30,6 +30,8 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
var startPosition = processor.Start;
|
||||
|
||||
var column = processor.ColumnBeforeIndent;
|
||||
processor.NextChar();
|
||||
processor.ParseIndent();
|
||||
@@ -49,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);
|
||||
|
||||
@@ -62,16 +63,22 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
|
||||
if (currentDefinitionList == null)
|
||||
{
|
||||
currentDefinitionList = new DefinitionList(this);
|
||||
currentDefinitionList = new DefinitionList(this)
|
||||
{
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
|
||||
Column = paragraphBlock.Column,
|
||||
Line = paragraphBlock.Line,
|
||||
};
|
||||
previousParent.Add(currentDefinitionList);
|
||||
}
|
||||
|
||||
var definitionItem = new DefinitionItem(this)
|
||||
{
|
||||
Column = processor.Column,
|
||||
Line = processor.LineIndex,
|
||||
Column = column,
|
||||
Span = new SourceSpan(startPosition, processor.Line.End),
|
||||
OpeningCharacter = processor.CurrentChar,
|
||||
};
|
||||
currentDefinitionList.Add(definitionItem);
|
||||
|
||||
for (int i = 0; i < paragraphBlock.Lines.Count; i++)
|
||||
{
|
||||
@@ -80,17 +87,34 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
{
|
||||
Column = paragraphBlock.Column,
|
||||
Line = line.Line,
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End),
|
||||
IsOpen = false
|
||||
};
|
||||
term.AppendLine(ref line.Slice, line.Column, line.Line);
|
||||
term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position);
|
||||
definitionItem.Add(term);
|
||||
}
|
||||
|
||||
currentDefinitionList.Add(definitionItem);
|
||||
processor.Open(definitionItem);
|
||||
|
||||
// Update the end position
|
||||
currentDefinitionList.UpdateSpanEnd(processor.Line.End);
|
||||
|
||||
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;
|
||||
@@ -100,11 +124,13 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
var list = (DefinitionList)definitionItem.Parent;
|
||||
var lastBlankLine = definitionItem.LastChild as BlankLineBlock;
|
||||
|
||||
// Check if we have another definition list
|
||||
if (Array.IndexOf(OpeningCharacters, processor.CurrentChar) >= 0)
|
||||
{
|
||||
var startPosition = processor.Start;
|
||||
var column = processor.ColumnBeforeIndent;
|
||||
processor.NextChar();
|
||||
processor.ParseIndent();
|
||||
@@ -118,6 +144,8 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
{
|
||||
definitionItem.RemoveAt(definitionItem.Count - 1);
|
||||
}
|
||||
|
||||
list.Span.End = list.LastChild.Span.End;
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
@@ -126,10 +154,11 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
processor.GoToColumn(column + 4);
|
||||
}
|
||||
|
||||
var list = (DefinitionList) definitionItem.Parent;
|
||||
processor.Close(definitionItem);
|
||||
var nextDefinitionItem = new DefinitionItem(this)
|
||||
{
|
||||
Span = new SourceSpan(startPosition, processor.Line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column,
|
||||
OpeningCharacter = processor.CurrentChar,
|
||||
};
|
||||
@@ -161,6 +190,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
definitionItem.RemoveAt(definitionItem.Count - 1);
|
||||
}
|
||||
|
||||
list.Span.End = list.LastChild.Span.End;
|
||||
return BlockState.Break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,17 +67,34 @@ 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;
|
||||
}
|
||||
|
||||
// Following char must be a space
|
||||
if (!slice.PeekCharExtra(match.Length).IsWhiteSpaceOrZero())
|
||||
{
|
||||
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))
|
||||
{
|
||||
@@ -89,7 +106,19 @@ namespace Markdig.Extensions.Emoji
|
||||
slice.Start += match.Length;
|
||||
|
||||
// Push the EmojiInline
|
||||
processor.Inline = new EmojiInline(unicode) {Match = match};
|
||||
int line;
|
||||
int column;
|
||||
processor.Inline = new EmojiInline(unicode)
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(startPosition, out line, out column),
|
||||
},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Match = match
|
||||
};
|
||||
processor.Inline.Span.End = processor.Inline.Span.Start + match.Length - 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.EmphasisExtra
|
||||
namespace Markdig.Extensions.EmphasisExtras
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for strikethrough, subscript, superscript, inserted and marked.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Markdig.Extensions.EmphasisExtra
|
||||
namespace Markdig.Extensions.EmphasisExtras
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for enabling support for extra emphasis.
|
||||
@@ -30,7 +30,9 @@ namespace Markdig.Extensions.Figures
|
||||
|
||||
// Match fenced char
|
||||
int count = 0;
|
||||
var column = processor.Column;
|
||||
var line = processor.Line;
|
||||
var startPosition = line.Start;
|
||||
char c = line.CurrentChar;
|
||||
var matchChar = c;
|
||||
while (c != '\0')
|
||||
@@ -51,6 +53,8 @@ namespace Markdig.Extensions.Figures
|
||||
|
||||
var figure = new Figure(this)
|
||||
{
|
||||
Span = new SourceSpan(startPosition, line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column,
|
||||
OpeningCharacter = matchChar,
|
||||
OpeningCharacterCount = count
|
||||
@@ -59,8 +63,14 @@ namespace Markdig.Extensions.Figures
|
||||
line.TrimStart();
|
||||
if (!line.IsEmpty)
|
||||
{
|
||||
var caption = new FigureCaption(this) {IsOpen = false};
|
||||
caption.AppendLine(ref line, line.Start, processor.LineIndex);
|
||||
var caption = new FigureCaption(this)
|
||||
{
|
||||
Span = new SourceSpan(line.Start, line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = column + line.Start - startPosition,
|
||||
IsOpen = false
|
||||
};
|
||||
caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition);
|
||||
figure.Add(caption);
|
||||
}
|
||||
processor.NewBlocks.Push(figure);
|
||||
@@ -76,8 +86,10 @@ namespace Markdig.Extensions.Figures
|
||||
var matchChar = figure.OpeningCharacter;
|
||||
var c = processor.CurrentChar;
|
||||
|
||||
var column = processor.Column;
|
||||
// Match if we have a closing fence
|
||||
var line = processor.Line;
|
||||
var startPosition = line.Start;
|
||||
while (c == matchChar)
|
||||
{
|
||||
c = line.NextChar();
|
||||
@@ -91,11 +103,19 @@ namespace Markdig.Extensions.Figures
|
||||
line.TrimStart();
|
||||
if (!line.IsEmpty)
|
||||
{
|
||||
var caption = new FigureCaption(this) {IsOpen = false};
|
||||
caption.AppendLine(ref line, line.Start, processor.LineIndex);
|
||||
var caption = new FigureCaption(this)
|
||||
{
|
||||
Span = new SourceSpan(line.Start, line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = column + line.Start - startPosition,
|
||||
IsOpen = false
|
||||
};
|
||||
caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition);
|
||||
figure.Add(caption);
|
||||
}
|
||||
|
||||
figure.UpdateSpanEnd(line.End);
|
||||
|
||||
// Don't keep the last line
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
@@ -103,6 +123,8 @@ namespace Markdig.Extensions.Figures
|
||||
// Reset the indentation to the column before the indent
|
||||
processor.GoToColumn(processor.ColumnBeforeIndent);
|
||||
|
||||
figure.UpdateSpanEnd(line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Extensions.Cites;
|
||||
using Markdig.Extensions.Footers;
|
||||
using Markdig.Renderers;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Markdig.Extensions.Footers
|
||||
}
|
||||
|
||||
var column = processor.Column;
|
||||
var startPosition = processor.Start;
|
||||
|
||||
// A footer
|
||||
// A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space.
|
||||
@@ -44,7 +45,13 @@ namespace Markdig.Extensions.Footers
|
||||
{
|
||||
processor.NextColumn();
|
||||
}
|
||||
processor.NewBlocks.Push(new FooterBlock(this) { OpeningCharacter = openingChar, Column = column});
|
||||
processor.NewBlocks.Push(new FooterBlock(this)
|
||||
{
|
||||
Span = new SourceSpan(startPosition, processor.Line.End),
|
||||
OpeningCharacter = openingChar,
|
||||
Column = column,
|
||||
Line = processor.LineIndex,
|
||||
});
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -60,19 +67,22 @@ namespace Markdig.Extensions.Footers
|
||||
// A footer
|
||||
// A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space.
|
||||
var c = processor.CurrentChar;
|
||||
var result = BlockState.Continue;
|
||||
if (c != quote.OpeningCharacter || processor.PeekChar(1) != c)
|
||||
{
|
||||
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
|
||||
result = processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
|
||||
}
|
||||
|
||||
processor.NextChar(); // Skip ^^ char (1st)
|
||||
c = processor.NextChar(); // Skip ^^ char (2nd)
|
||||
if (c.IsSpace())
|
||||
else
|
||||
{
|
||||
processor.NextChar(); // Skip following space
|
||||
processor.NextChar(); // Skip ^^ char (1st)
|
||||
c = processor.NextChar(); // Skip ^^ char (2nd)
|
||||
if (c.IsSpace())
|
||||
{
|
||||
processor.NextChar(); // Skip following space
|
||||
}
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
}
|
||||
|
||||
return BlockState.Continue;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ namespace Markdig.Extensions.Footnotes
|
||||
/// </summary>
|
||||
public List<FootnoteLink> Links { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label span
|
||||
/// </summary>
|
||||
public SourceSpan LabelSpan;
|
||||
|
||||
internal bool IsLastLineEmpty { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -36,19 +41,24 @@ namespace Markdig.Extensions.Footnotes
|
||||
var saved = processor.Column;
|
||||
string label;
|
||||
int start = processor.Start;
|
||||
if (!LinkHelper.TryParseLabel(ref processor.Line, false, out label) || !label.StartsWith("^") || processor.CurrentChar != ':')
|
||||
SourceSpan labelSpan;
|
||||
if (!LinkHelper.TryParseLabel(ref processor.Line, false, out label, out labelSpan) || !label.StartsWith("^") || processor.CurrentChar != ':')
|
||||
{
|
||||
processor.GoToColumn(saved);
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
|
||||
// Advance the column
|
||||
int deltaColumn = processor.Start - start;
|
||||
processor.Column = processor.Column + deltaColumn;
|
||||
|
||||
processor.NextChar(); // Skip ':'
|
||||
|
||||
var footnote = new Footnote(this) {Label = label};
|
||||
var footnote = new Footnote(this)
|
||||
{
|
||||
Label = label,
|
||||
LabelSpan = labelSpan,
|
||||
};
|
||||
|
||||
// Maintain a list of all footnotes at document level
|
||||
var footnotes = processor.Document.GetData(DocumentKey) as FootnoteGroup;
|
||||
@@ -83,9 +93,23 @@ namespace Markdig.Extensions.Footnotes
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
if (footnote.IsLastLineEmpty && processor.Start == 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;
|
||||
|
||||
@@ -46,17 +46,25 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
// Try to find if there is any attributes { in the info string on the first line of a FencedCodeBlock
|
||||
if (line.Start < line.End)
|
||||
{
|
||||
var indexOfAttributes = line.Text.LastIndexOf('{', line.End);
|
||||
int indexOfAttributes = line.IndexOf('{');
|
||||
if (indexOfAttributes >= 0)
|
||||
{
|
||||
// Work on a copy
|
||||
var copy = line;
|
||||
copy.Start = indexOfAttributes;
|
||||
var startOfAttributes = copy.Start;
|
||||
HtmlAttributes attributes;
|
||||
if (GenericAttributesParser.TryParse(ref copy, out attributes))
|
||||
{
|
||||
var htmlAttributes = block.GetAttributes();
|
||||
attributes.CopyTo(htmlAttributes);
|
||||
|
||||
// Update position for HtmlAttributes
|
||||
htmlAttributes.Line = processor.LineIndex;
|
||||
htmlAttributes.Column = startOfAttributes - processor.CurrentLineStartPosition; // This is not accurate with tabs!
|
||||
htmlAttributes.Span.Start = startOfAttributes;
|
||||
htmlAttributes.Span.End = copy.Start - 1;
|
||||
|
||||
line.End = indexOfAttributes - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
HtmlAttributes attributes;
|
||||
var startPosition = slice.Start;
|
||||
if (TryParse(ref slice, out attributes))
|
||||
{
|
||||
var inline = processor.Inline;
|
||||
@@ -63,7 +64,15 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
}
|
||||
|
||||
var currentHtmlAttributes = objectToAttach.GetAttributes();
|
||||
attributes.CopyTo(currentHtmlAttributes);
|
||||
attributes.CopyTo(currentHtmlAttributes, true, false);
|
||||
|
||||
// Update the position of the attributes
|
||||
int line;
|
||||
int column;
|
||||
currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out line, out column);
|
||||
currentHtmlAttributes.Line = line;
|
||||
currentHtmlAttributes.Column = column;
|
||||
currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1;
|
||||
|
||||
// We don't set the processor.Inline as we don't want to add attach attributes to a particular entity
|
||||
return true;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers;
|
||||
|
||||
namespace Markdig.Extensions.ListExtra
|
||||
namespace Markdig.Extensions.ListExtras
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for adding new type of list items (a., A., i., I.)
|
||||
@@ -5,8 +5,10 @@
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
|
||||
namespace Markdig.Extensions.ListExtra
|
||||
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.ListExtra
|
||||
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.ListExtra
|
||||
{
|
||||
// otherwise we expect a regular alpha lettered list with a single character.
|
||||
var isUpper = c.IsAlphaUpper();
|
||||
result.OrderedStart = c.ToString();
|
||||
result.OrderedStart = (Char.ToUpper(c) - 64).ToString();
|
||||
result.BulletType = isUpper ? 'A' : 'a';
|
||||
result.DefaultOrderedStart = isUpper ? "A" : "a";
|
||||
state.NextChar();
|
||||
@@ -5,6 +5,7 @@
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Mathematics
|
||||
{
|
||||
@@ -12,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>
|
||||
@@ -38,6 +39,8 @@ namespace Markdig.Extensions.Mathematics
|
||||
return false;
|
||||
}
|
||||
|
||||
var startPosition = slice.Start;
|
||||
|
||||
// Match the opened $ or $$
|
||||
int openDollars = 1; // we have at least a $
|
||||
var c = slice.NextChar();
|
||||
@@ -60,16 +63,10 @@ namespace Markdig.Extensions.Mathematics
|
||||
int closeDollars = 0;
|
||||
|
||||
var start = slice.Start;
|
||||
var end = 0;
|
||||
pc = match;
|
||||
while (c != '\0')
|
||||
{
|
||||
// Count new '\n'
|
||||
if (c == '\n')
|
||||
{
|
||||
processor.LocalLineIndex++;
|
||||
processor.LineIndex++;
|
||||
}
|
||||
|
||||
// Don't process sticks if we have a '\' as a previous char
|
||||
if (pc != '\\' )
|
||||
{
|
||||
@@ -105,17 +102,22 @@ namespace Markdig.Extensions.Mathematics
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
end = slice.Start - 1;
|
||||
// Create a new MathInline
|
||||
int line;
|
||||
int column;
|
||||
var inline = new MathInline()
|
||||
{
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.End)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Delimiter = match,
|
||||
DelimiterCount = openDollars,
|
||||
Content = slice
|
||||
};
|
||||
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)
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
// 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;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.Medias
|
||||
namespace Markdig.Extensions.MediaLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class MediaExtension : IMarkdownExtension
|
||||
public class MediaLinkExtension : IMarkdownExtension
|
||||
{
|
||||
public MediaExtension() : this(new MediaOptions())
|
||||
public MediaLinkExtension() : this(new MediaOptions())
|
||||
{
|
||||
}
|
||||
|
||||
public MediaExtension(MediaOptions options)
|
||||
public MediaLinkExtension(MediaOptions options)
|
||||
{
|
||||
Options = options ?? new MediaOptions();
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
// 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;
|
||||
|
||||
namespace Markdig.Extensions.Medias
|
||||
namespace Markdig.Extensions.MediaLinks
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for the <see cref="MediaExtension"/>.
|
||||
/// Options for the <see cref="MediaLinkExtension"/>.
|
||||
/// </summary>
|
||||
public class MediaOptions
|
||||
{
|
||||
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(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(IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
htmlRenderer.UseNonAsciiNoEscape = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs
Normal file
79
src/Markdig/Extensions/PragmaLines/PragmaLineExtension.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.PragmaLines
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to a span for each line containing the original line id (using id = pragma-line#line_number_zero_based)
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class PragmaLineExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.DocumentProcessed -= PipelineOnDocumentProcessed;
|
||||
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
{
|
||||
}
|
||||
|
||||
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
|
||||
{
|
||||
int index = 0;
|
||||
AddPragmas(document, ref index);
|
||||
}
|
||||
|
||||
private static void AddPragmas(Block block, ref int index)
|
||||
{
|
||||
var attribute = block.GetAttributes();
|
||||
var pragmaId = GetPragmaId(block);
|
||||
if ( attribute.Id == null)
|
||||
{
|
||||
attribute.Id = pragmaId;
|
||||
}
|
||||
else if (block.Parent != null)
|
||||
{
|
||||
var heading = block as HeadingBlock;
|
||||
|
||||
// If we have a heading, we will try to add the tag inside it
|
||||
// otherwise we will add it just before
|
||||
var tag = $"<a id=\"{pragmaId}\"></a>";
|
||||
if (heading?.Inline?.FirstChild != null)
|
||||
{
|
||||
heading.Inline.FirstChild.InsertBefore(new HtmlInline() { Tag = tag });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
block.Parent.Insert(index, new HtmlBlock(null) { Lines = new StringLineGroup(tag) });
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
var container = block as ContainerBlock;
|
||||
if (container != null)
|
||||
{
|
||||
for (int i = 0; i < container.Count; i++)
|
||||
{
|
||||
var subBlock = container[i];
|
||||
AddPragmas(subBlock, ref i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPragmaId(Block block)
|
||||
{
|
||||
return $"pragma-line-{block.Line}";
|
||||
}
|
||||
}
|
||||
}
|
||||
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(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,10 +28,10 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -37,18 +40,13 @@ namespace Markdig.Extensions.SmartyPants
|
||||
var c = slice.CurrentChar;
|
||||
var openingChar = c;
|
||||
|
||||
var startingPosition = slice.Start;
|
||||
|
||||
// undefined first
|
||||
var type = (SmartyPantType) 0;
|
||||
|
||||
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) == '\'')
|
||||
@@ -81,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;
|
||||
}
|
||||
@@ -161,20 +156,23 @@ namespace Markdig.Extensions.SmartyPants
|
||||
}
|
||||
|
||||
// Create the SmartyPant inline
|
||||
int line;
|
||||
int column;
|
||||
var pant = new SmartyPant()
|
||||
{
|
||||
Span = {Start = processor.GetSourcePosition(startingPosition, out line, out column)},
|
||||
Line = line,
|
||||
Column = column,
|
||||
OpeningCharacter = openingChar,
|
||||
Type = type
|
||||
};
|
||||
pant.Span.End = pant.Span.Start + slice.Start - startingPosition - 1;
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -187,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};
|
||||
@@ -241,14 +249,19 @@ namespace Markdig.Extensions.SmartyPants
|
||||
{
|
||||
if (quote.Type == expectedRightQuote)
|
||||
{
|
||||
// Replace all intermediate unmatched left or right SmartyPants to there literal equivalent
|
||||
// Replace all intermediate unmatched left or right SmartyPants to their literal equivalent
|
||||
pants.RemoveAt(i);
|
||||
i--;
|
||||
for (int j = i; j > previousIndex; j--)
|
||||
{
|
||||
var toReplace = pants[j];
|
||||
pants.RemoveAt(j);
|
||||
toReplace.ReplaceBy(new LiteralInline(toReplace.ToString()));
|
||||
toReplace.ReplaceBy(new LiteralInline(toReplace.ToString())
|
||||
{
|
||||
Span = toReplace.Span,
|
||||
Line = toReplace.Line,
|
||||
Column = toReplace.Column,
|
||||
});
|
||||
i--;
|
||||
}
|
||||
|
||||
@@ -266,10 +279,102 @@ namespace Markdig.Extensions.SmartyPants
|
||||
// If we have any quotes lefts, replace them by there literal equivalent
|
||||
foreach (var quote in pants)
|
||||
{
|
||||
quote.ReplaceBy(new LiteralInline(quote.ToString()));
|
||||
quote.ReplaceBy(new LiteralInline(quote.ToString())
|
||||
{
|
||||
Span = quote.Span,
|
||||
Line = quote.Line,
|
||||
Column = quote.Column,
|
||||
});
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,64 +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;
|
||||
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.Column,
|
||||
ExpectRow = true,
|
||||
};
|
||||
}
|
||||
tableState.AddColumn(startCharacter, line.Start - 1, 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
|
||||
@@ -88,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);
|
||||
}
|
||||
@@ -101,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 = column.Start + 1;
|
||||
if (nextColumn != null)
|
||||
{
|
||||
sliceForCell.End = 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 = 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.Find<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>");
|
||||
|
||||
69
src/Markdig/Extensions/Tables/PipeTableBlockParser.cs
Normal file
69
src/Markdig/Extensions/Tables/PipeTableBlockParser.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.Tables
|
||||
{
|
||||
/// <summary>
|
||||
/// This block parsers for pipe tables is used to by-pass list items that could start by a single '-'
|
||||
/// and would disallow to detect a pipe tables at inline parsing time, so we are basically forcing a line
|
||||
/// that starts by a '-' and have at least a '|' (and have optional spaces) and is a continuation of a
|
||||
/// paragraph.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.BlockParser" />
|
||||
public class PipeTableBlockParser : BlockParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipeTableBlockParser"/> class.
|
||||
/// </summary>
|
||||
public PipeTableBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'-'};
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
// Only if we have already a paragraph
|
||||
var paragraph = processor.CurrentBlock as ParagraphBlock;
|
||||
if (processor.IsCodeIndent || paragraph == null)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
// We require at least a pipe (and we allow only : - | and space characters)
|
||||
var line = processor.Line;
|
||||
var countPipe = 0;
|
||||
while (true)
|
||||
{
|
||||
var c = line.NextChar();
|
||||
if (c == '\0')
|
||||
{
|
||||
if (countPipe > 0)
|
||||
{
|
||||
// Mark the paragraph as open (important, otherwise we would have an infinite loop)
|
||||
paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start);
|
||||
paragraph.IsOpen = true;
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
if (c.IsSpace() || c == '-' || c == '|' || c == ':')
|
||||
{
|
||||
if (c == '|')
|
||||
{
|
||||
countPipe++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return BlockState.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -28,9 +28,16 @@ namespace Markdig.Extensions.Tables
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
// Pipe tables require precise source location
|
||||
pipeline.PreciseSourceLocation = true;
|
||||
if (!pipeline.BlockParsers.Contains<PipeTableBlockParser>())
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -55,14 +55,22 @@ namespace Markdig.Extensions.Tables
|
||||
// tracking other delimiters on following lines
|
||||
var tableState = processor.ParserStates[Index] as TableState;
|
||||
bool isFirstLineEmpty = false;
|
||||
|
||||
|
||||
int globalLineIndex;
|
||||
int column;
|
||||
var position = processor.GetSourcePosition(slice.Start, out globalLineIndex, out column);
|
||||
var localLineIndex = globalLineIndex - processor.LineIndex;
|
||||
|
||||
if (tableState == null)
|
||||
{
|
||||
|
||||
// A table could be preceded by an empty line or a line containing an inline
|
||||
// that has not been added to the stack, so we consider this as a valid
|
||||
// start for a table. Typically, with this, we can have an attributes {...}
|
||||
// starting on the first line of a pipe table, even if the first line
|
||||
// doesn't have a pipe
|
||||
if (processor.Inline != null &&(processor.LocalLineIndex > 0 || c == '\n'))
|
||||
if (processor.Inline != null && (localLineIndex > 0 || c == '\n'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -88,18 +96,25 @@ namespace Markdig.Extensions.Tables
|
||||
if (!isFirstLineEmpty)
|
||||
{
|
||||
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
|
||||
tableState.EndOfLines.Add(processor.Inline);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processor.Inline = new PiprTableDelimiterInline(this) { LocalLineIndex = processor.LocalLineIndex };
|
||||
var deltaLine = processor.LocalLineIndex - tableState.LineIndex;
|
||||
processor.Inline = new PipeTableDelimiterInline(this)
|
||||
{
|
||||
Span = new SourceSpan(position, position),
|
||||
Line = globalLineIndex,
|
||||
Column = column,
|
||||
LocalLineIndex = localLineIndex
|
||||
};
|
||||
var deltaLine = localLineIndex - tableState.LineIndex;
|
||||
if (deltaLine > 0)
|
||||
{
|
||||
tableState.IsInvalidTable = true;
|
||||
}
|
||||
tableState.LineHasPipe = true;
|
||||
tableState.LineIndex = processor.LocalLineIndex;
|
||||
tableState.LineIndex = localLineIndex;
|
||||
slice.NextChar(); // Skip the `|` character
|
||||
|
||||
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
|
||||
@@ -109,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;
|
||||
@@ -123,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);
|
||||
}
|
||||
@@ -154,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;
|
||||
@@ -163,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);
|
||||
@@ -184,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)
|
||||
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)
|
||||
@@ -209,111 +227,210 @@ 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();
|
||||
lastElement.InsertAfter(endOfTable);
|
||||
delimiters.Add(endOfTable);
|
||||
tableState.EndOfLines.Add(endOfTable);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var row = new TableRow();
|
||||
table.Add(row);
|
||||
|
||||
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 columnContainer = new ContainerInline();
|
||||
var item = column;
|
||||
TrimStart(item);
|
||||
while (item != null && !IsLine(item) && !(item is PiprTableDelimiterInline))
|
||||
{
|
||||
var nextSibling = item.NextSibling;
|
||||
item.Remove();
|
||||
columnContainer.AppendChild(item);
|
||||
item = nextSibling;
|
||||
}
|
||||
|
||||
var tableCell = new TableCell();
|
||||
var tableParagraph = new ParagraphBlock() {Inline = columnContainer};
|
||||
tableCell.Add(tableParagraph);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -323,20 +440,12 @@ 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();
|
||||
|
||||
@@ -344,7 +453,7 @@ namespace Markdig.Extensions.Tables
|
||||
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;
|
||||
@@ -373,60 +482,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;
|
||||
if (!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;
|
||||
@@ -461,21 +572,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))
|
||||
@@ -504,6 +600,7 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
ColumnAndLineDelimiters = new List<Inline>();
|
||||
Cells = new List<TableCell>();
|
||||
EndOfLines = new List<Inline>();
|
||||
}
|
||||
|
||||
public bool IsInvalidTable { get; set; }
|
||||
@@ -515,6 +612,8 @@ namespace Markdig.Extensions.Tables
|
||||
public List<Inline> ColumnAndLineDelimiters { get; }
|
||||
|
||||
public List<TableCell> Cells { get; }
|
||||
|
||||
public List<Inline> EndOfLines { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,40 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
35
src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs
Normal file
35
src/Markdig/Extensions/TaskLists/HtmlTaskListRenderer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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;
|
||||
using Markdig.Renderers.Html;
|
||||
|
||||
namespace Markdig.Extensions.TaskLists
|
||||
{
|
||||
/// <summary>
|
||||
/// A HTML renderer for a <see cref="TaskList"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{TaskList}" />
|
||||
public class HtmlTaskListRenderer : HtmlObjectRenderer<TaskList>
|
||||
{
|
||||
protected override void Write(HtmlRenderer renderer, TaskList obj)
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
renderer.Write("<input").WriteAttributes(obj).Write(" disabled=\"disabled\" type=\"checkbox\"");
|
||||
if (obj.Checked)
|
||||
{
|
||||
renderer.Write(" checked=\"checked\"");
|
||||
}
|
||||
renderer.Write(" />");
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Write('[');
|
||||
renderer.Write(obj.Checked ? "x" : " ");
|
||||
renderer.Write(']');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Markdig/Extensions/TaskLists/TaskList.cs
Normal file
17
src/Markdig/Extensions/TaskLists/TaskList.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.TaskLists
|
||||
{
|
||||
/// <summary>
|
||||
/// An inline for TaskList.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("TaskList {Checked}")]
|
||||
public class TaskList : LeafInline
|
||||
{
|
||||
public bool Checked { get; set; }
|
||||
}
|
||||
}
|
||||
33
src/Markdig/Extensions/TaskLists/TaskListExtension.cs
Normal file
33
src/Markdig/Extensions/TaskLists/TaskListExtension.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.Inlines;
|
||||
using Markdig.Renderers;
|
||||
|
||||
namespace Markdig.Extensions.TaskLists
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension to enable TaskList.
|
||||
/// </summary>
|
||||
public class TaskListExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
if (!pipeline.InlineParsers.Contains<TaskListInlineParser>())
|
||||
{
|
||||
// Insert the parser after the code span parser
|
||||
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new TaskListInlineParser());
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlTaskListRenderer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs
Normal file
90
src/Markdig/Extensions/TaskLists/TaskListInlineParser.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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.TaskLists
|
||||
{
|
||||
/// <summary>
|
||||
/// The inline parser for SmartyPants.
|
||||
/// </summary>
|
||||
public class TaskListInlineParser : InlineParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskListInlineParser"/> class.
|
||||
/// </summary>
|
||||
public TaskListInlineParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'['};
|
||||
ListClass = "contains-task-list";
|
||||
ListItemClass = "task-list-item";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list class used for a task list.
|
||||
/// </summary>
|
||||
public string ListClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list item class used for a task list.
|
||||
/// </summary>
|
||||
public string ListItemClass { get; set; }
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// A tasklist is either
|
||||
// [ ]
|
||||
// or [x] or [X]
|
||||
|
||||
var listItemBlock = processor.Block.Parent as ListItemBlock;
|
||||
|
||||
if (listItemBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startingPosition = slice.Start;
|
||||
var c = slice.NextChar();
|
||||
if (!c.IsSpace() && c != 'x' && c != 'X')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (slice.NextChar() != ']')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Skip last ]
|
||||
slice.NextChar();
|
||||
|
||||
// Create the TaskList
|
||||
int line;
|
||||
int column;
|
||||
var taskItem = new TaskList()
|
||||
{
|
||||
Span = { Start = processor.GetSourcePosition(startingPosition, out line, out column)},
|
||||
Line = line,
|
||||
Column = column,
|
||||
Checked = !c.IsSpace()
|
||||
};
|
||||
taskItem.Span.End = taskItem.Span.Start + 2;
|
||||
processor.Inline = taskItem;
|
||||
|
||||
// Add proper class for task list
|
||||
if (!string.IsNullOrEmpty(ListItemClass))
|
||||
{
|
||||
listItemBlock.GetAttributes().AddClass(ListItemClass);
|
||||
}
|
||||
|
||||
var listBlock = (ListBlock) listItemBlock.Parent;
|
||||
if (!string.IsNullOrEmpty(ListClass))
|
||||
{
|
||||
listBlock.GetAttributes().AddClass(ListClass);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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(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.ToUpper(text[i]);
|
||||
var candidate = romanMap[character];
|
||||
if (i + 1 < text.Length && candidate < romanMap[Char.ToUpper(text[i + 1])])
|
||||
{
|
||||
result -= candidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += candidate;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public static int AddTab(int column)
|
||||
{
|
||||
|
||||
71
src/Markdig/Helpers/LineReader.cs
Normal file
71
src/Markdig/Helpers/LineReader.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A line reader from a <see cref="TextReader"/> that can provide precise source position
|
||||
/// </summary>
|
||||
public struct LineReader
|
||||
{
|
||||
private readonly string text;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineReader"/> class.
|
||||
/// </summary>
|
||||
/// <exception cref="System.ArgumentNullException"></exception>
|
||||
/// <exception cref="System.ArgumentOutOfRangeException">bufferSize cannot be <= 0</exception>
|
||||
public LineReader(string text)
|
||||
{
|
||||
if (text == null) throw new ArgumentNullException(nameof(text));
|
||||
this.text = text;
|
||||
SourcePosition = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the char position of the line. Valid for the next line before calling <see cref="ReadLine"/>.
|
||||
/// </summary>
|
||||
public int SourcePosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads a new line from the underlying <see cref="TextReader"/> and update the <see cref="SourcePosition"/> for the next line.
|
||||
/// </summary>
|
||||
/// <returns>A new line or null if the end of <see cref="TextReader"/> has been reached</returns>
|
||||
public StringSlice? ReadLine()
|
||||
{
|
||||
if (SourcePosition >= text.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var startPosition = SourcePosition;
|
||||
var position = SourcePosition;
|
||||
var slice = new StringSlice(text, startPosition, startPosition);
|
||||
for (;position < text.Length; position++)
|
||||
{
|
||||
var c = text[position];
|
||||
if (c == '\r' || c == '\n')
|
||||
{
|
||||
slice.End = position - 1;
|
||||
if (c == '\r' && position + 1 < text.Length && text[position + 1] == '\n')
|
||||
{
|
||||
position++;
|
||||
}
|
||||
position++;
|
||||
SourcePosition = position;
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
|
||||
slice.End = position - 1;
|
||||
SourcePosition = position;
|
||||
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,11 @@ namespace Markdig.Helpers
|
||||
}
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (c.IsDigit())
|
||||
{
|
||||
headingBuffer.Append(c);
|
||||
previousIsSpace = false;
|
||||
}
|
||||
else if (!previousIsSpace && c.IsWhitespace())
|
||||
{
|
||||
var pc = headingBuffer[headingBuffer.Length - 1];
|
||||
@@ -297,10 +302,24 @@ namespace Markdig.Helpers
|
||||
|
||||
public static bool TryParseInlineLink(StringSlice text, out string link, out string title)
|
||||
{
|
||||
return TryParseInlineLink(ref text, out link, out title);
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseInlineLink(StringSlice text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)
|
||||
{
|
||||
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseInlineLink(ref StringSlice text, out string link, out string title)
|
||||
{
|
||||
SourceSpan linkSpan;
|
||||
SourceSpan titleSpan;
|
||||
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseInlineLink(ref StringSlice text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)
|
||||
{
|
||||
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
|
||||
// 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces?
|
||||
@@ -313,14 +332,25 @@ namespace Markdig.Helpers
|
||||
link = null;
|
||||
title = null;
|
||||
|
||||
linkSpan = SourceSpan.Empty;
|
||||
titleSpan = SourceSpan.Empty;
|
||||
|
||||
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
|
||||
if (c == '(')
|
||||
{
|
||||
text.NextChar();
|
||||
text.TrimStart();
|
||||
|
||||
var pos = text.Start;
|
||||
if (TryParseUrl(ref text, out link))
|
||||
{
|
||||
linkSpan.Start = pos;
|
||||
linkSpan.End = text.Start - 1;
|
||||
if (linkSpan.End < linkSpan.Start)
|
||||
{
|
||||
linkSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
int spaceCount;
|
||||
text.TrimStart(out spaceCount);
|
||||
var hasWhiteSpaces = spaceCount > 0;
|
||||
@@ -333,12 +363,19 @@ namespace Markdig.Helpers
|
||||
else if (hasWhiteSpaces)
|
||||
{
|
||||
c = text.CurrentChar;
|
||||
pos = text.Start;
|
||||
if (c == ')')
|
||||
{
|
||||
isValid = true;
|
||||
}
|
||||
else if (TryParseTitle(ref text, out title))
|
||||
{
|
||||
titleSpan.Start = pos;
|
||||
titleSpan.End = text.Start - 1;
|
||||
if (titleSpan.End < titleSpan.Start)
|
||||
{
|
||||
titleSpan = SourceSpan.Empty;
|
||||
}
|
||||
text.TrimStart();
|
||||
c = text.CurrentChar;
|
||||
|
||||
@@ -582,12 +619,25 @@ namespace Markdig.Helpers
|
||||
return TryParseLinkReferenceDefinition(ref text, out label, out url, out title);
|
||||
}
|
||||
|
||||
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url,
|
||||
out string title) where T : ICharIterator
|
||||
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url, out string title)
|
||||
where T : ICharIterator
|
||||
{
|
||||
SourceSpan labelSpan;
|
||||
SourceSpan urlSpan;
|
||||
SourceSpan titleSpan;
|
||||
return TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan,
|
||||
out titleSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator
|
||||
{
|
||||
url = null;
|
||||
title = null;
|
||||
if (!TryParseLabel(ref text, out label))
|
||||
|
||||
urlSpan = SourceSpan.Empty;
|
||||
titleSpan = SourceSpan.Empty;
|
||||
|
||||
if (!TryParseLabel(ref text, out label, out labelSpan))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -599,12 +649,15 @@ namespace Markdig.Helpers
|
||||
}
|
||||
text.NextChar(); // Skip ':'
|
||||
|
||||
// Skip any whitespaces before the url
|
||||
// Skip any whitespace before the url
|
||||
text.TrimStart();
|
||||
|
||||
urlSpan.Start = text.Start;
|
||||
if (!TryParseUrl(ref text, out url) || string.IsNullOrEmpty(url))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
urlSpan.End = text.Start - 1;
|
||||
|
||||
var saved = text;
|
||||
int newLineCount;
|
||||
@@ -612,8 +665,10 @@ namespace Markdig.Helpers
|
||||
var c = text.CurrentChar;
|
||||
if (c == '\'' || c == '"' || c == '(')
|
||||
{
|
||||
titleSpan.Start = text.Start;
|
||||
if (TryParseTitle(ref text, out title))
|
||||
{
|
||||
titleSpan.End = text.Start - 1;
|
||||
// If we have a title, it requires a whitespace after the url
|
||||
if (!hasWhiteSpaces)
|
||||
{
|
||||
@@ -662,24 +717,41 @@ namespace Markdig.Helpers
|
||||
|
||||
public static bool TryParseLabel<T>(T lines, out string label) where T : ICharIterator
|
||||
{
|
||||
return TryParseLabel(ref lines, false, out label);
|
||||
SourceSpan labelSpan;
|
||||
return TryParseLabel(ref lines, false, out label, out labelSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseLabel<T>(T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator
|
||||
{
|
||||
return TryParseLabel(ref lines, false, out label, out labelSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseLabel<T>(ref T lines, out string label) where T : ICharIterator
|
||||
{
|
||||
return TryParseLabel(ref lines, false, out label);
|
||||
SourceSpan labelSpan;
|
||||
return TryParseLabel(ref lines, false, out label, out labelSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseLabel<T>(ref T lines, bool allowEmpty, out string label) where T : ICharIterator
|
||||
|
||||
public static bool TryParseLabel<T>(ref T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator
|
||||
{
|
||||
return TryParseLabel(ref lines, false, out label, out labelSpan);
|
||||
}
|
||||
|
||||
public static bool TryParseLabel<T>(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator
|
||||
{
|
||||
label = null;
|
||||
char c = lines.CurrentChar;
|
||||
labelSpan = SourceSpan.Empty;
|
||||
if (c != '[')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var buffer = StringBuilderCache.Local();
|
||||
|
||||
var startLabel = -1;
|
||||
var endLabel = -1;
|
||||
|
||||
bool hasEscape = false;
|
||||
bool previousWhitespace = true;
|
||||
bool hasNonWhiteSpace = false;
|
||||
@@ -719,11 +791,19 @@ namespace Markdig.Helpers
|
||||
break;
|
||||
}
|
||||
buffer.Length = i;
|
||||
endLabel--;
|
||||
}
|
||||
|
||||
// Only valid if buffer is less than 1000 characters
|
||||
if (buffer.Length <= 999)
|
||||
{
|
||||
labelSpan.Start = startLabel;
|
||||
labelSpan.End = endLabel;
|
||||
if (labelSpan.Start > labelSpan.End)
|
||||
{
|
||||
labelSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
label = buffer.ToString();
|
||||
isValid = true;
|
||||
}
|
||||
@@ -741,6 +821,10 @@ namespace Markdig.Helpers
|
||||
|
||||
if (!hasEscape && c == '\\')
|
||||
{
|
||||
if (startLabel < 0)
|
||||
{
|
||||
startLabel = lines.Start;
|
||||
}
|
||||
hasEscape = true;
|
||||
}
|
||||
else
|
||||
@@ -749,6 +833,11 @@ namespace Markdig.Helpers
|
||||
|
||||
if (!previousWhitespace || !isWhitespace)
|
||||
{
|
||||
if (startLabel < 0)
|
||||
{
|
||||
startLabel = lines.Start;
|
||||
}
|
||||
endLabel = lines.Start;
|
||||
buffer.Append(c);
|
||||
if (!isWhitespace)
|
||||
{
|
||||
|
||||
@@ -101,19 +101,5 @@ namespace Markdig.Helpers
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ReplacyBy<TElement>(T element) where TElement : T
|
||||
{
|
||||
if (element == null) throw new ArgumentNullException(nameof(element));
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
if (this[i] is TElement)
|
||||
{
|
||||
this[i] = element;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,12 @@ namespace Markdig.Helpers
|
||||
/// <param name="slice">The slice.</param>
|
||||
/// <param name="line">The line.</param>
|
||||
/// <param name="column">The column.</param>
|
||||
public StringLine(StringSlice slice, int line, int column)
|
||||
public StringLine(StringSlice slice, int line, int column, int position)
|
||||
{
|
||||
Slice = slice;
|
||||
Line = line;
|
||||
Column = column;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,11 +37,12 @@ namespace Markdig.Helpers
|
||||
/// <param name="slice">The slice.</param>
|
||||
/// <param name="line">The line.</param>
|
||||
/// <param name="column">The column.</param>
|
||||
public StringLine(ref StringSlice slice, int line, int column)
|
||||
public StringLine(ref StringSlice slice, int line, int column, int position)
|
||||
{
|
||||
Slice = slice;
|
||||
Line = line;
|
||||
Column = column;
|
||||
Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,6 +55,11 @@ namespace Markdig.Helpers
|
||||
/// </summary>
|
||||
public int Line;
|
||||
|
||||
/// <summary>
|
||||
/// The position of the start of this line within the original source code
|
||||
/// </summary>
|
||||
public int Position;
|
||||
|
||||
/// <summary>
|
||||
/// The column position.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Markdig.Extensions.Tables;
|
||||
|
||||
namespace Markdig.Helpers
|
||||
{
|
||||
@@ -108,15 +110,11 @@ namespace Markdig.Helpers
|
||||
/// </summary>
|
||||
/// <param name="lineOffsets">The position of the `\n` line offsets from the beginning of the returned slice.</param>
|
||||
/// <returns>A single slice concatenating the lines of this instance</returns>
|
||||
public StringSlice ToSlice(List<int> lineOffsets = null)
|
||||
public StringSlice ToSlice(List<LineOffset> lineOffsets = null)
|
||||
{
|
||||
// Optimization case when no lines
|
||||
if (Count == 0)
|
||||
{
|
||||
if (lineOffsets != null)
|
||||
{
|
||||
lineOffsets.Add(1);
|
||||
}
|
||||
return new StringSlice(string.Empty);
|
||||
}
|
||||
|
||||
@@ -125,22 +123,24 @@ namespace Markdig.Helpers
|
||||
{
|
||||
if (lineOffsets != null)
|
||||
{
|
||||
lineOffsets.Add(Lines[0].Slice.End + 1);
|
||||
lineOffsets.Add(new LineOffset(Lines[0].Position, Lines[0].Column, Lines[0].Slice.Start - Lines[0].Position, Lines[0].Slice.Start, Lines[0].Slice.End + 1));
|
||||
}
|
||||
return Lines[0];
|
||||
}
|
||||
|
||||
// Else use a builder
|
||||
var builder = StringBuilderCache.Local();
|
||||
int previousStartOfLine = 0;
|
||||
for (int i = 0; i < Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
if (lineOffsets != null)
|
||||
{
|
||||
lineOffsets.Add(builder.Length + 1); // Add 1 for \n and 1 for next line
|
||||
lineOffsets.Add(new LineOffset(Lines[i - 1].Position, Lines[i - 1].Column, Lines[i - 1].Slice.Start - Lines[i - 1].Position, previousStartOfLine, builder.Length));
|
||||
}
|
||||
builder.Append('\n');
|
||||
previousStartOfLine = builder.Length;
|
||||
}
|
||||
if (!Lines[i].Slice.IsEmpty)
|
||||
{
|
||||
@@ -149,7 +149,7 @@ namespace Markdig.Helpers
|
||||
}
|
||||
if (lineOffsets != null)
|
||||
{
|
||||
lineOffsets.Add(builder.Length); // Add 1 for \0
|
||||
lineOffsets.Add(new LineOffset(Lines[Count - 1].Position, Lines[Count - 1].Column, Lines[Count - 1].Slice.Start - Lines[Count - 1].Position, previousStartOfLine, builder.Length));
|
||||
}
|
||||
var str = builder.ToString();
|
||||
builder.Length = 0;
|
||||
@@ -265,5 +265,27 @@ namespace Markdig.Helpers
|
||||
return hasSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
public struct LineOffset
|
||||
{
|
||||
public LineOffset(int linePosition, int column, int offset, int start, int end)
|
||||
{
|
||||
LinePosition = linePosition;
|
||||
Column = column;
|
||||
Offset = offset;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
public readonly int LinePosition;
|
||||
|
||||
public readonly int Column;
|
||||
|
||||
public readonly int Offset;
|
||||
|
||||
public readonly int Start;
|
||||
|
||||
public readonly int End;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,16 @@ namespace Markdig.Helpers
|
||||
return index >= Start && index <= End ? Text[index] : (char) 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Peeks a character at the specified offset from the current beginning of the string, without taking into account <see cref="Start"/> and <see cref="End"/>
|
||||
/// </summary>
|
||||
/// <returns>The character at offset, returns `\0` if none.</returns>
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public char PeekCharAbsolute(int index)
|
||||
{
|
||||
return index >= 0 && index < Text.Length ? Text[index] : (char)0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Peeks a character at the specified offset from the current begining of the slice
|
||||
/// without using the range <see cref="Start"/> or <see cref="End"/>, returns `\0` if outside the <see cref="Text"/>.
|
||||
@@ -185,37 +195,48 @@ namespace Markdig.Helpers
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="ignoreCase">true if ignore case</param>
|
||||
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
|
||||
public bool Search(string text, int offset = 0)
|
||||
public int IndexOf(string text, int offset = 0, bool ignoreCase = false)
|
||||
{
|
||||
var end = End - text.Length + 1;
|
||||
for (int i = Start; i <= end; i++)
|
||||
if (ignoreCase)
|
||||
{
|
||||
if (Match(text, End, i))
|
||||
for (int i = Start + offset; i <= end; i++)
|
||||
{
|
||||
return true;
|
||||
if (MatchLowercase(text, End, i - Start))
|
||||
{
|
||||
return i; ;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
else
|
||||
{
|
||||
for (int i = Start + offset; i <= end; i++)
|
||||
{
|
||||
if (Match(text, End, i - Start))
|
||||
{
|
||||
return i; ;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the specified text within this slice (matching lowercase).
|
||||
/// Searches for the specified character within this slice.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
|
||||
public bool SearchLowercase(string text, int offset = 0)
|
||||
/// <returns>A value >= 0 if the character was found, otherwise < 0</returns>
|
||||
public int IndexOf(char c)
|
||||
{
|
||||
var end = End - text.Length + 1;
|
||||
for (int i = Start; i <= end; i++)
|
||||
for (int i = Start; i <= End; i++)
|
||||
{
|
||||
if (MatchLowercase(text, End, i))
|
||||
if (Text[i] == c)
|
||||
{
|
||||
return true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -53,17 +53,17 @@ namespace Markdig.Helpers
|
||||
CharNode nextNode;
|
||||
if (!node.TryGetValue(c, out nextNode))
|
||||
{
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
node = nextNode;
|
||||
if (node.Content != null)
|
||||
{
|
||||
match = node.Content;
|
||||
return true;
|
||||
}
|
||||
offset++;
|
||||
length--;
|
||||
}
|
||||
if (node.Content != null)
|
||||
{
|
||||
match = node.Content;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
using System;
|
||||
using System.IO;
|
||||
using Markdig.Extensions.SelfPipeline;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Syntax;
|
||||
@@ -24,61 +25,52 @@ namespace Markdig
|
||||
public static string ToHtml(string markdown, MarkdownPipeline pipeline = null)
|
||||
{
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
var reader = new StringReader(markdown);
|
||||
return ToHtml(reader, pipeline) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string to HTML.
|
||||
/// </summary>
|
||||
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <returns>The result of the conversion</returns>
|
||||
/// <exception cref="System.ArgumentNullException">if markdown variable is null</exception>
|
||||
public static string ToHtml(TextReader reader, MarkdownPipeline pipeline = null)
|
||||
{
|
||||
if (reader == null) throw new ArgumentNullException(nameof(reader));
|
||||
var writer = new StringWriter();
|
||||
ToHtml(reader, writer, pipeline);
|
||||
ToHtml(markdown, writer, pipeline);
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string to HTML.
|
||||
/// Converts a Markdown string to HTML and output to the specified writer.
|
||||
/// </summary>
|
||||
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
|
||||
/// <param name="markdown">A Markdown text.</param>
|
||||
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <returns>The Markdown document that has been parsed</returns>
|
||||
/// <exception cref="System.ArgumentNullException">if reader or writer variable are null</exception>
|
||||
public static void ToHtml(TextReader reader, TextWriter writer, MarkdownPipeline pipeline = null)
|
||||
public static MarkdownDocument ToHtml(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
|
||||
{
|
||||
if (reader == null) throw new ArgumentNullException(nameof(reader));
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
if (writer == null) throw new ArgumentNullException(nameof(writer));
|
||||
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
|
||||
pipeline = CheckForSelfPipeline(pipeline, markdown);
|
||||
|
||||
// We override the renderer with our own writer
|
||||
var renderer = new HtmlRenderer(writer);
|
||||
pipeline.Setup(renderer);
|
||||
|
||||
var document = Parse(reader, pipeline);
|
||||
var document = Parse(markdown, pipeline);
|
||||
renderer.Render(document);
|
||||
writer.Flush();
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string using a custom <see cref="IMarkdownRenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
|
||||
/// <param name="markdown">A Markdown text.</param>
|
||||
/// <param name="renderer">The renderer to convert Markdown to.</param>
|
||||
/// <param name="pipeline">The pipeline used for the conversion.</param>
|
||||
/// <exception cref="System.ArgumentNullException">if reader or writer variable are null</exception>
|
||||
public static object Convert(TextReader reader, IMarkdownRenderer renderer, MarkdownPipeline pipeline = null)
|
||||
/// <exception cref="System.ArgumentNullException">if markdown or writer variable are null</exception>
|
||||
public static object Convert(string markdown, IMarkdownRenderer renderer, MarkdownPipeline pipeline = null)
|
||||
{
|
||||
if (reader == null) throw new ArgumentNullException(nameof(reader));
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
|
||||
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
|
||||
|
||||
var document = Parse(reader, pipeline);
|
||||
pipeline = CheckForSelfPipeline(pipeline, markdown);
|
||||
var document = Parse(markdown, pipeline);
|
||||
pipeline.Setup(renderer);
|
||||
return renderer.Render(document);
|
||||
}
|
||||
@@ -92,22 +84,33 @@ namespace Markdig
|
||||
public static MarkdownDocument Parse(string markdown)
|
||||
{
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
return Parse(new StringReader(markdown));
|
||||
return Parse(markdown, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified markdown into an AST <see cref="MarkdownDocument"/>
|
||||
/// </summary>
|
||||
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
|
||||
/// <param name="markdown">The markdown text.</param>
|
||||
/// <param name="pipeline">The pipeline used for the parsing.</param>
|
||||
/// <returns>An AST Markdown document</returns>
|
||||
/// <exception cref="System.ArgumentNullException">if reader variable is null</exception>
|
||||
public static MarkdownDocument Parse(TextReader reader, MarkdownPipeline pipeline = null)
|
||||
/// <exception cref="System.ArgumentNullException">if markdown variable is null</exception>
|
||||
public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline)
|
||||
{
|
||||
if (reader == null) throw new ArgumentNullException(nameof(reader));
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
|
||||
|
||||
return MarkdownParser.Parse(reader, pipeline);
|
||||
pipeline = CheckForSelfPipeline(pipeline, markdown);
|
||||
return MarkdownParser.Parse(markdown, pipeline);
|
||||
}
|
||||
|
||||
private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, string markdown)
|
||||
{
|
||||
var selfPipeline = pipeline.Extensions.Find<SelfPipelineExtension>();
|
||||
if (selfPipeline != null)
|
||||
{
|
||||
return selfPipeline.CreatePipelineFromInput(markdown);
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,28 @@ using System;
|
||||
using Markdig.Extensions.Abbreviations;
|
||||
using Markdig.Extensions.AutoIdentifiers;
|
||||
using Markdig.Extensions.Bootstrap;
|
||||
using Markdig.Extensions.Cites;
|
||||
using Markdig.Extensions.Citations;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.DefinitionLists;
|
||||
using Markdig.Extensions.Diagrams;
|
||||
using Markdig.Extensions.Emoji;
|
||||
using Markdig.Extensions.EmphasisExtra;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Figures;
|
||||
using Markdig.Extensions.Footers;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.GenericAttributes;
|
||||
using Markdig.Extensions.Hardlines;
|
||||
using Markdig.Extensions.ListExtra;
|
||||
using Markdig.Extensions.ListExtras;
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Extensions.Medias;
|
||||
using Markdig.Extensions.MediaLinks;
|
||||
using Markdig.Extensions.NoRefLinks;
|
||||
using Markdig.Extensions.PragmaLines;
|
||||
using Markdig.Extensions.SelfPipeline;
|
||||
using Markdig.Extensions.SmartyPants;
|
||||
using Markdig.Extensions.NonAsciiNoEscape;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Extensions.TaskLists;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Parsers.Inlines;
|
||||
|
||||
@@ -39,29 +46,115 @@ namespace Markdig
|
||||
public static MarkdownPipelineBuilder UseAdvancedExtensions(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
return pipeline
|
||||
.UseAbbreviation()
|
||||
.UseAutoIdentifier()
|
||||
.UseCite()
|
||||
.UseCustomContainer()
|
||||
.UseDefinitionList()
|
||||
.UseEmphasisExtra()
|
||||
.UseFigure()
|
||||
.UseFooter()
|
||||
.UseAbbreviations()
|
||||
.UseAutoIdentifiers()
|
||||
.UseCitations()
|
||||
.UseCustomContainers()
|
||||
.UseDefinitionLists()
|
||||
.UseEmphasisExtras()
|
||||
.UseFigures()
|
||||
.UseFooters()
|
||||
.UseFootnotes()
|
||||
.UseGridTable()
|
||||
.UseMath()
|
||||
.UseMedia()
|
||||
.UsePipeTable()
|
||||
.UseListExtra()
|
||||
.UseGridTables()
|
||||
.UseMathematics()
|
||||
.UseMediaLinks()
|
||||
.UsePipeTables()
|
||||
.UseListExtras()
|
||||
.UseTaskLists()
|
||||
.UseDiagrams()
|
||||
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses this extension to 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>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseNonAsciiNoEscape(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<NonAsciiNoEscapeExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses YAML frontmatter extension that will parse a YAML frontmatter into the MarkdownDocument. Note that they are not rendered by any default HTML renderer.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseYamlFrontMatter(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<YamlFrontMatterExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the self pipeline extension that will detect the pipeline to use from the markdown input that contains a special tag. See <see cref="SelfPipelineExtension"/>
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <param name="defaultTag">The default tag to use to match the self pipeline configuration. By default, <see cref="SelfPipelineExtension.DefaultTag"/>, meaning that the HTML tag will be <--markdig:extensions--></param>
|
||||
/// <param name="defaultExtensions">The default extensions to configure if no pipeline setup was found from the Markdown document</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseSelfPipeline(this MarkdownPipelineBuilder pipeline, string defaultTag = SelfPipelineExtension.DefaultTag, string defaultExtensions = null)
|
||||
{
|
||||
if (pipeline.Extensions.Count != 0)
|
||||
{
|
||||
throw new InvalidOperationException("The SelfPipeline extension cannot be used with other extensions");
|
||||
}
|
||||
|
||||
pipeline.Extensions.Add(new SelfPipelineExtension(defaultTag, defaultExtensions));
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses pragma lines to output span with an id containing the line number (pragma-line#line_number_zero_based`)
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UsePragmaLines(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<PragmaLineExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the diagrams extension
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseDiagrams(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<DiagramExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses precise source code location (useful for syntax highlighting).
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UsePreciseSourceLocation(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.PreciseSourceLocation = true;
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the task list extension.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseTaskLists(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<TaskListExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the custom container extension.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseCustomContainer(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseCustomContainers(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<CustomContainerExtension>();
|
||||
return pipeline;
|
||||
@@ -75,11 +168,11 @@ namespace Markdig
|
||||
/// <returns>
|
||||
/// The modified pipeline
|
||||
/// </returns>
|
||||
public static MarkdownPipelineBuilder UseMedia(this MarkdownPipelineBuilder pipeline, MediaOptions options = null)
|
||||
public static MarkdownPipelineBuilder UseMediaLinks(this MarkdownPipelineBuilder pipeline, MediaOptions options = null)
|
||||
{
|
||||
if (!pipeline.Extensions.Contains<MediaExtension>())
|
||||
if (!pipeline.Extensions.Contains<MediaLinkExtension>())
|
||||
{
|
||||
pipeline.Extensions.Add(new MediaExtension(options));
|
||||
pipeline.Extensions.Add(new MediaLinkExtension(options));
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
@@ -92,7 +185,7 @@ namespace Markdig
|
||||
/// <returns>
|
||||
/// The modified pipeline
|
||||
/// </returns>
|
||||
public static MarkdownPipelineBuilder UseAutoIdentifier(this MarkdownPipelineBuilder pipeline, AutoIdentifierOptions options = AutoIdentifierOptions.Default)
|
||||
public static MarkdownPipelineBuilder UseAutoIdentifiers(this MarkdownPipelineBuilder pipeline, AutoIdentifierOptions options = AutoIdentifierOptions.Default)
|
||||
{
|
||||
if (!pipeline.Extensions.Contains<AutoIdentifierExtension>())
|
||||
{
|
||||
@@ -119,7 +212,7 @@ namespace Markdig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the boostrap extension.
|
||||
/// Uses the bootstrap extension.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
@@ -134,7 +227,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseMath(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseMathematics(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<MathExtension>();
|
||||
return pipeline;
|
||||
@@ -145,7 +238,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseFigure(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseFigures(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<FigureExtension>();
|
||||
return pipeline;
|
||||
@@ -156,7 +249,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseAbbreviation(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseAbbreviations(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<AbbreviationExtension>();
|
||||
return pipeline;
|
||||
@@ -167,7 +260,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseDefinitionList(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseDefinitionLists(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<DefinitionListExtension>();
|
||||
return pipeline;
|
||||
@@ -181,7 +274,7 @@ namespace Markdig
|
||||
/// <returns>
|
||||
/// The modified pipeline
|
||||
/// </returns>
|
||||
public static MarkdownPipelineBuilder UsePipeTable(this MarkdownPipelineBuilder pipeline, PipeTableOptions options = null)
|
||||
public static MarkdownPipelineBuilder UsePipeTables(this MarkdownPipelineBuilder pipeline, PipeTableOptions options = null)
|
||||
{
|
||||
if (!pipeline.Extensions.Contains<PipeTableExtension>())
|
||||
{
|
||||
@@ -195,7 +288,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseGridTable(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseGridTables(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<GridTableExtension>();
|
||||
return pipeline;
|
||||
@@ -207,9 +300,9 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseCite(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseCitations(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<CiteExtension>();
|
||||
pipeline.Extensions.AddIfNotAlready<CitationExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
@@ -218,7 +311,7 @@ namespace Markdig
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder UseFooter(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseFooters(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<FooterExtension>();
|
||||
return pipeline;
|
||||
@@ -254,7 +347,7 @@ namespace Markdig
|
||||
/// <returns>
|
||||
/// The modified pipeline
|
||||
/// </returns>
|
||||
public static MarkdownPipelineBuilder UseEmphasisExtra(this MarkdownPipelineBuilder pipeline, EmphasisExtraOptions options = EmphasisExtraOptions.Default)
|
||||
public static MarkdownPipelineBuilder UseEmphasisExtras(this MarkdownPipelineBuilder pipeline, EmphasisExtraOptions options = EmphasisExtraOptions.Default)
|
||||
{
|
||||
if (!pipeline.Extensions.Contains<EmphasisExtraExtension>())
|
||||
{
|
||||
@@ -270,7 +363,7 @@ namespace Markdig
|
||||
/// <returns>
|
||||
/// The modified pipeline
|
||||
/// </returns>
|
||||
public static MarkdownPipelineBuilder UseListExtra(this MarkdownPipelineBuilder pipeline)
|
||||
public static MarkdownPipelineBuilder UseListExtras(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<ListExtraExtension>();
|
||||
return pipeline;
|
||||
@@ -298,6 +391,17 @@ namespace Markdig
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add rel=nofollow to all links rendered to HTML.
|
||||
/// </summary>
|
||||
/// <param name="pipeline"></param>
|
||||
/// <returns></returns>
|
||||
public static MarkdownPipelineBuilder UseNoFollowLinks(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<NoFollowLinksExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will disable the HTML support in the markdown processor (for constraint/safe parsing).
|
||||
/// </summary>
|
||||
@@ -322,7 +426,7 @@ namespace Markdig
|
||||
/// <summary>
|
||||
/// Configures the pipeline using a string that defines the extensions to activate.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline (e.g: advanced for <see cref="UseAdvancedExtensions"/>, pipetables+gridtables for <see cref="UsePipeTable"/> and <see cref="UseGridTable"/></param>
|
||||
/// <param name="pipeline">The pipeline (e.g: advanced for <see cref="UseAdvancedExtensions"/>, pipetables+gridtables for <see cref="UsePipeTables"/> and <see cref="UseGridTables"/></param>
|
||||
/// <param name="extensions">The extensions to activate as a string</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
public static MarkdownPipelineBuilder Configure(this MarkdownPipelineBuilder pipeline, string extensions)
|
||||
@@ -332,21 +436,25 @@ namespace Markdig
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
// TODO: the extension string should come from the extension itself instead of this hardcoded switch case.
|
||||
|
||||
foreach (var extension in extensions.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
switch (extension.ToLowerInvariant())
|
||||
{
|
||||
case "common":
|
||||
break;
|
||||
case "advanced":
|
||||
pipeline.UseAdvancedExtensions();
|
||||
break;
|
||||
case "pipetables":
|
||||
pipeline.UsePipeTable();
|
||||
pipeline.UsePipeTables();
|
||||
break;
|
||||
case "emphasisextra":
|
||||
pipeline.UseEmphasisExtra();
|
||||
case "emphasisextras":
|
||||
pipeline.UseEmphasisExtras();
|
||||
break;
|
||||
case "listextra":
|
||||
pipeline.UseListExtra();
|
||||
case "listextras":
|
||||
pipeline.UseListExtras();
|
||||
break;
|
||||
case "hardlinebreak":
|
||||
pipeline.UseSoftlineBreakAsHardlineBreak();
|
||||
@@ -355,52 +463,70 @@ namespace Markdig
|
||||
pipeline.UseFootnotes();
|
||||
break;
|
||||
case "footers":
|
||||
pipeline.UseFooter();
|
||||
pipeline.UseFooters();
|
||||
break;
|
||||
case "cites":
|
||||
pipeline.UseCite();
|
||||
case "citations":
|
||||
pipeline.UseCitations();
|
||||
break;
|
||||
case "attributes":
|
||||
pipeline.UseGenericAttributes();
|
||||
break;
|
||||
case "gridtables":
|
||||
pipeline.UseGridTable();
|
||||
pipeline.UseGridTables();
|
||||
break;
|
||||
case "abbreviations":
|
||||
pipeline.UseAbbreviation();
|
||||
pipeline.UseAbbreviations();
|
||||
break;
|
||||
case "emojis":
|
||||
pipeline.UseEmojiAndSmiley();
|
||||
break;
|
||||
case "definitionlists":
|
||||
pipeline.UseDefinitionList();
|
||||
pipeline.UseDefinitionLists();
|
||||
break;
|
||||
case "customcontainers":
|
||||
pipeline.UseCustomContainer();
|
||||
pipeline.UseCustomContainers();
|
||||
break;
|
||||
case "figures":
|
||||
pipeline.UseFigure();
|
||||
pipeline.UseFigures();
|
||||
break;
|
||||
case "math":
|
||||
pipeline.UseMath();
|
||||
case "mathematics":
|
||||
pipeline.UseMathematics();
|
||||
break;
|
||||
case "bootstrap":
|
||||
pipeline.UseBootstrap();
|
||||
break;
|
||||
case "medias":
|
||||
pipeline.UseMedia();
|
||||
case "medialinks":
|
||||
pipeline.UseMediaLinks();
|
||||
break;
|
||||
case "smartypants":
|
||||
pipeline.UseSmartyPants();
|
||||
break;
|
||||
case "autoidentifiers":
|
||||
pipeline.UseAutoIdentifier();
|
||||
pipeline.UseAutoIdentifiers();
|
||||
break;
|
||||
case "tasklists":
|
||||
pipeline.UseTaskLists();
|
||||
break;
|
||||
case "diagrams":
|
||||
pipeline.UseDiagrams();
|
||||
break;
|
||||
case "nofollowlinks":
|
||||
pipeline.UseNoFollowLinks();
|
||||
break;
|
||||
case "nohtml":
|
||||
pipeline.DisableHtml();
|
||||
break;
|
||||
case "yaml":
|
||||
pipeline.UseYamlFrontMatter();
|
||||
break;
|
||||
case "nonascii-noescape":
|
||||
pipeline.UseNonAsciiNoEscape();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"unknown extension {extension}");
|
||||
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
|
||||
}
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Markdig
|
||||
DebugLog = debugLog;
|
||||
DocumentProcessed = documentProcessed;
|
||||
}
|
||||
|
||||
internal bool PreciseSourceLocation { get; set; }
|
||||
|
||||
internal OrderedList<IMarkdownExtension> Extensions { get; }
|
||||
|
||||
internal BlockParserList BlockParsers { get; }
|
||||
@@ -44,7 +47,11 @@ namespace Markdig
|
||||
|
||||
internal ProcessDocumentDelegate DocumentProcessed;
|
||||
|
||||
internal void Setup(IMarkdownRenderer renderer)
|
||||
/// <summary>
|
||||
/// Allows to setup a <see cref="IMarkdownRenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The markdown renderer to setup</param>
|
||||
public void Setup(IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
|
||||
foreach (var extension in Extensions)
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Markdig
|
||||
public MarkdownPipelineBuilder()
|
||||
{
|
||||
// Add all default parsers
|
||||
BlockParsers = new BlockParserList()
|
||||
BlockParsers = new OrderedList<BlockParser>()
|
||||
{
|
||||
new ThematicBreakParser(),
|
||||
new HeadingBlockParser(),
|
||||
@@ -37,7 +37,7 @@ namespace Markdig
|
||||
new ParagraphBlockParser(),
|
||||
};
|
||||
|
||||
InlineParsers = new InlineParserList()
|
||||
InlineParsers = new OrderedList<InlineParser>()
|
||||
{
|
||||
new HtmlEntityParser(),
|
||||
new LinkInlineParser(),
|
||||
@@ -56,12 +56,12 @@ namespace Markdig
|
||||
/// <summary>
|
||||
/// Gets the block parsers.
|
||||
/// </summary>
|
||||
public BlockParserList BlockParsers { get; private set; }
|
||||
public OrderedList<BlockParser> BlockParsers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the inline parsers.
|
||||
/// </summary>
|
||||
public InlineParserList InlineParsers { get; private set; }
|
||||
public OrderedList<InlineParser> InlineParsers { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the register extensions.
|
||||
@@ -73,6 +73,11 @@ namespace Markdig
|
||||
/// </summary>
|
||||
public StringBuilderCache StringBuilderCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable precise source location (slower parsing but accurate position for block and inline elements)
|
||||
/// </summary>
|
||||
public bool PreciseSourceLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the debug log.
|
||||
/// </summary>
|
||||
@@ -111,7 +116,9 @@ namespace Markdig
|
||||
extension.Setup(this);
|
||||
}
|
||||
|
||||
pipeline = new MarkdownPipeline(new OrderedList<IMarkdownExtension>(Extensions), new BlockParserList(BlockParsers), new InlineParserList(InlineParsers), StringBuilderCache, DebugLog, GetDocumentProcessed);
|
||||
pipeline = new MarkdownPipeline(new OrderedList<IMarkdownExtension>(Extensions),
|
||||
new BlockParserList(BlockParsers), new InlineParserList(InlineParsers), StringBuilderCache, DebugLog,
|
||||
GetDocumentProcessed) {PreciseSourceLocation = PreciseSourceLocation};
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,6 @@ namespace Markdig.Parsers
|
||||
/// <seealso cref="Markdig.Parsers.ParserList{Markdig.Parsers.BlockParser, Markdig.Parsers.BlockParserState}" />
|
||||
public class BlockParserList : ParserList<BlockParser, BlockProcessor>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockParserList"/> class.
|
||||
/// </summary>
|
||||
public BlockParserList()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockParserList"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -51,7 +51,6 @@ namespace Markdig.Parsers
|
||||
Document = document;
|
||||
document.IsOpen = true;
|
||||
Parsers = parsers;
|
||||
parsers.Initialize(this);
|
||||
OpenedBlocks = new List<Block>();
|
||||
NewBlocks = new Stack<Block>();
|
||||
root = this;
|
||||
@@ -98,6 +97,11 @@ namespace Markdig.Parsers
|
||||
/// </summary>
|
||||
public StringSlice Line;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current line start position.
|
||||
/// </summary>
|
||||
public int CurrentLineStartPosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the line in the source text.
|
||||
/// </summary>
|
||||
@@ -300,6 +304,64 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unwind any previous indent from the current character back to the first space.
|
||||
/// </summary>
|
||||
public void UnwindAllIndents()
|
||||
{
|
||||
// Find the previous first space on the current line
|
||||
var previousStart = Line.Start;
|
||||
for (; Line.Start > originalLineStart; Line.Start--)
|
||||
{
|
||||
var c = Line.PeekCharAbsolute(Line.Start - 1);
|
||||
if (c == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!c.IsSpaceOrTab())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
var targetStart = Line.Start;
|
||||
// Nothing changed? Early exit
|
||||
if (previousStart == targetStart)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: factorize the following code with what is done with GoToColumn
|
||||
|
||||
// If we have found the first space, we need to recalculate the correct column
|
||||
Line.Start = originalLineStart;
|
||||
Column = 0;
|
||||
ColumnBeforeIndent = 0;
|
||||
StartBeforeIndent = originalLineStart;
|
||||
|
||||
for (; Line.Start < targetStart; Line.Start++)
|
||||
{
|
||||
var c = Line.Text[Line.Start];
|
||||
if (c == '\t')
|
||||
{
|
||||
Column = CharHelper.AddTab(Column);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!c.IsSpaceOrTab())
|
||||
{
|
||||
ColumnBeforeIndent = Column + 1;
|
||||
StartBeforeIndent = Line.Start + 1;
|
||||
}
|
||||
|
||||
Column++;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the indent
|
||||
ColumnBeforeIndent = Column;
|
||||
StartBeforeIndent = Start;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to the position to the code indent (<see cref="ColumnBeforeIndent"/> + 4 spaces).
|
||||
/// </summary>
|
||||
@@ -365,6 +427,8 @@ namespace Markdig.Parsers
|
||||
/// <param name="newLine">The new line.</param>
|
||||
public void ProcessLine(StringSlice newLine)
|
||||
{
|
||||
CurrentLineStartPosition = newLine.Start;
|
||||
|
||||
ContinueProcessingLine = true;
|
||||
|
||||
ResetLine(newLine);
|
||||
@@ -395,6 +459,11 @@ namespace Markdig.Parsers
|
||||
parserStateCache.Release(this);
|
||||
}
|
||||
|
||||
internal bool IsOpen(Block block)
|
||||
{
|
||||
return OpenedBlocks.Contains(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a block at the specified index.
|
||||
/// </summary>
|
||||
@@ -546,7 +615,7 @@ namespace Markdig.Parsers
|
||||
ContinueProcessingLine = false;
|
||||
if (!result.IsDiscard())
|
||||
{
|
||||
leaf.AppendLine(ref Line, Column, LineIndex);
|
||||
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
|
||||
}
|
||||
|
||||
if (NewBlocks.Count > 0)
|
||||
@@ -583,8 +652,16 @@ namespace Markdig.Parsers
|
||||
/// </summary>
|
||||
private void TryOpenBlocks()
|
||||
{
|
||||
int previousStart = -1;
|
||||
while (ContinueProcessingLine)
|
||||
{
|
||||
// Security check so that the parser can't go into a crazy infinite loop if one extension is messing
|
||||
if (previousStart == Start)
|
||||
{
|
||||
throw new InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse blocks at line [{LineIndex}] with line [{Line}]");
|
||||
}
|
||||
previousStart = Start;
|
||||
|
||||
// Eat indent spaces before checking the character
|
||||
ParseIndent();
|
||||
|
||||
@@ -669,7 +746,7 @@ namespace Markdig.Parsers
|
||||
|
||||
if (!result.IsDiscard())
|
||||
{
|
||||
paragraph.AppendLine(ref Line, Column, LineIndex);
|
||||
paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
|
||||
}
|
||||
|
||||
// We have just found a lazy continuation for a paragraph, early exit
|
||||
@@ -722,7 +799,7 @@ namespace Markdig.Parsers
|
||||
{
|
||||
if (!result.IsDiscard())
|
||||
{
|
||||
leaf.AppendLine(ref Line, Column, LineIndex);
|
||||
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
|
||||
}
|
||||
|
||||
if (newBlocks.Count > 0)
|
||||
|
||||
@@ -128,6 +128,8 @@ namespace Markdig.Parsers
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
var startPosition = processor.Start;
|
||||
|
||||
// Match fenced char
|
||||
int count = 0;
|
||||
var line = processor.Line;
|
||||
@@ -157,6 +159,8 @@ namespace Markdig.Parsers
|
||||
fenced.Column = processor.Column;
|
||||
fenced.FencedChar = matchChar;
|
||||
fenced.FencedCharCount = count;
|
||||
fenced.Span.Start = startPosition;
|
||||
fenced.Span.End = line.Start;
|
||||
};
|
||||
|
||||
// Try to parse any attached attributes
|
||||
@@ -212,6 +216,8 @@ namespace Markdig.Parsers
|
||||
// The line must contain only fence opening character followed only by whitespaces.
|
||||
if (count <=0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
|
||||
{
|
||||
block.UpdateSpanEnd(line.Start - 1);
|
||||
|
||||
// Don't keep the last line
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,15 @@ namespace Markdig.Parsers
|
||||
/// <seealso cref="Markdig.Parsers.BlockParser" />
|
||||
public class FencedCodeBlockParser : FencedBlockParserBase<FencedCodeBlock>
|
||||
{
|
||||
public const string DefaultInfoPrefix = "language-";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FencedCodeBlockParser"/> class.
|
||||
/// </summary>
|
||||
public FencedCodeBlockParser()
|
||||
{
|
||||
OpeningCharacters = new[] {'`', '~'};
|
||||
InfoPrefix = "language-";
|
||||
InfoPrefix = DefaultInfoPrefix;
|
||||
}
|
||||
|
||||
protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor)
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Markdig.Parsers
|
||||
// opening sequence.
|
||||
var column = processor.Column;
|
||||
var line = processor.Line;
|
||||
var sourcePosition = line.Start;
|
||||
var c = line.CurrentChar;
|
||||
var matchingChar = c;
|
||||
|
||||
@@ -61,17 +62,18 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
// A space is required after leading #
|
||||
if (leadingCount > 0 && leadingCount <= 6 && (c.IsSpace() || c == '\0'))
|
||||
if (leadingCount > 0 && leadingCount <= 6 && (c.IsSpaceOrTab() || c == '\0'))
|
||||
{
|
||||
// Move to the content
|
||||
processor.Line.Start = line.Start + 1;
|
||||
var headingBlock = new HeadingBlock(this)
|
||||
{
|
||||
HeaderChar = matchingChar,
|
||||
Level = leadingCount,
|
||||
Column = column
|
||||
Column = column,
|
||||
Span = { Start = sourcePosition }
|
||||
};
|
||||
processor.NewBlocks.Push(headingBlock);
|
||||
processor.GoToColumn(column + leadingCount + 1);
|
||||
|
||||
// Gives a chance to parse attributes
|
||||
if (TryParseAttributes != null)
|
||||
@@ -87,7 +89,7 @@ namespace Markdig.Parsers
|
||||
c = processor.Line.Text[i];
|
||||
if (endState == 0)
|
||||
{
|
||||
if (c.IsSpace()) // TODO: Not clear if it is a space or space+tab in the specs
|
||||
if (c.IsSpaceOrTab())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -103,7 +105,7 @@ namespace Markdig.Parsers
|
||||
|
||||
if (countClosingTags > 0)
|
||||
{
|
||||
if (c.IsSpace())
|
||||
if (c.IsSpaceOrTab())
|
||||
{
|
||||
processor.Line.End = i - 1;
|
||||
}
|
||||
@@ -116,6 +118,9 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the source end position of this element
|
||||
headingBlock.Span.End = processor.Line.End;
|
||||
|
||||
// We expect a single line, so don't continue
|
||||
return BlockState.Break;
|
||||
}
|
||||
|
||||
@@ -47,18 +47,19 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
var line = state.Line;
|
||||
var startPosition = line.Start;
|
||||
line.NextChar();
|
||||
var result = TryParseTagType16(state, line, state.ColumnBeforeIndent);
|
||||
var result = TryParseTagType16(state, line, state.ColumnBeforeIndent, startPosition);
|
||||
|
||||
// HTML blocks of type 7 cannot interrupt a paragraph:
|
||||
if (result == BlockState.None && !(state.CurrentBlock is ParagraphBlock))
|
||||
{
|
||||
result = TryParseTagType7(state, line, state.ColumnBeforeIndent);
|
||||
result = TryParseTagType7(state, line, state.ColumnBeforeIndent, startPosition);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn)
|
||||
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
|
||||
{
|
||||
var builder = StringBuilderCache.Local();
|
||||
var c = line.CurrentChar;
|
||||
@@ -84,7 +85,7 @@ namespace Markdig.Parsers
|
||||
|
||||
if (hasOnlySpaces)
|
||||
{
|
||||
result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn);
|
||||
result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn, startPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ namespace Markdig.Parsers
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn)
|
||||
private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
|
||||
{
|
||||
char c;
|
||||
c = line.CurrentChar;
|
||||
@@ -101,15 +102,15 @@ namespace Markdig.Parsers
|
||||
c = line.NextChar();
|
||||
if (c == '-' && line.PeekChar(1) == '-')
|
||||
{
|
||||
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn); // group 2
|
||||
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn, startPosition); // group 2
|
||||
}
|
||||
if (c.IsAlphaUpper())
|
||||
{
|
||||
return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn); // group 4
|
||||
return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn, startPosition); // group 4
|
||||
}
|
||||
if (c == '[' && line.Match("CDATA[", 1))
|
||||
{
|
||||
return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn); // group 5
|
||||
return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn, startPosition); // group 5
|
||||
}
|
||||
|
||||
return BlockState.None;
|
||||
@@ -117,7 +118,7 @@ namespace Markdig.Parsers
|
||||
|
||||
if (c == '?')
|
||||
{
|
||||
return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn); // group 3
|
||||
return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn, startPosition); // group 3
|
||||
}
|
||||
|
||||
var hasLeadingClose = c == '/';
|
||||
@@ -164,71 +165,117 @@ namespace Markdig.Parsers
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn);
|
||||
return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn, startPosition);
|
||||
}
|
||||
|
||||
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn);
|
||||
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn, startPosition);
|
||||
}
|
||||
|
||||
private const string EndOfComment = "-->";
|
||||
private const string EndOfCDATA = "]]>";
|
||||
private const string EndOfProcessingInstruction = "?>";
|
||||
|
||||
|
||||
private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock)
|
||||
{
|
||||
state.GoToColumn(state.ColumnBeforeIndent);
|
||||
|
||||
// Early exit if it is not starting by an HTML tag
|
||||
var line = state.Line;
|
||||
var c = line.CurrentChar;
|
||||
var result = BlockState.Continue;
|
||||
int index;
|
||||
switch (htmlBlock.Type)
|
||||
{
|
||||
case HtmlBlockType.Comment:
|
||||
if (line.Search("-->"))
|
||||
index = line.IndexOf(EndOfComment);
|
||||
if (index >= 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfComment.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.CData:
|
||||
if (line.Search("]]>"))
|
||||
index = line.IndexOf(EndOfCDATA);
|
||||
if (index >= 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfCDATA.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.ProcessingInstruction:
|
||||
if (line.Search("?>"))
|
||||
index = line.IndexOf(EndOfProcessingInstruction);
|
||||
if (index >= 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfProcessingInstruction.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.DocumentType:
|
||||
if (line.Search(">"))
|
||||
index = line.IndexOf('>');
|
||||
if (index >= 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
htmlBlock.UpdateSpanEnd(index + 1);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.ScriptPreOrStyle:
|
||||
if (line.SearchLowercase("</script>") || line.SearchLowercase("</pre>") || line.SearchLowercase("</style>"))
|
||||
index = line.IndexOf("</script>", 0, true);
|
||||
if (index >= 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
htmlBlock.UpdateSpanEnd(index + "</script>".Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = line.IndexOf("</pre>", 0, true);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.UpdateSpanEnd(index + "</pre>".Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = line.IndexOf("</style>", 0, true);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.UpdateSpanEnd(index + "</style>".Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.InterruptingBlock:
|
||||
if (state.IsBlankLine)
|
||||
{
|
||||
return BlockState.BreakDiscard;
|
||||
result = BlockState.BreakDiscard;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.NonInterruptingBlock:
|
||||
if (state.IsBlankLine)
|
||||
{
|
||||
return BlockState.BreakDiscard;
|
||||
result = BlockState.BreakDiscard;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return BlockState.Continue;
|
||||
// Update only if we don't have a break discard
|
||||
if (result != BlockState.BreakDiscard)
|
||||
{
|
||||
htmlBlock.Span.End = line.End;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn)
|
||||
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition)
|
||||
{
|
||||
state.NewBlocks.Push(new HtmlBlock(this) {Column = startColumn, Type = type});
|
||||
state.NewBlocks.Push(new HtmlBlock(this)
|
||||
{
|
||||
Column = startColumn,
|
||||
Type = type,
|
||||
// By default, setup to the end of line
|
||||
Span = new SourceSpan(startPosition, startPosition + state.Line.End)
|
||||
});
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ namespace Markdig.Parsers
|
||||
/// <summary>
|
||||
/// Initializes this parser with the specified parser processor.
|
||||
/// </summary>
|
||||
/// <param name="processor">The parser processor.</param>
|
||||
void Initialize(TProcessor processor);
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of this parser in <see cref="BlockParserList"/> or <see cref="InlineParserList"/>.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user