Compare commits

...

61 Commits

Author SHA1 Message Date
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
Alexandre Mutel
c31cb6da27 Bump version to 0.5.1 2016-06-16 06:40:22 +09:00
Alexandre Mutel
5503929d15 Add support for task lists (issue #7) 2016-06-16 06:39:20 +09:00
Alexandre Mutel
ca32dda1fe Bump version to 0.5.0 2016-06-15 21:15:03 +09:00
Alexandre Mutel
12111e0b63 Update the readme with latest version 2016-06-15 21:13:19 +09:00
Alexandre Mutel
bdd46c0fc0 Merge branch 'precise_position' 2016-06-15 21:12:32 +09:00
Alexandre Mutel
daf4c8fe86 Activate only calculation for precise location when PreciseSourceLocation is setup on the pipeline 2016-06-15 21:11:34 +09:00
Alexandre Mutel
bd2c2aff9c Add credits to CommonMark.NET 2016-06-15 18:19:26 +09:00
Alexandre Mutel
921f75e1f3 Add precise position for pipetables 2016-06-15 17:02:50 +09:00
Alexandre Mutel
44a3b85f0b Add precise position for smarty pants 2016-06-15 16:36:43 +09:00
Alexandre Mutel
f9e827395b Add precise position for mathematics extension 2016-06-15 16:19:20 +09:00
Alexandre Mutel
64a9a80774 Add test precise position for HtmlAttributes 2016-06-15 16:08:46 +09:00
Alexandre Mutel
e10594391d Add test source position for footers 2016-06-15 15:46:41 +09:00
Alexandre Mutel
60eb03a221 Add test precise position for figure captions 2016-06-15 15:37:43 +09:00
Alexandre Mutel
c0a0f10af0 Add test precise location for figures 2016-06-15 15:27:08 +09:00
Alexandre Mutel
56e1ed0e25 Add test precise location for emphasis extras 2016-06-15 15:26:47 +09:00
Alexandre Mutel
a9f33cbca6 Add test precise position for emojis 2016-06-15 15:26:31 +09:00
Alexandre Mutel
6f1d39e1bb Add precise position for definition lists 2016-06-15 15:01:41 +09:00
Alexandre Mutel
838f7c5598 Fix eol for TestSourcePosition for abbreviations 2016-06-15 14:04:59 +09:00
Alexandre Mutel
0f54cc5927 Add support for precise positions for abbreviations 2016-06-15 13:54:37 +09:00
Alexandre Mutel
12745f70cf Add test position for HtmlEntity inline 2016-06-15 12:11:31 +09:00
Alexandre Mutel
442737767f Add test for escape inline 2016-06-15 12:03:03 +09:00
Alexandre Mutel
61ac46e467 Add test for ListBlock 2016-06-15 12:02:52 +09:00
Alexandre Mutel
85550580d5 Add tests for QuoteBlock position 2016-06-15 12:02:35 +09:00
Alexandre Mutel
ed69ac5fe0 Add test for thematic break position 2016-06-15 11:25:01 +09:00
Alexandre Mutel
0aec5a5783 Improve html block test 2016-06-15 11:20:48 +09:00
Alexandre Mutel
c1885fe31b Add test for HtmlBlock position 2016-06-15 11:16:57 +09:00
Alexandre Mutel
c2270a2b3a Add test for code span, link, html inline, autolink, fenced code block 2016-06-15 11:16:31 +09:00
Alexandre Mutel
3e60515bb3 Fix offset for lines within a StringLineGroup 2016-06-15 11:15:53 +09:00
Alexandre Mutel
8550c13688 Add precise position for heading 2016-06-15 10:05:08 +09:00
Alexandre Mutel
3aa65694aa Improve InlineProcessor.GetSourcePosition 2016-06-15 00:33:06 +09:00
Alexandre Mutel
52403687db Improve handling of position for core elements. Add Line and Column for inline elements. 2016-06-15 00:22:28 +09:00
Alexandre Mutel
3e83409cf4 Cleanup code with source position for BlockProcessor 2016-06-14 21:18:16 +09:00
Alexandre Mutel
9b051955bd Switch to string only parsing instead of TextReader to avoid allocations of line. Use StringSlice instead on the whole input for each line. 2016-06-14 21:00:18 +09:00
Alexandre Mutel
35c8126add Add MarkdownObject.SourceStartPosition and SourceEndPosition and start to fill this for all core syntax 2016-06-14 17:05:49 +09:00
Alexandre Mutel
67e1c8ce7f Rename images to img 2016-06-13 22:44:04 +09:00
86 changed files with 2755 additions and 402 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

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

106
readme.md
View File

@@ -1,12 +1,12 @@
# Markdig [![Build status](https://ci.appveyor.com/api/projects/status/hk391x8jcskxt1u8?svg=true)](https://ci.appveyor.com/project/xoofx/markdig) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/)
<img align="right" width="160px" height="160px" src="images/markdig.png">
<img align="right" width="160px" height="160px" src="img/markdig.png">
Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
You can **try Markdig online** and compare it to other implems on [babelmark3](http://babelmark.github.io/)
You can **try Markdig online** and compare it to other implementations on [babelmark3](https://babelmark.github.io/?text=Hello+**Markdig**!)
## Features
@@ -18,7 +18,7 @@ You can **try Markdig online** and compare it to other implems on [babelmark3](h
- 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 +32,7 @@ You can **try Markdig online** and compare it to other implems on [babelmark3](h
- **Definition lists** (inspired from [PHP Markdown Extra - Definitions Lists](https://michelf.ca/projects/php-markdown/extra/#def-list))
- **Footnotes** (inspired from [PHP Markdown Extra - Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes))
- **Auto-identifiers** for headings (similar to [Pandoc - Auto Identifiers](http://pandoc.org/README.html#extension-auto_identifiers))
- **Task Lists** inspired from [Github Task lists](https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments).
- **Extra bullet lists**, supporting alpha bullet `a.` `b.` and roman bullet (`i`, `ii`...etc.)
- **Media support** for media url (youtube, vimeo, mp4...etc.) (inspired from this [CommonMark discussion](https://talk.commonmark.org/t/embedded-audio-and-video/441))
- **Abbreviations** (inspired from [PHP Markdown Extra - Abbreviations](https://michelf.ca/projects/php-markdown/extra/#abbr))
@@ -46,6 +47,13 @@ You can **try Markdig online** and compare it to other implems on [babelmark3](h
- **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,24 +88,44 @@ This software is released under the [BSD-Clause 2 license](https://github.com/lu
This is an early preview of the benchmarking against various implementations:
- Markdig: itself
- CommonMarkCpp: [cmark](https://github.com/jgm/cmark), Reference C implementation of CommonMark, no support for extensions
- [CommonMark.NET](https://github.com/Knagis/CommonMark.NET): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMarkNet (devel)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Moonshine](https://github.com/brandonc/moonshine): popular C Markdown processor
**C implementations**:
Markdig is roughly x100 times faster than MarkdownSharp and extremelly competitive to other implems (that are not feature wise comparable)
- [cmark](https://github.com/jgm/cmark) (version: 0.25.0): Reference C implementation of CommonMark, no support for extensions
- [Moonshine](https://github.com/brandonc/moonshine) (version: : popular C Markdown processor
Performance in x86:
**.NET implementations**:
- [Markdig](https://github.com/lunet-io/markdig) (version: 0.5.x): itself
- [CommonMark.NET(master)](https://github.com/Knagis/CommonMark.NET) (version: 0.11.0): CommonMark implementation for .NET, no support for extensions, port of cmark
- [CommonMark.NET(pipe_tables)](https://github.com/AMDL/CommonMark.NET/tree/pipe-tables): An evolution of CommonMark.NET, supports extensions, not released yet
- [MarkdownDeep](https://github.com/toptensoftware/markdowndeep) (version: 1.5.0): another .NET implementation
- [MarkdownSharp](https://github.com/Kiri-rin/markdownsharp) (version: 1.13.0): Open source C# implementation of Markdown processor, as featured on Stack Overflow, regexp based.
- [Marked.NET](https://github.com/T-Alex/MarkedNet) (version: 1.0.5) port of original [marked.js](https://github.com/chjj/marked) project
- [Microsoft.DocAsCode.MarkdownLite](https://github.com/dotnet/docfx/tree/dev/src/Microsoft.DocAsCode.MarkdownLite) (version: 2.0.1) used by the [docfx](https://github.com/dotnet/docfx) project
**JavaScript/V8 implementations**:
- [Strike.V8](https://github.com/SimonCropp/Strike) (version: 1.5.0) [marked.js](https://github.com/chjj/marked) running in Google V8 (not .NET based)
### Analysis of the results:
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
- **Among the best in CPU**, 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:
![BenchMark CPU Time](img/BenchmarkCPU.png)
![BenchMark Memory](img/BenchmarkMemory.png)
### Performance for x86:
```
// * Summary *
BenchmarkDotNet-Dev=v0.9.6.0+
BenchmarkDotNet-Dev=v0.9.7.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
Processor=Intel(R) Core(TM) i7-4770 CPU 3.40GHz, ProcessorCount=8
Frequency=3319351 ticks, Resolution=301.2637 ns, Timer=TSC
HostCLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
JitModules=clrjit-v4.6.1080.0
@@ -105,26 +133,23 @@ JitModules=clrjit-v4.6.1080.0
Type=Program Mode=SingleRun LaunchCount=2
WarmupCount=2 TargetCount=10
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
--------------------- |---------- |---------- |------- |------ |------- |------------------- |
TestMarkdig | 5.4870 ms | 0.0158 ms | 193.00 | 12.00 | 84.00 | 1,425,192.72 |
TestCommonMarkCpp | 4.0134 ms | 0.1008 ms | - | - | 180.00 | 454,859.74 |
TestCommonMarkNet | 4.6139 ms | 0.0581 ms | 193.00 | 12.00 | 84.00 | 1,406,367.27 |
TestCommonMarkNetNew | 5.5327 ms | 0.0461 ms | 193.00 | 96.00 | 84.00 | 1,738,465.42 |
TestMarkdownDeep | 7.5910 ms | 0.1006 ms | 205.00 | 96.00 | 84.00 | 1,758,383.79 |
TestMoonshine | 5.8843 ms | 0.1758 ms | - | - | 215.00 | 565,000.73 |
// * Diagnostic Output - MemoryDiagnoser *
// ***** BenchmarkRunner: End *****
Method | Median | StdDev |Scaled | Gen 0 | Gen 1| Gen 2|Bytes Allocated/Op |
--------------------------- |------------ |---------- |------ | ------ |------|---------|------------------ |
Markdig | 5.5316 ms | 0.0372 ms | 0.71 | 56.00| 21.00| 49.00| 1,285,917.31 |
CommonMark.NET(master) | 4.7035 ms | 0.0422 ms | 0.60 | 113.00| 7.00| 49.00| 1,502,404.60 |
CommonMark.NET(pipe_tables) | 5.6164 ms | 0.0298 ms | 0.72 | 111.00| 56.00| 49.00| 1,863,128.13 |
MarkdownDeep | 7.8193 ms | 0.0334 ms | 1.00 | 120.00| 56.00| 49.00| 1,884,854.85 |
cmark | 4.2698 ms | 0.1526 ms | 0.55 | -| -| -| NA |
Moonshine | 6.0929 ms | 0.1053 ms | 1.28 | -| -| -| NA |
Strike.V8 | 10.5895 ms | 0.0492 ms | 1.35 | -| -| -| NA |
Marked.NET | 207.3169 ms | 5.2628 ms | 26.51 | 0.00| 0.00| 0.00| 303,125,228.65 |
MarkdownSharp | 675.0185 ms | 2.8447 ms | 86.32 | 40.00| 27.00| 41.00| 2,413,394.17 |
Microsoft DocfxMarkdownLite | 166.3357 ms | 0.4529 ms | 21.27 |4,452.00|948.00|11,167.00| 180,218,359.60 |
```
Performance for x64:
### Performance for x64:
```
// * Summary *
BenchmarkDotNet-Dev=v0.9.6.0+
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz, ProcessorCount=8
@@ -137,15 +162,10 @@ WarmupCount=2 TargetCount=10
Method | Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |
--------------------- |---------- |---------- |------- |------- |------ |------------------- |
TestMarkdig | 5.9539 ms | 0.0495 ms | 157.00 | 96.00 | 84.00 | 1,767,834.52 |
TestCommonMarkNet | 4.3158 ms | 0.0161 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
TestCommonMarkNetNew | 5.3421 ms | 0.0435 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
TestMarkdownDeep | 7.4750 ms | 0.0281 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
// * Diagnostic Output - MemoryDiagnoser *
// ***** BenchmarkRunner: End *****
TestMarkdig | 5.5276 ms | 0.0402 ms | 109.00 | 96.00 | 84.00 | 1,537,027.66 |
TestCommonMarkNet | 4.4661 ms | 0.1190 ms | 157.00 | 96.00 | 84.00 | 1,747,432.06 |
TestCommonMarkNetNew | 5.3151 ms | 0.0815 ms | 229.00 | 168.00 | 84.00 | 2,323,922.97 |
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
```
## Credits
@@ -154,7 +174,9 @@ Thanks to the fantastic work done by [John Mac Farlane](http://johnmacfarlane.ne
This project would not have been possible without this huge foundation.
Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/BenchmarkDotNet) that makes benchmarking so easy to setup!
Thanks also to the project [BenchmarkDotNet](https://github.com/PerfDotNet/BenchmarkDotNet) that makes benchmarking so easy to setup!
Some decoding part (e.g HTML [EntityHelper.cs](https://github.com/lunet-io/markdig/blob/master/src/Markdig/Helpers/EntityHelper.cs)) have been re-used from [CommonMark.NET](https://github.com/Knagis/CommonMark.NET)
## Author

View File

@@ -60,7 +60,9 @@
<DependentUpon>Specs.tt</DependentUpon>
</Compile>
<Compile Include="TestHtmlHelper.cs" />
<Compile Include="TestLineReader.cs" />
<Compile Include="TestLinkHelper.cs" />
<Compile Include="TestSourcePosition.cs" />
<Compile Include="TestStringSliceList.cs" />
<Compile Include="TestPlayParser.cs" />
<Compile Include="TextAssert.cs" />
@@ -81,6 +83,7 @@
<None Include="Specs\GridTableSpecs.md" />
<None Include="Specs\HardlineBreakSpecs.md" />
<None Include="Specs\BootstrapSpecs.md" />
<None Include="Specs\TaskListSpecs.md" />
<None Include="Specs\SmartyPantsSpecs.md" />
<None Include="Specs\MediaSpecs.md" />
<None Include="Specs\MathSpecs.md" />

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

View File

@@ -16631,7 +16631,7 @@ namespace Markdig.Tests
TestParser.TestSpec(" a | b |\n-- | --\n| 0 | 1\n| 2 | 3 |\n 4 | 5 ", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n<tr>\n<td>2</td>\n<td>3</td>\n</tr>\n<tr>\n<td>4</td>\n<td>5</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
// Single column table can be declared with lines starting only by a column delimiter:
// A pipe may be present at both the beginning/ending of each line:
[TestFixture]
public partial class TestExtensionsPipeTable
{
@@ -16642,6 +16642,110 @@ namespace Markdig.Tests
// Section: Extensions Pipe Table
//
// The following CommonMark:
// |a|b|
// |-|-|
// |0|1|
//
// Should be rendered as:
// <table>
// <thead>
// <tr>
// <th>a</th>
// <th>b</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>0</td>
// <td>1</td>
// </tr>
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 10, "Extensions Pipe Table");
TestParser.TestSpec("|a|b|\n|-|-|\n|0|1|", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
// Or may be ommitted on one side:
[TestFixture]
public partial class TestExtensionsPipeTable
{
[Test]
public void Example011()
{
// Example 11
// Section: Extensions Pipe Table
//
// The following CommonMark:
// a|b|
// -|-|
// 0|1|
//
// Should be rendered as:
// <table>
// <thead>
// <tr>
// <th>a</th>
// <th>b</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>0</td>
// <td>1</td>
// </tr>
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 11, "Extensions Pipe Table");
TestParser.TestSpec("a|b|\n-|-|\n0|1|", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
[TestFixture]
public partial class TestExtensionsPipeTable
{
[Test]
public void Example012()
{
// Example 12
// Section: Extensions Pipe Table
//
// The following CommonMark:
// |a|b
// |-|-
// |0|1
//
// Should be rendered as:
// <table>
// <thead>
// <tr>
// <th>a</th>
// <th>b</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>0</td>
// <td>1</td>
// </tr>
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 12, "Extensions Pipe Table");
TestParser.TestSpec("|a|b\n|-|-\n|0|1", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
// Single column table can be declared with lines starting only by a column delimiter:
[TestFixture]
public partial class TestExtensionsPipeTable
{
[Test]
public void Example013()
{
// Example 13
// Section: Extensions Pipe Table
//
// The following CommonMark:
// | a
// | --
// | b
@@ -16664,7 +16768,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 10, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 13, "Extensions Pipe Table");
TestParser.TestSpec("| a\n| --\n| b\n| c ", "<table>\n<thead>\n<tr>\n<th>a</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>b</td>\n</tr>\n<tr>\n<td>c</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -16681,9 +16785,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example011()
public void Example014()
{
// Example 11
// Example 14
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16712,7 +16816,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 11, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 14, "Extensions Pipe Table");
TestParser.TestSpec(" a | b \n-------|-------\n 0 | 1 \n 2 | 3 ", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n<tr>\n<td>2</td>\n<td>3</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -16722,9 +16826,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example012()
public void Example015()
{
// Example 12
// Example 15
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16756,7 +16860,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 12, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 15, "Extensions Pipe Table");
TestParser.TestSpec(" a | b | c \n:------|:-------:| ----:\n 0 | 1 | 2 \n 3 | 4 | 5 ", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th style=\"text-align: center;\">b</th>\n<th style=\"text-align: right;\">c</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: right;\">2</td>\n</tr>\n<tr>\n<td>3</td>\n<td style=\"text-align: center;\">4</td>\n<td style=\"text-align: right;\">5</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -16765,9 +16869,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example013()
public void Example016()
{
// Example 13
// Example 16
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16782,7 +16886,7 @@ namespace Markdig.Tests
// 0 | 1
// 2 | 3</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 13, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 16, "Extensions Pipe Table");
TestParser.TestSpec(" a | b\n-------|---x---\n 0 | 1\n 2 | 3 ", "<p>a | b\n-------|---x---\n0 | 1\n2 | 3</p> ", "pipetables");
}
}
@@ -16793,9 +16897,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example014()
public void Example017()
{
// Example 14
// Example 17
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16824,7 +16928,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 14, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 17, "Extensions Pipe Table");
TestParser.TestSpec(" *a* | b\n----- |-----\n 0 | _1_\n _2 | 3* ", "<table>\n<thead>\n<tr>\n<th><em>a</em></th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td><em>1</em></td>\n</tr>\n<tr>\n<td>_2</td>\n<td>3*</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -16835,9 +16939,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example015()
public void Example018()
{
// Example 15
// Example 18
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16847,7 +16951,7 @@ namespace Markdig.Tests
// Should be rendered as:
// <p>a | b <code>0 |</code></p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 15, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 18, "Extensions Pipe Table");
TestParser.TestSpec("a | b `\n0 | ` ", "<p>a | b <code>0 |</code></p> ", "pipetables");
}
}
@@ -16858,9 +16962,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example016()
public void Example019()
{
// Example 16
// Example 19
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16884,7 +16988,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 16, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 19, "Extensions Pipe Table");
TestParser.TestSpec("a <a href=\"\" title=\"|\"></a> | b\n-- | --\n0 | 1", "<table>\n<thead>\n<tr>\n<th>a <a href=\"\" title=\"|\"></a></th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -16895,9 +16999,9 @@ namespace Markdig.Tests
public partial class TestExtensionsPipeTable
{
[Test]
public void Example017()
public void Example020()
{
// Example 17
// Example 20
// Section: Extensions Pipe Table
//
// The following CommonMark:
@@ -16921,7 +17025,7 @@ namespace Markdig.Tests
// </tbody>
// </table>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 17, "Extensions Pipe Table");
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 20, "Extensions Pipe Table");
TestParser.TestSpec("a | b\n-- | --\n[This is a link with a | inside the label](http://google.com) | 1", "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"http://google.com\">This is a link with a | inside the label</a></td>\n<td>1</td>\n</tr>\n</tbody>\n</table>", "pipetables");
}
}
@@ -19190,4 +19294,58 @@ namespace Markdig.Tests
TestParser.TestSpec("[With a new text][This is a heading]\n# This is a heading", "<p><a href=\"#this-is-a-heading\">With a new text</a></p>\n<h1 id=\"this-is-a-heading\">This is a heading</h1>", "autoidentifiers");
}
}
// # Extensions
//
// Adds support for task lists:
//
// ## TaskLists
//
// A task list item consist of `[ ]` or `[x]` or `[X]` inside a list item (ordered or unordered)
[TestFixture]
public partial class TestExtensionsTaskLists
{
[Test]
public void Example001()
{
// Example 1
// Section: Extensions TaskLists
//
// The following CommonMark:
// - [ ] Item1
// - [x] Item2
// - [ ] Item3
// - Item4
//
// Should be rendered as:
// <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>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 1, "Extensions TaskLists");
TestParser.TestSpec("- [ ] Item1\n- [x] Item2\n- [ ] Item3\n- Item4", "<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input disabled=\"disabled\" type=\"checkbox\" /> Item1</li>\n<li class=\"task-list-item\"><input disabled=\"disabled\" type=\"checkbox\" checked=\"checked\" /> Item2</li>\n<li class=\"task-list-item\"><input disabled=\"disabled\" type=\"checkbox\" /> Item3</li>\n<li>Item4</li>\n</ul>", "tasklists");
}
}
// A task is not recognized outside a list item:
[TestFixture]
public partial class TestExtensionsTaskLists
{
[Test]
public void Example002()
{
// Example 2
// Section: Extensions TaskLists
//
// The following CommonMark:
// [ ] This is not a task list
//
// Should be rendered as:
// <p>[ ] This is not a task list</p>
Console.WriteLine("Example {0}" + Environment.NewLine + "Section: {0}" + Environment.NewLine, 2, "Extensions TaskLists");
TestParser.TestSpec("[ ] This is not a task list", "<p>[ ] This is not a task list</p>", "tasklists");
}
}
}

View File

@@ -56,6 +56,7 @@ SOFTWARE.
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"),
};
var emptyLines = false;
var displayEmptyLines = false;

View File

@@ -0,0 +1,29 @@
# Extensions
Adds support for task lists:
## TaskLists
A task list item consist of `[ ]` or `[x]` or `[X]` inside a list item (ordered or unordered)
```````````````````````````````` example
- [ ] Item1
- [x] Item2
- [ ] Item3
- Item4
.
<ul class="contains-task-list">
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item1</li>
<li class="task-list-item"><input disabled="disabled" type="checkbox" checked="checked" /> Item2</li>
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item3</li>
<li>Item4</li>
</ul>
````````````````````````````````
A task is not recognized outside a list item:
```````````````````````````````` example
[ ] This is not a task list
.
<p>[ ] This is not a task list</p>
````````````````````````````````

View File

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

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]

View File

@@ -69,7 +69,7 @@ namespace Markdig.Tests
}
}
private static string DisplaySpaceAndTabs(string text)
public static string DisplaySpaceAndTabs(string text)
{
// Output special characters to check correctly the results
return text.Replace('\t', '→').Replace(' ', '·');

View File

@@ -0,0 +1,780 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
{
/// <summary>
/// Test the precise source location of all Markdown elements, including extensions
/// </summary>
[TestFixture]
public class TestSourcePosition
{
[Test]
public void TestParagraph()
{
Check("0123456789", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-9
");
}
[Test]
public void TestParagraphAndNewLine()
{
Check("0123456789\n0123456789", @"
paragraph ( 0, 0) 0-20
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
literal ( 1, 0) 11-20
");
Check("0123456789\r\n0123456789", @"
paragraph ( 0, 0) 0-21
literal ( 0, 0) 0-9
linebreak ( 0,10) 10-10
literal ( 1, 0) 12-21
");
}
[Test]
public void TestParagraphNewLineAndSpaces()
{
// 0123 45678
Check("012\n 345", @"
paragraph ( 0, 0) 0-8
literal ( 0, 0) 0-2
linebreak ( 0, 3) 3-3
literal ( 1, 2) 6-8
");
}
[Test]
public void TestParagraph2()
{
Check("0123456789\n\n0123456789", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-9
paragraph ( 2, 0) 12-21
literal ( 2, 0) 12-21
");
}
[Test]
public void TestEmphasis()
{
Check("012**3456789**", @"
paragraph ( 0, 0) 0-13
literal ( 0, 0) 0-2
emphasis ( 0, 3) 3-13
literal ( 0, 5) 5-11
");
}
[Test]
public void TestEmphasis2()
{
// 01234567
Check("01*2**3*", @"
paragraph ( 0, 0) 0-7
literal ( 0, 0) 0-1
emphasis ( 0, 2) 2-4
literal ( 0, 3) 3-3
emphasis ( 0, 5) 5-7
literal ( 0, 6) 6-6
");
}
[Test]
public void TestEmphasis3()
{
// 0123456789
Check("01**2***3*", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-1
emphasis ( 0, 2) 2-6
literal ( 0, 4) 4-4
emphasis ( 0, 7) 7-9
literal ( 0, 8) 8-8
");
}
[Test]
public void TestEmphasisFalse()
{
Check("0123456789**0123", @"
paragraph ( 0, 0) 0-15
literal ( 0, 0) 0-9
literal ( 0,10) 10-11
literal ( 0,12) 12-15
");
}
[Test]
public void TestHeading()
{
// 012345
Check("# 2345", @"
heading ( 0, 0) 0-5
literal ( 0, 2) 2-5
");
}
[Test]
public void TestHeadingWithEmphasis()
{
// 0123456789
Check("# 23**45**", @"
heading ( 0, 0) 0-9
literal ( 0, 2) 2-3
emphasis ( 0, 4) 4-9
literal ( 0, 6) 6-7
");
}
[Test]
public void TestCodeSpan()
{
// 012345678
Check("0123`456`", @"
paragraph ( 0, 0) 0-8
literal ( 0, 0) 0-3
code ( 0, 4) 4-8
");
}
[Test]
public void TestLink()
{
// 0123456789
Check("012[45](#)", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-2
link ( 0, 3) 3-9
literal ( 0, 4) 4-5
");
}
[Test]
public void TestLinkParts1()
{
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01 [234](/56)", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
}
[Test]
public void TestLinkParts2()
{
// 0 1
// 01 2 34567890123456789
var link = Markdown.Parse("0\n\n01 [234](/56 'yo')", new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build()).Descendants().OfType<LinkInline>().FirstOrDefault();
Assert.NotNull(link);
Assert.AreEqual(new SourceSpan(7, 9), link.LabelSpan);
Assert.AreEqual(new SourceSpan(12, 14), link.UrlSpan);
Assert.AreEqual(new SourceSpan(16, 19), link.TitleSpan);
}
[Test]
public void TestLinkParts3()
{
// 0 1
// 01 2 3456789012345
var link = Markdown.Parse("0\n\n01![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]
public void TestAutolinkInline()
{
// 0123456789ABCD
Check("01<http://yes>", @"
paragraph ( 0, 0) 0-13
literal ( 0, 0) 0-1
autolink ( 0, 2) 2-13
");
}
[Test]
public void TestFencedCodeBlock()
{
// 012 3456 78 9ABC
Check("01\n```\n3\n```\n", @"
paragraph ( 0, 0) 0-1
literal ( 0, 0) 0-1
fencedcode ( 1, 0) 3-11
");
}
[Test]
public void TestHtmlBlock()
{
// 012345 67 89ABCDE F 0
Check("<div>\n0\n</div>\n\n1", @"
html ( 0, 0) 0-13
paragraph ( 4, 0) 16-16
literal ( 4, 0) 16-16
");
}
[Test]
public void TestHtmlBlock1()
{
// 0 1
// 01 2 345678901 23
Check("0\n\n<!--A-->\n1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
html ( 2, 0) 3-10
paragraph ( 3, 0) 12-12
literal ( 3, 0) 12-12
");
}
[Test]
public void TestHtmlComment()
{
// 0 1 2
// 012345678901 234567890 1234
Check("# 012345678\n<!--0-->\n123\n", @"
heading ( 0, 0) 0-10
literal ( 0, 2) 2-10
html ( 1, 0) 12-19
paragraph ( 2, 0) 21-23
literal ( 2, 0) 21-23
");
}
[Test]
public void TestHtmlInline()
{
// 0123456789
Check("01<b>4</b>", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-1
html ( 0, 2) 2-4
literal ( 0, 5) 5-5
html ( 0, 6) 6-9
");
}
[Test]
public void TestHtmlInline1()
{
// 0
// 0123456789
Check("0<!--A-->1", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-0
html ( 0, 1) 1-8
literal ( 0, 9) 9-9
");
}
[Test]
public void TestThematicBreak()
{
// 0123 4567
Check("---\n---\n", @"
thematicbreak ( 0, 0) 0-2
thematicbreak ( 1, 0) 4-6
");
}
[Test]
public void TestQuoteBlock()
{
// 0123456
Check("> 2345\n", @"
quote ( 0, 0) 0-5
paragraph ( 0, 2) 2-5
literal ( 0, 2) 2-5
");
}
[Test]
public void TestQuoteBlockWithLines()
{
// 01234 56789A
Check("> 01\n> 23\n", @"
quote ( 0, 0) 0-9
paragraph ( 0, 2) 2-9
literal ( 0, 2) 2-3
linebreak ( 0, 4) 4-4
literal ( 1, 3) 8-9
");
}
[Test]
public void TestQuoteBlockWithLazyContinuation()
{
// 01234 56
Check("> 01\n23\n", @"
quote ( 0, 0) 0-6
paragraph ( 0, 2) 2-6
literal ( 0, 2) 2-3
linebreak ( 0, 4) 4-4
literal ( 1, 0) 5-6
");
}
[Test]
public void TestListBlock()
{
// 0123 4567
Check("- 0\n- 1\n", @"
list ( 0, 0) 0-6
listitem ( 0, 0) 0-2
paragraph ( 0, 2) 2-2
literal ( 0, 2) 2-2
listitem ( 1, 0) 4-6
paragraph ( 1, 2) 6-6
literal ( 1, 2) 6-6
");
}
[Test]
public void TestEscapeInline()
{
// 0123
Check(@"\-\)", @"
paragraph ( 0, 0) 0-3
literal ( 0, 0) 0-1
literal ( 0, 2) 2-3
");
}
[Test]
public void TestHtmlEntityInline()
{
// 01234567
Check("0&nbsp;1", @"
paragraph ( 0, 0) 0-7
literal ( 0, 0) 0-0
htmlentity ( 0, 1) 1-6
literal ( 0, 7) 7-7
");
}
[Test]
public void TestAbbreviations()
{
Check("*[HTML]: Hypertext Markup Language\r\n\r\nLater in a text we are using HTML and it becomes an abbr tag HTML", @"
paragraph ( 2, 0) 38-102
container ( 2, 0) 38-102
literal ( 2, 0) 38-66
abbreviation ( 2,29) 67-70
literal ( 2,33) 71-98
abbreviation ( 2,61) 99-102
", "abbreviations");
}
[Test]
public void TestCitation()
{
// 0123 4 567 8
Check("01 \"\"23\"\"", @"
paragraph ( 0, 0) 0-8
literal ( 0, 0) 0-2
emphasis ( 0, 3) 3-8
literal ( 0, 5) 5-6
", "citations");
}
[Test]
public void TestCustomContainer()
{
// 01 2345 678 9ABC DEF
Check("0\n:::\n23\n:::\n45\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
customcontainer ( 1, 0) 2-11
paragraph ( 2, 0) 6-7
literal ( 2, 0) 6-7
paragraph ( 4, 0) 13-14
literal ( 4, 0) 13-14
", "customcontainers");
}
[Test]
public void TestDefinitionList()
{
// 012 3456789A
Check("a0\n: 1234", @"
definitionlist ( 0, 0) 0-10
definitionitem ( 1, 0) 3-10
definitionterm ( 0, 0) 0-1
literal ( 0, 0) 0-1
paragraph ( 1, 4) 7-10
literal ( 1, 4) 7-10
", "definitionlists");
}
[Test]
public void TestDefinitionList2()
{
// 012 3456789AB CDEF01234
Check("a0\n: 1234\n: 5678", @"
definitionlist ( 0, 0) 0-20
definitionitem ( 1, 0) 3-10
definitionterm ( 0, 0) 0-1
literal ( 0, 0) 0-1
paragraph ( 1, 4) 7-10
literal ( 1, 4) 7-10
definitionitem ( 2, 4) 12-20
paragraph ( 2, 5) 17-20
literal ( 2, 5) 17-20
", "definitionlists");
}
[Test]
public void TestEmoji()
{
// 01 2345
Check("0\n :)\n", @"
paragraph ( 0, 0) 0-4
literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
emoji ( 1, 1) 3-4
", "emojis");
}
[Test]
public void TestEmphasisExtra()
{
// 0123456
Check("0 ~~1~~", @"
paragraph ( 0, 0) 0-6
literal ( 0, 0) 0-1
emphasis ( 0, 2) 2-6
literal ( 0, 4) 4-4
", "emphasisextras");
}
[Test]
public void TestFigures()
{
// 01 2345 67 89AB
Check("0\n^^^\n0\n^^^\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
figure ( 1, 0) 2-10
paragraph ( 2, 0) 6-6
literal ( 2, 0) 6-6
", "figures");
}
[Test]
public void TestFiguresCaption1()
{
// 01 234567 89 ABCD
Check("0\n^^^ab\n0\n^^^\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
figure ( 1, 0) 2-12
figurecaption ( 1, 3) 5-6
literal ( 1, 3) 5-6
paragraph ( 2, 0) 8-8
literal ( 2, 0) 8-8
", "figures");
}
[Test]
public void TestFiguresCaption2()
{
// 01 2345 67 89ABCD
Check("0\n^^^\n0\n^^^ab\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
figure ( 1, 0) 2-12
paragraph ( 2, 0) 6-6
literal ( 2, 0) 6-6
figurecaption ( 3, 3) 11-12
literal ( 3, 3) 11-12
", "figures");
}
[Test]
public void TestFooters()
{
// 01 234567 89ABCD
Check("0\n^^ 12\n^^ 34\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
footer ( 1, 0) 2-12
paragraph ( 1, 3) 5-12
literal ( 1, 3) 5-6
linebreak ( 1, 5) 7-7
literal ( 2, 3) 11-12
", "footers");
}
[Test]
public void TestAttributes()
{
// 0123456789
Check("0123{#456}", @"
paragraph ( 0, 0) 0-9
attributes ( 0, 4) 4-9
literal ( 0, 0) 0-3
", "attributes");
}
[Test]
public void TestAttributesForHeading()
{
// 0123456789ABC
Check("# 01 {#456}", @"
heading ( 0, 0) 0-4
attributes ( 0, 5) 5-10
literal ( 0, 2) 2-3
", "attributes");
}
[Test]
public void TestMathematicsInline()
{
// 01 23456789AB
Check("0\n012 $abcd$", @"
paragraph ( 0, 0) 0-11
literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
literal ( 1, 0) 2-5
math ( 1, 4) 6-11
attributes ( 0, 0) 0--1
", "mathematics");
}
[Test]
public void TestSmartyPants()
{
// 01234567
// 01 23456789
Check("0\n2 <<45>>", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
literal ( 1, 0) 2-3
smartypant ( 1, 2) 4-5
literal ( 1, 4) 6-7
smartypant ( 1, 6) 8-9
", "smartypants");
}
[Test]
public void TestSmartyPantsUnbalanced()
{
// 012345
// 01 234567
Check("0\n2 <<45", @"
paragraph ( 0, 0) 0-7
literal ( 0, 0) 0-0
linebreak ( 0, 1) 1-1
literal ( 1, 0) 2-3
literal ( 1, 2) 4-5
literal ( 1, 4) 6-7
", "smartypants");
}
[Test]
public void TestPipeTable()
{
// 0123 4567 89AB
Check("a|b\n-|-\n0|1\n", @"
table ( 0, 0) 0-10
tablerow ( 0, 0) 0-2
tablecell ( 0, 0) 0-0
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
tablecell ( 0, 2) 2-2
paragraph ( 0, 2) 2-2
literal ( 0, 2) 2-2
tablerow ( 2, 0) 8-10
tablecell ( 2, 0) 8-8
paragraph ( 2, 0) 8-8
literal ( 2, 0) 8-8
tablecell ( 2, 2) 10-10
paragraph ( 2, 2) 10-10
literal ( 2, 2) 10-10
", "pipetables");
}
[Test]
public void TestPipeTable2()
{
// 01 2 3456 789A BCD
Check("0\n\na|b\n-|-\n0|1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
table ( 2, 0) 3-13
tablerow ( 2, 0) 3-5
tablecell ( 2, 0) 3-3
paragraph ( 2, 0) 3-3
literal ( 2, 0) 3-3
tablecell ( 2, 2) 5-5
paragraph ( 2, 2) 5-5
literal ( 2, 2) 5-5
tablerow ( 4, 0) 11-13
tablecell ( 4, 0) 11-11
paragraph ( 4, 0) 11-11
literal ( 4, 0) 11-11
tablecell ( 4, 2) 13-13
paragraph ( 4, 2) 13-13
literal ( 4, 2) 13-13
", "pipetables");
}
[Test]
public void TestIndentedCode()
{
// 01 2 345678 9ABCDE
Check("0\n\n 0\n 1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 4) 7-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, 4) 4-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, 4) 5-9
");
}
[Test]
public void TestTabsInList()
{
// 012 34 567 89
Check("- \t0\n- \t1\n", @"
list ( 0, 0) 0-8
listitem ( 0, 0) 0-3
paragraph ( 0, 4) 3-3
literal ( 0, 4) 3-3
listitem ( 1, 0) 5-8
paragraph ( 1, 4) 8-8
literal ( 1, 4) 8-8
");
}
[Test]
public void TestDocument()
{
// L0 L0 L1L2 L3 L4 L5L6 L7L8
// 0 10 20 30 40 50 60 70 80 90
// 012345678901234567890 1 2345678901 2345678901 2345678901 2 345678901234567890123 4 5678901234567890123
Check("# This is a document\n\n1) item 1\n2) item 2\n3) item 4\n\nWith an **emphasis**\n\n> and a blockquote\n", @"
heading ( 0, 0) 0-19
literal ( 0, 2) 2-19
list ( 2, 0) 22-51
listitem ( 2, 0) 22-30
paragraph ( 2, 3) 25-30
literal ( 2, 3) 25-30
listitem ( 3, 0) 32-40
paragraph ( 3, 3) 35-40
literal ( 3, 3) 35-40
listitem ( 4, 0) 42-51
paragraph ( 4, 3) 45-50
literal ( 4, 3) 45-50
paragraph ( 6, 0) 53-72
literal ( 6, 0) 53-60
emphasis ( 6, 8) 61-72
literal ( 6,10) 63-70
quote ( 8, 0) 75-92
paragraph ( 8, 2) 77-92
literal ( 8, 2) 77-92
");
}
private static void Check(string text, string expectedResult, string extensions = null)
{
var pipelineBuilder = new MarkdownPipelineBuilder().UsePreciseSourceLocation();
if (extensions != null)
{
pipelineBuilder.Configure(extensions);
}
var pipeline = pipelineBuilder.Build();
var document = Markdown.Parse(text, pipeline);
var build = new StringBuilder();
foreach (var val in document.Descendants())
{
var name = GetTypeName(val.GetType());
build.Append($"{name,-12} ({val.Line,2},{val.Column,2}) {val.Span.Start,2}-{val.Span.End}\n");
var attributes = val.TryGetAttributes();
if (attributes != null)
{
build.Append($"{"attributes",-12} ({attributes.Line,2},{attributes.Column,2}) {attributes.Span.Start,2}-{attributes.Span.End}\n");
}
}
var result = build.ToString().Trim();
expectedResult = expectedResult.Trim();
expectedResult = expectedResult.Replace("\r\n", "\n").Replace("\r", "\n");
if (expectedResult != result)
{
Console.WriteLine("```````````````````Source");
Console.WriteLine(TestParser.DisplaySpaceAndTabs(text));
Console.WriteLine("```````````````````Result");
Console.WriteLine(result);
Console.WriteLine("```````````````````Expected");
Console.WriteLine(expectedResult);
Console.WriteLine("```````````````````");
Console.WriteLine();
}
TextAssert.AreEqual(expectedResult, result);
}
private static string GetTypeName(Type type)
{
return type.Name.ToLowerInvariant()
.Replace("block", string.Empty)
.Replace("inline", string.Empty);
}
}
}

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
@@ -31,14 +32,16 @@ namespace Markdig.Extensions.Abbreviations
// A link must be of the form *[Some Text]: An abbreviation
var slice = processor.Line;
var startPosition = slice.Start;
var c = slice.NextChar();
if (c != '[')
{
return BlockState.None;
}
SourceSpan labelSpan;
string label;
if (!LinkHelper.TryParseLabel(ref slice, out label))
if (!LinkHelper.TryParseLabel(ref slice, out label, out labelSpan))
{
return BlockState.None;
}
@@ -55,7 +58,11 @@ namespace Markdig.Extensions.Abbreviations
var abbr = new Abbreviation(this)
{
Label = label,
Text = slice, Line = processor.LineIndex, Column = processor.Column
Text = slice,
Span = new SourceSpan(startPosition, slice.End),
Line = processor.LineIndex,
Column = processor.Column,
LabelSpan = labelSpan,
};
if (!processor.Document.HasAbbreviations())
{
@@ -84,6 +91,7 @@ namespace Markdig.Extensions.Abbreviations
inlineProcessor.LiteralInlineParser.PostMatch += (InlineProcessor processor, ref StringSlice slice) =>
{
var literal = (LiteralInline) processor.Inline;
var originalLiteral = literal;
ContainerInline container = null;
@@ -96,13 +104,13 @@ namespace Markdig.Extensions.Abbreviations
if (matcher.TryMatch(text, i, content.End - i + 1, out match))
{
// The word matched must be embraced by punctuation or whitespace or \0.
var c = content.PeekCharExtra(i - 1);
var c = content.PeekCharAbsolute(i - 1);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
continue;
}
var indexAfterMatch = i + match.Length;
c = content.PeekCharExtra(indexAfterMatch);
c = content.PeekCharAbsolute(indexAfterMatch);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
continue;
@@ -118,15 +126,33 @@ namespace Markdig.Extensions.Abbreviations
// If we don't have a container, create a new one
if (container == null)
{
container = new ContainerInline();
container = new ContainerInline()
{
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
}
var abbrInline = new AbbreviationInline(abbr);
int line;
int column;
var abbrInline = new AbbreviationInline(abbr)
{
Span =
{
Start = processor.GetSourcePosition(i, out line, out column),
},
Line = line,
Column = column
};
abbrInline.Span.End = abbrInline.Span.Start + match.Length - 1;
// Append the previous literal
if (i > content.Start)
{
container.AppendChild(literal);
literal.Span.End = abbrInline.Span.Start - 1;
// Truncate it before the abbreviation
literal.Content.End = i - 1;
}
@@ -143,7 +169,12 @@ namespace Markdig.Extensions.Abbreviations
}
// Process the remaining literal
literal = new LiteralInline();
literal = new LiteralInline()
{
Span = new SourceSpan(abbrInline.Span.End + 1, literal.Span.End),
Line = line,
Column = column + match.Length,
};
content.Start = indexAfterMatch;
literal.Content = content;
@@ -153,12 +184,11 @@ namespace Markdig.Extensions.Abbreviations
if (container != null)
{
processor.Inline = container;
// If we have a pending literal, we can add it
if (literal != null)
{
container.AppendChild(literal);
}
processor.Inline = container;
}
};
}

View File

@@ -30,6 +30,8 @@ namespace Markdig.Extensions.DefinitionLists
return BlockState.None;
}
var startPosition = processor.Start;
var column = processor.ColumnBeforeIndent;
processor.NextChar();
processor.ParseIndent();
@@ -62,16 +64,22 @@ namespace Markdig.Extensions.DefinitionLists
if (currentDefinitionList == null)
{
currentDefinitionList = new DefinitionList(this);
currentDefinitionList = new DefinitionList(this)
{
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
Column = paragraphBlock.Column,
Line = paragraphBlock.Line,
};
previousParent.Add(currentDefinitionList);
}
var definitionItem = new DefinitionItem(this)
{
Column = processor.Column,
Line = processor.LineIndex,
Column = column,
Span = new SourceSpan(startPosition, processor.Line.End),
OpeningCharacter = processor.CurrentChar,
};
currentDefinitionList.Add(definitionItem);
for (int i = 0; i < paragraphBlock.Lines.Count; i++)
{
@@ -80,14 +88,18 @@ namespace Markdig.Extensions.DefinitionLists
{
Column = paragraphBlock.Column,
Line = line.Line,
Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End),
IsOpen = false
};
term.AppendLine(ref line.Slice, line.Column, line.Line);
term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position);
definitionItem.Add(term);
}
currentDefinitionList.Add(definitionItem);
processor.Open(definitionItem);
// Update the end position
currentDefinitionList.Span.End = processor.Line.End;
return BlockState.Continue;
}
@@ -100,11 +112,13 @@ namespace Markdig.Extensions.DefinitionLists
return BlockState.Continue;
}
var list = (DefinitionList)definitionItem.Parent;
var lastBlankLine = definitionItem.LastChild as BlankLineBlock;
// Check if we have another definition list
if (Array.IndexOf(OpeningCharacters, processor.CurrentChar) >= 0)
{
var startPosition = processor.Start;
var column = processor.ColumnBeforeIndent;
processor.NextChar();
processor.ParseIndent();
@@ -118,6 +132,8 @@ namespace Markdig.Extensions.DefinitionLists
{
definitionItem.RemoveAt(definitionItem.Count - 1);
}
list.Span.End = list.LastChild.Span.End;
return BlockState.None;
}
@@ -126,10 +142,11 @@ namespace Markdig.Extensions.DefinitionLists
processor.GoToColumn(column + 4);
}
var list = (DefinitionList) definitionItem.Parent;
processor.Close(definitionItem);
var nextDefinitionItem = new DefinitionItem(this)
{
Span = new SourceSpan(startPosition, processor.Line.End),
Line = processor.LineIndex,
Column = processor.Column,
OpeningCharacter = processor.CurrentChar,
};
@@ -161,6 +178,7 @@ namespace Markdig.Extensions.DefinitionLists
definitionItem.RemoveAt(definitionItem.Count - 1);
}
list.Span.End = list.LastChild.Span.End;
return BlockState.Break;
}
}

View File

@@ -67,6 +67,7 @@ namespace Markdig.Extensions.Emoji
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
string match;
var startPosition = slice.Start;
if (!textMatchHelper.TryMatch(slice.Text, slice.Start, slice.Length, out match))
{
return false;
@@ -89,7 +90,19 @@ namespace Markdig.Extensions.Emoji
slice.Start += match.Length;
// Push the EmojiInline
processor.Inline = new EmojiInline(unicode) {Match = match};
int line;
int column;
processor.Inline = new EmojiInline(unicode)
{
Span =
{
Start = processor.GetSourcePosition(startPosition, out line, out column),
},
Line = line,
Column = column,
Match = match
};
processor.Inline.Span.End = processor.Inline.Span.Start + match.Length - 1;
return true;
}

View File

@@ -30,7 +30,9 @@ namespace Markdig.Extensions.Figures
// Match fenced char
int count = 0;
var column = processor.Column;
var line = processor.Line;
var startPosition = line.Start;
char c = line.CurrentChar;
var matchChar = c;
while (c != '\0')
@@ -51,6 +53,8 @@ namespace Markdig.Extensions.Figures
var figure = new Figure(this)
{
Span = new SourceSpan(startPosition, line.End),
Line = processor.LineIndex,
Column = processor.Column,
OpeningCharacter = matchChar,
OpeningCharacterCount = count
@@ -59,8 +63,14 @@ namespace Markdig.Extensions.Figures
line.TrimStart();
if (!line.IsEmpty)
{
var caption = new FigureCaption(this) {IsOpen = false};
caption.AppendLine(ref line, line.Start, processor.LineIndex);
var caption = new FigureCaption(this)
{
Span = new SourceSpan(line.Start, line.End),
Line = processor.LineIndex,
Column = column + line.Start - startPosition,
IsOpen = false
};
caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition);
figure.Add(caption);
}
processor.NewBlocks.Push(figure);
@@ -76,8 +86,10 @@ namespace Markdig.Extensions.Figures
var matchChar = figure.OpeningCharacter;
var c = processor.CurrentChar;
var column = processor.Column;
// Match if we have a closing fence
var line = processor.Line;
var startPosition = line.Start;
while (c == matchChar)
{
c = line.NextChar();
@@ -91,11 +103,19 @@ namespace Markdig.Extensions.Figures
line.TrimStart();
if (!line.IsEmpty)
{
var caption = new FigureCaption(this) {IsOpen = false};
caption.AppendLine(ref line, line.Start, processor.LineIndex);
var caption = new FigureCaption(this)
{
Span = new SourceSpan(line.Start, line.End),
Line = processor.LineIndex,
Column = column + line.Start - startPosition,
IsOpen = false
};
caption.AppendLine(ref line, caption.Column, processor.LineIndex, processor.CurrentLineStartPosition);
figure.Add(caption);
}
figure.Span.End = line.End;
// Don't keep the last line
return BlockState.BreakDiscard;
}
@@ -103,6 +123,8 @@ namespace Markdig.Extensions.Figures
// Reset the indentation to the column before the indent
processor.GoToColumn(processor.ColumnBeforeIndent);
figure.Span.End = line.End;
return BlockState.Continue;
}
}

View File

@@ -30,6 +30,7 @@ namespace Markdig.Extensions.Footers
}
var column = processor.Column;
var startPosition = processor.Start;
// A footer
// A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space.
@@ -44,7 +45,13 @@ namespace Markdig.Extensions.Footers
{
processor.NextColumn();
}
processor.NewBlocks.Push(new FooterBlock(this) { OpeningCharacter = openingChar, Column = column});
processor.NewBlocks.Push(new FooterBlock(this)
{
Span = new SourceSpan(startPosition, processor.Line.End),
OpeningCharacter = openingChar,
Column = column,
Line = processor.LineIndex,
});
return BlockState.Continue;
}
@@ -60,19 +67,22 @@ namespace Markdig.Extensions.Footers
// A footer
// A Footer marker consists of 0-3 spaces of initial indent, plus (a) the characters ^^ together with a following space, or (b) a double character ^^ not followed by a space.
var c = processor.CurrentChar;
var result = BlockState.Continue;
if (c != quote.OpeningCharacter || processor.PeekChar(1) != c)
{
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
result = processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
}
processor.NextChar(); // Skip ^^ char (1st)
c = processor.NextChar(); // Skip ^^ char (2nd)
if (c.IsSpace())
else
{
processor.NextChar(); // Skip following space
processor.NextChar(); // Skip ^^ char (1st)
c = processor.NextChar(); // Skip ^^ char (2nd)
if (c.IsSpace())
{
processor.NextChar(); // Skip following space
}
block.Span.End = processor.Line.End;
}
return BlockState.Continue;
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

@@ -36,7 +36,8 @@ 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;
@@ -48,7 +49,11 @@ namespace Markdig.Extensions.Footnotes
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,7 +88,7 @@ namespace Markdig.Extensions.Footnotes
return BlockState.ContinueDiscard;
}
if (footnote.IsLastLineEmpty && processor.Start == 0)
if (footnote.IsLastLineEmpty && processor.Column == 0)
{
return BlockState.Break;
}

View File

@@ -52,11 +52,19 @@ namespace Markdig.Extensions.GenericAttributes
// Work on a copy
var copy = line;
copy.Start = indexOfAttributes;
var startOfAttributes = copy.Start;
HtmlAttributes attributes;
if (GenericAttributesParser.TryParse(ref copy, out attributes))
{
var htmlAttributes = block.GetAttributes();
attributes.CopyTo(htmlAttributes);
// Update position for HtmlAttributes
htmlAttributes.Line = processor.LineIndex;
htmlAttributes.Column = startOfAttributes - processor.CurrentLineStartPosition; // This is not accurate with tabs!
htmlAttributes.Span.Start = startOfAttributes;
htmlAttributes.Span.End = copy.Start - 1;
line.End = indexOfAttributes - 1;
return true;
}

View File

@@ -28,6 +28,7 @@ namespace Markdig.Extensions.GenericAttributes
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
HtmlAttributes attributes;
var startPosition = slice.Start;
if (TryParse(ref slice, out attributes))
{
var inline = processor.Inline;
@@ -65,6 +66,14 @@ namespace Markdig.Extensions.GenericAttributes
var currentHtmlAttributes = objectToAttach.GetAttributes();
attributes.CopyTo(currentHtmlAttributes);
// Update the position of the attributes
int line;
int column;
currentHtmlAttributes.Span.Start = processor.GetSourcePosition(startPosition, out line, out column);
currentHtmlAttributes.Line = line;
currentHtmlAttributes.Column = column;
currentHtmlAttributes.Span.End = currentHtmlAttributes.Span.Start + slice.Start - startPosition - 1;
// We don't set the processor.Inline as we don't want to add attach attributes to a particular entity
return true;
}

View File

@@ -5,6 +5,7 @@
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace Markdig.Extensions.Mathematics
{
@@ -38,6 +39,8 @@ namespace Markdig.Extensions.Mathematics
return false;
}
var startPosition = slice.Start;
// Match the opened $ or $$
int openDollars = 1; // we have at least a $
var c = slice.NextChar();
@@ -63,13 +66,6 @@ namespace Markdig.Extensions.Mathematics
pc = match;
while (c != '\0')
{
// Count new '\n'
if (c == '\n')
{
processor.LocalLineIndex++;
processor.LineIndex++;
}
// Don't process sticks if we have a '\' as a previous char
if (pc != '\\' )
{
@@ -107,8 +103,13 @@ namespace Markdig.Extensions.Mathematics
}
// Create a new MathInline
int line;
int column;
var inline = new MathInline()
{
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.End)),
Line = line,
Column = column,
Delimiter = match,
DelimiterCount = openDollars,
Content = slice

View File

@@ -0,0 +1,40 @@
// 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;
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)
{
}
public void Setup(IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
htmlRenderer.ObjectWriteBefore -= HtmlRendererOnObjectWriteBefore;
htmlRenderer.ObjectWriteBefore += HtmlRendererOnObjectWriteBefore;
}
}
private static void HtmlRendererOnObjectWriteBefore(IMarkdownRenderer renderer, MarkdownObject markdownObject)
{
if (markdownObject is Block)
{
var htmlRenderer = (HtmlRenderer) renderer;
htmlRenderer.EnsureLine();
htmlRenderer.WriteLine($"<span id=\"pragma-line-{markdownObject.Line}\"></span>");
}
}
}
}

View File

@@ -37,6 +37,8 @@ namespace Markdig.Extensions.SmartyPants
var c = slice.CurrentChar;
var openingChar = c;
var startingPosition = slice.Start;
// undefined first
var type = (SmartyPantType) 0;
@@ -161,11 +163,17 @@ namespace Markdig.Extensions.SmartyPants
}
// Create the SmartyPant inline
int line;
int column;
var pant = new SmartyPant()
{
Span = {Start = processor.GetSourcePosition(startingPosition, out line, out column)},
Line = line,
Column = column,
OpeningCharacter = openingChar,
Type = type
};
pant.Span.End = pant.Span.Start + slice.Start - startingPosition - 1;
// We will check in a post-process step for balanaced open/close quotes
if (postProcess)
@@ -241,14 +249,19 @@ namespace Markdig.Extensions.SmartyPants
{
if (quote.Type == expectedRightQuote)
{
// Replace all intermediate unmatched left or right SmartyPants to there literal equivalent
// Replace all intermediate unmatched left or right SmartyPants to their literal equivalent
pants.RemoveAt(i);
i--;
for (int j = i; j > previousIndex; j--)
{
var toReplace = pants[j];
pants.RemoveAt(j);
toReplace.ReplaceBy(new LiteralInline(toReplace.ToString()));
toReplace.ReplaceBy(new LiteralInline(toReplace.ToString())
{
Span = toReplace.Span,
Line = toReplace.Line,
Column = toReplace.Column,
});
i--;
}
@@ -266,7 +279,12 @@ namespace Markdig.Extensions.SmartyPants
// If we have any quotes lefts, replace them by there literal equivalent
foreach (var quote in pants)
{
quote.ReplaceBy(new LiteralInline(quote.ToString()));
quote.ReplaceBy(new LiteralInline(quote.ToString())
{
Span = quote.Span,
Line = quote.Line,
Column = quote.Column,
});
}
pants.Clear();

View File

@@ -29,6 +29,7 @@ namespace Markdig.Extensions.Tables
GridTableState tableState = null;
var c = line.CurrentChar;
var startPosition = processor.Start;
while (true)
{
if (c == '+')
@@ -51,11 +52,11 @@ namespace Markdig.Extensions.Tables
{
tableState = new GridTableState()
{
Start = processor.Column,
Start = processor.Start,
ExpectRow = true,
};
}
tableState.AddColumn(startCharacter, line.Start - 1, align);
tableState.AddColumn(startCharacter - startPosition, line.Start - 1 - startPosition, align);
c = line.CurrentChar;
continue;
@@ -105,7 +106,7 @@ namespace Markdig.Extensions.Tables
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
// We expect to start at the same
if (processor.Start == tableState.Start)
//if (processor.Start == tableState.Start)
{
var columns = tableState.ColumnSlices;
@@ -172,10 +173,10 @@ namespace Markdig.Extensions.Tables
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
var sliceForCell = line;
sliceForCell.Start = column.Start + 1;
sliceForCell.Start = line.Start + column.Start + 1;
if (nextColumn != null)
{
sliceForCell.End = nextColumn.Start - 1;
sliceForCell.End = line.Start + nextColumn.Start - 1;
}
else
{
@@ -184,7 +185,7 @@ namespace Markdig.Extensions.Tables
// otherwise we allow to have the last cell of a row to be open for longer cell content
if (line.PeekCharExtra(columnEnd + 1) == '|')
{
sliceForCell.End = columnEnd;
sliceForCell.End = line.Start + columnEnd;
}
}
sliceForCell.TrimEnd();

View File

@@ -45,7 +45,7 @@ namespace Markdig.Extensions.Tables
if (countPipe > 0)
{
// Mark the paragraph as open (important, otherwise we would have an infinite loop)
paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex);
paragraph.AppendLine(ref processor.Line, processor.Column, processor.LineIndex, processor.Line.Start);
paragraph.IsOpen = true;
return BlockState.BreakDiscard;
}

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

@@ -55,14 +55,22 @@ namespace Markdig.Extensions.Tables
// tracking other delimiters on following lines
var tableState = processor.ParserStates[Index] as TableState;
bool isFirstLineEmpty = false;
int globalLineIndex;
int column;
var position = processor.GetSourcePosition(slice.Start, out globalLineIndex, out column);
var localLineIndex = globalLineIndex - processor.LineIndex;
if (tableState == null)
{
// A table could be preceded by an empty line or a line containing an inline
// that has not been added to the stack, so we consider this as a valid
// start for a table. Typically, with this, we can have an attributes {...}
// starting on the first line of a pipe table, even if the first line
// doesn't have a pipe
if (processor.Inline != null &&(processor.LocalLineIndex > 0 || c == '\n'))
if (processor.Inline != null && (localLineIndex > 0 || c == '\n'))
{
return false;
}
@@ -92,14 +100,20 @@ namespace Markdig.Extensions.Tables
}
else
{
processor.Inline = new PiprTableDelimiterInline(this) { LocalLineIndex = processor.LocalLineIndex };
var deltaLine = processor.LocalLineIndex - tableState.LineIndex;
processor.Inline = new PiprTableDelimiterInline(this)
{
Span = new SourceSpan(position, position),
Line = globalLineIndex,
Column = column,
LocalLineIndex = localLineIndex
};
var deltaLine = localLineIndex - tableState.LineIndex;
if (deltaLine > 0)
{
tableState.IsInvalidTable = true;
}
tableState.LineHasPipe = true;
tableState.LineIndex = processor.LocalLineIndex;
tableState.LineIndex = localLineIndex;
slice.NextChar(); // Skip the `|` character
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
@@ -185,7 +199,7 @@ namespace Markdig.Extensions.Tables
}
// Continue
if (tableState == null || container == null || tableState.IsInvalidTable || !tableState.LineHasPipe || tableState.LineIndex != state.LocalLineIndex)
if (tableState == null || container == null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex)
{
return true;
}
@@ -220,6 +234,12 @@ namespace Markdig.Extensions.Tables
column = ((PiprTableDelimiterInline)column).FirstChild;
}
// TODO: This is not accurate for the table
table.Span.Start = column.Span.Start;
table.Span.End = column.Span.End;
table.Line = column.Line;
table.Column = column.Column;
int lastIndex = 0;
for (int i = 0; i < delimiters.Count; i++)
{
@@ -229,8 +249,7 @@ namespace Markdig.Extensions.Tables
var beforeDelimiter = delimiter?.PreviousSibling;
var nextLineColumn = delimiter?.NextSibling;
var row = new TableRow();
table.Add(row);
TableRow row = null;
for (int j = lastIndex; j <= i; j++)
{
@@ -254,20 +273,52 @@ namespace Markdig.Extensions.Tables
continue;
}
var columnContainer = new ContainerInline();
var cellContainer = new ContainerInline();
var item = column;
var isFirstItem = true;
TrimStart(item);
while (item != null && !IsLine(item) && !(item is PiprTableDelimiterInline))
{
var nextSibling = item.NextSibling;
item.Remove();
columnContainer.AppendChild(item);
cellContainer.AppendChild(item);
if (isFirstItem)
{
cellContainer.Line = item.Line;
cellContainer.Column = item.Column;
cellContainer.Span.Start = item.Span.Start;
isFirstItem = false;
}
cellContainer.Span.End = item.Span.End;
item = nextSibling;
}
var tableCell = new TableCell();
var tableParagraph = new ParagraphBlock() {Inline = columnContainer};
var tableParagraph = new ParagraphBlock()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
Inline = cellContainer
};
var tableCell = new TableCell()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
tableCell.Add(tableParagraph);
if (row == null)
{
row = new TableRow()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
}
row.Add(tableCell);
cells.Add(tableCell);
@@ -291,6 +342,11 @@ namespace Markdig.Extensions.Tables
}
}
if (row != null)
{
table.Add(row);
}
TrimEnd(beforeDelimiter);
if (delimiter != null)
@@ -388,8 +444,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

@@ -0,0 +1,35 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.TaskLists
{
/// <summary>
/// A HTML renderer for a <see cref="TaskList"/>.
/// </summary>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{TaskList}" />
public class HtmlTaskListRenderer : HtmlObjectRenderer<TaskList>
{
protected override void Write(HtmlRenderer renderer, TaskList obj)
{
if (renderer.EnableHtmlForInline)
{
renderer.Write("<input").WriteAttributes(obj).Write(" disabled=\"disabled\" type=\"checkbox\"");
if (obj.Checked)
{
renderer.Write(" checked=\"checked\"");
}
renderer.Write(" />");
}
else
{
renderer.Write('[');
renderer.Write(obj.Checked ? "x" : " ");
renderer.Write(']');
}
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.TaskLists
{
/// <summary>
/// An inline for TaskList.
/// </summary>
[DebuggerDisplay("TaskList {Checked}")]
public class TaskList : LeafInline
{
public bool Checked { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
namespace Markdig.Extensions.TaskLists
{
/// <summary>
/// Extension to enable TaskList.
/// </summary>
public class TaskListExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<TaskListInlineParser>())
{
// Insert the parser after the code span parser
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(new TaskListInlineParser());
}
}
public void Setup(IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlTaskListRenderer>();
}
}
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace Markdig.Extensions.TaskLists
{
/// <summary>
/// The inline parser for SmartyPants.
/// </summary>
public class TaskListInlineParser : InlineParser
{
/// <summary>
/// Initializes a new instance of the <see cref="TaskListInlineParser"/> class.
/// </summary>
public TaskListInlineParser()
{
OpeningCharacters = new[] {'['};
ListClass = "contains-task-list";
ListItemClass = "task-list-item";
}
/// <summary>
/// Gets or sets the list class used for a task list.
/// </summary>
public string ListClass { get; set; }
/// <summary>
/// Gets or sets the list item class used for a task list.
/// </summary>
public string ListItemClass { get; set; }
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// A tasklist is either
// [ ]
// or [x] or [X]
var listItemBlock = processor.Block.Parent as ListItemBlock;
if (listItemBlock == null)
{
return false;
}
var startingPosition = slice.Start;
var c = slice.NextChar();
if (!c.IsSpace() && c != 'x' && c != 'X')
{
return false;
}
if (slice.NextChar() != ']')
{
return false;
}
// Skip last ]
slice.NextChar();
// Create the TaskList
int line;
int column;
var taskItem = new TaskList()
{
Span = { Start = processor.GetSourcePosition(startingPosition, out line, out column)},
Line = line,
Column = column,
Checked = !c.IsSpace()
};
taskItem.Span.End = taskItem.Span.Start + 2;
processor.Inline = taskItem;
// Add proper class for task list
if (!string.IsNullOrEmpty(ListItemClass))
{
listItemBlock.GetAttributes().AddClass(ListItemClass);
}
var listBlock = (ListBlock) listItemBlock.Parent;
if (!string.IsNullOrEmpty(ListClass))
{
listBlock.GetAttributes().AddClass(ListClass);
}
return true;
}
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.IO;
using System.Text;
namespace Markdig.Helpers
{
/// <summary>
/// A line reader from a <see cref="TextReader"/> that can provide precise source position
/// </summary>
public struct LineReader
{
private readonly string text;
/// <summary>
/// Initializes a new instance of the <see cref="LineReader"/> class.
/// </summary>
/// <exception cref="System.ArgumentNullException"></exception>
/// <exception cref="System.ArgumentOutOfRangeException">bufferSize cannot be &lt;= 0</exception>
public LineReader(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
this.text = text;
SourcePosition = 0;
}
/// <summary>
/// Gets the char position of the line. Valid for the next line before calling <see cref="ReadLine"/>.
/// </summary>
public int SourcePosition { get; private set; }
/// <summary>
/// Reads a new line from the underlying <see cref="TextReader"/> and update the <see cref="SourcePosition"/> for the next line.
/// </summary>
/// <returns>A new line or null if the end of <see cref="TextReader"/> has been reached</returns>
public StringSlice? ReadLine()
{
if (SourcePosition >= text.Length)
{
return null;
}
var startPosition = SourcePosition;
var position = SourcePosition;
var slice = new StringSlice(text, startPosition, startPosition);
for (;position < text.Length; position++)
{
var c = text[position];
if (c == '\r' || c == '\n')
{
slice.End = position - 1;
if (c == '\r' && position + 1 < text.Length && text[position + 1] == '\n')
{
position++;
}
position++;
SourcePosition = position;
return slice;
}
}
slice.End = position - 1;
SourcePosition = position;
return slice;
}
}
}

View File

@@ -297,10 +297,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 +327,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 +358,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 +614,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 +646,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 +660,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 +712,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 +786,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 +816,10 @@ namespace Markdig.Helpers
if (!hasEscape && c == '\\')
{
if (startLabel < 0)
{
startLabel = lines.Start;
}
hasEscape = true;
}
else
@@ -749,6 +828,11 @@ namespace Markdig.Helpers
if (!previousWhitespace || !isWhitespace)
{
if (startLabel < 0)
{
startLabel = lines.Start;
}
endLabel = lines.Start;
buffer.Append(c);
if (!isWhitespace)
{

View File

@@ -23,11 +23,12 @@ namespace Markdig.Helpers
/// <param name="slice">The slice.</param>
/// <param name="line">The line.</param>
/// <param name="column">The column.</param>
public StringLine(StringSlice slice, int line, int column)
public StringLine(StringSlice slice, int line, int column, int position)
{
Slice = slice;
Line = line;
Column = column;
Position = position;
}
/// <summary>
@@ -36,11 +37,12 @@ namespace Markdig.Helpers
/// <param name="slice">The slice.</param>
/// <param name="line">The line.</param>
/// <param name="column">The column.</param>
public StringLine(ref StringSlice slice, int line, int column)
public StringLine(ref StringSlice slice, int line, int column, int position)
{
Slice = slice;
Line = line;
Column = column;
Position = position;
}
/// <summary>
@@ -53,6 +55,11 @@ namespace Markdig.Helpers
/// </summary>
public int Line;
/// <summary>
/// The position of the start of this line within the original source code
/// </summary>
public int Position;
/// <summary>
/// The column position.
/// </summary>

View File

@@ -5,7 +5,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Markdig.Extensions.Tables;
namespace Markdig.Helpers
{
@@ -108,15 +110,11 @@ namespace Markdig.Helpers
/// </summary>
/// <param name="lineOffsets">The position of the `\n` line offsets from the beginning of the returned slice.</param>
/// <returns>A single slice concatenating the lines of this instance</returns>
public StringSlice ToSlice(List<int> lineOffsets = null)
public StringSlice ToSlice(List<LineOffset> lineOffsets = null)
{
// Optimization case when no lines
if (Count == 0)
{
if (lineOffsets != null)
{
lineOffsets.Add(1);
}
return new StringSlice(string.Empty);
}
@@ -125,22 +123,24 @@ namespace Markdig.Helpers
{
if (lineOffsets != null)
{
lineOffsets.Add(Lines[0].Slice.End + 1);
lineOffsets.Add(new LineOffset(Lines[0].Position, Lines[0].Column, Lines[0].Slice.Start - Lines[0].Position, Lines[0].Slice.Start, Lines[0].Slice.End + 1));
}
return Lines[0];
}
// Else use a builder
var builder = StringBuilderCache.Local();
int previousStartOfLine = 0;
for (int i = 0; i < Count; i++)
{
if (i > 0)
{
if (lineOffsets != null)
{
lineOffsets.Add(builder.Length + 1); // Add 1 for \n and 1 for next line
lineOffsets.Add(new LineOffset(Lines[i - 1].Position, Lines[i - 1].Column, Lines[i - 1].Slice.Start - Lines[i - 1].Position, previousStartOfLine, builder.Length));
}
builder.Append('\n');
previousStartOfLine = builder.Length;
}
if (!Lines[i].Slice.IsEmpty)
{
@@ -149,7 +149,7 @@ namespace Markdig.Helpers
}
if (lineOffsets != null)
{
lineOffsets.Add(builder.Length); // Add 1 for \0
lineOffsets.Add(new LineOffset(Lines[Count - 1].Position, Lines[Count - 1].Column, Lines[Count - 1].Slice.Start - Lines[Count - 1].Position, previousStartOfLine, builder.Length));
}
var str = builder.ToString();
builder.Length = 0;
@@ -265,5 +265,27 @@ namespace Markdig.Helpers
return hasSpaces;
}
}
public struct LineOffset
{
public LineOffset(int linePosition, int column, int offset, int start, int end)
{
LinePosition = linePosition;
Column = column;
Offset = offset;
Start = start;
End = end;
}
public readonly int LinePosition;
public readonly int Column;
public readonly int Offset;
public readonly int Start;
public readonly int End;
}
}
}

View File

@@ -112,6 +112,16 @@ namespace Markdig.Helpers
return index >= Start && index <= End ? Text[index] : (char) 0;
}
/// <summary>
/// Peeks a character at the specified offset from the current beginning of the string, without taking into account <see cref="Start"/> and <see cref="End"/>
/// </summary>
/// <returns>The character at offset, returns `\0` if none.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public char PeekCharAbsolute(int index)
{
return index >= 0 && index < Text.Length ? Text[index] : (char)0;
}
/// <summary>
/// Peeks a character at the specified offset from the current begining of the slice
/// without using the range <see cref="Start"/> or <see cref="End"/>, returns `\0` if outside the <see cref="Text"/>.
@@ -180,19 +190,31 @@ namespace Markdig.Helpers
return i == text.Length;
}
/// <summary>
/// Searches the specified text within this slice.
/// </summary>
/// <param name="text">The text.</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public bool Search(string text, out int index)
{
return Search(text, 0, out index);
}
/// <summary>
/// Searches the specified text 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 = 0)
public bool Search(string text, int offset, out int index)
{
var end = End - text.Length + 1;
for (int i = Start; i <= end; i++)
index = Start + offset;
for (int i = index; i <= end; i ++)
{
if (Match(text, End, i))
if (Match(text, End, i - Start))
{
index = i + text.Length;
return true;
}
}
@@ -205,13 +227,15 @@ namespace Markdig.Helpers
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public bool SearchLowercase(string text, int offset = 0)
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;
}
}

View File

@@ -24,35 +24,21 @@ namespace Markdig
public static string ToHtml(string markdown, MarkdownPipeline pipeline = null)
{
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
var reader = new StringReader(markdown);
return ToHtml(reader, pipeline) ?? string.Empty;
}
/// <summary>
/// Converts a Markdown string to HTML.
/// </summary>
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The result of the conversion</returns>
/// <exception cref="System.ArgumentNullException">if markdown variable is null</exception>
public static string ToHtml(TextReader reader, MarkdownPipeline pipeline = null)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
var writer = new StringWriter();
ToHtml(reader, writer, pipeline);
ToHtml(markdown, writer, pipeline);
return writer.ToString();
}
/// <summary>
/// Converts a Markdown string to HTML.
/// </summary>
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
/// <param name="markdown">A Markdown text.</param>
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <exception cref="System.ArgumentNullException">if reader or writer variable are null</exception>
public static void ToHtml(TextReader reader, TextWriter writer, MarkdownPipeline pipeline = null)
public static void ToHtml(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
if (writer == null) throw new ArgumentNullException(nameof(writer));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
@@ -60,7 +46,7 @@ namespace Markdig
var renderer = new HtmlRenderer(writer);
pipeline.Setup(renderer);
var document = Parse(reader, pipeline);
var document = Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
}
@@ -68,17 +54,17 @@ namespace Markdig
/// <summary>
/// Converts a Markdown string using a custom <see cref="IMarkdownRenderer"/>.
/// </summary>
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
/// <param name="markdown">A Markdown text.</param>
/// <param name="renderer">The renderer to convert Markdown to.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <exception cref="System.ArgumentNullException">if reader or writer variable are null</exception>
public static object Convert(TextReader reader, IMarkdownRenderer renderer, MarkdownPipeline pipeline = null)
/// <exception cref="System.ArgumentNullException">if markdown or writer variable are null</exception>
public static object Convert(string markdown, IMarkdownRenderer renderer, MarkdownPipeline pipeline = null)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
var document = Parse(reader, pipeline);
var document = Parse(markdown, pipeline);
pipeline.Setup(renderer);
return renderer.Render(document);
}
@@ -92,22 +78,22 @@ namespace Markdig
public static MarkdownDocument Parse(string markdown)
{
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
return Parse(new StringReader(markdown));
return Parse(markdown, null);
}
/// <summary>
/// Parses the specified markdown into an AST <see cref="MarkdownDocument"/>
/// </summary>
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
/// <param name="markdown">The markdown text.</param>
/// <param name="pipeline">The pipeline used for the parsing.</param>
/// <returns>An AST Markdown document</returns>
/// <exception cref="System.ArgumentNullException">if reader variable is null</exception>
public static MarkdownDocument Parse(TextReader reader, MarkdownPipeline pipeline = null)
/// <exception cref="System.ArgumentNullException">if markdown variable is null</exception>
public static MarkdownDocument Parse(string markdown, MarkdownPipeline pipeline)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
return MarkdownParser.Parse(reader, pipeline);
return MarkdownParser.Parse(markdown, pipeline);
}
}
}

View File

@@ -19,8 +19,10 @@ using Markdig.Extensions.Hardlines;
using Markdig.Extensions.ListExtras;
using Markdig.Extensions.Mathematics;
using Markdig.Extensions.MediaLinks;
using Markdig.Extensions.PragmaLines;
using Markdig.Extensions.SmartyPants;
using Markdig.Extensions.Tables;
using Markdig.Extensions.TaskLists;
using Markdig.Parsers;
using Markdig.Parsers.Inlines;
@@ -53,9 +55,43 @@ namespace Markdig
.UseMediaLinks()
.UsePipeTables()
.UseListExtras()
.UseTaskLists()
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
}
/// <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>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UsePreciseSourceLocation(this MarkdownPipelineBuilder pipeline)
{
pipeline.PreciseSourceLocation = true;
return pipeline;
}
/// <summary>
/// Uses the task list extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseTaskLists(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<TaskListExtension>();
return pipeline;
}
/// <summary>
/// Uses the custom container extension.
/// </summary>
@@ -396,6 +432,9 @@ namespace Markdig
case "autoidentifiers":
pipeline.UseAutoIdentifiers();
break;
case "tasklists":
pipeline.UseTaskLists();
break;
default:
throw new ArgumentException($"unknown extension {extension}");
}

View File

@@ -31,6 +31,9 @@ namespace Markdig
DebugLog = debugLog;
DocumentProcessed = documentProcessed;
}
internal bool PreciseSourceLocation { get; set; }
internal OrderedList<IMarkdownExtension> Extensions { get; }
internal BlockParserList BlockParsers { get; }

View File

@@ -73,6 +73,11 @@ namespace Markdig
/// </summary>
public StringBuilderCache StringBuilderCache { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable precise source location (slower parsing but accurate position for block and inline elements)
/// </summary>
public bool PreciseSourceLocation { get; set; }
/// <summary>
/// Gets or sets the debug log.
/// </summary>
@@ -111,7 +116,9 @@ namespace Markdig
extension.Setup(this);
}
pipeline = new MarkdownPipeline(new OrderedList<IMarkdownExtension>(Extensions), new BlockParserList(BlockParsers), new InlineParserList(InlineParsers), StringBuilderCache, DebugLog, GetDocumentProcessed);
pipeline = new MarkdownPipeline(new OrderedList<IMarkdownExtension>(Extensions),
new BlockParserList(BlockParsers), new InlineParserList(InlineParsers), StringBuilderCache, DebugLog,
GetDocumentProcessed) {PreciseSourceLocation = PreciseSourceLocation};
return pipeline;
}
}

View File

@@ -98,6 +98,11 @@ namespace Markdig.Parsers
/// </summary>
public StringSlice Line;
/// <summary>
/// Gets or sets the current line start position.
/// </summary>
public int CurrentLineStartPosition { get; private set; }
/// <summary>
/// Gets the index of the line in the source text.
/// </summary>
@@ -365,6 +370,8 @@ namespace Markdig.Parsers
/// <param name="newLine">The new line.</param>
public void ProcessLine(StringSlice newLine)
{
CurrentLineStartPosition = newLine.Start;
ContinueProcessingLine = true;
ResetLine(newLine);
@@ -546,7 +553,7 @@ namespace Markdig.Parsers
ContinueProcessingLine = false;
if (!result.IsDiscard())
{
leaf.AppendLine(ref Line, Column, LineIndex);
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
}
if (NewBlocks.Count > 0)
@@ -583,8 +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();
@@ -669,7 +684,7 @@ namespace Markdig.Parsers
if (!result.IsDiscard())
{
paragraph.AppendLine(ref Line, Column, LineIndex);
paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
}
// We have just found a lazy continuation for a paragraph, early exit
@@ -722,7 +737,7 @@ namespace Markdig.Parsers
{
if (!result.IsDiscard())
{
leaf.AppendLine(ref Line, Column, LineIndex);
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition);
}
if (newBlocks.Count > 0)

View File

@@ -128,6 +128,8 @@ namespace Markdig.Parsers
return BlockState.None;
}
var startPosition = processor.Start;
// Match fenced char
int count = 0;
var line = processor.Line;
@@ -157,6 +159,8 @@ namespace Markdig.Parsers
fenced.Column = processor.Column;
fenced.FencedChar = matchChar;
fenced.FencedCharCount = count;
fenced.Span.Start = startPosition;
fenced.Span.End = line.Start;
};
// Try to parse any attached attributes
@@ -212,6 +216,8 @@ namespace Markdig.Parsers
// The line must contain only fence opening character followed only by whitespaces.
if (count <=0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
{
block.Span.End = line.Start - 1;
// Don't keep the last line
return BlockState.BreakDiscard;
}

View File

@@ -46,6 +46,7 @@ namespace Markdig.Parsers
// opening sequence.
var column = processor.Column;
var line = processor.Line;
var sourcePosition = line.Start;
var c = line.CurrentChar;
var matchingChar = c;
@@ -64,14 +65,15 @@ namespace Markdig.Parsers
if (leadingCount > 0 && leadingCount <= 6 && (c.IsSpace() || c == '\0'))
{
// Move to the content
processor.Line.Start = line.Start + 1;
var headingBlock = new HeadingBlock(this)
{
HeaderChar = matchingChar,
Level = leadingCount,
Column = column
Column = column,
Span = { Start = sourcePosition }
};
processor.NewBlocks.Push(headingBlock);
processor.GoToColumn(column + leadingCount + 1);
// Gives a chance to parse attributes
if (TryParseAttributes != null)
@@ -116,6 +118,9 @@ namespace Markdig.Parsers
}
}
// Setup the source end position of this element
headingBlock.Span.End = processor.Line.End;
// We expect a single line, so don't continue
return BlockState.Break;
}

View File

@@ -47,18 +47,19 @@ namespace Markdig.Parsers
}
var line = state.Line;
var startPosition = line.Start;
line.NextChar();
var result = TryParseTagType16(state, line, state.ColumnBeforeIndent);
var result = TryParseTagType16(state, line, state.ColumnBeforeIndent, startPosition);
// HTML blocks of type 7 cannot interrupt a paragraph:
if (result == BlockState.None && !(state.CurrentBlock is ParagraphBlock))
{
result = TryParseTagType7(state, line, state.ColumnBeforeIndent);
result = TryParseTagType7(state, line, state.ColumnBeforeIndent, startPosition);
}
return result;
}
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn)
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
{
var builder = StringBuilderCache.Local();
var c = line.CurrentChar;
@@ -84,7 +85,7 @@ namespace Markdig.Parsers
if (hasOnlySpaces)
{
result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn);
result = CreateHtmlBlock(state, HtmlBlockType.NonInterruptingBlock, startColumn, startPosition);
}
}
@@ -92,7 +93,7 @@ namespace Markdig.Parsers
return result;
}
private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn)
private BlockState TryParseTagType16(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
{
char c;
c = line.CurrentChar;
@@ -101,15 +102,15 @@ namespace Markdig.Parsers
c = line.NextChar();
if (c == '-' && line.PeekChar(1) == '-')
{
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn); // group 2
return CreateHtmlBlock(state, HtmlBlockType.Comment, startColumn, startPosition); // group 2
}
if (c.IsAlphaUpper())
{
return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn); // group 4
return CreateHtmlBlock(state, HtmlBlockType.DocumentType, startColumn, startPosition); // group 4
}
if (c == '[' && line.Match("CDATA[", 1))
{
return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn); // group 5
return CreateHtmlBlock(state, HtmlBlockType.CData, startColumn, startPosition); // group 5
}
return BlockState.None;
@@ -117,7 +118,7 @@ namespace Markdig.Parsers
if (c == '?')
{
return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn); // group 3
return CreateHtmlBlock(state, HtmlBlockType.ProcessingInstruction, startColumn, startPosition); // group 3
}
var hasLeadingClose = c == '/';
@@ -164,10 +165,10 @@ namespace Markdig.Parsers
{
return BlockState.None;
}
return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn);
return CreateHtmlBlock(state, HtmlBlockType.ScriptPreOrStyle, startColumn, startPosition);
}
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn);
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn, startPosition);
}
private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock)
@@ -177,58 +178,77 @@ namespace Markdig.Parsers
// 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;
switch (htmlBlock.Type)
{
case HtmlBlockType.Comment:
if (line.Search("-->"))
if (line.Search("-->", out endof))
{
return BlockState.Break;
htmlBlock.Span.End = endof - 1;
result = BlockState.Break;
}
break;
case HtmlBlockType.CData:
if (line.Search("]]>"))
if (line.Search("]]>", out endof))
{
return BlockState.Break;
htmlBlock.Span.End = endof - 1;
result = BlockState.Break;
}
break;
case HtmlBlockType.ProcessingInstruction:
if (line.Search("?>"))
if (line.Search("?>", out endof))
{
return BlockState.Break;
htmlBlock.Span.End = endof - 1;
result = BlockState.Break;
}
break;
case HtmlBlockType.DocumentType:
if (line.Search(">"))
if (line.Search(">", out endof))
{
return BlockState.Break;
htmlBlock.Span.End = endof - 1;
result = BlockState.Break;
}
break;
case HtmlBlockType.ScriptPreOrStyle:
if (line.SearchLowercase("</script>") || line.SearchLowercase("</pre>") || line.SearchLowercase("</style>"))
if (line.SearchLowercase("</script>", out endof) || line.SearchLowercase("</pre>", out endof) || line.SearchLowercase("</style>", out endof))
{
return BlockState.Break;
htmlBlock.Span.End = endof - 1;
result = BlockState.Break;
}
break;
case HtmlBlockType.InterruptingBlock:
if (state.IsBlankLine)
{
return BlockState.BreakDiscard;
result = BlockState.BreakDiscard;
}
break;
case HtmlBlockType.NonInterruptingBlock:
if (state.IsBlankLine)
{
return BlockState.BreakDiscard;
result = BlockState.BreakDiscard;
}
break;
}
return BlockState.Continue;
// Update only if we don't have a break discard
if (result != BlockState.BreakDiscard)
{
htmlBlock.Span.End = line.End;
}
return result;
}
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn)
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition)
{
state.NewBlocks.Push(new HtmlBlock(this) {Column = startColumn, Type = type});
state.NewBlocks.Push(new HtmlBlock(this)
{
Column = startColumn,
Type = type,
// By default, setup to the end of line
Span = new SourceSpan(startPosition, startPosition + state.Line.End)
});
return BlockState.Continue;
}

View File

@@ -18,10 +18,15 @@ namespace Markdig.Parsers
public override BlockState TryOpen(BlockProcessor processor)
{
var startPosition = processor.Line.Start;
var result = TryContinue(processor, null);
if (result == BlockState.Continue)
{
processor.NewBlocks.Push(new CodeBlock(this) { Column = processor.Column });
processor.NewBlocks.Push(new CodeBlock(this)
{
Column = processor.Column,
Span = new SourceSpan(startPosition, processor.Line.End)
});
}
return result;
}
@@ -41,6 +46,10 @@ namespace Markdig.Parsers
{
processor.GoToCodeIndent();
}
if (block != null)
{
block.Span.End = processor.Line.End;
}
return BlockState.Continue;
}

View File

@@ -23,7 +23,9 @@ namespace Markdig.Parsers
/// </summary>
public class InlineProcessor
{
private readonly List<int> lineOffsets;
private readonly List<StringLineGroup.LineOffset> lineOffsets;
private int previousSliceOffset;
private int previousLineIndexForSliceOffset;
/// <summary>
/// Initializes a new instance of the <see cref="InlineProcessor" /> class.
@@ -34,7 +36,7 @@ namespace Markdig.Parsers
/// <param name="inlineCreated">The inline created event.</param>
/// <exception cref="System.ArgumentNullException">
/// </exception>
public InlineProcessor(StringBuilderCache stringBuilders, MarkdownDocument document, InlineParserList parsers)
public InlineProcessor(StringBuilderCache stringBuilders, MarkdownDocument document, InlineParserList parsers, bool preciseSourcelocation)
{
if (stringBuilders == null) throw new ArgumentNullException(nameof(stringBuilders));
if (document == null) throw new ArgumentNullException(nameof(document));
@@ -42,7 +44,8 @@ namespace Markdig.Parsers
StringBuilders = stringBuilders;
Document = document;
Parsers = parsers;
lineOffsets = new List<int>();
PreciseSourceLocation = preciseSourcelocation;
lineOffsets = new List<StringLineGroup.LineOffset>();
Parsers.Initialize(this);
ParserStates = new object[Parsers.Count];
LiteralInlineParser = new LiteralInlineParser();
@@ -53,6 +56,11 @@ namespace Markdig.Parsers
/// </summary>
public LeafBlock Block { get; private set; }
/// <summary>
/// Gets a value indicating whether to provide precise source location.
/// </summary>
public bool PreciseSourceLocation { get; }
/// <summary>
/// Gets or sets the new block to replace the block being processed.
/// </summary>
@@ -86,12 +94,7 @@ namespace Markdig.Parsers
/// <summary>
/// Gets or sets the index of the line from the begining of the document being processed.
/// </summary>
public int LineIndex { get; set; }
/// <summary>
/// Gets or sets the index of the local line from the beginning of the block being processed.
/// </summary>
public int LocalLineIndex { get; set; }
public int LineIndex { get; private set; }
/// <summary>
/// Gets the parser states that can be used by <see cref="InlineParser"/> using their <see cref="InlineParser.Index"/> property.
@@ -108,12 +111,68 @@ namespace Markdig.Parsers
/// </summary>
public LiteralInlineParser LiteralInlineParser { get; }
public int GetSourcePosition(int sliceOffset)
{
int column;
int lineIndex;
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>
/// <param name="sliceOffset">The slice offset.</param>
/// <returns>The source position</returns>
public int GetSourcePosition(int sliceOffset, out int lineIndex, out int column)
{
column = 0;
lineIndex = sliceOffset >= previousSliceOffset ? previousLineIndexForSliceOffset : 0;
int position = 0;
if (PreciseSourceLocation)
{
for (; lineIndex < lineOffsets.Count; lineIndex++)
{
var lineOffset = lineOffsets[lineIndex];
if (sliceOffset <= lineOffset.End)
{
// Use the beginning of the line as a previous slice offset
// (since it is on the same line)
previousSliceOffset = lineOffsets[lineIndex].Start;
var delta = sliceOffset - previousSliceOffset;
column = lineOffsets[lineIndex].Column + delta;
position = lineOffset.LinePosition + delta + lineOffsets[lineIndex].Offset;
previousLineIndexForSliceOffset = lineIndex;
// Return an absolute line index
lineIndex = lineIndex + LineIndex;
break;
}
}
}
return position;
}
/// <summary>
/// Processes the inline of the specified <see cref="LeafBlock"/>.
/// </summary>
/// <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);
@@ -124,21 +183,24 @@ namespace Markdig.Parsers
BlockNew = null;
LineIndex = leafBlock.Line;
previousSliceOffset = 0;
previousLineIndexForSliceOffset = 0;
lineOffsets.Clear();
LocalLineIndex = 0;
var text = leafBlock.Lines.ToSlice(lineOffsets);
leafBlock.Lines = new StringLineGroup();
int previousStart = -1;
while (!text.IsEmpty)
{
var c = text.CurrentChar;
// Update line index
if (text.Start >= lineOffsets[LocalLineIndex])
// Security check so that the parser can't go into a crazy infinite loop if one extension is messing
if (previousStart == text.Start)
{
LineIndex++;
LocalLineIndex++;
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;
var parsers = Parsers.GetParsersForOpeningCharacter(c);

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
@@ -31,9 +32,18 @@ namespace Markdig.Parsers.Inlines
string link;
bool isEmail;
var saved = slice;
int line;
int column;
if (LinkHelper.TryParseAutolink(ref slice, out link, out isEmail))
{
processor.Inline = new AutolinkInline() {IsEmail = isEmail, Url = link};
processor.Inline = new AutolinkInline()
{
IsEmail = isEmail,
Url = link,
Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
}
else if (EnableHtmlParsing)
{
@@ -44,7 +54,13 @@ namespace Markdig.Parsers.Inlines
return false;
}
processor.Inline = new HtmlInline() { Tag = htmlTag };
processor.Inline = new HtmlInline()
{
Tag = htmlTag,
Span = new SourceSpan(processor.GetSourcePosition(saved.Start, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
}
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
@@ -29,6 +30,8 @@ namespace Markdig.Parsers.Inlines
return false;
}
var startPosition = slice.Start;
// Match the opened sticks
var c = slice.CurrentChar;
while (c == match)
@@ -98,15 +101,17 @@ namespace Markdig.Parsers.Inlines
builder.Length--;
}
}
int line;
int column;
processor.Inline = new CodeInline()
{
Delimiter = match,
Content = builder.ToString()
Content = builder.ToString(),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
isMatching = true;
processor.LocalLineIndex += newLinesFound;
processor.LineIndex += newLinesFound;
}
// Release the builder if not used

View File

@@ -144,6 +144,8 @@ namespace Markdig.Parsers.Inlines
return false;
}
var startPosition = slice.Start;
int delimiterCount = 0;
char c;
do
@@ -177,10 +179,15 @@ namespace Markdig.Parsers.Inlines
delimiterType |= DelimiterType.Close;
}
int line;
int column;
var delimiter = new EmphasisDelimiterInline(this, emphasisDesc)
{
DelimiterCount = delimiterCount,
Type = delimiterType,
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(slice.Start - 1)),
Column = column,
Line = line,
};
processor.Inline = delimiter;
@@ -241,6 +248,24 @@ namespace Markdig.Parsers.Inlines
IsDouble = isStrong
};
// Update position for emphasis
var openDelimitercount = openDelimiter.DelimiterCount;
var closeDelimitercount = closeDelimiter.DelimiterCount;
var delimiterDelta = isStrong ? 2 : 1;
emphasis.Span.Start = openDelimiter.Span.Start;
emphasis.Line = openDelimiter.Line;
emphasis.Column = openDelimiter.Column;
emphasis.Span.End = closeDelimiter.Span.End - closeDelimitercount + delimiterDelta;
openDelimiter.Span.Start += delimiterDelta;
openDelimiter.Column += delimiterDelta;
closeDelimiter.Span.Start += delimiterDelta;
closeDelimiter.Column += delimiterDelta;
openDelimiter.DelimiterCount -= delimiterDelta;
closeDelimiter.DelimiterCount -= delimiterDelta;
var embracer = (ContainerInline)openDelimiter;
// Go down to the first emphasis with a lower level
@@ -267,9 +292,6 @@ namespace Markdig.Parsers.Inlines
// Embrace all delimiters
embracer.EmbraceChildrenBy(emphasis);
openDelimiter.DelimiterCount -= isStrong ? 2 : 1;
closeDelimiter.DelimiterCount -= isStrong ? 2 : 1;
// Remove any intermediate emphasis
for (int k = i - 1; k >= openDelimiterIndex + 1; k--)
{
@@ -277,7 +299,10 @@ namespace Markdig.Parsers.Inlines
var literal = new LiteralInline()
{
Content = new StringSlice(literalDelimiter.ToLiteral()),
IsClosed = true
IsClosed = true,
Span = literalDelimiter.Span,
Line = literalDelimiter.Line,
Column = literalDelimiter.Column
};
literalDelimiter.ReplaceBy(literal);
@@ -327,7 +352,10 @@ namespace Markdig.Parsers.Inlines
var literal = new LiteralInline()
{
Content = new StringSlice(closeDelimiter.ToLiteral()),
IsClosed = true
IsClosed = true,
Span = closeDelimiter.Span,
Line = closeDelimiter.Line,
Column = closeDelimiter.Column
};
closeDelimiter.ReplaceBy(literal);
@@ -351,7 +379,10 @@ namespace Markdig.Parsers.Inlines
var literal = new LiteralInline()
{
Content = new StringSlice(delimiter.ToLiteral()),
IsClosed = true
IsClosed = true,
Span = delimiter.Span,
Line = delimiter.Line,
Column = delimiter.Column
};
delimiter.ReplaceBy(literal);

View File

@@ -19,11 +19,21 @@ namespace Markdig.Parsers.Inlines
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
var startPosition = slice.Start;
// Go to escape character
var c = slice.NextChar();
int line;
int column;
if (c.IsAsciiPunctuation())
{
processor.Inline = new LiteralInline() {Content = new StringSlice(new string(c, 1))};
processor.Inline = new LiteralInline()
{
Content = new StringSlice(new string(c, 1)),
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column
};
processor.Inline.Span.End = processor.Inline.Span.Start + 1;
slice.NextChar();
return true;
}
@@ -31,7 +41,14 @@ namespace Markdig.Parsers.Inlines
// A backslash at the end of the line is a [hard line break]:
if (c == '\n')
{
processor.Inline = new HardlineBreakInline();
processor.Inline = new LineBreakInline()
{
IsHard = true,
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column) },
Line = line,
Column = column
};
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
@@ -24,6 +25,7 @@ namespace Markdig.Parsers.Inlines
{
string entityName;
int entityValue;
var startPosition = slice.Start;
int match = HtmlHelper.ScanEntity(slice.Text, slice.Start, slice.Length, out entityName, out entityValue);
if (match == 0)
{
@@ -44,10 +46,15 @@ namespace Markdig.Parsers.Inlines
{
var matched = slice;
matched.End = match - 1;
int line;
int column;
processor.Inline = new HtmlEntityInline()
{
Original = matched,
Transcoded = new StringSlice(literal)
Transcoded = new StringSlice(literal),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End + 1)),
Line = line,
Column = column
};
slice.Start = slice.Start + match;
return true;

View File

@@ -8,7 +8,7 @@ using Markdig.Syntax.Inlines;
namespace Markdig.Parsers.Inlines
{
/// <summary>
/// An inline parser for <see cref="SoftlineBreakInline"/> and <see cref="HardlineBreakInline"/>.
/// An inline parser for <see cref="LineBreakInline"/>.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
public class LineBreakInlineParser : InlineParser
@@ -34,10 +34,20 @@ namespace Markdig.Parsers.Inlines
return false;
}
var startPosition = slice.Start;
var hasDoubleSpacesBefore = slice.PeekCharExtra(-1).IsSpace() && slice.PeekCharExtra(-2).IsSpace();
slice.NextChar(); // Skip \n
processor.Inline = !EnableSoftAsHard && (slice.Start == 0 || !hasDoubleSpacesBefore) ? (Inline)new SoftlineBreakInline() : new HardlineBreakInline();
int line;
int column;
processor.Inline = new LineBreakInline
{
Span = { Start = processor.GetSourcePosition(startPosition, out line, out column)},
IsHard = EnableSoftAsHard || (slice.Start != 0 && hasDoubleSpacesBefore),
Line = line,
Column = column
};
processor.Inline.Span.End = processor.Inline.Span.Start;
return true;
}
}

View File

@@ -28,6 +28,10 @@ namespace Markdig.Parsers.Inlines
var c = slice.CurrentChar;
int line;
int column;
var startPosition = processor.GetSourcePosition(slice.Start, out line, out column);
bool isImage = false;
if (c == '!')
{
@@ -47,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))
{
@@ -63,7 +68,11 @@ namespace Markdig.Parsers.Inlines
{
Type = DelimiterType.Open,
Label = label,
IsImage = isImage
LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan),
IsImage = isImage,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
return true;
@@ -86,7 +95,7 @@ namespace Markdig.Parsers.Inlines
return false;
}
private bool ProcessLinkReference(InlineProcessor state, string label, bool isImage, Inline child = null)
private bool ProcessLinkReference(InlineProcessor state, string label, SourceSpan labelSpan, LinkDelimiterInline parent, int endPosition)
{
bool isValidLink = false;
LinkReferenceDefinition linkRef;
@@ -96,7 +105,7 @@ namespace Markdig.Parsers.Inlines
// Try to use a callback directly defined on the LinkReferenceDefinition
if (linkRef.CreateLinkInline != null)
{
link = linkRef.CreateLinkInline(state, linkRef, child);
link = linkRef.CreateLinkInline(state, linkRef, parent.FirstChild);
}
// Create a default link if the callback was not found
@@ -107,19 +116,30 @@ namespace Markdig.Parsers.Inlines
{
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
IsImage = isImage,
Label = label,
LabelSpan = labelSpan,
IsImage = parent.IsImage,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
Line = parent.Line,
Column = parent.Column,
};
}
var containerLink = link as ContainerInline;
if (containerLink != null)
{
var child = parent.FirstChild;
if (child == null)
{
child = new LiteralInline()
{
Content = new StringSlice(label),
IsClosed = true
IsClosed = true,
// Not exact but we leave it like this
Span = parent.Span,
Line = parent.Line,
Column = parent.Column,
};
containerLink.AppendChild(child);
}
@@ -175,7 +195,10 @@ namespace Markdig.Parsers.Inlines
{
inlineState.Inline = new LiteralInline()
{
Content = new StringSlice("[")
Content = new StringSlice("["),
Span = openParent.Span,
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(inlineState.Inline);
return false;
@@ -192,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()
@@ -200,6 +225,12 @@ namespace Markdig.Parsers.Inlines
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
IsImage = openParent.IsImage,
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,
};
openParent.ReplaceBy(link);
@@ -224,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 ]
}
@@ -240,10 +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.IsImage,
openParent.FirstChild))
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
}
if (ProcessLinkReference(inlineState, label, labelSpan, openParent, inlineState.GetSourcePosition(text.Start - 1)))
{
// Remove the open parent
openParent.Remove();
@@ -267,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
@@ -33,6 +34,10 @@ namespace Markdig.Parsers.Inlines
{
var text = slice.Text;
int line;
int column;
var startPosition = processor.GetSourcePosition(slice.Start, out line, out column);
// Sligthly faster to perform our own search for opening characters
var nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End);
//var nextStart = str.IndexOfAny(processor.SpecialCharacters, slice.Start + 1, slice.Length - 1);
@@ -59,7 +64,15 @@ namespace Markdig.Parsers.Inlines
}
// The LiteralInlineParser is always matching (at least an empty string)
processor.Inline = length > 0 ? new LiteralInline {Content = new StringSlice(slice.Text, slice.Start, slice.Start + length - 1)} : new LiteralInline();
var endPosition = slice.Start + length - 1;
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;
// Call only PostMatch if necessary

View File

@@ -152,6 +152,9 @@ namespace Markdig.Parsers
return BlockState.Continue;
}
// Update list-item source end position
listItem.Span.End = state.Line.End;
return BlockState.Continue;
}
@@ -170,6 +173,9 @@ namespace Markdig.Parsers
state.GoToColumn(columWidth);
}
// Update list-item source end position
listItem.Span.End = state.Line.End;
return BlockState.Continue;
}
@@ -189,6 +195,8 @@ namespace Markdig.Parsers
var initColumnBeforeIndent = state.ColumnBeforeIndent;
var initColumn = state.Column;
var sourcePosition = state.Start;
var sourceEndPosition = state.Line.End;
var c = state.CurrentChar;
var itemParser = mapItemParsers[c];
@@ -249,7 +257,8 @@ namespace Markdig.Parsers
var newListItem = new ListItemBlock(this)
{
Column = initColumn,
ColumnWidth = columnWidth
ColumnWidth = columnWidth,
Span = new SourceSpan(sourcePosition, sourceEndPosition)
};
state.NewBlocks.Push(newListItem);
@@ -276,6 +285,7 @@ namespace Markdig.Parsers
var newList = new ListBlock(this)
{
Column = initColumn,
Span = new SourceSpan(sourcePosition, sourceEndPosition),
IsOrdered = isOrdered,
BulletType = listInfo.BulletType,
OrderedDelimiter = listInfo.OrderedDelimiter,
@@ -342,6 +352,12 @@ namespace Markdig.Parsers
isLastListItem = false;
}
// Update end-position for the list
if (listBlock.Count > 0)
{
listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
}
return true;
}
}

View File

@@ -26,19 +26,24 @@ namespace Markdig.Parsers
private readonly InlineProcessor inlineProcessor;
private readonly MarkdownDocument document;
private readonly ProcessDocumentDelegate documentProcessed;
private readonly bool preciseSourceLocation;
private LineReader lineReader;
/// <summary>
/// Initializes a new instance of the <see cref="MarkdownParser" /> class.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="text">The reader.</param>
/// <param name="pipeline">The pipeline.</param>
/// <exception cref="System.ArgumentNullException">
/// </exception>
private MarkdownParser(TextReader reader, MarkdownPipeline pipeline)
private MarkdownParser(string text, MarkdownPipeline pipeline)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
if (text == null) throw new ArgumentNullException(nameof(text));
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
Reader = reader;
text = FixupZero(text);
lineReader = new LineReader(text);
preciseSourceLocation = pipeline.PreciseSourceLocation;
// Initialize the pipeline
var stringBuilderCache = pipeline.StringBuilderCache ?? new StringBuilderCache();
@@ -53,7 +58,7 @@ namespace Markdig.Parsers
// Initialize the inline parsers
var inlineParserList = new InlineParserList();
inlineParserList.AddRange(pipeline.InlineParsers);
inlineProcessor = new InlineProcessor(stringBuilderCache, document, inlineParserList)
inlineProcessor = new InlineProcessor(stringBuilderCache, document, inlineParserList, pipeline.PreciseSourceLocation)
{
DebugLog = pipeline.DebugLog
};
@@ -64,25 +69,20 @@ namespace Markdig.Parsers
/// <summary>
/// Parses the specified markdown into an AST <see cref="MarkdownDocument"/>
/// </summary>
/// <param name="reader">A Markdown text from a <see cref="TextReader"/>.</param>
/// <param name="text">A Markdown text</param>
/// <param name="pipeline">The pipeline used for the parsing.</param>
/// <returns>An AST Markdown document</returns>
/// <exception cref="System.ArgumentNullException">if reader variable is null</exception>
public static MarkdownDocument Parse(TextReader reader, MarkdownPipeline pipeline = null)
public static MarkdownDocument Parse(string text, MarkdownPipeline pipeline = null)
{
if (reader == null) throw new ArgumentNullException(nameof(reader));
if (text == null) throw new ArgumentNullException(nameof(text));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
// Perform the parsing
var markdownParser = new MarkdownParser(reader, pipeline);
var markdownParser = new MarkdownParser(text, pipeline);
return markdownParser.Parse();
}
/// <summary>
/// Gets the text reader used.
/// </summary>
private TextReader Reader { get; }
/// <summary>
/// Parses the current <see cref="Reader"/> into a Markdown <see cref="MarkdownDocument"/>.
/// </summary>
@@ -101,17 +101,15 @@ namespace Markdig.Parsers
{
while (true)
{
// TODO: A TextReader doesn't allow to precisely track position in file due to line endings
var lineText = Reader.ReadLine();
// Get the precise position of the begining of the line
var lineText = lineReader.ReadLine();
// If this is the end of file and the last line is empty
if (lineText == null)
{
break;
}
lineText = FixupZero(lineText);
blockProcessor.ProcessLine(new StringSlice(lineText));
blockProcessor.ProcessLine(lineText.Value);
}
blockProcessor.CloseAll(true);
}

View File

@@ -20,7 +20,11 @@ namespace Markdig.Parsers
}
// We continue trying to match by default
processor.NewBlocks.Push(new ParagraphBlock(this) {Column = processor.Column});
processor.NewBlocks.Push(new ParagraphBlock(this)
{
Column = processor.Column,
Span = new SourceSpan(processor.Line.Start, processor.Line.End)
});
return BlockState.Continue;
}
@@ -35,6 +39,8 @@ namespace Markdig.Parsers
{
return TryParseSetexHeading(processor, block);
}
block.Span.End = processor.Line.End;
return BlockState.Continue;
}
@@ -121,6 +127,7 @@ namespace Markdig.Parsers
var heading = new HeadingBlock(this)
{
Column = paragraph.Column,
Span = new SourceSpan(paragraph.Span.Start, line.Start),
Level = level,
Lines = paragraph.Lines,
};
@@ -132,6 +139,8 @@ namespace Markdig.Parsers
return BlockState.BreakDiscard;
}
block.Span.End = state.Line.End;
return BlockState.Continue;
}

View File

@@ -28,6 +28,7 @@ namespace Markdig.Parsers
}
var column = processor.Column;
var sourcePosition = processor.Start;
// 5.1 Block quotes
// A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space.
@@ -37,7 +38,12 @@ namespace Markdig.Parsers
{
processor.NextColumn();
}
processor.NewBlocks.Push(new QuoteBlock(this) {QuoteChar = quoteChar, Column = column});
processor.NewBlocks.Push(new QuoteBlock(this)
{
QuoteChar = quoteChar,
Column = column,
Span = new SourceSpan(sourcePosition, processor.Line.End),
});
return BlockState.Continue;
}
@@ -55,6 +61,7 @@ namespace Markdig.Parsers
var c = processor.CurrentChar;
if (c != quote.QuoteChar)
{
block.Span.End = processor.Start - 1;
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
}
@@ -64,7 +71,18 @@ namespace Markdig.Parsers
processor.NextChar(); // Skip following space
}
block.Span.End = processor.Line.End;
return BlockState.Continue;
}
public override bool Close(BlockProcessor processor, Block block)
{
var quoteBlock = block as QuoteBlock;
if (quoteBlock?.LastChild != null)
{
quoteBlock.Span.End = quoteBlock.LastChild.Span.End;
}
return true;
}
}
}

View File

@@ -32,6 +32,8 @@ namespace Markdig.Parsers
return BlockState.None;
}
var startPosition = processor.Start;
var line = processor.Line;
// 4.1 Thematic breaks
@@ -83,7 +85,11 @@ namespace Markdig.Parsers
}
// Push a new block
processor.NewBlocks.Push(new ThematicBreakBlock(this) { Column = processor.Column });
processor.NewBlocks.Push(new ThematicBreakBlock(this)
{
Column = processor.Column,
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.4.0";
public const string Version = "0.5.5";
}
}

View File

@@ -11,7 +11,7 @@ namespace Markdig.Renderers.Html
/// <summary>
/// Attached HTML attributes to a <see cref="MarkdownObject"/>.
/// </summary>
public class HtmlAttributes
public class HtmlAttributes : MarkdownObject
{
/// <summary>
/// Initializes a new instance of the <see cref="HtmlAttributes"/> class.
@@ -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>

View File

@@ -1,26 +0,0 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Syntax.Inlines;
namespace Markdig.Renderers.Html.Inlines
{
/// <summary>
/// A HTML renderer for a <see cref="HardlineBreakInline"/>.
/// </summary>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.HardlineBreakInline}" />
public class HardlineBreakInlineRenderer : HtmlObjectRenderer<HardlineBreakInline>
{
protected override void Write(HtmlRenderer renderer, HardlineBreakInline obj)
{
if (renderer.EnableHtmlForInline)
{
renderer.WriteLine("<br />");
}
else
{
renderer.Write(" ");
}
}
}
}

View File

@@ -6,21 +6,21 @@ using Markdig.Syntax.Inlines;
namespace Markdig.Renderers.Html.Inlines
{
/// <summary>
/// A HTML renderer for a <see cref="SoftlineBreakInline"/>.
/// A HTML renderer for a <see cref="LineBreakInline"/>.
/// </summary>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.SoftlineBreakInline}" />
public class SoftlineBreakInlineRenderer : HtmlObjectRenderer<SoftlineBreakInline>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.LineBreakInline}" />
public class LineBreakInlineRenderer : HtmlObjectRenderer<LineBreakInline>
{
/// <summary>
/// Gets or sets a value indicating whether to render this softline break as a HTML hardline break tag (&lt;br /&gt;)
/// </summary>
public bool RenderAsHardlineBreak { get; set; }
protected override void Write(HtmlRenderer renderer, SoftlineBreakInline obj)
protected override void Write(HtmlRenderer renderer, LineBreakInline obj)
{
if (renderer.EnableHtmlForInline)
{
if (RenderAsHardlineBreak)
if (obj.IsHard || RenderAsHardlineBreak)
{
renderer.WriteLine("<br />");
}

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

@@ -39,8 +39,7 @@ namespace Markdig.Renderers
ObjectRenderers.Add(new CodeInlineRenderer());
ObjectRenderers.Add(new DelimiterInlineRenderer());
ObjectRenderers.Add(new EmphasisInlineRenderer());
ObjectRenderers.Add(new HardlineBreakInlineRenderer());
ObjectRenderers.Add(new SoftlineBreakInlineRenderer());
ObjectRenderers.Add(new LineBreakInlineRenderer());
ObjectRenderers.Add(new HtmlInlineRenderer());
ObjectRenderers.Add(new HtmlEntityInlineRenderer());
ObjectRenderers.Add(new LinkInlineRenderer());

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

@@ -23,16 +23,6 @@ namespace Markdig.Syntax
IsBreakable = true;
}
/// <summary>
/// Gets or sets the text column this instance was declared (zero-based).
/// </summary>
public int Column { get; set; }
/// <summary>
/// Gets or sets the text line this instance was declared (zero-based).
/// </summary>
public int Line { get; set; }
/// <summary>
/// Gets the parent of this container. May be null.
/// </summary>

View File

@@ -68,6 +68,11 @@ namespace Markdig.Syntax
}
children[Count++] = item;
item.Parent = this;
if (item.Span.End > Span.End)
{
Span.End = item.Span.End;
}
}
private void EnsureCapacity(int min)

View File

@@ -1,17 +0,0 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
namespace Markdig.Syntax.Inlines
{
/// <summary>
/// A hard line break (Section 6.9 CommonMark specs).
/// </summary>
/// <seealso cref="Markdig.Syntax.Inlines.LeafInline" />
[DebuggerDisplay("<br >/")]
public class HardlineBreakInline : LineBreakInline
{
}
}

View File

@@ -7,7 +7,8 @@ namespace Markdig.Syntax.Inlines
/// A base class for a line break.
/// </summary>
/// <seealso cref="Markdig.Syntax.Inlines.LeafInline" />
public abstract class LineBreakInline : LeafInline
public class LineBreakInline : LeafInline
{
public bool IsHard { get; set; }
}
}

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

@@ -1,17 +0,0 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Diagnostics;
namespace Markdig.Syntax.Inlines
{
/// <summary>
/// A soft line break (Section 6.10 CommonMark specs)
/// </summary>
/// <seealso cref="Markdig.Syntax.Inlines.LineBreakInline" />
[DebuggerDisplay("\\n")]
public class SoftlineBreakInline : LineBreakInline
{
}
}

View File

@@ -46,14 +46,15 @@ namespace Markdig.Syntax
/// <param name="slice">The slice.</param>
/// <param name="column">The column.</param>
/// <param name="line">The line.</param>
public void AppendLine(ref StringSlice slice, int column, int line)
/// <param name="sourceLinePosition"></param>
public void AppendLine(ref StringSlice slice, int column, int line, int sourceLinePosition)
{
if (Lines.Lines == null)
{
Lines = new StringLineGroup(4);
}
var stringLine = new StringLine(ref slice, line, column);
var stringLine = new StringLine(ref slice, line, column, sourceLinePosition);
// Regular case, we are not in the middle of a tab
if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column))
{

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)
@@ -18,6 +23,30 @@ namespace Markdig.Syntax
private DataEntry[] attachedDatas;
private int count;
/// <summary>
/// Gets or sets the text column this instance was declared (zero-based).
/// </summary>
public int Column { get; set; }
/// <summary>
/// Gets or sets the text line this instance was declared (zero-based).
/// </summary>
public int Line { get; set; }
/// <summary>
/// The source span
/// </summary>
public SourceSpan Span;
/// <summary>
/// Gets a string of the location in the text.
/// </summary>
/// <returns></returns>
public string ToPositionText()
{
return $"${Line}, {Column}, {Span.Start}-{Span.End}";
}
/// <summary>
/// Stores a key/value pair for this instance.
/// </summary>

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,6 +1,6 @@
{
"title": "Markdig",
"version": "0.4.0",
"version": "0.5.5",
"authors": [ "Alexandre Mutel" ],
"description": "A fast, powerfull, CommonMark compliant, extensible Markdown processor for .NET",
"copyright": "Alexandre Mutel",
@@ -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": "Breaking change: Rename several extension method UseXXX with an UseXXXs (e.g UseListExtras instead of UseListExtra). Fix pipe tables limitation to avoid conflict with list items.",
"releaseNotes": "> 0.5.5\n- Add same github class for task lists\n- Add pragma lines extension\n> 0.5.4:\nFix bug in HTML block parsing which could break parsing of remaining document",
"tags": [ "Markdown CommonMark md html md2html" ]
},
"configurations": {