mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-10 21:40:00 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
15
readme.md
15
readme.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<img align="right" width="160px" height="160px" src="img/markdig.png">
|
||||
|
||||
Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
Markdig is a fast, powerful, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
|
||||
|
||||
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
|
||||
|
||||
@@ -10,12 +10,13 @@ You can **try Markdig online** and compare it to other implementations on [babel
|
||||
|
||||
## 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 **20+ extensions**, including:
|
||||
@@ -79,6 +80,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).
|
||||
@@ -110,7 +115,7 @@ This is an early preview of the benchmarking against various implementations:
|
||||
### Analysis of the results:
|
||||
|
||||
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
|
||||
- **Among the best in CPU**, Extremelly competitive and often faster than other implementations (not feature wise equivalent)
|
||||
- **Among the best in CPU**, Extremely competitive and often faster than other implementations (not feature wise equivalent)
|
||||
- **15% to 30% less allocations** and GC pressure
|
||||
|
||||
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:
|
||||
|
||||
@@ -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,9 +60,11 @@
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Specs.tt</DependentUpon>
|
||||
</Compile>
|
||||
<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" />
|
||||
@@ -83,6 +86,7 @@
|
||||
<None Include="Specs\GridTableSpecs.md" />
|
||||
<None Include="Specs\HardlineBreakSpecs.md" />
|
||||
<None Include="Specs\BootstrapSpecs.md" />
|
||||
<None Include="Specs\DiagramsSpecs.md" />
|
||||
<None Include="Specs\TaskListSpecs.md" />
|
||||
<None Include="Specs\SmartyPantsSpecs.md" />
|
||||
<None Include="Specs\MediaSpecs.md" />
|
||||
@@ -98,6 +102,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.
|
||||
|
||||
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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -377,6 +377,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
|
||||
|
||||
@@ -83,29 +83,6 @@ They are' not matching 'quotes
|
||||
.
|
||||
<p>They are' not matching 'quotes</p>
|
||||
````````````````````````````````
|
||||
|
||||
Double quotes using ``` `` ``` are working if they match another `''` pair, and there is no other double quotes on the line (otherwise they would be parsed as a code span):
|
||||
|
||||
```````````````````````````````` example
|
||||
This is ``a double quote''
|
||||
.
|
||||
<p>This is “a double quote”</p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
This is ``a code span''``
|
||||
.
|
||||
<p>This is <code>a code span''</code></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` example
|
||||
hello ``there```
|
||||
test
|
||||
.
|
||||
<p>hello “there”`
|
||||
test</p>
|
||||
````````````````````````````````
|
||||
|
||||
An emphasis starting inside left/right quotes will span over the right quote:
|
||||
|
||||
```````````````````````````````` example
|
||||
@@ -133,3 +110,36 @@ This is a en ellipsis...
|
||||
.
|
||||
<p>This is a en ellipsis…</p>
|
||||
````````````````````````````````
|
||||
|
||||
Check that a smartypants are not breaking pipetable parsing:
|
||||
|
||||
```````````````````````````````` example
|
||||
a | b
|
||||
-- | --
|
||||
0 | 1
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>a</th>
|
||||
<th>b</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
Check quotes and dash:
|
||||
|
||||
```````````````````````````````` example
|
||||
A "quote" with a ---
|
||||
.
|
||||
<p>A “quote” with a —</p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,25 +38,27 @@ SOFTWARE.
|
||||
<#@ import namespace="System.CodeDom.Compiler" #>
|
||||
<#@ output extension=".cs" #><#
|
||||
var specFiles = new KeyValuePair<string, string>[] {
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics"),
|
||||
// new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/91e045ca370258903ed138450373043a496ec64b/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"), "medialinks"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "smartypants"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "medialinks|advanced+medialinks"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "pipetables+smartypants|advanced+smartypants"), // Check with smartypants to make sure that it doesn't break pipetables
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists|advanced"),
|
||||
new KeyValuePair<string, string>(Host.ResolvePath("DiagramsSpecs.md"), "diagrams|advanced"),
|
||||
};
|
||||
var emptyLines = false;
|
||||
var displayEmptyLines = false;
|
||||
|
||||
@@ -12,10 +12,10 @@ A task list item consist of `[ ]` or `[x]` or `[X]` inside a list item (ordered
|
||||
- [ ] Item3
|
||||
- Item4
|
||||
.
|
||||
<ul>
|
||||
<li><input disabled="disabled" type="checkbox" /> Item1</li>
|
||||
<li><input disabled="disabled" type="checkbox" checked="checked" /> Item2</li>
|
||||
<li><input disabled="disabled" type="checkbox" /> Item3</li>
|
||||
<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>
|
||||
````````````````````````````````
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ 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();
|
||||
var pipeline = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText);
|
||||
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, pipeline.Build());
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,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);
|
||||
|
||||
@@ -27,6 +27,112 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
|
||||
Console.WriteLine(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPipeTables()
|
||||
{
|
||||
TestParser.TestSpec(@"
|
||||
| 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>
|
||||
", "advanced");
|
||||
}
|
||||
|
||||
[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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,9 @@ literal ( 0, 5) 5-11
|
||||
Check("01*2**3*", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
literal ( 0, 0) 0-1
|
||||
emphasis ( 0, 2) 2-4
|
||||
emphasis ( 0, 2) 2-7
|
||||
literal ( 0, 3) 3-3
|
||||
emphasis ( 0, 5) 5-7
|
||||
literal ( 0, 4) 4-5
|
||||
literal ( 0, 6) 6-6
|
||||
");
|
||||
}
|
||||
@@ -206,19 +206,6 @@ literal ( 0, 4) 4-5
|
||||
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
|
||||
}
|
||||
|
||||
[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 TestAutolinkInline()
|
||||
{
|
||||
@@ -252,6 +239,60 @@ 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()
|
||||
{
|
||||
@@ -328,12 +369,13 @@ literal ( 0, 2) 2-3
|
||||
[Test]
|
||||
public void TestHtmlEntityInline()
|
||||
{
|
||||
// 01234567
|
||||
Check("0 1", @"
|
||||
paragraph ( 0, 0) 0-7
|
||||
// 01 23456789
|
||||
Check("0\n 1", @"
|
||||
paragraph ( 0, 0) 0-9
|
||||
literal ( 0, 0) 0-0
|
||||
htmlentity ( 0, 1) 1-6
|
||||
literal ( 0, 7) 7-7
|
||||
linebreak ( 0, 1) 1-1
|
||||
htmlentity ( 1, 0) 2-7
|
||||
literal ( 1, 6) 8-9
|
||||
");
|
||||
}
|
||||
|
||||
@@ -613,7 +655,7 @@ literal ( 4, 2) 13-13
|
||||
Check("0\n\n 0\n 1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 7-13
|
||||
code ( 2, 0) 3-13
|
||||
");
|
||||
}
|
||||
|
||||
@@ -624,7 +666,7 @@ code ( 2, 4) 7-13
|
||||
Check("0\n\n\t0\n\t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 4-7
|
||||
code ( 2, 0) 3-7
|
||||
");
|
||||
}
|
||||
|
||||
@@ -635,7 +677,7 @@ code ( 2, 4) 4-7
|
||||
Check("0\n\n \t0\n \t1\n", @"
|
||||
paragraph ( 0, 0) 0-0
|
||||
literal ( 0, 0) 0-0
|
||||
code ( 2, 4) 5-9
|
||||
code ( 2, 0) 3-9
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"NUnit": "3.2.0"
|
||||
"NUnit": "3.2.0",
|
||||
"NUnit3TestAdapter": "3.2.0"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
processor.Open(definitionItem);
|
||||
|
||||
// Update the end position
|
||||
currentDefinitionList.Span.End = processor.Line.End;
|
||||
currentDefinitionList.UpdateSpanEnd(processor.Line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,18 +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))
|
||||
{
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace Markdig.Extensions.Figures
|
||||
figure.Add(caption);
|
||||
}
|
||||
|
||||
figure.Span.End = line.End;
|
||||
figure.UpdateSpanEnd(line.End);
|
||||
|
||||
// Don't keep the last line
|
||||
return BlockState.BreakDiscard;
|
||||
@@ -123,7 +123,7 @@ namespace Markdig.Extensions.Figures
|
||||
// Reset the indentation to the column before the indent
|
||||
processor.GoToColumn(processor.ColumnBeforeIndent);
|
||||
|
||||
figure.Span.End = line.End;
|
||||
figure.UpdateSpanEnd(line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Markdig.Extensions.Footers
|
||||
{
|
||||
processor.NextChar(); // Skip following space
|
||||
}
|
||||
block.Span.End = processor.Line.End;
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,14 @@ namespace Markdig.Extensions.Footnotes
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
return TryOpen(processor, false);
|
||||
}
|
||||
|
||||
private BlockState TryOpen(BlockProcessor processor, bool isContinue)
|
||||
{
|
||||
// We expect footnote to appear only at document level and not indented more than a code indent block
|
||||
if (processor.IsCodeIndent || processor.CurrentContainer.GetType() != typeof(MarkdownDocument) )
|
||||
if (processor.IsCodeIndent || (!isContinue && processor.CurrentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(processor.CurrentContainer is Footnote)))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
@@ -42,7 +47,7 @@ namespace Markdig.Extensions.Footnotes
|
||||
processor.GoToColumn(saved);
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
|
||||
// Advance the column
|
||||
int deltaColumn = processor.Start - start;
|
||||
processor.Column = processor.Column + deltaColumn;
|
||||
@@ -88,9 +93,23 @@ namespace Markdig.Extensions.Footnotes
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
if (footnote.IsLastLineEmpty && processor.Column == 0)
|
||||
if (processor.Column == 0)
|
||||
{
|
||||
return BlockState.Break;
|
||||
if (footnote.IsLastLineEmpty)
|
||||
{
|
||||
// Close the current footnote
|
||||
processor.Close(footnote);
|
||||
|
||||
// Parse any opening footnote
|
||||
return TryOpen(processor);
|
||||
}
|
||||
|
||||
// Make sure that consecutive footnotes without a blanklines are parsed correctly
|
||||
if (TryOpen(processor, true) == BlockState.Continue)
|
||||
{
|
||||
processor.Close(footnote);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
footnote.IsLastLineEmpty = false;
|
||||
|
||||
@@ -46,7 +46,7 @@ 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
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
}
|
||||
|
||||
var currentHtmlAttributes = objectToAttach.GetAttributes();
|
||||
attributes.CopyTo(currentHtmlAttributes);
|
||||
attributes.CopyTo(currentHtmlAttributes, false, false);
|
||||
|
||||
// Update the position of the attributes
|
||||
int line;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Markdig.Extensions.Mathematics
|
||||
/// An inline parser for <see cref="MathInline"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.InlineParser" />
|
||||
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
|
||||
/// <seealso cref="IPostInlineProcessor" />
|
||||
public class MathInlineParser : InlineParser
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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'
|
||||
|
||||
@@ -44,13 +47,6 @@ namespace Markdig.Extensions.SmartyPants
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case '`':
|
||||
if (slice.PeekChar(1) == '`')
|
||||
{
|
||||
slice.NextChar();
|
||||
type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines
|
||||
}
|
||||
break;
|
||||
case '\'':
|
||||
type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines
|
||||
if (slice.PeekChar(1) == '\'')
|
||||
@@ -83,12 +79,9 @@ namespace Markdig.Extensions.SmartyPants
|
||||
case '-':
|
||||
if (slice.NextChar() == '-')
|
||||
{
|
||||
type = SmartyPantType.Dash2;
|
||||
if (slice.PeekChar(1) == '-')
|
||||
{
|
||||
slice.NextChar();
|
||||
type = SmartyPantType.Dash3;
|
||||
}
|
||||
var quotePants = GetOrCreateState(processor);
|
||||
quotePants.HasDash = true;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -178,11 +171,8 @@ namespace Markdig.Extensions.SmartyPants
|
||||
// We will check in a post-process step for balanaced open/close quotes
|
||||
if (postProcess)
|
||||
{
|
||||
var quotePants = processor.ParserStates[Index] as List<SmartyPant>;
|
||||
if (quotePants == null)
|
||||
{
|
||||
processor.ParserStates[Index] = quotePants = new List<SmartyPant>();
|
||||
}
|
||||
var quotePants = GetOrCreateState(processor);
|
||||
|
||||
// Register only if we don't have yet any quotes
|
||||
if (quotePants.Count == 0)
|
||||
{
|
||||
@@ -195,11 +185,21 @@ namespace Markdig.Extensions.SmartyPants
|
||||
return true;
|
||||
}
|
||||
|
||||
private ListSmartyPants GetOrCreateState(InlineProcessor processor)
|
||||
{
|
||||
var quotePants = processor.ParserStates[Index] as ListSmartyPants;
|
||||
if (quotePants == null)
|
||||
{
|
||||
processor.ParserStates[Index] = quotePants = new ListSmartyPants();
|
||||
}
|
||||
return quotePants;
|
||||
}
|
||||
|
||||
private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline)
|
||||
{
|
||||
processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd;
|
||||
|
||||
var pants = (List<SmartyPant>) processor.ParserStates[Index];
|
||||
var pants = (ListSmartyPants) processor.ParserStates[Index];
|
||||
|
||||
// We only change quote into left or right quotes if we find proper balancing
|
||||
var previousIndices = new int[3] {-1, -1, -1};
|
||||
@@ -289,5 +289,92 @@ namespace Markdig.Extensions.SmartyPants
|
||||
|
||||
pants.Clear();
|
||||
}
|
||||
|
||||
bool IPostInlineProcessor.PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex,
|
||||
bool isFinalProcessing)
|
||||
{
|
||||
// Don't try to process anything if there are no dash
|
||||
var quotePants = state.ParserStates[Index] as ListSmartyPants;
|
||||
if (quotePants == null || !quotePants.HasDash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var child = root;
|
||||
var pendingContainers = new Stack<Inline>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
while (child != null)
|
||||
{
|
||||
var next = child.NextSibling;
|
||||
|
||||
if (child is LiteralInline)
|
||||
{
|
||||
var literal = (LiteralInline) child;
|
||||
|
||||
var startIndex = 0;
|
||||
|
||||
var indexOfDash = literal.Content.IndexOf("--", startIndex);
|
||||
if (indexOfDash >= 0)
|
||||
{
|
||||
var type = SmartyPantType.Dash2;
|
||||
if (literal.Content.PeekCharAbsolute(indexOfDash + 2) == '-')
|
||||
{
|
||||
type = SmartyPantType.Dash3;
|
||||
}
|
||||
var nextContent = literal.Content;
|
||||
var originalSpan = literal.Span;
|
||||
literal.Span.End -= literal.Content.End - indexOfDash + 1;
|
||||
literal.Content.End = indexOfDash - 1;
|
||||
nextContent.Start = indexOfDash + (type == SmartyPantType.Dash2 ? 2 : 3);
|
||||
|
||||
var pant = new SmartyPant()
|
||||
{
|
||||
Span = new SourceSpan(literal.Content.End + 1, nextContent.Start - 1),
|
||||
Line = literal.Line,
|
||||
Column = literal.Column,
|
||||
OpeningCharacter = '-',
|
||||
Type = type
|
||||
};
|
||||
literal.InsertAfter(pant);
|
||||
|
||||
var postLiteral = new LiteralInline()
|
||||
{
|
||||
Span = new SourceSpan(pant.Span.End + 1, originalSpan.End),
|
||||
Line = literal.Line,
|
||||
Column = literal.Column,
|
||||
Content = nextContent
|
||||
};
|
||||
pant.InsertAfter(postLiteral);
|
||||
|
||||
// Use the pending literal to proceed further
|
||||
next = postLiteral;
|
||||
}
|
||||
}
|
||||
else if (child is ContainerInline)
|
||||
{
|
||||
pendingContainers.Push(((ContainerInline)child).FirstChild);
|
||||
}
|
||||
|
||||
child = next;
|
||||
}
|
||||
if (pendingContainers.Count > 0)
|
||||
{
|
||||
child = pendingContainers.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private class ListSmartyPants : List<SmartyPant>
|
||||
{
|
||||
public bool HasDash { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,11 +131,12 @@ namespace Markdig.Extensions.Tables
|
||||
// | ------------- | ------------ | ---------------------------------------- |
|
||||
// Calculate the colspan for the new row
|
||||
int columnIndex = -1;
|
||||
foreach (var columnSlice in columns)
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
if (line.PeekCharExtra(columnSlice.Start) == '|')
|
||||
{
|
||||
columnIndex++;
|
||||
columnIndex = i;
|
||||
}
|
||||
if (columnIndex >= 0)
|
||||
{
|
||||
@@ -301,8 +302,9 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
TableRow currentRow = null;
|
||||
foreach (var columnSlice in columns)
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
if (currentRow == null)
|
||||
@@ -332,7 +334,8 @@ namespace Markdig.Extensions.Tables
|
||||
// Else we can create a new cell
|
||||
columnSlice.CurrentCell = new TableCell(this)
|
||||
{
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan,
|
||||
ColumnIndex = i
|
||||
};
|
||||
|
||||
if (columnSlice.BlockProcessor == null)
|
||||
|
||||
@@ -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,10 +81,10 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
|
||||
}
|
||||
|
||||
if (table.ColumnDefinitions != null && i < table.ColumnDefinitions.Count)
|
||||
var columnIndex = cell.ColumnIndex == -1 ? i : cell.ColumnIndex;
|
||||
if (table.ColumnDefinitions != null && columnIndex < table.ColumnDefinitions.Count)
|
||||
{
|
||||
switch (table.ColumnDefinitions[i].Alignment)
|
||||
switch (table.ColumnDefinitions[columnIndex].Alignment)
|
||||
{
|
||||
case TableColumnAlign.Center:
|
||||
renderer.Write(" style=\"text-align: center;\"");
|
||||
|
||||
@@ -15,8 +15,8 @@ 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;
|
||||
|
||||
@@ -123,7 +123,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;
|
||||
@@ -379,20 +379,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();
|
||||
|
||||
|
||||
@@ -27,8 +27,14 @@ namespace Markdig.Extensions.Tables
|
||||
public TableCell(BlockParser parser) : base(parser)
|
||||
{
|
||||
ColumnSpan = 1;
|
||||
ColumnIndex = -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>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// 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
|
||||
@@ -18,15 +19,29 @@ namespace Markdig.Extensions.TaskLists
|
||||
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]
|
||||
|
||||
if (!(processor.Block.Parent is ListItemBlock))
|
||||
var listItemBlock = processor.Block.Parent as ListItemBlock;
|
||||
|
||||
if (listItemBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -56,6 +71,19 @@ namespace Markdig.Extensions.TaskLists
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,52 +194,49 @@ namespace Markdig.Helpers
|
||||
/// Searches the specified text within this slice.
|
||||
/// </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, out int index)
|
||||
public int IndexOf(string text, int offset = 0, bool ignoreCase = false)
|
||||
{
|
||||
return Search(text, 0, out index);
|
||||
var end = End - text.Length + 1;
|
||||
if (ignoreCase)
|
||||
{
|
||||
for (int i = Start + offset; i <= end; i++)
|
||||
{
|
||||
if (MatchLowercase(text, End, i - Start))
|
||||
{
|
||||
return i; ;
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
/// 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 Search(string text, int offset, out int index)
|
||||
/// <returns>A value >= 0 if the character was found, otherwise < 0</returns>
|
||||
public int IndexOf(char c)
|
||||
{
|
||||
var end = End - text.Length + 1;
|
||||
index = Start + offset;
|
||||
for (int i = index; i <= end; i ++)
|
||||
for (int i = Start; i <= End; i++)
|
||||
{
|
||||
if (Match(text, End, i))
|
||||
if (Text[i] == c)
|
||||
{
|
||||
index = i + text.Length;
|
||||
return true;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches the specified text within this slice (matching lowercase).
|
||||
/// </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, out int endOfIndex)
|
||||
{
|
||||
var end = End - text.Length + 1;
|
||||
endOfIndex = 0;
|
||||
for (int i = Start; i <= end; i++)
|
||||
{
|
||||
if (MatchLowercase(text, End, i))
|
||||
{
|
||||
endOfIndex = i + text.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
@@ -30,17 +31,19 @@ namespace Markdig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Markdown string to HTML.
|
||||
/// Converts a Markdown string to HTML and output to the specified writer.
|
||||
/// </summary>
|
||||
/// <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(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
|
||||
public static MarkdownDocument ToHtml(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
|
||||
{
|
||||
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);
|
||||
@@ -49,6 +52,8 @@ namespace Markdig
|
||||
var document = Parse(markdown, pipeline);
|
||||
renderer.Render(document);
|
||||
writer.Flush();
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +69,7 @@ namespace Markdig
|
||||
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
|
||||
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
|
||||
|
||||
pipeline = CheckForSelfPipeline(pipeline, markdown);
|
||||
var document = Parse(markdown, pipeline);
|
||||
pipeline.Setup(renderer);
|
||||
return renderer.Render(document);
|
||||
@@ -93,7 +99,18 @@ namespace Markdig
|
||||
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
|
||||
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Markdig.Extensions.Bootstrap;
|
||||
using Markdig.Extensions.Citations;
|
||||
using Markdig.Extensions.CustomContainers;
|
||||
using Markdig.Extensions.DefinitionLists;
|
||||
using Markdig.Extensions.Diagrams;
|
||||
using Markdig.Extensions.Emoji;
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
using Markdig.Extensions.Figures;
|
||||
@@ -19,6 +20,9 @@ using Markdig.Extensions.Hardlines;
|
||||
using Markdig.Extensions.ListExtras;
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Extensions.MediaLinks;
|
||||
using Markdig.Extensions.NoRefLinks;
|
||||
using Markdig.Extensions.PragmaLines;
|
||||
using Markdig.Extensions.SelfPipeline;
|
||||
using Markdig.Extensions.SmartyPants;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Extensions.TaskLists;
|
||||
@@ -55,9 +59,50 @@ namespace Markdig
|
||||
.UsePipeTables()
|
||||
.UseListExtras()
|
||||
.UseTaskLists()
|
||||
.UseDiagrams()
|
||||
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -143,7 +188,7 @@ namespace Markdig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the boostrap extension.
|
||||
/// Uses the bootstrap extension.
|
||||
/// </summary>
|
||||
/// <param name="pipeline">The pipeline.</param>
|
||||
/// <returns>The modified pipeline</returns>
|
||||
@@ -322,6 +367,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>
|
||||
@@ -360,6 +416,8 @@ namespace Markdig
|
||||
{
|
||||
switch (extension.ToLowerInvariant())
|
||||
{
|
||||
case "common":
|
||||
break;
|
||||
case "advanced":
|
||||
pipeline.UseAdvancedExtensions();
|
||||
break;
|
||||
@@ -423,11 +481,17 @@ namespace Markdig
|
||||
case "tasklists":
|
||||
pipeline.UseTaskLists();
|
||||
break;
|
||||
case "diagrams":
|
||||
pipeline.UseDiagrams();
|
||||
break;
|
||||
case "nofollowlinks":
|
||||
pipeline.UseNoFollowLinks();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"unknown extension {extension}");
|
||||
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
|
||||
}
|
||||
}
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,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)
|
||||
|
||||
@@ -216,7 +216,7 @@ 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.Span.End = line.Start - 1;
|
||||
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)
|
||||
|
||||
@@ -62,7 +62,7 @@ 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
|
||||
var headingBlock = new HeadingBlock(this)
|
||||
@@ -89,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;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ namespace Markdig.Parsers
|
||||
|
||||
if (countClosingTags > 0)
|
||||
{
|
||||
if (c.IsSpace())
|
||||
if (c.IsSpaceOrTab())
|
||||
{
|
||||
processor.Line.End = i - 1;
|
||||
}
|
||||
|
||||
@@ -171,51 +171,78 @@ namespace Markdig.Parsers
|
||||
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 endof;
|
||||
int index;
|
||||
switch (htmlBlock.Type)
|
||||
{
|
||||
case HtmlBlockType.Comment:
|
||||
if (line.Search("-->", out endof))
|
||||
index = line.IndexOf(EndOfComment);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.Span.End = endof - 1;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfComment.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.CData:
|
||||
if (line.Search("]]>", out endof))
|
||||
index = line.IndexOf(EndOfCDATA);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.Span.End = endof - 1;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfCDATA.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.ProcessingInstruction:
|
||||
if (line.Search("?>", out endof))
|
||||
index = line.IndexOf(EndOfProcessingInstruction);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.Span.End = endof - 1;
|
||||
htmlBlock.UpdateSpanEnd(index + EndOfProcessingInstruction.Length);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.DocumentType:
|
||||
if (line.Search(">", out endof))
|
||||
index = line.IndexOf('>');
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.Span.End = endof - 1;
|
||||
htmlBlock.UpdateSpanEnd(index + 1);
|
||||
result = BlockState.Break;
|
||||
}
|
||||
break;
|
||||
case HtmlBlockType.ScriptPreOrStyle:
|
||||
if (line.SearchLowercase("</script>", out endof) || line.SearchLowercase("</pre>", out endof) || line.SearchLowercase("</style>", out endof))
|
||||
index = line.IndexOf("</script>", 0, true);
|
||||
if (index >= 0)
|
||||
{
|
||||
htmlBlock.Span.End = endof - 1;
|
||||
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)
|
||||
|
||||
@@ -6,9 +6,9 @@ using Markdig.Syntax.Inlines;
|
||||
namespace Markdig.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// A procesor used for <see cref="DelimiterInline"/>.
|
||||
/// A procesor called at the end of processing all inlines.
|
||||
/// </summary>
|
||||
public interface IDelimiterProcessor
|
||||
public interface IPostInlineProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the delimiters.
|
||||
@@ -16,10 +16,10 @@ namespace Markdig.Parsers
|
||||
/// <param name="state">The parser state.</param>
|
||||
/// <param name="root">The root inline.</param>
|
||||
/// <param name="lastChild">The last child.</param>
|
||||
/// <param name="delimiterProcessorIndex">Index of this delimiter processor.</param>
|
||||
/// <param name="postInlineProcessorIndex">Index of this delimiter processor.</param>
|
||||
/// <param name="isFinalProcessing"></param>
|
||||
/// <returns><c>true</c> to continue to the next delimiter processor;
|
||||
/// <c>false</c> to stop the process (in case a processor is perfoming sub-sequent processor itself)</returns>
|
||||
bool ProcessDelimiters(InlineProcessor state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing);
|
||||
bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing);
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,14 @@ namespace Markdig.Parsers
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
var startPosition = processor.Line.Start;
|
||||
var startColumn = processor.ColumnBeforeIndent;
|
||||
var startPosition = processor.StartBeforeIndent;
|
||||
var result = TryContinue(processor, null);
|
||||
if (result == BlockState.Continue)
|
||||
{
|
||||
processor.NewBlocks.Push(new CodeBlock(this)
|
||||
{
|
||||
Column = processor.Column,
|
||||
Column = startColumn,
|
||||
Span = new SourceSpan(startPosition, processor.Line.End)
|
||||
});
|
||||
}
|
||||
@@ -48,7 +49,7 @@ namespace Markdig.Parsers
|
||||
}
|
||||
if (block != null)
|
||||
{
|
||||
block.Span.End = processor.Line.End;
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
}
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -20,23 +20,23 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registered delimiter processors.
|
||||
/// Gets the registered post inline processors.
|
||||
/// </summary>
|
||||
public IDelimiterProcessor[] DelimiterProcessors { get; private set; }
|
||||
public IPostInlineProcessor[] PostInlineProcessors { get; private set; }
|
||||
|
||||
public override void Initialize(InlineProcessor initState)
|
||||
{
|
||||
// Prepare the list of delimiter processors
|
||||
var delimiterProcessors = new List<IDelimiterProcessor>();
|
||||
// Prepare the list of post inline processors
|
||||
var postInlineProcessors = new List<IPostInlineProcessor>();
|
||||
foreach (var parser in this)
|
||||
{
|
||||
var delimProcessor = parser as IDelimiterProcessor;
|
||||
var delimProcessor = parser as IPostInlineProcessor;
|
||||
if (delimProcessor != null)
|
||||
{
|
||||
delimiterProcessors.Add(delimProcessor);
|
||||
postInlineProcessors.Add(delimProcessor);
|
||||
}
|
||||
}
|
||||
DelimiterProcessors = delimiterProcessors.ToArray();
|
||||
PostInlineProcessors = postInlineProcessors.ToArray();
|
||||
|
||||
base.Initialize(initState);
|
||||
}
|
||||
|
||||
@@ -268,8 +268,8 @@ namespace Markdig.Parsers
|
||||
leafBlock.Inline.DumpTo(DebugLog);
|
||||
}
|
||||
|
||||
// Process all delimiters
|
||||
ProcessDelimiters(0, Root, null, true);
|
||||
// PostProcess all inlines
|
||||
PostProcessInlines(0, Root, null, true);
|
||||
|
||||
//TransformDelimitersToLiterals();
|
||||
|
||||
@@ -281,12 +281,12 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessDelimiters(int startingIndex, Inline root, Inline lastChild, bool isFinalProcessing)
|
||||
public void PostProcessInlines(int startingIndex, Inline root, Inline lastChild, bool isFinalProcessing)
|
||||
{
|
||||
for (int i = startingIndex; i < Parsers.DelimiterProcessors.Length; i++)
|
||||
for (int i = startingIndex; i < Parsers.PostInlineProcessors.Length; i++)
|
||||
{
|
||||
var delimiterProcessor = Parsers.DelimiterProcessors[i];
|
||||
if (!delimiterProcessor.ProcessDelimiters(this, root, lastChild, i, isFinalProcessing))
|
||||
var postInlineProcessor = Parsers.PostInlineProcessors[i];
|
||||
if (!postInlineProcessor.PostProcess(this, root, lastChild, i, isFinalProcessing))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace Markdig.Parsers.Inlines
|
||||
/// An inline parser for <see cref="EmphasisInline"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.Parsers.InlineParser" />
|
||||
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
|
||||
public class EmphasisInlineParser : InlineParser, IDelimiterProcessor
|
||||
/// <seealso cref="IPostInlineProcessor" />
|
||||
public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
{
|
||||
private CharacterMap<EmphasisDescriptor> emphasisMap;
|
||||
private readonly DelimitersObjectCache inlinesCache;
|
||||
@@ -85,7 +85,7 @@ namespace Markdig.Parsers.Inlines
|
||||
emphasisMap = new CharacterMap<EmphasisDescriptor>(tempMap);
|
||||
}
|
||||
|
||||
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;
|
||||
if (container == null)
|
||||
@@ -225,9 +225,15 @@ namespace Markdig.Parsers.Inlines
|
||||
for (int j = i - 1; j >= 0; j--)
|
||||
{
|
||||
var previousOpenDelimiter = delimiters[j];
|
||||
|
||||
var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 ||
|
||||
(previousOpenDelimiter.Type & DelimiterType.Close) != 0) &&
|
||||
previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount &&
|
||||
(previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0;
|
||||
|
||||
if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar &&
|
||||
(previousOpenDelimiter.Type & DelimiterType.Open) != 0 &&
|
||||
previousOpenDelimiter.DelimiterCount > 0)
|
||||
previousOpenDelimiter.DelimiterCount > 0 && !isOddMatch)
|
||||
{
|
||||
openDelimiter = previousOpenDelimiter;
|
||||
openDelimiterIndex = j;
|
||||
|
||||
@@ -45,14 +45,14 @@ namespace Markdig.Parsers.Inlines
|
||||
if (literal != null)
|
||||
{
|
||||
var matched = slice;
|
||||
matched.End = match - 1;
|
||||
matched.End = slice.Start + match - 1;
|
||||
int line;
|
||||
int column;
|
||||
processor.Inline = new HtmlEntityInline()
|
||||
{
|
||||
Original = matched,
|
||||
Transcoded = new StringSlice(literal),
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End + 1)),
|
||||
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End)),
|
||||
Line = line,
|
||||
Column = column
|
||||
};
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace Markdig.Parsers.Inlines
|
||||
link.IsClosed = true;
|
||||
|
||||
// Process emphasis delimiters
|
||||
state.ProcessDelimiters(0, link, null, false);
|
||||
state.PostProcessInlines(0, link, null, false);
|
||||
|
||||
state.Inline = link;
|
||||
isValidLink = true;
|
||||
@@ -238,7 +238,7 @@ namespace Markdig.Parsers.Inlines
|
||||
inlineState.Inline = link;
|
||||
|
||||
// Process emphasis delimiters
|
||||
inlineState.ProcessDelimiters(0, link, null, false);
|
||||
inlineState.PostProcessInlines(0, link, null, false);
|
||||
|
||||
// If we have a link (and not an image),
|
||||
// we also set all [ delimiters before the opening delimiter to inactive.
|
||||
|
||||
@@ -65,13 +65,24 @@ namespace Markdig.Parsers.Inlines
|
||||
|
||||
// The LiteralInlineParser is always matching (at least an empty string)
|
||||
var endPosition = slice.Start + length - 1;
|
||||
processor.Inline = new LiteralInline()
|
||||
|
||||
var previousInline = processor.Inline as LiteralInline;
|
||||
if (previousInline != null && ReferenceEquals(previousInline.Content.Text, slice.Text) &&
|
||||
previousInline.Content.End + 1 == slice.Start)
|
||||
{
|
||||
Content = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty,
|
||||
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
};
|
||||
previousInline.Content.End = endPosition;
|
||||
previousInline.Span.End = processor.GetSourcePosition(endPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
processor.Inline = new LiteralInline()
|
||||
{
|
||||
Content = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty,
|
||||
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
};
|
||||
}
|
||||
|
||||
slice.Start = nextStart;
|
||||
|
||||
|
||||
@@ -138,11 +138,6 @@ namespace Markdig.Parsers
|
||||
list.CountBlankLinesReset++;
|
||||
}
|
||||
|
||||
if (list.CountBlankLinesReset > 1)
|
||||
{
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
|
||||
if (list.CountBlankLinesReset == 1 && listItem.ColumnWidth < 0)
|
||||
{
|
||||
state.Close(listItem);
|
||||
@@ -153,7 +148,7 @@ namespace Markdig.Parsers
|
||||
}
|
||||
|
||||
// Update list-item source end position
|
||||
listItem.Span.End = state.Line.End;
|
||||
listItem.UpdateSpanEnd(state.Line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
@@ -170,11 +165,11 @@ namespace Markdig.Parsers
|
||||
{
|
||||
if (state.Indent > columWidth && state.IsCodeIndent)
|
||||
{
|
||||
state.GoToColumn(columWidth);
|
||||
state.GoToColumn(state.ColumnBeforeIndent + columWidth);
|
||||
}
|
||||
|
||||
// Update list-item source end position
|
||||
listItem.Span.End = state.Line.End;
|
||||
listItem.UpdateSpanEnd(state.Line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
@@ -254,6 +249,16 @@ namespace Markdig.Parsers
|
||||
columnWidth = (state.IsBlankLine ? columnBeforeIndent : state.Column) - initColumnBeforeIndent;
|
||||
}
|
||||
|
||||
// Starts/continue the list unless:
|
||||
// - an empty list item follows a paragraph
|
||||
// - an ordered list is not starting by '1'
|
||||
var isPreviousParagraph = (block ?? state.LastBlock) is ParagraphBlock;
|
||||
if (isPreviousParagraph && (state.IsBlankLine || (listInfo.BulletType == '1' && listInfo.OrderedStart != "1")))
|
||||
{
|
||||
state.GoToColumn(initColumn);
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
var newListItem = new ListItemBlock(this)
|
||||
{
|
||||
Column = initColumn,
|
||||
@@ -352,11 +357,11 @@ namespace Markdig.Parsers
|
||||
isLastListItem = false;
|
||||
}
|
||||
|
||||
// Update end-position for the list
|
||||
if (listBlock.Count > 0)
|
||||
{
|
||||
listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
|
||||
}
|
||||
//// Update end-position for the list
|
||||
//if (listBlock.Count > 0)
|
||||
//{
|
||||
// listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Markdig.Parsers
|
||||
return TryParseSetexHeading(processor, block);
|
||||
}
|
||||
|
||||
block.Span.End = processor.Line.End;
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ namespace Markdig.Parsers
|
||||
return BlockState.BreakDiscard;
|
||||
}
|
||||
|
||||
block.Span.End = state.Line.End;
|
||||
block.UpdateSpanEnd(state.Line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ namespace Markdig.Parsers
|
||||
var c = processor.CurrentChar;
|
||||
if (c != quote.QuoteChar)
|
||||
{
|
||||
block.Span.End = processor.Start - 1;
|
||||
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
|
||||
}
|
||||
|
||||
@@ -71,18 +70,8 @@ namespace Markdig.Parsers
|
||||
processor.NextChar(); // Skip following space
|
||||
}
|
||||
|
||||
block.Span.End = processor.Line.End;
|
||||
block.UpdateSpanEnd(processor.Line.End);
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
public override bool Close(BlockProcessor processor, Block block)
|
||||
{
|
||||
var quoteBlock = block as QuoteBlock;
|
||||
if (quoteBlock?.LastChild != null)
|
||||
{
|
||||
quoteBlock.Span.End = quoteBlock.LastChild.Span.End;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ namespace Markdig.Parsers
|
||||
|
||||
breakCharCount++;
|
||||
}
|
||||
else if (c.IsSpace())
|
||||
else if (c.IsSpaceOrTab())
|
||||
{
|
||||
hasSpacesSinceLastMatch = true;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ namespace Markdig
|
||||
{
|
||||
public static partial class Markdown
|
||||
{
|
||||
public const string Version = "0.5.3";
|
||||
public const string Version = "0.7.2";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +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 Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Renderers.Html
|
||||
@@ -11,24 +15,57 @@ namespace Markdig.Renderers.Html
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.CodeBlock}" />
|
||||
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
|
||||
/// </summary>
|
||||
public CodeBlockRenderer()
|
||||
{
|
||||
BlocksAsDiv = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool OutputAttributesOnPre { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
|
||||
/// </summary>
|
||||
public HashSet<string> BlocksAsDiv { get; }
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
|
||||
{
|
||||
renderer.EnsureLine();
|
||||
renderer.Write("<pre");
|
||||
if (OutputAttributesOnPre)
|
||||
|
||||
var fencedCodeBlock = obj as FencedCodeBlock;
|
||||
if (fencedCodeBlock?.Info != null && BlocksAsDiv.Contains(fencedCodeBlock.Info))
|
||||
{
|
||||
renderer.WriteAttributes(obj);
|
||||
var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ??
|
||||
FencedCodeBlockParser.DefaultInfoPrefix;
|
||||
|
||||
// We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block
|
||||
// NOTE that we are allocating a closure here
|
||||
renderer.Write("<div")
|
||||
.WriteAttributes(obj.TryGetAttributes(),
|
||||
cls => cls.StartsWith(infoPrefix) ? cls.Substring(infoPrefix.Length) : cls)
|
||||
.Write(">");
|
||||
renderer.WriteLeafRawLines(obj, true, true, true);
|
||||
renderer.WriteLine("</div>");
|
||||
|
||||
}
|
||||
renderer.Write("><code");
|
||||
if (!OutputAttributesOnPre)
|
||||
else
|
||||
{
|
||||
renderer.WriteAttributes(obj);
|
||||
renderer.Write("<pre");
|
||||
if (OutputAttributesOnPre)
|
||||
{
|
||||
renderer.WriteAttributes(obj);
|
||||
}
|
||||
renderer.Write("><code");
|
||||
if (!OutputAttributesOnPre)
|
||||
{
|
||||
renderer.WriteAttributes(obj);
|
||||
}
|
||||
renderer.Write(">");
|
||||
renderer.WriteLeafRawLines(obj, true, true);
|
||||
renderer.WriteLine("</code></pre>");
|
||||
}
|
||||
renderer.Write(">");
|
||||
renderer.WriteLeafRawLines(obj, true, true);
|
||||
renderer.WriteLine("</code></pre>");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,14 @@ namespace Markdig.Renderers.Html
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
if (Classes == null)
|
||||
{
|
||||
Classes = new List<string>(2); // Use half list compare to default capacity (4), as we don't expect lots of classes
|
||||
Classes = new List<string>(2);
|
||||
// Use half list compare to default capacity (4), as we don't expect lots of classes
|
||||
}
|
||||
|
||||
if (!Classes.Contains(name))
|
||||
{
|
||||
Classes.Add(name);
|
||||
}
|
||||
Classes.Add(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,7 +118,7 @@ namespace Markdig.Renderers.Html
|
||||
}
|
||||
if (htmlAttributes.Classes == null)
|
||||
{
|
||||
htmlAttributes.Classes = shared ? Classes : new List<string>(Classes);
|
||||
htmlAttributes.Classes = shared ? Classes : Classes != null ? new List<string>(Classes) : null;
|
||||
}
|
||||
else if (Classes != null)
|
||||
{
|
||||
@@ -122,7 +127,7 @@ namespace Markdig.Renderers.Html
|
||||
|
||||
if (htmlAttributes.Properties == null)
|
||||
{
|
||||
htmlAttributes.Properties = shared ? Properties : new List<KeyValuePair<string, string>>(Properties);
|
||||
htmlAttributes.Properties = shared ? Properties : Properties != null ? new List<KeyValuePair<string, string>>(Properties) : null;
|
||||
}
|
||||
else if (Properties != null)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.AutolinkInline}" />
|
||||
public class AutolinkInlineRenderer : HtmlObjectRenderer<AutolinkInline>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to always add rel="nofollow" for links or not.
|
||||
/// </summary>
|
||||
public bool AutoRelNoFollow { get; set; }
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, AutolinkInline obj)
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
@@ -22,6 +27,12 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
}
|
||||
renderer.WriteEscapeUrl(obj.Url);
|
||||
renderer.WriteAttributes(obj);
|
||||
|
||||
if (!obj.IsEmail && AutoRelNoFollow)
|
||||
{
|
||||
renderer.Write(" rel=\"nofollow\"");
|
||||
}
|
||||
|
||||
renderer.Write("\">");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.LinkInline}" />
|
||||
public class LinkInlineRenderer : HtmlObjectRenderer<LinkInline>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to always add rel="nofollow" for links or not.
|
||||
/// </summary>
|
||||
public bool AutoRelNoFollow { get; set; }
|
||||
|
||||
protected override void Write(HtmlRenderer renderer, LinkInline link)
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
@@ -54,6 +59,10 @@ namespace Markdig.Renderers.Html.Inlines
|
||||
{
|
||||
if (renderer.EnableHtmlForInline)
|
||||
{
|
||||
if (AutoRelNoFollow)
|
||||
{
|
||||
renderer.Write(" rel=\"nofollow\"");
|
||||
}
|
||||
renderer.Write(">");
|
||||
}
|
||||
renderer.WriteChildren(link);
|
||||
|
||||
@@ -32,7 +32,9 @@ namespace Markdig.Renderers.Html
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.WriteLine("<ul>");
|
||||
renderer.Write("<ul");
|
||||
renderer.WriteAttributes(listBlock);
|
||||
renderer.WriteLine(">");
|
||||
}
|
||||
foreach (var item in listBlock)
|
||||
{
|
||||
|
||||
@@ -83,26 +83,28 @@ namespace Markdig.Renderers
|
||||
/// Writes the content escaped for HTML.
|
||||
/// </summary>
|
||||
/// <param name="slice">The slice.</param>
|
||||
/// <param name="softEscape">Only escape < and &</param>
|
||||
/// <returns>This instance</returns>
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public HtmlRenderer WriteEscape(ref StringSlice slice)
|
||||
public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false)
|
||||
{
|
||||
if (slice.Start > slice.End)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
return WriteEscape(slice.Text, slice.Start, slice.Length);
|
||||
return WriteEscape(slice.Text, slice.Start, slice.Length, softEscape);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the content escaped for HTML.
|
||||
/// </summary>
|
||||
/// <param name="slice">The slice.</param>
|
||||
/// <param name="softEscape">Only escape < and &</param>
|
||||
/// <returns>This instance</returns>
|
||||
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
|
||||
public HtmlRenderer WriteEscape(StringSlice slice)
|
||||
public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false)
|
||||
{
|
||||
return WriteEscape(ref slice);
|
||||
return WriteEscape(ref slice, softEscape);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,8 +113,9 @@ namespace Markdig.Renderers
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="offset">The offset.</param>
|
||||
/// <param name="length">The length.</param>
|
||||
/// <param name="softEscape">Only escape < and &</param>
|
||||
/// <returns>This instance</returns>
|
||||
public HtmlRenderer WriteEscape(string content, int offset, int length)
|
||||
public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content) || length == 0)
|
||||
return this;
|
||||
@@ -132,12 +135,15 @@ namespace Markdig.Renderers
|
||||
previousOffset = offset + 1;
|
||||
break;
|
||||
case '>':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
if (EnableHtmlEscape)
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(">");
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(">");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
break;
|
||||
case '&':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
@@ -148,12 +154,15 @@ namespace Markdig.Renderers
|
||||
previousOffset = offset + 1;
|
||||
break;
|
||||
case '"':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
if (EnableHtmlEscape)
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(""");
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(""");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -233,8 +242,9 @@ namespace Markdig.Renderers
|
||||
/// Writes the specified <see cref="HtmlAttributes"/>.
|
||||
/// </summary>
|
||||
/// <param name="attributes">The attributes to render.</param>
|
||||
/// <param name="classFilter">A class filter used to transform a class into another class at writing time</param>
|
||||
/// <returns>This instance</returns>
|
||||
public HtmlRenderer WriteAttributes(HtmlAttributes attributes)
|
||||
public HtmlRenderer WriteAttributes(HtmlAttributes attributes, Func<string, string> classFilter = null)
|
||||
{
|
||||
if (attributes == null)
|
||||
{
|
||||
@@ -256,7 +266,7 @@ namespace Markdig.Renderers
|
||||
{
|
||||
Write(" ");
|
||||
}
|
||||
WriteEscape(cssClass);
|
||||
WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass);
|
||||
}
|
||||
Write("\"");
|
||||
}
|
||||
@@ -284,8 +294,9 @@ namespace Markdig.Renderers
|
||||
/// <param name="leafBlock">The leaf block.</param>
|
||||
/// <param name="writeEndOfLines">if set to <c>true</c> write end of lines.</param>
|
||||
/// <param name="escape">if set to <c>true</c> escape the content for HTML</param>
|
||||
/// <param name="softEscape">Only escape < and &</param>
|
||||
/// <returns>This instance</returns>
|
||||
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape)
|
||||
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false)
|
||||
{
|
||||
if (leafBlock == null) throw new ArgumentNullException(nameof(leafBlock));
|
||||
if (leafBlock.Lines.Lines != null)
|
||||
@@ -300,7 +311,7 @@ namespace Markdig.Renderers
|
||||
}
|
||||
if (escape)
|
||||
{
|
||||
WriteEscape(ref slices[i].Slice);
|
||||
WriteEscape(ref slices[i].Slice, softEscape);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
@@ -11,6 +13,16 @@ namespace Markdig.Renderers
|
||||
/// </summary>
|
||||
public interface IMarkdownRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when before writing an object.
|
||||
/// </summary>
|
||||
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when after writing an object.
|
||||
/// </summary>
|
||||
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object renderers that will render <see cref="Block"/> and <see cref="Inline"/> elements.
|
||||
/// </summary>
|
||||
|
||||
@@ -35,6 +35,16 @@ namespace Markdig.Renderers
|
||||
|
||||
public bool IsLastInContainer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when before writing an object.
|
||||
/// </summary>
|
||||
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when after writing an object.
|
||||
/// </summary>
|
||||
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
|
||||
|
||||
/// <summary>
|
||||
/// Writes the children of the specified <see cref="ContainerBlock"/>.
|
||||
/// </summary>
|
||||
@@ -105,6 +115,10 @@ namespace Markdig.Renderers
|
||||
|
||||
var objectType = obj.GetType();
|
||||
|
||||
// Calls before writing an object
|
||||
var writeBefore = ObjectWriteBefore;
|
||||
writeBefore?.Invoke(this, obj);
|
||||
|
||||
// Handle regular renderers
|
||||
IMarkdownObjectRenderer renderer = previousObjectType == objectType ? previousRenderer : null;
|
||||
if (renderer == null && !renderersPerType.TryGetValue(objectType, out renderer))
|
||||
@@ -142,6 +156,10 @@ namespace Markdig.Renderers
|
||||
|
||||
previousObjectType = objectType;
|
||||
previousRenderer = renderer;
|
||||
|
||||
// Calls after writing an object
|
||||
var writeAfter = ObjectWriteAfter;
|
||||
writeAfter?.Invoke(this, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,5 +75,19 @@ namespace Markdig.Syntax
|
||||
{
|
||||
ProcessInlinesEnd?.Invoke(state, null);
|
||||
}
|
||||
|
||||
public void UpdateSpanEnd(int spanEnd)
|
||||
{
|
||||
// Update parent spans
|
||||
var parent = this;
|
||||
while (parent != null)
|
||||
{
|
||||
if (spanEnd > parent.Span.End)
|
||||
{
|
||||
parent.Span.End = spanEnd;
|
||||
}
|
||||
parent = parent.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/Markdig/Syntax/BlockExtensions.cs
Normal file
138
src/Markdig/Syntax/BlockExtensions.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
// 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.
|
||||
namespace Markdig.Syntax
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="Block"/>
|
||||
/// </summary>
|
||||
public static class BlockExtensions
|
||||
{
|
||||
// TODO: Add test for this code
|
||||
|
||||
public static Block FindBlockAtPosition(this Block rootBlock, int position)
|
||||
{
|
||||
var contains = rootBlock.CompareToPosition(position) == 0;
|
||||
var blocks = rootBlock as ContainerBlock;
|
||||
if (blocks == null || blocks.Count == 0 || !contains)
|
||||
{
|
||||
return contains ? rootBlock : null;
|
||||
}
|
||||
|
||||
var lowerIndex = 0;
|
||||
var upperIndex = blocks.Count - 1;
|
||||
|
||||
// binary search on lines
|
||||
Block block = null;
|
||||
while (lowerIndex <= upperIndex)
|
||||
{
|
||||
int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex;
|
||||
block = blocks[midIndex];
|
||||
int comparison = block.CompareToPosition(position);
|
||||
if (comparison == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
block = null;
|
||||
if (comparison < 0)
|
||||
lowerIndex = midIndex + 1;
|
||||
else
|
||||
upperIndex = midIndex - 1;
|
||||
}
|
||||
|
||||
if (block == null)
|
||||
{
|
||||
return rootBlock;
|
||||
}
|
||||
|
||||
// Recursively go deep into the block
|
||||
return FindBlockAtPosition(block, position);
|
||||
}
|
||||
|
||||
|
||||
public static int FindClosestLine(this MarkdownDocument root, int line)
|
||||
{
|
||||
var closestBlock = root.FindClosestBlock(line);
|
||||
return closestBlock?.Line ?? 0;
|
||||
}
|
||||
|
||||
public static Block FindClosestBlock(this Block rootBlock, int line)
|
||||
{
|
||||
var blocks = rootBlock as ContainerBlock;
|
||||
if (blocks == null || blocks.Count == 0)
|
||||
{
|
||||
return rootBlock.Line == line ? rootBlock : null;
|
||||
}
|
||||
|
||||
var lowerIndex = 0;
|
||||
var upperIndex = blocks.Count - 1;
|
||||
|
||||
// binary search on lines
|
||||
while (lowerIndex <= upperIndex)
|
||||
{
|
||||
int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex;
|
||||
var block = blocks[midIndex];
|
||||
int comparison = block.Line.CompareTo(line);
|
||||
if (comparison == 0)
|
||||
{
|
||||
return block;
|
||||
}
|
||||
if (comparison < 0)
|
||||
lowerIndex = midIndex + 1;
|
||||
else
|
||||
upperIndex = midIndex - 1;
|
||||
}
|
||||
|
||||
// If we are between two lines, try to find the best spot
|
||||
if (lowerIndex > 0 && lowerIndex < blocks.Count)
|
||||
{
|
||||
var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1];
|
||||
var nextBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex];
|
||||
|
||||
if (prevBlock.Line == line)
|
||||
{
|
||||
return prevBlock;
|
||||
}
|
||||
|
||||
if (nextBlock.Line == line)
|
||||
{
|
||||
return nextBlock;
|
||||
}
|
||||
|
||||
// we calculate the position of the current line relative to the line found and previous line
|
||||
var prevLine = prevBlock.Line;
|
||||
var nextLine = nextBlock.Line;
|
||||
|
||||
var middle = (line - prevLine) * 1.0 / (nextLine - prevLine);
|
||||
// If relative position < 0.5, we select the previous line, otherwise we select the line found
|
||||
return middle < 0.5 ? prevBlock : nextBlock;
|
||||
}
|
||||
|
||||
if (lowerIndex == 0)
|
||||
{
|
||||
var prevBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex];
|
||||
return prevBlock;
|
||||
}
|
||||
|
||||
if (lowerIndex == blocks.Count)
|
||||
{
|
||||
var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1];
|
||||
return prevBlock;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static bool ContainsPosition(this Block block, int position)
|
||||
{
|
||||
return CompareToPosition(block, position) == 0;
|
||||
}
|
||||
|
||||
public static int CompareToPosition(this Block block, int position)
|
||||
{
|
||||
return position < block.Span.Start ? 1 : position > block.Span.End + 1 ? -1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,10 +69,7 @@ namespace Markdig.Syntax
|
||||
children[Count++] = item;
|
||||
item.Parent = this;
|
||||
|
||||
if (item.Span.End > Span.End)
|
||||
{
|
||||
Span.End = item.Span.End;
|
||||
}
|
||||
UpdateSpanEnd(item.Span.End);
|
||||
}
|
||||
|
||||
private void EnsureCapacity(int min)
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Markdig.Syntax
|
||||
/// <returns>An iteration over the descendant elements</returns>
|
||||
public static IEnumerable<MarkdownObject> Descendants(this MarkdownObject markdownObject)
|
||||
{
|
||||
// TODO: implement a recursiveless method
|
||||
|
||||
var block = markdownObject as ContainerBlock;
|
||||
if (block != null)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"title": "Markdig",
|
||||
"version": "0.5.3",
|
||||
"version": "0.7.2",
|
||||
"authors": [ "Alexandre Mutel" ],
|
||||
"description": "A fast, powerfull, CommonMark compliant, extensible Markdown processor for .NET",
|
||||
"description": "A fast, powerfull, CommonMark compliant, extensible Markdown processor for .NET with 20+ builtin extensions (pipetables, footnotes, definition lists... etc.)",
|
||||
"copyright": "Alexandre Mutel",
|
||||
"language": "en-US",
|
||||
"packOptions": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"projectUrl": "https://github.com/lunet-io/markdig",
|
||||
"iconUrl": "https://raw.githubusercontent.com/lunet-io/markdig/master/img/markdig.png",
|
||||
"requireLicenseAcceptance": false,
|
||||
"releaseNotes": "> 0.5.3:\nFix bug in pipetables when a trailing `|` was put on the header row separator.\n> 0.5.2:\nAdd better support for precise source location for link parts (label, url...)\n> 0.5.1:\nAdd support for task lists.\nAdd support for precise source location pipelineBuilder.UsePreciseSourceLocation.\nBreaking change: Markdown.ToHtml() accept now only a string instead of a TextReader as this improve the overall performance.",
|
||||
"releaseNotes": "> 0.7.2\n- Fix rendering of table colspan with non english locale\n- Fix grid table colspan parsing\n- Add nofollow extension for links\n> 0.7.1\n- Fix issue in smarty pants which could lead to an InvalidCastException\n- Update parsers to latest CommonMark specs\n>0.7.0\n- Update to latest NETStandard.Library 1.6.0\n- Fix issue with digits in auto-identifier extension\n- Fix incorrect start of span calculated for code indented blocks",
|
||||
"tags": [ "Markdown CommonMark md html md2html" ]
|
||||
},
|
||||
"configurations": {
|
||||
@@ -60,10 +60,7 @@
|
||||
},
|
||||
"netstandard1.1": {
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "1.5.0-rc2-24027",
|
||||
"System.Threading": "4.0.11-rc2-24027",
|
||||
"System.Threading.Tasks": "4.0.11-rc2-24027",
|
||||
"System.Threading.Tasks.Parallel": "4.0.1-rc2-24027"
|
||||
"NETStandard.Library": "1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user