Compare commits

...

68 Commits

Author SHA1 Message Date
Alexandre Mutel
fdaf301bd6 Update to latest dotnet Core RTM, NETStandard.Library 1.6.0. Bump to version 0.7.0 2016-06-27 21:44:31 +09:00
Alexandre Mutel
5a9f2f3afe Fix calculated span for indented code blocks that was starting after the first 4 chars and not before (issue https://github.com/madskristensen/MarkdownEditor/issues/8) 2016-06-27 20:32:24 +09:00
Alexandre Mutel
6a1b30761a Merge branch 'pr/n30_Jither' 2016-06-27 06:59:13 +09:00
Jimmi Thøgersen
8f8b08fad6 Include digits after first letter in Urilize (for Auto-Identifiers)
Added dependency: NUnit3TestAdapter (for VS Test Runner)
Disabled "Prefer 32-bit" on Markdig.Tests to accommodate NUnit.
2016-06-26 23:39:43 +02:00
Alexandre Mutel
312b63a4c8 Bump to 0.6.2 2016-06-25 18:27:06 +09:00
Alexandre Mutel
1598b538ab Update to latest CommonMark spec. Handle new corner cases for emphasis (see https://talk.commonmark.org/t/emphasis-strong-emphasis-corner-cases/2123/1) 2016-06-25 18:25:24 +09:00
Alexandre Mutel
16ce46a741 Bump to 0.6.1 2016-06-24 20:33:30 +09:00
Alexandre Mutel
4456598228 Double check all extensions with advanced profiles to make sure there are no conflicts between existing extensions 2016-06-24 17:44:57 +09:00
Alexandre Mutel
2a937c63b9 Fix issue with auto identifiers extension overriding manual ids for headings 2016-06-24 17:43:46 +09:00
Alexandre Mutel
191bc940c7 Bump version to 0.6.0 2016-06-24 16:10:58 +09:00
Alexandre Mutel
05673758e3 Add new SelfPipeline extension 2016-06-24 15:05:37 +09:00
Alexandre Mutel
cff2b9a8ca Fix conflict between SmartyPants extension and pipetables (issue #13) 2016-06-24 12:04:29 +09:00
Alexandre Mutel
586095a475 Refactor StringSlice.Search to StringSlice.IndexOf to make it more familiar and consistent 2016-06-24 12:03:16 +09:00
Alexandre Mutel
eae2082a1e Rename IDelimiterProcessor to IPostInlineProcessor as it can be used outside of a delimiter processing (for example SmartyPants mdash) 2016-06-24 12:02:25 +09:00
Alexandre Mutel
4576548df3 Add comment to Descendants() method that should be implemented without recursive 2016-06-24 12:01:03 +09:00
Alexandre Mutel
266e0c8bfd Fix consecutive footnotes without blanklines between (issue #26) 2016-06-24 06:01:53 +09:00
Alexandre Mutel
f5c07dbab5 Bump to 0.5.12 2016-06-24 05:43:17 +09:00
Alexandre Mutel
c8a28a1ad7 Fix an issue where a consecutive footnote is parsed as a paragraph (issue #26) 2016-06-24 05:42:36 +09:00
Alexandre Mutel
72cc454314 Remove backsticks in SmartyPantsInlineParser. Rename SmaryPantsInlineParser to SmartyPantsInlineParser 2016-06-24 05:27:12 +09:00
Alexandre Mutel
7fe2c1f939 Remove OrderedList.ReplacyBy method, as it is not used and might be not predictable (issue #25) 2016-06-24 05:25:54 +09:00
Alexandre Mutel
087e7a68b6 Fix emoji parsing and allow them only if they are surrounded by spaces (issue #15) 2016-06-23 17:28:33 +09:00
Alexandre Mutel
abeabf15a1 Remove non necessary check in HtmlAttributes.CopyTo 2016-06-23 11:49:01 +09:00
Alexandre Mutel
f9bfcaab7b Bump version to 0.5.11 2016-06-23 11:46:22 +09:00
Alexandre Mutel
afa0182f02 Fix ArgumentNullReference exception in HtmlAttributes.CopyTo (issue #23) 2016-06-23 11:46:07 +09:00
Alexandre Mutel
b83de5934d Bump version to 0.5.10 2016-06-23 05:52:06 +09:00
Alexandre Mutel
c294d3bfb4 Improve compact method for tests taking into account \r\n and \r 2016-06-23 05:52:06 +09:00
Alexandre Mutel
a7cdb2351a Fix regression for html block parsing (issue #21) 2016-06-23 05:52:05 +09:00
Alexandre Mutel
46ef21a3ed Merge pull request #19 from jorrit/patch-1
Fix typo in MarkdownExtensions.cs
2016-06-22 20:55:51 +09:00
Jorrit Schippers
18c8d0178c Fix typo in MarkdownExtensions.cs 2016-06-22 13:55:09 +02:00
Alexandre Mutel
ebc79dafbd Bump version to 0.5.9 2016-06-22 08:37:37 +09:00
Alexandre Mutel
a1d2467643 Fix bug for deep nested list items that were incorrectly detected (not CommonMark compliant. Check with CommonMark specs to add a test case) 2016-06-22 08:35:42 +09:00
Alexandre Mutel
8220f0fa56 Fix ArgumentOutOfRangeException in Block.FindClosestLine. Add tests for Pragmalines 2016-06-22 08:35:08 +09:00
Alexandre Mutel
ec385acc7f Bump version to 0.5.8 2016-06-21 23:45:48 +09:00
Alexandre Mutel
04c1cc62d4 Add block extension methods for looking for a block from a position or for the closest line (typically used for syntax highlighting by MarkdownEditor) 2016-06-21 23:45:40 +09:00
Alexandre Mutel
abdbd65f60 Fix calculation of spans for nested blocks 2016-06-21 23:45:28 +09:00
Alexandre Mutel
1f32a060da Update readme and link to MarkdownEditor 2016-06-20 15:05:39 +09:00
Alexandre Mutel
699d80c150 Add missing test for issue #16 2016-06-20 13:48:22 +09:00
Alexandre Mutel
56bcac7600 Bump version to 0.5.7 2016-06-20 13:42:32 +09:00
Alexandre Mutel
cab3365104 Fix issue while detecting generic attributes that was breaking parsing (issue #16) 2016-06-20 13:42:15 +09:00
Alexandre Mutel
3821bd00fe Bump version to 0.5.6 2016-06-20 12:30:25 +09:00
Alexandre Mutel
62701fd0f1 Improve pragma line output 2016-06-20 12:29:13 +09:00
Alexandre Mutel
1be5e60506 Make to MarkdownPipeline.Setup public. Make Markdown.ToHtml returning a MarkdownDocument 2016-06-20 12:27:50 +09:00
Alexandre Mutel
c9f1512358 Bump to 0.5.5 2016-06-20 09:06:29 +09:00
Alexandre Mutel
8f23aed6af Add pragma line extension 2016-06-20 09:04:33 +09:00
Alexandre Mutel
6a62ae9c69 Add github like class for taslk lists (issue #14) 2016-06-20 09:04:13 +09:00
Alexandre Mutel
2c3de5688b Don't add a class an HtmlAttributes that is already in the list 2016-06-20 09:00:54 +09:00
Alexandre Mutel
f3c08b4ec4 Output HtmlAttributes for unordered list 2016-06-20 09:00:31 +09:00
Alexandre Mutel
69e3baafe5 Add support for callbacks to RendererBase, IMarkdownRenderer 2016-06-20 09:00:10 +09:00
Alexandre Mutel
5844ccc395 Bump to 0.5.4 2016-06-20 06:58:55 +09:00
Alexandre Mutel
be9c6fa54b Fix bug for html block parsing in StringSlice.Search (issue #12) 2016-06-20 06:56:13 +09:00
Alexandre Mutel
d14f277c7b Bump version to 0.5.3 2016-06-19 11:37:26 +09:00
Alexandre Mutel
593bf08b92 Fix bug in pipetables with a trailing | on the header row separator (issue #10) 2016-06-19 11:37:12 +09:00
Alexandre Mutel
85f631f868 Bump version to 0.5.2 2016-06-17 13:58:31 +09:00
Alexandre Mutel
5ad964bcb6 Rename MarkdownObject.SourceSpan to Span 2016-06-17 13:56:09 +09:00
Alexandre Mutel
137a404bdc Add property handling of precise position for links 2016-06-17 13:45:43 +09:00
Alexandre Mutel
5204ec758a Add version used for benchmarks. Add docfx Microsoft.DocAsCode.MarkdownLite for comparison 2016-06-17 13:15:32 +09:00
Alexandre Mutel
6f4fb69c62 Add precise source location for title, span for inlines/footnote/abbreviations (#8) 2016-06-17 09:34:46 +09:00
Alexandre Mutel
0a1b37c965 Breaking change: Add SourceSpan and replace SourceStartPosition and SourceEndPosition 2016-06-16 23:45:18 +09:00
Alexandre Mutel
90bdafb05a Update link to babelmark3 2016-06-16 13:35:45 +09:00
Alexandre Mutel
8cc668ae6d Update link to babelmark3 https 2016-06-16 13:22:23 +09:00
Alexandre Mutel
1787dc4590 Update readme.md with link to article 2016-06-16 12:43:32 +09:00
Alexandre Mutel
0a9cc8fcd7 Fix typo 2016-06-16 12:31:31 +09:00
Alexandre Mutel
10c06daf5d Make benchmark results shorter 2016-06-16 12:26:28 +09:00
Alexandre Mutel
a262e42980 Update readme and benchmarks 2016-06-16 12:24:01 +09:00
Alexandre Mutel
4cd3d045d1 Add tests precise position with text containing tabs 2016-06-16 10:03:04 +09:00
Alexandre Mutel
92357576b1 Add test precise position for indented code 2016-06-16 09:54:07 +09:00
Alexandre Mutel
d45f67f8c2 Add protection against infinite loops if an extension was messing the parsing of blocks or inlines 2016-06-16 09:22:45 +09:00
Alexandre Mutel
2571cdffee Add tasklists to readme.md 2016-06-16 06:43:19 +09:00
79 changed files with 2767 additions and 911 deletions

BIN
img/BenchmarkCPU.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
img/BenchmarkMemory.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -6,19 +6,20 @@ Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, ex
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
You can **try Markdig online** and compare it to other implementations on [babelmark3](http://babelmark.github.io/)
You can **try Markdig online** and compare it to other implementations on [babelmark3](https://babelmark.github.io/?text=Hello+**Markdig**!)
## Features
- **Very fast parser** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree**
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
- Converter to **HTML**
- Passing more than **600+ tests** from the latest [CommonMark specs](http://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
- including GFM fenced code blocks.
- including **GFM fenced code blocks**.
- **Extensible** architecture
- Even the core Markdown/CommonMark parsing is pluggable, so it allows to disable builtin Markdown/Commonmark parsing (e.g [Disable HTML parsing](https://github.com/lunet-io/markdig/blob/7964bd0160d4c18e4155127a4c863d61ebd8944a/src/Markdig/MarkdownExtensions.cs#L306)) or change behaviour (e.g change matching `#` of a headers with `@`)
- Built-in with **18+ extensions**, including:
- Built-in with **20+ extensions**, including:
- 2 kind of tables:
- **Pipe tables** (inspired from Github tables and [PanDoc - Pipe Tables](http://pandoc.org/README.html#extension-pipe_tables))
- **Grid tables** (inspired from [Pandoc - Grid Tables](http://pandoc.org/README.html#extension-grid_tables))
@@ -32,6 +33,7 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Definition lists** (inspired from [PHP Markdown Extra - Definitions Lists](https://michelf.ca/projects/php-markdown/extra/#def-list))
- **Footnotes** (inspired from [PHP Markdown Extra - Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes))
- **Auto-identifiers** for headings (similar to [Pandoc - Auto Identifiers](http://pandoc.org/README.html#extension-auto_identifiers))
- **Task Lists** inspired from [Github Task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments).
- **Extra bullet lists**, supporting alpha bullet `a.` `b.` and roman bullet (`i`, `ii`...etc.)
- **Media support** for media url (youtube, vimeo, mp4...etc.) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/embedded-audio-and-video/441))
- **Abbreviations** (inspired from [PHP Markdown Extra - Abbreviations](https://michelf.ca/projects/php-markdown/extra/#abbr))
@@ -46,6 +48,13 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Bootstrap** class (to output bootstrap class)
- Compatible with .NET 3.5, 4.0+ and .NET Core (`netstandard1.1+`)
## Documentation
> The repository is under construction. There will be a dedicated website and proper documentation at some point!
In the meantime, you can have a "behind the scene" article about Markdig in my blog post ["Implementing a Markdown Engine for .NET"](http://xoofx.com/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/)
## Download
Markdig is available as a NuGet package: [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/)
@@ -80,27 +89,44 @@ This software is released under the [BSD-Clause 2 license](https://github.com/lu
This is an early preview of the benchmarking against various implementations:
- Markdig: itself
- CommonMarkCpp: [cmark](https://github.com/jgm/cmark), Reference C implementation of CommonMark, no support for extensions
- [CommonMark.NET](https://github.com/Knagis/CommonMark.NET): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMarkNet (devel)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Moonshine](https://github.com/brandonc/moonshine): popular C Markdown processor
**C implementations**:
- [cmark](https://github.com/jgm/cmark) (version: 0.25.0): Reference C implementation of CommonMark, no support for extensions
- [Moonshine](https://github.com/brandonc/moonshine) (version: : popular C Markdown processor
**.NET implementations**:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.5.x): itself
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.11.0): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMark.NET(pipe_tables)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) (version: 1.5.0): another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 1.13.0): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Marked.NET](https://github.com/T-Alex/MarkedNet) (version: 1.0.5) port of original [marked.js](https://github.com/chjj/marked) project
- [Microsoft.DocAsCode.MarkdownLite](https://github.com/dotnet/docfx/tree/dev/src/Microsoft.DocAsCode.MarkdownLite) (version: 2.0.1) used by the [docfx](https://github.com/dotnet/docfx) project
**JavaScript/V8 implementations**:
- [Strike.V8](https://github.com/SimonCropp/Strike) (version: 1.5.0) [marked.js](https://github.com/chjj/marked) running in Google V8 (not .NET based)
### Analysis of the results:
- Markdig is roughly **x100 times faster than MarkdownSharp**
- 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)
- **15% to 30% less allocations** and GC pressure
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:
### Performance in x86:
![BenchMark CPU Time](img/BenchmarkCPU.png)
![BenchMark Memory](img/BenchmarkMemory.png)
### Performance for x86:
```
BenchmarkDotNet-Dev=v0.9.6.0+
BenchmarkDotNet-Dev=v0.9.7.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
Processor=Intel(R) Core(TM) i7-4770 CPU 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
JitModules=clrjit-v4.6.1080.0
@@ -108,14 +134,18 @@ JitModules=clrjit-v4.6.1080.0
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
--------------------- |---------- |---------- |------- |------ |------- |------------------- |
TestMarkdig | 5.4332 ms | 0.0809 ms | 96.00 | 36.00 | 84.00 | 1,218,643.59 |
TestCommonMarkCpp | 4.0150 ms | 0.1208 ms | - | - | 180.00 | 454,859.74 |
TestCommonMarkNet | 4.5448 ms | 0.0323 ms | 193.00 | 12.00 | 84.00 | 1,406,367.27 |
TestCommonMarkNetNew | 5.4811 ms | 0.0327 ms | 193.00 | 96.00 | 84.00 | 1,738,465.42 |
TestMarkdownDeep | 7.5881 ms | 0.0554 ms | 205.00 | 96.00 | 84.00 | 1,758,383.79 |
TestMoonshine | 5.8586 ms | 0.0764 ms | - | - | 215.00 | 565,000.72 |
Method | Median | StdDev |Scaled | Gen 0 | Gen 1| Gen 2|Bytes Allocated/Op |
--------------------------- |------------ |---------- |------ | ------ |------|---------|------------------ |
Markdig | 5.5316 ms | 0.0372 ms | 0.71 | 56.00| 21.00| 49.00| 1,285,917.31 |
CommonMark.NET(master) | 4.7035 ms | 0.0422 ms | 0.60 | 113.00| 7.00| 49.00| 1,502,404.60 |
CommonMark.NET(pipe_tables) | 5.6164 ms | 0.0298 ms | 0.72 | 111.00| 56.00| 49.00| 1,863,128.13 |
MarkdownDeep | 7.8193 ms | 0.0334 ms | 1.00 | 120.00| 56.00| 49.00| 1,884,854.85 |
cmark | 4.2698 ms | 0.1526 ms | 0.55 | -| -| -| NA |
Moonshine | 6.0929 ms | 0.1053 ms | 1.28 | -| -| -| NA |
Strike.V8 | 10.5895 ms | 0.0492 ms | 1.35 | -| -| -| NA |
Marked.NET | 207.3169 ms | 5.2628 ms | 26.51 | 0.00| 0.00| 0.00| 303,125,228.65 |
MarkdownSharp | 675.0185 ms | 2.8447 ms | 86.32 | 40.00| 27.00| 41.00| 2,413,394.17 |
Microsoft DocfxMarkdownLite | 166.3357 ms | 0.4529 ms | 21.27 |4,452.00|948.00|11,167.00| 180,218,359.60 |
```
### Performance for x64:

View File

@@ -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" />
@@ -98,6 +101,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.

View File

@@ -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>
````````````````````````````````

View File

@@ -61,3 +61,60 @@ multi-paragraph list items.<a href="#fnref:3" class="footnote-back-ref">&#8617;<
</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">&#8617;</a></p></li>
<li id="fn:2">
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:3">
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:4">
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">&#8617;</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">&#8617;</a></p></li>
<li id="fn:2">
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:3">
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:4">
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">&#8617;</a></p></li>
</ol>
</div>
````````````````````````````````

View File

@@ -214,6 +214,76 @@ Column delimiters `|` at the very beginning of a line or just before a line endi
</tbody>
</table>
````````````````````````````````
A pipe may be present at both the beginning/ending of each line:
```````````````````````````````` example
|a|b|
|-|-|
|0|1|
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
Or may be ommitted on one side:
```````````````````````````````` example
a|b|
-|-|
0|1|
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
```````````````````````````````` example
|a|b
|-|-
|0|1
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
Single column table can be declared with lines starting only by a column delimiter:
```````````````````````````````` example
@@ -307,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

View File

@@ -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 &ldquo;a double quote&rdquo;</p>
````````````````````````````````
```````````````````````````````` example
This is ``a code span''``
.
<p>This is <code>a code span''</code></p>
````````````````````````````````
```````````````````````````````` example
hello ``there```
test
.
<p>hello &ldquo;there&rdquo;`
test</p>
````````````````````````````````
An emphasis starting inside left/right quotes will span over the right quote:
```````````````````````````````` example
@@ -133,3 +110,26 @@ This is a en ellipsis...
.
<p>This is a en ellipsis&hellip;</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>
````````````````````````````````

File diff suppressed because it is too large Load Diff

View File

@@ -39,24 +39,24 @@ SOFTWARE.
<#@ 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>(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"),
};
var emptyLines = false;
var displayEmptyLines = false;

View File

@@ -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>
````````````````````````````````

View 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);
}
}
}

View File

@@ -80,36 +80,52 @@ namespace Markdig.Tests
[Test]
public void TestUrlAndTitle()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"(http://google.com 'this is a title')ABC");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("this is a title", title);
Assert.AreEqual(new SourceSpan(1, 17), linkSpan);
Assert.AreEqual(new SourceSpan(19, 35), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty()
{
// 01234
var text = new StringSlice(@"(<>)A");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(new SourceSpan(1, 2), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestUrlAndTitleEmpty2()
{
// 012345
var text = new StringSlice(@"( <> )A");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(new SourceSpan(2, 3), linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
@@ -117,12 +133,18 @@ namespace Markdig.Tests
[Test]
public void TestUrlEmptyWithTitleWithMultipleSpaces()
{
// 0 1 2
// 0123456789012345678901234567
var text = new StringSlice(@"( <> 'toto' )A");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual("toto", title);
Assert.AreEqual(new SourceSpan(4, 5), linkSpan);
Assert.AreEqual(new SourceSpan(12, 17), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
@@ -132,50 +154,67 @@ namespace Markdig.Tests
var text = new StringSlice(@"()A");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual(SourceSpan.Empty, linkSpan);
Assert.AreEqual(SourceSpan.Empty, titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestMultipleLines()
{
var text = new StringSlice(@"(
<http://google.com>
'toto' )A");
// 0 1 2 3
// 01 2345678901234567890 1234567890123456789
var text = new StringSlice("(\n<http://google.com>\n 'toto' )A");
string link;
string title;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title));
SourceSpan linkSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("toto", title);
Assert.AreEqual(new SourceSpan(2, 20), linkSpan);
Assert.AreEqual(new SourceSpan(26, 31), titleSpan);
Assert.AreEqual('A', text.CurrentChar);
}
[Test]
public void TestLabelSimple()
{
// 01234
var text = new StringSlice("[foo]");
string label;
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
SourceSpan labelSpan;
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual("foo", label);
}
[Test]
public void TestLabelEscape()
{
// 012345678
var text = new StringSlice(@"[fo\[\]o]");
string label;
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
SourceSpan labelSpan;
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
Assert.AreEqual(new SourceSpan(1, 7), labelSpan);
Assert.AreEqual(@"fo[]o", label);
}
[Test]
public void TestLabelEscape2()
{
// 0123
var text = new StringSlice(@"[\]]");
string label;
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
SourceSpan labelSpan;
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
Assert.AreEqual(new SourceSpan(1, 2), labelSpan);
Assert.AreEqual(@"]", label);
}
@@ -194,23 +233,36 @@ namespace Markdig.Tests
[Test]
public void TestLabelWhitespaceCollapsedAndTrim()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[ fo o z ]");
string label;
Assert.True(LinkHelper.TryParseLabel(ref text, out label));
SourceSpan labelSpan;
Assert.True(LinkHelper.TryParseLabel(ref text, out label, out labelSpan));
Assert.AreEqual(new SourceSpan(6, 17), labelSpan);
Assert.AreEqual(@"fo o z", label);
}
[Test]
public void TestlLinkReferenceDefinitionSimple()
{
// 0 1 2 3
// 0123456789012345678901234567890123456789
var text = new StringSlice(@"[foo]: /toto 'title'");
string label;
string url;
string title;
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title));
SourceSpan labelSpan;
SourceSpan urlSpan;
SourceSpan titleSpan;
Assert.True(LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan, out titleSpan));
Assert.AreEqual(@"foo", label);
Assert.AreEqual(@"/toto", url);
Assert.AreEqual(@"title", title);
Assert.AreEqual(new SourceSpan(1, 3), labelSpan);
Assert.AreEqual(new SourceSpan(7, 11), urlSpan);
Assert.AreEqual(new SourceSpan(13, 19), titleSpan);
}
[Test]
@@ -245,5 +297,102 @@ namespace Markdig.Tests
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<ab"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<user@>"), out text, out isEmail));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in...
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "applications")]
[TestCase("33", "")]
public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("abc", "abc")]
[TestCase("a-c", "a-c")]
[TestCase("a c", "a-c")]
[TestCase("a_c", "a_c")]
[TestCase("a.c", "a.c")]
[TestCase("a,c", "ac")]
[TestCase("a--", "a")] // Not Pandoc-equivalent: a--
[TestCase("a__", "a")] // Not Pandoc-equivalent: a__
[TestCase("a..", "a")] // Not Pandoc-equivalent: a..
[TestCase("a??", "a")]
[TestCase("a ", "a")]
[TestCase("a--d", "a-d")]
[TestCase("a__d", "a_d")]
[TestCase("a??d", "ad")]
[TestCase("a d", "a-d")]
[TestCase("a..d", "a.d")]
[TestCase("-bc", "bc")]
[TestCase("_bc", "bc")]
[TestCase(" bc", "bc")]
[TestCase("?bc", "bc")]
[TestCase(".bc", "bc")]
[TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.-
public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bær", "br")]
[TestCase("bør", "br")]
[TestCase("bΘr", "br")]
[TestCase("四五", "")]
public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bár", "bar")]
[TestCase("àrrivé", "arrive")]
public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("123", "")]
[TestCase("1,-b", "b")]
[TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1-
[TestCase("ab3", "ab3")]
[TestCase("ab3de", "ab3de")]
public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("一二三四五", "一二三四五")]
[TestCase("一,-b", "一-b")]
public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("bær", "bær")]
[TestCase("æ5el", "æ5el")]
[TestCase("-æ5el", "æ5el")]
[TestCase("-frø-", "frø")]
[TestCase("-fr-ø", "fr-ø")]
public void TestUrilizeNonAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
// Just to be sure, test for characters expressly forbidden in URI fragments:
[TestCase("b#r", "br")]
[TestCase("b%r", "br")] // Invalid except as an escape character
[TestCase("b^r", "br")]
[TestCase("b[r", "br")]
[TestCase("b]r", "br")]
[TestCase("b{r", "br")]
[TestCase("b}r", "br")]
[TestCase("b<r", "br")]
[TestCase("b>r", "br")]
[TestCase(@"b\r", "br")]
[TestCase(@"b""r", "br")]
public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
}
}

View File

@@ -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);

View File

@@ -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()
{

View File

@@ -0,0 +1,81 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using 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));
}
}
}

View File

@@ -3,10 +3,13 @@
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
@@ -85,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
");
}
@@ -163,16 +166,44 @@ literal ( 0, 4) 4-5
}
[Test]
public void TestHtmlInline()
public void TestLinkParts1()
{
// 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
");
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
}
[Test]
public void TestLinkParts2()
{
// 0 1
// 01 2 34567890123456789
var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
Assert.AreEqual(new SourceSpan(16, 19), link.TitleSpan);
}
[Test]
public void TestLinkParts3()
{
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01![234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(5, 15), link.Span);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
}
[Test]
@@ -208,6 +239,64 @@ 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()
{
@@ -479,7 +568,7 @@ literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
literal ( 1, 0) 2-5
math ( 1, 4) 6-11
attributes ( 0, 0) 0-0
attributes ( 0, 0) 0--1
", "mathematics");
}
@@ -562,6 +651,54 @@ literal ( 4, 2) 13-13
", "pipetables");
}
[Test]
public void TestIndentedCode()
{
// 01 2 345678 9ABCDE
Check("0\n\n 0\n 1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 0) 3-13
");
}
[Test]
public void TestIndentedCodeWithTabs()
{
// 01 2 3 45 6 78
Check("0\n\n\t0\n\t1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 0) 3-7
");
}
[Test]
public void TestIndentedCodeWithMixedTabs()
{
// 01 2 34 56 78 9
Check("0\n\n \t0\n \t1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 0) 3-9
");
}
[Test]
public void TestTabsInList()
{
// 012 34 567 89
Check("- \t0\n- \t1\n", @"
list ( 0, 0) 0-8
listitem ( 0, 0) 0-3
paragraph ( 0, 4) 3-3
literal ( 0, 4) 3-3
listitem ( 1, 0) 5-8
paragraph ( 1, 4) 8-8
literal ( 1, 4) 8-8
");
}
[Test]
public void TestDocument()
{
@@ -606,11 +743,11 @@ literal ( 8, 2) 77-92
foreach (var val in document.Descendants())
{
var name = GetTypeName(val.GetType());
build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.SourceStartPosition,2}-{val.SourceEndPosition}\n");
build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.Span.Start,2}-{val.Span.End}\n");
var attributes = val.TryGetAttributes();
if (attributes != null)
{
build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.SourceStartPosition,2}-{attributes.SourceEndPosition}\n");
build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.Span.Start,2}-{attributes.Span.End}\n");
}
}
var result = build.ToString().Trim();

View File

@@ -18,6 +18,7 @@
}
},
"dependencies": {
"NUnit": "3.2.0"
"NUnit": "3.2.0",
"NUnit3TestAdapter": "3.2.0"
}
}

View File

@@ -33,5 +33,10 @@ namespace Markdig.Extensions.Abbreviations
/// The text associated to this label.
/// </summary>
public StringSlice Text;
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.Abbreviations
@@ -38,8 +39,9 @@ namespace Markdig.Extensions.Abbreviations
return BlockState.None;
}
SourceSpan labelSpan;
string label;
if (!LinkHelper.TryParseLabel(ref slice, out label))
if (!LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
{
return BlockState.None;
}
@@ -57,10 +59,10 @@ namespace Markdig.Extensions.Abbreviations
{
Label = label,
Text = slice,
SourceStartPosition = startPosition,
SourceEndPosition = slice.End,
Span = new SourceSpan(startPosition, slice.End),
Line = processor.LineIndex,
Column = processor.Column
Column = processor.Column,
LabelSpan = labelSpan,
};
if (!processor.Document.HasAbbreviations())
{
@@ -126,8 +128,7 @@ namespace Markdig.Extensions.Abbreviations
{
container = new ContainerInline()
{
SourceStartPosition = originalLiteral.SourceStartPosition,
SourceEndPosition = originalLiteral.SourceEndPosition,
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
@@ -137,18 +138,21 @@ namespace Markdig.Extensions.Abbreviations
int column;
var abbrInline = new AbbreviationInline(abbr)
{
SourceStartPosition = processor.GetSourcePosition(i, out line, out column),
Span =
{
Start = processor.GetSourcePosition(i, out line, out column),
},
Line = line,
Column = column
};
abbrInline.SourceEndPosition = abbrInline.SourceStartPosition + match.Length - 1;
abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1;
// Append the previous literal
if (i > content.Start)
{
container.AppendChild(literal);
literal.SourceEndPosition = abbrInline.SourceStartPosition - 1;
literal.Span.End = abbrInline.Span.Start - 1;
// Truncate it before the abbreviation
literal.Content.End = i - 1;
}
@@ -167,8 +171,7 @@ namespace Markdig.Extensions.Abbreviations
// Process the remaining literal
literal = new LiteralInline()
{
SourceStartPosition = abbrInline.SourceEndPosition + 1,
SourceEndPosition = literal.SourceEndPosition,
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
Line = line,
Column = column + match.Length,
};

View File

@@ -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;
}
}
}

View File

@@ -66,8 +66,7 @@ namespace Markdig.Extensions.DefinitionLists
{
currentDefinitionList = new DefinitionList(this)
{
SourceStartPosition = paragraphBlock.SourceStartPosition,
SourceEndPosition = processor.Line.End,
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
Column = paragraphBlock.Column,
Line = paragraphBlock.Line,
};
@@ -78,8 +77,7 @@ namespace Markdig.Extensions.DefinitionLists
{
Line = processor.LineIndex,
Column = column,
SourceStartPosition = startPosition,
SourceEndPosition = processor.Line.End,
Span = new SourceSpan(startPosition, processor.Line.End),
OpeningCharacter = processor.CurrentChar,
};
@@ -90,8 +88,7 @@ namespace Markdig.Extensions.DefinitionLists
{
Column = paragraphBlock.Column,
Line = line.Line,
SourceStartPosition = paragraphBlock.SourceStartPosition,
SourceEndPosition = paragraphBlock.SourceEndPosition,
Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End),
IsOpen = false
};
term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position);
@@ -101,7 +98,7 @@ namespace Markdig.Extensions.DefinitionLists
processor.Open(definitionItem);
// Update the end position
currentDefinitionList.SourceEndPosition = processor.Line.End;
currentDefinitionList.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}
@@ -136,7 +133,7 @@ namespace Markdig.Extensions.DefinitionLists
definitionItem.RemoveAt(definitionItem.Count - 1);
}
list.SourceEndPosition = list.LastChild.SourceEndPosition;
list.Span.End = list.LastChild.Span.End;
return BlockState.None;
}
@@ -148,8 +145,7 @@ namespace Markdig.Extensions.DefinitionLists
processor.Close(definitionItem);
var nextDefinitionItem = new DefinitionItem(this)
{
SourceStartPosition = startPosition,
SourceEndPosition = processor.Line.End,
Span = new SourceSpan(startPosition, processor.Line.End),
Line = processor.LineIndex,
Column = processor.Column,
OpeningCharacter = processor.CurrentChar,
@@ -182,7 +178,7 @@ namespace Markdig.Extensions.DefinitionLists
definitionItem.RemoveAt(definitionItem.Count - 1);
}
list.SourceEndPosition = list.LastChild.SourceEndPosition;
list.Span.End = list.LastChild.Span.End;
return BlockState.Break;
}
}

View File

@@ -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))
{
@@ -94,12 +110,15 @@ namespace Markdig.Extensions.Emoji
int column;
processor.Inline = new EmojiInline(unicode)
{
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
Span =
{
Start = processor.GetSourcePosition(startPosition, out line, out column),
},
Line = line,
Column = column,
Match = match
};
processor.Inline.SourceEndPosition = processor.Inline.SourceStartPosition + match.Length - 1;
processor.Inline.Span.End = processor.Inline.Span.Start + match.Length - 1;
return true;
}

View File

@@ -53,8 +53,7 @@ namespace Markdig.Extensions.Figures
var figure = new Figure(this)
{
SourceStartPosition = startPosition,
SourceEndPosition = line.End,
Span = new SourceSpan(startPosition, line.End),
Line = processor.LineIndex,
Column = processor.Column,
OpeningCharacter = matchChar,
@@ -66,8 +65,7 @@ namespace Markdig.Extensions.Figures
{
var caption = new FigureCaption(this)
{
SourceStartPosition = line.Start,
SourceEndPosition = line.End,
Span = new SourceSpan(line.Start, line.End),
Line = processor.LineIndex,
Column = column + line.Start - startPosition,
IsOpen = false
@@ -107,8 +105,7 @@ namespace Markdig.Extensions.Figures
{
var caption = new FigureCaption(this)
{
SourceStartPosition = line.Start,
SourceEndPosition = line.End,
Span = new SourceSpan(line.Start, line.End),
Line = processor.LineIndex,
Column = column + line.Start - startPosition,
IsOpen = false
@@ -117,7 +114,7 @@ namespace Markdig.Extensions.Figures
figure.Add(caption);
}
figure.SourceEndPosition = line.End;
figure.UpdateSpanEnd(line.End);
// Don't keep the last line
return BlockState.BreakDiscard;
@@ -126,7 +123,7 @@ namespace Markdig.Extensions.Figures
// Reset the indentation to the column before the indent
processor.GoToColumn(processor.ColumnBeforeIndent);
figure.SourceEndPosition = line.End;
figure.UpdateSpanEnd(line.End);
return BlockState.Continue;
}

View File

@@ -47,8 +47,7 @@ namespace Markdig.Extensions.Footers
}
processor.NewBlocks.Push(new FooterBlock(this)
{
SourceStartPosition = startPosition,
SourceEndPosition = processor.Line.End,
Span = new SourceSpan(startPosition, processor.Line.End),
OpeningCharacter = openingChar,
Column = column,
Line = processor.LineIndex,
@@ -81,7 +80,7 @@ namespace Markdig.Extensions.Footers
{
processor.NextChar(); // Skip following space
}
block.SourceEndPosition = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
}
return result;
}

View File

@@ -34,6 +34,11 @@ namespace Markdig.Extensions.Footnotes
/// </summary>
public List<FootnoteLink> Links { get; private set; }
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
internal bool IsLastLineEmpty { get; set; }
}
}

View File

@@ -26,9 +26,14 @@ namespace Markdig.Extensions.Footnotes
}
public override BlockState TryOpen(BlockProcessor processor)
{
return TryOpen(processor, false);
}
private BlockState TryOpen(BlockProcessor processor, bool isContinue)
{
// We expect footnote to appear only at document level and not indented more than a code indent block
if (processor.IsCodeIndent || processor.CurrentContainer.GetType() != typeof(MarkdownDocument) )
if (processor.IsCodeIndent || (!isContinue && processor.CurrentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(processor.CurrentContainer is Footnote)))
{
return BlockState.None;
}
@@ -36,19 +41,24 @@ namespace Markdig.Extensions.Footnotes
var saved = processor.Column;
string label;
int start = processor.Start;
if (!LinkHelper.TryParseLabel(ref processor.Line, false, out label) || !label.StartsWith("^") || processor.CurrentChar != ':')
SourceSpan labelSpan;
if (!LinkHelper.TryParseLabel(ref processor.Line, false, out label, out labelSpan) || !label.StartsWith("^") || processor.CurrentChar != ':')
{
processor.GoToColumn(saved);
return BlockState.None;
}
// Advance the column
int deltaColumn = processor.Start - start;
processor.Column = processor.Column + deltaColumn;
processor.NextChar(); // Skip ':'
var footnote = new Footnote(this) {Label = label};
var footnote = new Footnote(this)
{
Label = label,
LabelSpan = labelSpan,
};
// Maintain a list of all footnotes at document level
var footnotes = processor.Document.GetData(DocumentKey) as FootnoteGroup;
@@ -83,9 +93,23 @@ namespace Markdig.Extensions.Footnotes
return BlockState.ContinueDiscard;
}
if (footnote.IsLastLineEmpty && processor.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;

View File

@@ -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
@@ -62,8 +62,8 @@ namespace Markdig.Extensions.GenericAttributes
// Update position for HtmlAttributes
htmlAttributes.Line = processor.LineIndex;
htmlAttributes.Column = startOfAttributes - processor.CurrentLineStartPosition; // This is not accurate with tabs!
htmlAttributes.SourceStartPosition = startOfAttributes;
htmlAttributes.SourceEndPosition = copy.Start - 1;
htmlAttributes.Span.Start = startOfAttributes;
htmlAttributes.Span.End = copy.Start - 1;
line.End = indexOfAttributes - 1;
return true;

View File

@@ -64,15 +64,15 @@ namespace Markdig.Extensions.GenericAttributes
}
var currentHtmlAttributes = objectToAttach.GetAttributes();
attributes.CopyTo(currentHtmlAttributes);
attributes.CopyTo(currentHtmlAttributes, false, false);
// Update the position of the attributes
int line;
int column;
currentHtmlAttributes.SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column);
currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out line, out column);
currentHtmlAttributes.Line = line;
currentHtmlAttributes.Column = column;
currentHtmlAttributes.SourceEndPosition = currentHtmlAttributes.SourceStartPosition + slice.Start - startPosition - 1;
currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1;
// We don't set the processor.Inline as we don't want to add attach attributes to a particular entity
return true;

View File

@@ -5,6 +5,7 @@
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace Markdig.Extensions.Mathematics
{
@@ -12,7 +13,7 @@ namespace Markdig.Extensions.Mathematics
/// An inline parser for <see cref="MathInline"/>.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
/// <seealso cref="IPostInlineProcessor" />
public class MathInlineParser : InlineParser
{
/// <summary>
@@ -106,8 +107,7 @@ namespace Markdig.Extensions.Mathematics
int column;
var inline = new MathInline()
{
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
SourceEndPosition = processor.GetSourcePosition(slice.End),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.End)),
Line = line,
Column = column,
Delimiter = match,

View File

@@ -0,0 +1,79 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using 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}";
}
}
}

View 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>&lt;!--markdig:extensions--&gt;</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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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
// " “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// << >> « » &laquo; &raquo; 'left-angle-quote', 'right-angle-quote'
// ... … &hellip; 'ellipsis'
// Special case: &ndash; and &mdash; are handle as a PostProcess step to avoid conflicts with pipetables header separator row
// -- &ndash; 'ndash'
// --- — &mdash; '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,8 @@ namespace Markdig.Extensions.SmartyPants
case '-':
if (slice.NextChar() == '-')
{
type = SmartyPantType.Dash2;
if (slice.PeekChar(1) == '-')
{
slice.NextChar();
type = SmartyPantType.Dash3;
}
processor.ParserStates[Index] = string.Empty;
return false;
}
break;
}
@@ -167,13 +159,13 @@ namespace Markdig.Extensions.SmartyPants
int column;
var pant = new SmartyPant()
{
SourceStartPosition = processor.GetSourcePosition(startingPosition, out line, out column),
Span = {Start = processor.GetSourcePosition(startingPosition, out line, out column)},
Line = line,
Column = column,
OpeningCharacter = openingChar,
Type = type
};
pant.SourceEndPosition = pant.SourceStartPosition + slice.Start - startingPosition - 1;
pant.Span.End = pant.Span.Start + slice.Start - startingPosition - 1;
// We will check in a post-process step for balanaced open/close quotes
if (postProcess)
@@ -258,8 +250,7 @@ namespace Markdig.Extensions.SmartyPants
pants.RemoveAt(j);
toReplace.ReplaceBy(new LiteralInline(toReplace.ToString())
{
SourceStartPosition = toReplace.SourceStartPosition,
SourceEndPosition = toReplace.SourceEndPosition,
Span = toReplace.Span,
Line = toReplace.Line,
Column = toReplace.Column,
});
@@ -282,8 +273,7 @@ namespace Markdig.Extensions.SmartyPants
{
quote.ReplaceBy(new LiteralInline(quote.ToString())
{
SourceStartPosition = quote.SourceStartPosition,
SourceEndPosition = quote.SourceEndPosition,
Span = quote.Span,
Line = quote.Line,
Column = quote.Column,
});
@@ -291,5 +281,85 @@ 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
if (state.ParserStates[Index] == null)
{
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;
}
}
}

View File

@@ -28,6 +28,8 @@ namespace Markdig.Extensions.Tables
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Pipe tables require precise source location
pipeline.PreciseSourceLocation = true;
if (!pipeline.BlockParsers.Contains<PipeTableBlockParser>())
{
pipeline.BlockParsers.Insert(0, new PipeTableBlockParser());

View File

@@ -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;
@@ -102,8 +102,7 @@ namespace Markdig.Extensions.Tables
{
processor.Inline = new PiprTableDelimiterInline(this)
{
SourceStartPosition = position,
SourceEndPosition = position,
Span = new SourceSpan(position, position),
Line = globalLineIndex,
Column = column,
LocalLineIndex = localLineIndex
@@ -124,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;
@@ -236,8 +235,8 @@ namespace Markdig.Extensions.Tables
}
// TODO: This is not accurate for the table
table.SourceStartPosition = column.SourceStartPosition;
table.SourceEndPosition = column.SourceEndPosition;
table.Span.Start = column.Span.Start;
table.Span.End = column.Span.End;
table.Line = column.Line;
table.Column = column.Column;
@@ -287,17 +286,16 @@ namespace Markdig.Extensions.Tables
{
cellContainer.Line = item.Line;
cellContainer.Column = item.Column;
cellContainer.SourceStartPosition = item.SourceStartPosition;
cellContainer.Span.Start = item.Span.Start;
isFirstItem = false;
}
cellContainer.SourceEndPosition = item.SourceEndPosition;
cellContainer.Span.End = item.Span.End;
item = nextSibling;
}
var tableParagraph = new ParagraphBlock()
{
SourceStartPosition = cellContainer.SourceStartPosition,
SourceEndPosition = cellContainer.SourceEndPosition,
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
Inline = cellContainer
@@ -305,8 +303,7 @@ namespace Markdig.Extensions.Tables
var tableCell = new TableCell()
{
SourceStartPosition = cellContainer.SourceStartPosition,
SourceEndPosition = cellContainer.SourceEndPosition,
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
@@ -317,8 +314,7 @@ namespace Markdig.Extensions.Tables
{
row = new TableRow()
{
SourceStartPosition = cellContainer.SourceStartPosition,
SourceEndPosition = cellContainer.SourceEndPosition,
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
@@ -383,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();
@@ -448,8 +436,8 @@ namespace Markdig.Extensions.Tables
}
// Check the left side of a `|` delimiter
TableColumnAlign align;
if (!ParseHeaderString(delimiter.PreviousSibling, out align))
TableColumnAlign align = TableColumnAlign.Left;
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
{
break;
}

View File

@@ -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;
}
@@ -49,13 +64,26 @@ namespace Markdig.Extensions.TaskLists
int column;
var taskItem = new TaskList()
{
SourceStartPosition = processor.GetSourcePosition(startingPosition, out line, out column),
Span = { Start = processor.GetSourcePosition(startingPosition, out line, out column)},
Line = line,
Column = column,
Checked = !c.IsSpace()
};
taskItem.SourceEndPosition = taskItem.SourceStartPosition + 2;
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;
}
}

View File

@@ -61,6 +61,11 @@ namespace Markdig.Helpers
}
previousIsSpace = false;
}
else if (c.IsDigit())
{
headingBuffer.Append(c);
previousIsSpace = false;
}
else if (!previousIsSpace && c.IsWhitespace())
{
var pc = headingBuffer[headingBuffer.Length - 1];
@@ -297,10 +302,24 @@ namespace Markdig.Helpers
public static bool TryParseInlineLink(StringSlice text, out string link, out string title)
{
return TryParseInlineLink(ref text, out link, out title);
SourceSpan linkSpan;
SourceSpan titleSpan;
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
}
public static bool TryParseInlineLink(StringSlice text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)
{
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
}
public static bool TryParseInlineLink(ref StringSlice text, out string link, out string title)
{
SourceSpan linkSpan;
SourceSpan titleSpan;
return TryParseInlineLink(ref text, out link, out title, out linkSpan, out titleSpan);
}
public static bool TryParseInlineLink(ref StringSlice text, out string link, out string title, out SourceSpan linkSpan, out SourceSpan titleSpan)
{
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
// 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces?
@@ -313,14 +332,25 @@ namespace Markdig.Helpers
link = null;
title = null;
linkSpan = SourceSpan.Empty;
titleSpan = SourceSpan.Empty;
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
if (c == '(')
{
text.NextChar();
text.TrimStart();
var pos = text.Start;
if (TryParseUrl(ref text, out link))
{
linkSpan.Start = pos;
linkSpan.End = text.Start - 1;
if (linkSpan.End < linkSpan.Start)
{
linkSpan = SourceSpan.Empty;
}
int spaceCount;
text.TrimStart(out spaceCount);
var hasWhiteSpaces = spaceCount > 0;
@@ -333,12 +363,19 @@ namespace Markdig.Helpers
else if (hasWhiteSpaces)
{
c = text.CurrentChar;
pos = text.Start;
if (c == ')')
{
isValid = true;
}
else if (TryParseTitle(ref text, out title))
{
titleSpan.Start = pos;
titleSpan.End = text.Start - 1;
if (titleSpan.End < titleSpan.Start)
{
titleSpan = SourceSpan.Empty;
}
text.TrimStart();
c = text.CurrentChar;
@@ -582,12 +619,25 @@ namespace Markdig.Helpers
return TryParseLinkReferenceDefinition(ref text, out label, out url, out title);
}
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url,
out string title) where T : ICharIterator
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url, out string title)
where T : ICharIterator
{
SourceSpan labelSpan;
SourceSpan urlSpan;
SourceSpan titleSpan;
return TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan,
out titleSpan);
}
public static bool TryParseLinkReferenceDefinition<T>(ref T text, out string label, out string url, out string title, out SourceSpan labelSpan, out SourceSpan urlSpan, out SourceSpan titleSpan) where T : ICharIterator
{
url = null;
title = null;
if (!TryParseLabel(ref text, out label))
urlSpan = SourceSpan.Empty;
titleSpan = SourceSpan.Empty;
if (!TryParseLabel(ref text, out label, out labelSpan))
{
return false;
}
@@ -601,10 +651,13 @@ namespace Markdig.Helpers
// Skip any whitespaces before the url
text.TrimStart();
urlSpan.Start = text.Start;
if (!TryParseUrl(ref text, out url) || string.IsNullOrEmpty(url))
{
return false;
}
urlSpan.End = text.Start - 1;
var saved = text;
int newLineCount;
@@ -612,8 +665,10 @@ namespace Markdig.Helpers
var c = text.CurrentChar;
if (c == '\'' || c == '"' || c == '(')
{
titleSpan.Start = text.Start;
if (TryParseTitle(ref text, out title))
{
titleSpan.End = text.Start - 1;
// If we have a title, it requires a whitespace after the url
if (!hasWhiteSpaces)
{
@@ -662,24 +717,41 @@ namespace Markdig.Helpers
public static bool TryParseLabel<T>(T lines, out string label) where T : ICharIterator
{
return TryParseLabel(ref lines, false, out label);
SourceSpan labelSpan;
return TryParseLabel(ref lines, false, out label, out labelSpan);
}
public static bool TryParseLabel<T>(T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator
{
return TryParseLabel(ref lines, false, out label, out labelSpan);
}
public static bool TryParseLabel<T>(ref T lines, out string label) where T : ICharIterator
{
return TryParseLabel(ref lines, false, out label);
SourceSpan labelSpan;
return TryParseLabel(ref lines, false, out label, out labelSpan);
}
public static bool TryParseLabel<T>(ref T lines, bool allowEmpty, out string label) where T : ICharIterator
public static bool TryParseLabel<T>(ref T lines, out string label, out SourceSpan labelSpan) where T : ICharIterator
{
return TryParseLabel(ref lines, false, out label, out labelSpan);
}
public static bool TryParseLabel<T>(ref T lines, bool allowEmpty, out string label, out SourceSpan labelSpan) where T : ICharIterator
{
label = null;
char c = lines.CurrentChar;
labelSpan = SourceSpan.Empty;
if (c != '[')
{
return false;
}
var buffer = StringBuilderCache.Local();
var startLabel = -1;
var endLabel = -1;
bool hasEscape = false;
bool previousWhitespace = true;
bool hasNonWhiteSpace = false;
@@ -719,11 +791,19 @@ namespace Markdig.Helpers
break;
}
buffer.Length = i;
endLabel--;
}
// Only valid if buffer is less than 1000 characters
if (buffer.Length <= 999)
{
labelSpan.Start = startLabel;
labelSpan.End = endLabel;
if (labelSpan.Start > labelSpan.End)
{
labelSpan = SourceSpan.Empty;
}
label = buffer.ToString();
isValid = true;
}
@@ -741,6 +821,10 @@ namespace Markdig.Helpers
if (!hasEscape && c == '\\')
{
if (startLabel < 0)
{
startLabel = lines.Start;
}
hasEscape = true;
}
else
@@ -749,6 +833,11 @@ namespace Markdig.Helpers
if (!previousWhitespace || !isWhitespace)
{
if (startLabel < 0)
{
startLabel = lines.Start;
}
endLabel = lines.Start;
buffer.Append(c);
if (!isWhitespace)
{

View File

@@ -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;
}
}
}

View File

@@ -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 &lt; 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>

View File

@@ -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;
}
}
}

View File

@@ -19,6 +19,8 @@ using Markdig.Extensions.Hardlines;
using Markdig.Extensions.ListExtras;
using Markdig.Extensions.Mathematics;
using Markdig.Extensions.MediaLinks;
using Markdig.Extensions.PragmaLines;
using Markdig.Extensions.SelfPipeline;
using Markdig.Extensions.SmartyPants;
using Markdig.Extensions.Tables;
using Markdig.Extensions.TaskLists;
@@ -58,6 +60,35 @@ namespace Markdig
.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 &lt;--markdig:extensions--&gt;</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 precise source code location (useful for syntax highlighting).
/// </summary>
@@ -143,7 +174,7 @@ namespace Markdig
}
/// <summary>
/// Uses the boostrap extension.
/// Uses the bootstrap extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
@@ -360,6 +391,8 @@ namespace Markdig
{
switch (extension.ToLowerInvariant())
{
case "common":
break;
case "advanced":
pipeline.UseAdvancedExtensions();
break;
@@ -424,10 +457,10 @@ namespace Markdig
pipeline.UseTaskLists();
break;
default:
throw new ArgumentException($"unknown extension {extension}");
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
}
}
return pipeline;
}
}
}
}

View File

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

View File

@@ -590,8 +590,16 @@ namespace Markdig.Parsers
/// </summary>
private void TryOpenBlocks()
{
int previousStart = -1;
while (ContinueProcessingLine)
{
// Security check so that the parser can't go into a crazy infinite loop if one extension is messing
if (previousStart == Start)
{
throw new InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse blocks at line [{LineIndex}] with line [{Line}]");
}
previousStart = Start;
// Eat indent spaces before checking the character
ParseIndent();

View File

@@ -159,8 +159,8 @@ namespace Markdig.Parsers
fenced.Column = processor.Column;
fenced.FencedChar = matchChar;
fenced.FencedCharCount = count;
fenced.SourceStartPosition = startPosition;
fenced.SourceEndPosition = line.Start;
fenced.Span.Start = startPosition;
fenced.Span.End = line.Start;
};
// Try to parse any attached attributes
@@ -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.SourceEndPosition = line.Start - 1;
block.UpdateSpanEnd(line.Start - 1);
// Don't keep the last line
return BlockState.BreakDiscard;

View File

@@ -70,7 +70,7 @@ namespace Markdig.Parsers
HeaderChar = matchingChar,
Level = leadingCount,
Column = column,
SourceStartPosition = sourcePosition
Span = { Start = sourcePosition }
};
processor.NewBlocks.Push(headingBlock);
processor.GoToColumn(column + leadingCount + 1);
@@ -119,7 +119,7 @@ namespace Markdig.Parsers
}
// Setup the source end position of this element
headingBlock.SourceEndPosition = processor.Line.End;
headingBlock.Span.End = processor.Line.End;
// We expect a single line, so don't continue
return BlockState.Break;

View File

@@ -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.SourceEndPosition = 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.SourceEndPosition = 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.SourceEndPosition = 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.SourceEndPosition = 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.SourceEndPosition = 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)
@@ -234,7 +261,7 @@ namespace Markdig.Parsers
// Update only if we don't have a break discard
if (result != BlockState.BreakDiscard)
{
htmlBlock.SourceEndPosition = line.End;
htmlBlock.Span.End = line.End;
}
return result;
@@ -246,9 +273,8 @@ namespace Markdig.Parsers
{
Column = startColumn,
Type = type,
SourceStartPosition = startPosition,
// By default, setup to the end of line
SourceEndPosition = startPosition + state.Line.End
Span = new SourceSpan(startPosition, startPosition + state.Line.End)
});
return BlockState.Continue;
}

View File

@@ -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);
}
}

View File

@@ -18,15 +18,15 @@ 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,
SourceStartPosition = startPosition,
SourceEndPosition = processor.Line.End
Column = startColumn,
Span = new SourceSpan(startPosition, processor.Line.End)
});
}
return result;
@@ -49,7 +49,7 @@ namespace Markdig.Parsers
}
if (block != null)
{
block.SourceEndPosition = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
}
return BlockState.Continue;
}

View File

@@ -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);
}

View File

@@ -119,6 +119,18 @@ namespace Markdig.Parsers
return GetSourcePosition(sliceOffset, out lineIndex, out column);
}
public SourceSpan GetSourcePositionFromLocalSpan(SourceSpan span)
{
if (span.IsEmpty)
{
return SourceSpan.Empty;
}
int column;
int lineIndex;
return new SourceSpan(GetSourcePosition(span.Start, out lineIndex, out column), GetSourcePosition(span.End, out lineIndex, out column));
}
/// <summary>
/// Gets the source position for the specified offset within the current slice.
/// </summary>
@@ -159,6 +171,8 @@ namespace Markdig.Parsers
/// <param name="leafBlock">The leaf block.</param>
public void ProcessInlineLeaf(LeafBlock leafBlock)
{
if (leafBlock == null) throw new ArgumentNullException(nameof(leafBlock));
// clear parser states
Array.Clear(ParserStates, 0, ParserStates.Length);
@@ -175,8 +189,17 @@ namespace Markdig.Parsers
var text = leafBlock.Lines.ToSlice(lineOffsets);
leafBlock.Lines = new StringLineGroup();
int previousStart = -1;
while (!text.IsEmpty)
{
// Security check so that the parser can't go into a crazy infinite loop if one extension is messing
if (previousStart == text.Start)
{
throw new InvalidOperationException($"The parser is in an invalid infinite loop while trying to parse inlines for block [{leafBlock.GetType().Name}] at position ({leafBlock.ToPositionText()}");
}
previousStart = text.Start;
var c = text.CurrentChar;
var textSaved = text;
@@ -245,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();
@@ -258,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;
}

View File

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines
@@ -39,8 +40,7 @@ namespace Markdig.Parsers.Inlines
{
IsEmail = isEmail,
Url = link,
SourceStartPosition = processor.GetSourcePosition(saved.Start, out line, out column),
SourceEndPosition = processor.GetSourcePosition(slice.Start - 1),
Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
@@ -57,8 +57,7 @@ namespace Markdig.Parsers.Inlines
processor.Inline = new HtmlInline()
{
Tag = htmlTag,
SourceStartPosition = processor.GetSourcePosition(saved.Start, out line, out column),
SourceEndPosition = processor.GetSourcePosition(slice.Start - 1),
Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};

View File

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines
@@ -106,8 +107,7 @@ namespace Markdig.Parsers.Inlines
{
Delimiter = match,
Content = builder.ToString(),
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
SourceEndPosition = processor.GetSourcePosition(slice.Start - 1),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};

View File

@@ -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)
@@ -185,8 +185,7 @@ namespace Markdig.Parsers.Inlines
{
DelimiterCount = delimiterCount,
Type = delimiterType,
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
SourceEndPosition = processor.GetSourcePosition(slice.Start - 1),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Column = column,
Line = line,
};
@@ -226,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;
@@ -254,14 +259,14 @@ namespace Markdig.Parsers.Inlines
var closeDelimitercount = closeDelimiter.DelimiterCount;
var delimiterDelta = isStrong ? 2 : 1;
emphasis.SourceStartPosition = openDelimiter.SourceStartPosition;
emphasis.Span.Start = openDelimiter.Span.Start;
emphasis.Line = openDelimiter.Line;
emphasis.Column = openDelimiter.Column;
emphasis.SourceEndPosition = closeDelimiter.SourceEndPosition - closeDelimitercount + delimiterDelta;
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
openDelimiter.SourceStartPosition += delimiterDelta;
openDelimiter.Span.Start += delimiterDelta;
openDelimiter.Column += delimiterDelta;
closeDelimiter.SourceStartPosition += delimiterDelta;
closeDelimiter.Span.Start += delimiterDelta;
closeDelimiter.Column += delimiterDelta;
openDelimiter.DelimiterCount -= delimiterDelta;
@@ -301,8 +306,7 @@ namespace Markdig.Parsers.Inlines
{
Content = new StringSlice(literalDelimiter.ToLiteral()),
IsClosed = true,
SourceStartPosition = literalDelimiter.SourceStartPosition,
SourceEndPosition = literalDelimiter.SourceEndPosition,
Span = literalDelimiter.Span,
Line = literalDelimiter.Line,
Column = literalDelimiter.Column
};
@@ -355,8 +359,7 @@ namespace Markdig.Parsers.Inlines
{
Content = new StringSlice(closeDelimiter.ToLiteral()),
IsClosed = true,
SourceStartPosition = closeDelimiter.SourceStartPosition,
SourceEndPosition = closeDelimiter.SourceEndPosition,
Span = closeDelimiter.Span,
Line = closeDelimiter.Line,
Column = closeDelimiter.Column
};
@@ -383,8 +386,7 @@ namespace Markdig.Parsers.Inlines
{
Content = new StringSlice(delimiter.ToLiteral()),
IsClosed = true,
SourceStartPosition = delimiter.SourceStartPosition,
SourceEndPosition = delimiter.SourceEndPosition,
Span = delimiter.Span,
Line = delimiter.Line,
Column = delimiter.Column
};

View File

@@ -29,11 +29,11 @@ namespace Markdig.Parsers.Inlines
processor.Inline = new LiteralInline()
{
Content = new StringSlice(new string(c, 1)),
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column
};
processor.Inline.SourceEndPosition = processor.Inline.SourceStartPosition + 1;
processor.Inline.Span.End = processor.Inline.Span.Start + 1;
slice.NextChar();
return true;
}
@@ -44,11 +44,11 @@ namespace Markdig.Parsers.Inlines
processor.Inline = new LineBreakInline()
{
IsHard = true,
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column
};
processor.Inline.SourceEndPosition = processor.Inline.SourceStartPosition + 1;
processor.Inline.Span.End = processor.Inline.Span.Start + 1;
slice.NextChar();
return true;
}

View File

@@ -2,6 +2,7 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines
@@ -51,8 +52,7 @@ namespace Markdig.Parsers.Inlines
{
Original = matched,
Transcoded = new StringSlice(literal),
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
SourceEndPosition = processor.GetSourcePosition(matched.End + 1),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End + 1)),
Line = line,
Column = column
};

View File

@@ -42,12 +42,12 @@ namespace Markdig.Parsers.Inlines
int column;
processor.Inline = new LineBreakInline
{
SourceStartPosition = processor.GetSourcePosition(startPosition, out line, out column),
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column)},
IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore),
Line = line,
Column = column
};
processor.Inline.SourceEndPosition = processor.Inline.SourceStartPosition;
processor.Inline.Span.End = processor.Inline.Span.Start;
return true;
}
}

View File

@@ -51,8 +51,9 @@ namespace Markdig.Parsers.Inlines
var saved = slice;
string label;
SourceSpan labelSpan;
// If the label is followed by either a ( or a [, this is not a shortcut
if (LinkHelper.TryParseLabel(ref slice, out label))
if (LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
{
if (!processor.Document.ContainsLinkReferenceDefinition(label))
{
@@ -67,9 +68,9 @@ namespace Markdig.Parsers.Inlines
{
Type = DelimiterType.Open,
Label = label,
LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan),
IsImage = isImage,
SourceStartPosition = startPosition,
SourceEndPosition = processor.GetSourcePosition(slice.Start - 1),
Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
@@ -94,7 +95,7 @@ namespace Markdig.Parsers.Inlines
return false;
}
private bool ProcessLinkReference(InlineProcessor state, string label, LinkDelimiterInline parent, int endPosition)
private bool ProcessLinkReference(InlineProcessor state, string label, SourceSpan labelSpan, LinkDelimiterInline parent, int endPosition)
{
bool isValidLink = false;
LinkReferenceDefinition linkRef;
@@ -115,9 +116,11 @@ namespace Markdig.Parsers.Inlines
{
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
Label = label,
LabelSpan = labelSpan,
IsImage = parent.IsImage,
SourceStartPosition = parent.SourceStartPosition,
SourceEndPosition = endPosition,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
Line = parent.Line,
Column = parent.Column,
};
@@ -134,8 +137,7 @@ namespace Markdig.Parsers.Inlines
Content = new StringSlice(label),
IsClosed = true,
// Not exact but we leave it like this
SourceStartPosition = parent.SourceStartPosition,
SourceEndPosition = parent.SourceEndPosition,
Span = parent.Span,
Line = parent.Line,
Column = parent.Column,
};
@@ -157,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;
@@ -194,8 +196,7 @@ namespace Markdig.Parsers.Inlines
inlineState.Inline = new LiteralInline()
{
Content = new StringSlice("["),
SourceStartPosition = openParent.SourceStartPosition,
SourceEndPosition = openParent.SourceStartPosition,
Span = openParent.Span,
Line = openParent.Line,
Column = openParent.Column,
};
@@ -214,7 +215,9 @@ namespace Markdig.Parsers.Inlines
case '(':
string url;
string title;
if (LinkHelper.TryParseInlineLink(ref text, out url, out title))
SourceSpan linkSpan;
SourceSpan titleSpan;
if (LinkHelper.TryParseInlineLink(ref text, out url, out title, out linkSpan, out titleSpan))
{
// Inline Link
var link = new LinkInline()
@@ -222,8 +225,10 @@ namespace Markdig.Parsers.Inlines
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
IsImage = openParent.IsImage,
SourceStartPosition = openParent.SourceStartPosition,
SourceEndPosition = inlineState.GetSourcePosition(text.Start -1),
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start -1)),
Line = openParent.Line,
Column = openParent.Column,
};
@@ -233,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.
@@ -250,13 +255,17 @@ namespace Markdig.Parsers.Inlines
break;
default:
var labelSpan = SourceSpan.Empty;
string label = null;
bool isLabelSpanLocal = true;
// Handle Collapsed links
if (text.CurrentChar == '[')
{
if (text.PeekChar(1) == ']')
{
label = openParent.Label;
labelSpan = openParent.LabelSpan;
isLabelSpanLocal = false;
text.NextChar(); // Skip [
text.NextChar(); // Skip ]
}
@@ -266,9 +275,14 @@ namespace Markdig.Parsers.Inlines
label = openParent.Label;
}
if (label != null || LinkHelper.TryParseLabel(ref text, true, out label))
if (label != null || LinkHelper.TryParseLabel(ref text, true, out label, out labelSpan))
{
if (ProcessLinkReference(inlineState, label, openParent, inlineState.GetSourcePosition(text.Start - 1)))
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
}
if (ProcessLinkReference(inlineState, label, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1)))
{
// Remove the open parent
openParent.Remove();
@@ -292,6 +306,7 @@ namespace Markdig.Parsers.Inlines
var literal = new LiteralInline()
{
Span = openParent.Span,
Content = new StringSlice(openParent.IsImage ? "![" : "[")
};

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines
@@ -64,14 +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,
SourceStartPosition = startPosition,
SourceEndPosition = 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;

View File

@@ -153,7 +153,7 @@ namespace Markdig.Parsers
}
// Update list-item source end position
listItem.SourceEndPosition = state.Line.End;
listItem.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}
@@ -170,11 +170,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.SourceEndPosition = state.Line.End;
listItem.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}
@@ -258,8 +258,7 @@ namespace Markdig.Parsers
{
Column = initColumn,
ColumnWidth = columnWidth,
SourceStartPosition = sourcePosition,
SourceEndPosition = sourceEndPosition
Span = new SourceSpan(sourcePosition, sourceEndPosition)
};
state.NewBlocks.Push(newListItem);
@@ -286,8 +285,7 @@ namespace Markdig.Parsers
var newList = new ListBlock(this)
{
Column = initColumn,
SourceStartPosition = sourcePosition,
SourceEndPosition = sourceEndPosition,
Span = new SourceSpan(sourcePosition, sourceEndPosition),
IsOrdered = isOrdered,
BulletType = listInfo.BulletType,
OrderedDelimiter = listInfo.OrderedDelimiter,
@@ -354,11 +352,11 @@ namespace Markdig.Parsers
isLastListItem = false;
}
// Update end-position for the list
if (listBlock.Count > 0)
{
listBlock.SourceEndPosition = listBlock[listBlock.Count - 1].SourceEndPosition;
}
//// Update end-position for the list
//if (listBlock.Count > 0)
//{
// listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
//}
return true;
}

View File

@@ -23,8 +23,7 @@ namespace Markdig.Parsers
processor.NewBlocks.Push(new ParagraphBlock(this)
{
Column = processor.Column,
SourceStartPosition = processor.Line.Start,
SourceEndPosition = processor.Line.End
Span = new SourceSpan(processor.Line.Start, processor.Line.End)
});
return BlockState.Continue;
}
@@ -41,7 +40,7 @@ namespace Markdig.Parsers
return TryParseSetexHeading(processor, block);
}
block.SourceEndPosition = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}
@@ -128,8 +127,7 @@ namespace Markdig.Parsers
var heading = new HeadingBlock(this)
{
Column = paragraph.Column,
SourceStartPosition = paragraph.SourceStartPosition,
SourceEndPosition = line.Start,
Span = new SourceSpan(paragraph.Span.Start, line.Start),
Level = level,
Lines = paragraph.Lines,
};
@@ -141,7 +139,7 @@ namespace Markdig.Parsers
return BlockState.BreakDiscard;
}
block.SourceEndPosition = state.Line.End;
block.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}

View File

@@ -42,8 +42,7 @@ namespace Markdig.Parsers
{
QuoteChar = quoteChar,
Column = column,
SourceStartPosition = sourcePosition,
SourceEndPosition = processor.Line.End,
Span = new SourceSpan(sourcePosition, processor.Line.End),
});
return BlockState.Continue;
}
@@ -62,7 +61,6 @@ namespace Markdig.Parsers
var c = processor.CurrentChar;
if (c != quote.QuoteChar)
{
block.SourceEndPosition = processor.Start - 1;
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
}
@@ -72,18 +70,8 @@ namespace Markdig.Parsers
processor.NextChar(); // Skip following space
}
block.SourceEndPosition = 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.SourceEndPosition = quoteBlock.LastChild.SourceEndPosition;
}
return true;
}
}
}

View File

@@ -88,8 +88,7 @@ namespace Markdig.Parsers
processor.NewBlocks.Push(new ThematicBreakBlock(this)
{
Column = processor.Column,
SourceStartPosition = startPosition,
SourceEndPosition = line.End
Span = new SourceSpan(startPosition, line.End)
});
return BlockState.BreakDiscard;
}

View File

@@ -25,6 +25,6 @@ namespace Markdig
{
public static partial class Markdown
{
public const string Version = "0.5.1";
public const string Version = "0.7.0";
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}
}

View 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;
}
}
}

View File

@@ -69,10 +69,7 @@ namespace Markdig.Syntax
children[Count++] = item;
item.Parent = this;
if (item.SourceEndPosition > SourceEndPosition)
{
SourceEndPosition = item.SourceEndPosition;
}
UpdateSpanEnd(item.Span.End);
}
private void EnsureCapacity(int min)

View File

@@ -25,6 +25,11 @@ namespace Markdig.Syntax.Inlines
/// </summary>
public string Label { get; set; }
/// <summary>
/// The label span
/// </summary>
public SourceSpan LabelSpan;
public override string ToLiteral()
{
return IsImage ? "![" : "[";

View File

@@ -54,9 +54,34 @@ namespace Markdig.Syntax.Inlines
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the label.
/// </summary>
public string Label { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is an image link.
/// </summary>
public bool IsImage { get; set; }
/// <summary>
/// Gets or sets the reference this link is attached to. May be null.
/// </summary>
public LinkReferenceDefinition Reference { get; set; }
/// <summary>
/// The URL source span.
/// </summary>
public SourceSpan? UrlSpan;
/// <summary>
/// The title source span.
/// </summary>
public SourceSpan? TitleSpan;
/// <summary>
/// The label span
/// </summary>
public SourceSpan? LabelSpan;
}
}

View File

@@ -11,56 +11,46 @@ namespace Markdig.Syntax
/// </summary>
public static class LinkReferenceDefinitionExtensions
{
private static readonly object DocumentKey = typeof(LinkReferenceDefinition);
private static readonly object DocumentKey = typeof(LinkReferenceDefinitionGroup);
public static bool ContainsLinkReferenceDefinition(this MarkdownDocument document, string label)
{
if (label == null) throw new ArgumentNullException(nameof(label));
var references = document.GetData(DocumentKey) as Dictionary<string, LinkReferenceDefinition>;
var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup;
if (references == null)
{
return false;
}
return references.ContainsKey(label);
return references.Links.ContainsKey(label);
}
public static void SetLinkReferenceDefinition(this MarkdownDocument document, string label, LinkReferenceDefinition linkReferenceDefinition)
{
if (label == null) throw new ArgumentNullException(nameof(label));
var references = document.GetLinkReferenceDefinitions();
references[label] = linkReferenceDefinition;
}
public static bool RemoveLinkReferenceDefinition(this MarkdownDocument document, string label)
{
if (label == null) throw new ArgumentNullException(nameof(label));
var references = document.GetData(DocumentKey) as Dictionary<string, LinkReferenceDefinition>;
if (references == null)
{
return false;
}
return references.Remove(label);
references.Set(label, linkReferenceDefinition);
}
public static bool TryGetLinkReferenceDefinition(this MarkdownDocument document, string label, out LinkReferenceDefinition linkReferenceDefinition)
{
if (label == null) throw new ArgumentNullException(nameof(label));
linkReferenceDefinition = null;
var references = document.GetData(DocumentKey) as Dictionary<string, LinkReferenceDefinition>;
var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup;
if (references == null)
{
return false;
}
return references.TryGetValue(label, out linkReferenceDefinition);
return references.TryGet(label, out linkReferenceDefinition);
}
public static Dictionary<string, LinkReferenceDefinition> GetLinkReferenceDefinitions(this MarkdownDocument document)
public static LinkReferenceDefinitionGroup GetLinkReferenceDefinitions(this MarkdownDocument document)
{
var references = document.GetData(DocumentKey) as Dictionary<string, LinkReferenceDefinition>;
var references = document.GetData(DocumentKey) as LinkReferenceDefinitionGroup;
if (references == null)
{
references = new Dictionary<string, LinkReferenceDefinition>(StringComparer.OrdinalIgnoreCase);
references = new LinkReferenceDefinitionGroup();
document.SetData(DocumentKey, references);
document.Add(references);
}
return references;
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
namespace Markdig.Syntax
{
/// <summary>
/// Contains all the <see cref="LinkReferenceDefinition"/> found in a document.
/// </summary>
/// <seealso cref="Markdig.Syntax.ContainerBlock" />
public class LinkReferenceDefinitionGroup : ContainerBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="LinkReferenceDefinitionGroup"/> class.
/// </summary>
public LinkReferenceDefinitionGroup() : base(null)
{
Links = new Dictionary<string, LinkReferenceDefinition>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Gets an association between a label and the corresponding <see cref="LinkReferenceDefinition"/>
/// </summary>
public Dictionary<string, LinkReferenceDefinition> Links { get; }
public void Set(string label, LinkReferenceDefinition link)
{
if (link == null) throw new ArgumentNullException(nameof(link));
Links[label] = link;
if (!Contains(link))
{
Add(link);
}
}
public bool TryGet(string label, out LinkReferenceDefinition link)
{
return Links.TryGetValue(label, out link);
}
}
}

View File

@@ -10,6 +10,11 @@ namespace Markdig.Syntax
/// </summary>
public abstract class MarkdownObject : IMarkdownObject
{
protected MarkdownObject()
{
Span = SourceSpan.Empty;
}
/// <summary>
/// The attached datas. Use internally a simple array instead of a Dictionary{Object,Object}
/// as we expect less than 5~10 entries, usually typically 1 (HtmlAttributes)
@@ -29,21 +34,18 @@ namespace Markdig.Syntax
public int Line { get; set; }
/// <summary>
/// Gets or sets the starting character position from the original text source.
/// Note that for inline elements, this is only valid if <see cref="MarkdownExtensions.UsePreciseSourceLocation"/> is setup on the pipeline.
/// The source span
/// </summary>
public int SourceStartPosition { get; set; }
public SourceSpan Span;
/// <summary>
/// Gets or sets the ending character position from the original text source.
/// Note that for inline elements, this is only valid if <see cref="MarkdownExtensions.UsePreciseSourceLocation"/> is setup on the pipeline.
/// Gets a string of the location in the text.
/// </summary>
public int SourceEndPosition { get; set; }
/// <summary>
/// Gets the character length of this element within the original source code.
/// </summary>
public int SourceLength => SourceEndPosition - SourceStartPosition + 1;
/// <returns></returns>
public string ToPositionText()
{
return $"${Line}, {Column}, {Span.Start}-{Span.End}";
}
/// <summary>
/// Stores a key/value pair for this instance.

View File

@@ -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)
{

View File

@@ -0,0 +1,80 @@
// 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;
namespace Markdig.Syntax
{
/// <summary>
/// A span of text.
/// </summary>
public struct SourceSpan : IEquatable<SourceSpan>
{
public static readonly SourceSpan Empty = new SourceSpan(0, -1);
/// <summary>
/// Initializes a new instance of the <see cref="SourceSpan"/> struct.
/// </summary>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
public SourceSpan(int start, int end)
{
Start = start;
End = end;
}
/// <summary>
/// Gets or sets the starting character position from the original text source.
/// Note that for inline elements, this is only valid if <see cref="MarkdownExtensions.UsePreciseSourceLocation"/> is setup on the pipeline.
/// </summary>
public int Start { get; set; }
/// <summary>
/// Gets or sets the ending character position from the original text source.
/// Note that for inline elements, this is only valid if <see cref="MarkdownExtensions.UsePreciseSourceLocation"/> is setup on the pipeline.
/// </summary>
public int End { get; set; }
/// <summary>
/// Gets the character length of this element within the original source code.
/// </summary>
public int Length => End - Start + 1;
public bool IsEmpty => Start > End;
public bool Equals(SourceSpan other)
{
return Start == other.Start && End == other.End;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is SourceSpan && Equals((SourceSpan) obj);
}
public override int GetHashCode()
{
unchecked
{
return (Start*397) ^ End;
}
}
public static bool operator ==(SourceSpan left, SourceSpan right)
{
return left.Equals(right);
}
public static bool operator !=(SourceSpan left, SourceSpan right)
{
return !left.Equals(right);
}
public override string ToString()
{
return $"{Start}-{End}";
}
}
}

View File

@@ -1,8 +1,8 @@
{
"title": "Markdig",
"version": "0.5.1",
"version": "0.7.0",
"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": "Add 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.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\n> 0.6.2\n- Handle latest CommonMark specs for corner cases for emphasis (See https://talk.commonmark.org/t/emphasis-strong-emphasis-corner-cases/2123/1 )\n> 0.6.1:\n- Fix issue with autoidentifier extension overriding manual HTML attributes id on headings\n> 0.6.0\n- Fix conflicts between PipeTables and SmartyPants extensions\n- Add SelfPipeline extension\n",
"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"
}
}
}