Compare commits

...

107 Commits

Author SHA1 Message Date
Alexandre Mutel
e1032e5094 Bump version to 0.7.5 2016-09-18 11:21:17 +02:00
Alexandre Mutel
a32ac298c5 Merge branch 'pr/n58_christophano'
# Conflicts:
#	src/Markdig/Parsers/InlineProcessor.cs
2016-09-18 11:12:10 +02:00
Alexandre Mutel
2e6ab670cb Keep UrlSpan (and LabelSpan and TitleSpan) for a LinkReferenceDefinition (issue #51) 2016-09-18 11:07:56 +02:00
Alexandre Mutel
72b7fce48c Fix extension DisableHtml not working properly with inline HTML (issue #46) 2016-09-18 10:59:12 +02:00
Alexandre Mutel
fa281f1ca1 Fix pipetable parsing (issue #44) 2016-09-18 10:44:26 +02:00
Chris Rodgers
1b92311aeb Fixes issue when using colon (among others) on lines with abbreviations 2016-09-15 14:18:58 +01:00
Alexandre Mutel
a593212f03 Merge pull request #45 from christophano/feature/rowspan
Adds support for rowspans in grid tables
2016-09-15 09:03:44 +02:00
Alexandre Mutel
c4ec928953 Merge pull request #57 from christophano/bugfix/punctuation-exceptions
Adds exceptions to allow certain punctuation characters to behave as inline delimiters
2016-09-15 09:00:57 +02:00
Chris Rodgers
e7df7fabeb Adds exceptions to allow certain punctuation characters to behave as inline delimiters 2016-09-14 12:00:31 +01:00
Alexandre Mutel
1c187f2d81 Merge pull request #56 from christophano/bugfix/similar-abbreviations
Fixes problem with multiple, similar abbreviations not parsing correctly
2016-09-14 07:42:26 +02:00
Alexandre Mutel
da66cf90c3 Merge pull request #55 from christophano/bugfix/restart-list-after-paragraph
Checks if paragraph block is closed when deciding if continuation list is allowable with character other than '1.'
2016-09-14 07:40:33 +02:00
Alexandre Mutel
0e8bd7407f Merge pull request #49 from christophano/bugfix/nested-definition-lists
Fixes issue where multiple definition lists are created when they are nested in a list item.
2016-09-14 07:39:24 +02:00
Chris Rodgers
d70f14addb Fixes problem with multiple, similar abbreviations not parsing correctly. 2016-09-13 16:53:25 +01:00
Chris Rodgers
e7b9eea3a5 Checks if paragraph block is closed when deciding if continuation list is allowable with character other than '1.'
Fixes #54
2016-09-09 14:09:52 +01:00
Alexandre Mutel
97af9d822d Merge pull request #50 from christophano/bugfix/ordered-list-start-attribute
Changes List Extra to output valid integer for start attribute, instead of roman or latin character.
2016-08-27 09:07:46 +02:00
Alexandre Mutel
cf7a09ab76 Merge pull request #52 from christophano/bugfix/merge-auto-generated-id
fixes issue where using special attributes overwites element id, even when id is not specified
2016-08-27 09:05:22 +02:00
Chris Rodgers
e755627421 fixes issue where using special attributes overwites element id, even when id is not specified 2016-08-25 09:11:50 +01:00
Chris Rodgers
f9be64a988 Changes List Extra to output valid integer for start attribute, instead of roman or latin character. 2016-08-23 13:58:06 +01:00
Alexandre Mutel
d65431e6cc Merge pull request #47 from christophano/bugfix/inline-maths-with-trailing-text
Fixes issue where trailing text after an inline math block is both included in the math block and appended after the math block.
2016-08-23 08:19:01 +02:00
Chris Rodgers
86fb962fdb Removes BlankLineBlock if it is found after an active definition list. 2016-08-22 15:41:39 +01:00
Chris Rodgers
d003837b27 Fixes issue where multiple definition lists are created when they are nested in a list item. 2016-08-22 14:47:43 +01:00
Chris Rodgers
fd813e3c5a Fixes issue where trailing text after an inline math block is both included in the math block and appended after the math block. 2016-08-19 12:15:17 +01:00
Chris Rodgers
a3691c4423 Fixed issue when malformed tables can result in an unhandled exception being thrown. 2016-08-12 15:57:35 +01:00
Chris Rodgers
9506f22025 Adds support for rowspans in grid tables 2016-08-12 15:57:28 +01:00
Alexandre Mutel
06ae907949 Bump version to 0.7.4 2016-07-30 11:35:36 +02:00
Alexandre Mutel
555523b2af Add additional tests for emphasis (issue #43) 2016-07-30 11:33:45 +02:00
Alexandre Mutel
3b9772f772 Merge branch 'pr/n43_christophano' 2016-07-30 11:20:48 +02:00
Chris Rodgers
891c80f48c Fixes issue where sentences containing strong emphasis words did not correctly delimit the strong tag 2016-07-28 12:04:49 +01:00
Alexandre Mutel
105b09e1ec Bump version to 0.7.3 2016-07-23 12:47:04 +02:00
Alexandre Mutel
9fe7596a23 Update project.json for WebApp to .NET Core RTM 2016-07-23 12:46:48 +02:00
Alexandre Mutel
82af7cadc5 Fix issue with MarkdownPipeline that was not threadsafe and initialization was deferred at parsing time instead of pipeline build time (issue #40) 2016-07-23 12:46:26 +02:00
Alexandre Mutel
800c81bb9a Bump version to 0.7.2 2016-07-20 11:03:07 +02:00
Alexandre Mutel
5653a4f7ee Render table colspan with optional decimals for percentage 2016-07-20 10:51:05 +02:00
Alexandre Mutel
4f14ffe63b Merge commit 'fba96774f427ccdc08b5cb89a6dd02e84b2ef9d6' from capjan/markdig 2016-07-20 10:31:03 +02:00
Alexandre Mutel
d57acefe56 Merge pull request #39 from christophano/bugfix/cell-after-colspan
Bugfix colspan for grid tables
2016-07-19 10:06:25 +02:00
Alexandre Mutel
fb61e5a8da Update to latest CommonMark 0.26 specs. Two consecutive blank lines in a list no longer break it. Ordered list can break a paragraph if it starts at '1' 2016-07-19 09:41:59 +02:00
Chris Rodgers
264516bfdb Updated tests via md file and regenerated from t4 2016-07-19 08:40:34 +01:00
Chris Rodgers
6d8f8996d5 Fixes cell contents not displaying after colspan merged cell 2016-07-19 08:38:05 +01:00
Chris Rodgers
ba8557d3bf Corrects spec for TestExtensionsGridTable Example003 2016-07-19 08:38:05 +01:00
Alexandre Mutel
d18fd0b957 Link to a fixed version of the CommonMark specs to avoid unexpected upgrades to latest changes in the specs 2016-07-19 08:03:23 +02:00
Jan Ruhländer
fba96774f4 Fix: Table extension column width doesn't work on computers that use a comma as demimal point 2016-07-12 14:47:35 +02:00
Alexandre Mutel
0b2764ea62 Merge pull request #36 from synhershko/norel-links
Adding support for rel=nofollow in links
2016-07-10 08:14:52 +09:00
Itamar Syn-Hershko
64d81ed47b Removing config 2016-07-10 02:01:53 +03:00
Itamar Syn-Hershko
9a9742888b Fixing last bits 2016-07-10 01:47:38 +03:00
Itamar Syn-Hershko
5400b30a90 Fixing for renderer.EnableHtmlForInline case 2016-07-10 00:27:50 +03:00
Itamar Syn-Hershko
673f4a4beb Avoid adding rel to images 2016-07-10 00:13:51 +03:00
Itamar Syn-Hershko
05a27649aa Adding support for rel=nofollow in links 2016-07-09 23:56:58 +03:00
Alexandre Mutel
26c6b12dea Merge pull request #35 from ChrisMissal/readme-changes
Update readme.md, fix typos
2016-07-08 09:09:28 +09:00
Chris Missal
9a18ca222f Update readme.md
fixes typos
2016-07-07 18:30:09 -05:00
Alexandre Mutel
354f31b870 Fix calculation of span for HTML entities 2016-07-06 06:45:48 +09:00
Alexandre Mutel
c6c2f58ec0 Add support for diagrams extension. wip with mermaid (issue #34) 2016-07-05 23:24:37 +09:00
Alexandre Mutel
4a146f00f6 Bump to 0.7.1 2016-06-30 21:56:05 +09:00
Alexandre Mutel
2c10ac59d3 Update to latest specs 2016-06-30 21:54:18 +09:00
Alexandre Mutel
9a98fb9453 Fix issue InvalidCastException with SmartyPants extension when using both a mdash with quotes. 2016-06-30 21:53:53 +09:00
Alexandre Mutel
1c7f843a4c Update readme for building Markdig (issue #28) 2016-06-27 22:55:21 +09:00
Alexandre Mutel
fdaf301bd6 Update to latest dotnet Core RTM, NETStandard.Library 1.6.0. Bump to version 0.7.0 2016-06-27 21:44:31 +09:00
Alexandre Mutel
5a9f2f3afe Fix calculated span for indented code blocks that was starting after the first 4 chars and not before (issue https://github.com/madskristensen/MarkdownEditor/issues/8) 2016-06-27 20:32:24 +09:00
Alexandre Mutel
6a1b30761a Merge branch 'pr/n30_Jither' 2016-06-27 06:59:13 +09:00
Jimmi Thøgersen
8f8b08fad6 Include digits after first letter in Urilize (for Auto-Identifiers)
Added dependency: NUnit3TestAdapter (for VS Test Runner)
Disabled "Prefer 32-bit" on Markdig.Tests to accommodate NUnit.
2016-06-26 23:39:43 +02:00
Alexandre Mutel
312b63a4c8 Bump to 0.6.2 2016-06-25 18:27:06 +09:00
Alexandre Mutel
1598b538ab Update to latest CommonMark spec. Handle new corner cases for emphasis (see https://talk.commonmark.org/t/emphasis-strong-emphasis-corner-cases/2123/1) 2016-06-25 18:25:24 +09:00
Alexandre Mutel
16ce46a741 Bump to 0.6.1 2016-06-24 20:33:30 +09:00
Alexandre Mutel
4456598228 Double check all extensions with advanced profiles to make sure there are no conflicts between existing extensions 2016-06-24 17:44:57 +09:00
Alexandre Mutel
2a937c63b9 Fix issue with auto identifiers extension overriding manual ids for headings 2016-06-24 17:43:46 +09:00
Alexandre Mutel
191bc940c7 Bump version to 0.6.0 2016-06-24 16:10:58 +09:00
Alexandre Mutel
05673758e3 Add new SelfPipeline extension 2016-06-24 15:05:37 +09:00
Alexandre Mutel
cff2b9a8ca Fix conflict between SmartyPants extension and pipetables (issue #13) 2016-06-24 12:04:29 +09:00
Alexandre Mutel
586095a475 Refactor StringSlice.Search to StringSlice.IndexOf to make it more familiar and consistent 2016-06-24 12:03:16 +09:00
Alexandre Mutel
eae2082a1e Rename IDelimiterProcessor to IPostInlineProcessor as it can be used outside of a delimiter processing (for example SmartyPants mdash) 2016-06-24 12:02:25 +09:00
Alexandre Mutel
4576548df3 Add comment to Descendants() method that should be implemented without recursive 2016-06-24 12:01:03 +09:00
Alexandre Mutel
266e0c8bfd Fix consecutive footnotes without blanklines between (issue #26) 2016-06-24 06:01:53 +09:00
Alexandre Mutel
f5c07dbab5 Bump to 0.5.12 2016-06-24 05:43:17 +09:00
Alexandre Mutel
c8a28a1ad7 Fix an issue where a consecutive footnote is parsed as a paragraph (issue #26) 2016-06-24 05:42:36 +09:00
Alexandre Mutel
72cc454314 Remove backsticks in SmartyPantsInlineParser. Rename SmaryPantsInlineParser to SmartyPantsInlineParser 2016-06-24 05:27:12 +09:00
Alexandre Mutel
7fe2c1f939 Remove OrderedList.ReplacyBy method, as it is not used and might be not predictable (issue #25) 2016-06-24 05:25:54 +09:00
Alexandre Mutel
087e7a68b6 Fix emoji parsing and allow them only if they are surrounded by spaces (issue #15) 2016-06-23 17:28:33 +09:00
Alexandre Mutel
abeabf15a1 Remove non necessary check in HtmlAttributes.CopyTo 2016-06-23 11:49:01 +09:00
Alexandre Mutel
f9bfcaab7b Bump version to 0.5.11 2016-06-23 11:46:22 +09:00
Alexandre Mutel
afa0182f02 Fix ArgumentNullReference exception in HtmlAttributes.CopyTo (issue #23) 2016-06-23 11:46:07 +09:00
Alexandre Mutel
b83de5934d Bump version to 0.5.10 2016-06-23 05:52:06 +09:00
Alexandre Mutel
c294d3bfb4 Improve compact method for tests taking into account \r\n and \r 2016-06-23 05:52:06 +09:00
Alexandre Mutel
a7cdb2351a Fix regression for html block parsing (issue #21) 2016-06-23 05:52:05 +09:00
Alexandre Mutel
46ef21a3ed Merge pull request #19 from jorrit/patch-1
Fix typo in MarkdownExtensions.cs
2016-06-22 20:55:51 +09:00
Jorrit Schippers
18c8d0178c Fix typo in MarkdownExtensions.cs 2016-06-22 13:55:09 +02:00
Alexandre Mutel
ebc79dafbd Bump version to 0.5.9 2016-06-22 08:37:37 +09:00
Alexandre Mutel
a1d2467643 Fix bug for deep nested list items that were incorrectly detected (not CommonMark compliant. Check with CommonMark specs to add a test case) 2016-06-22 08:35:42 +09:00
Alexandre Mutel
8220f0fa56 Fix ArgumentOutOfRangeException in Block.FindClosestLine. Add tests for Pragmalines 2016-06-22 08:35:08 +09:00
Alexandre Mutel
ec385acc7f Bump version to 0.5.8 2016-06-21 23:45:48 +09:00
Alexandre Mutel
04c1cc62d4 Add block extension methods for looking for a block from a position or for the closest line (typically used for syntax highlighting by MarkdownEditor) 2016-06-21 23:45:40 +09:00
Alexandre Mutel
abdbd65f60 Fix calculation of spans for nested blocks 2016-06-21 23:45:28 +09:00
Alexandre Mutel
1f32a060da Update readme and link to MarkdownEditor 2016-06-20 15:05:39 +09:00
Alexandre Mutel
699d80c150 Add missing test for issue #16 2016-06-20 13:48:22 +09:00
Alexandre Mutel
56bcac7600 Bump version to 0.5.7 2016-06-20 13:42:32 +09:00
Alexandre Mutel
cab3365104 Fix issue while detecting generic attributes that was breaking parsing (issue #16) 2016-06-20 13:42:15 +09:00
Alexandre Mutel
3821bd00fe Bump version to 0.5.6 2016-06-20 12:30:25 +09:00
Alexandre Mutel
62701fd0f1 Improve pragma line output 2016-06-20 12:29:13 +09:00
Alexandre Mutel
1be5e60506 Make to MarkdownPipeline.Setup public. Make Markdown.ToHtml returning a MarkdownDocument 2016-06-20 12:27:50 +09:00
Alexandre Mutel
c9f1512358 Bump to 0.5.5 2016-06-20 09:06:29 +09:00
Alexandre Mutel
8f23aed6af Add pragma line extension 2016-06-20 09:04:33 +09:00
Alexandre Mutel
6a62ae9c69 Add github like class for taslk lists (issue #14) 2016-06-20 09:04:13 +09:00
Alexandre Mutel
2c3de5688b Don't add a class an HtmlAttributes that is already in the list 2016-06-20 09:00:54 +09:00
Alexandre Mutel
f3c08b4ec4 Output HtmlAttributes for unordered list 2016-06-20 09:00:31 +09:00
Alexandre Mutel
69e3baafe5 Add support for callbacks to RendererBase, IMarkdownRenderer 2016-06-20 09:00:10 +09:00
Alexandre Mutel
5844ccc395 Bump to 0.5.4 2016-06-20 06:58:55 +09:00
Alexandre Mutel
be9c6fa54b Fix bug for html block parsing in StringSlice.Search (issue #12) 2016-06-20 06:56:13 +09:00
Alexandre Mutel
d14f277c7b Bump version to 0.5.3 2016-06-19 11:37:26 +09:00
Alexandre Mutel
593bf08b92 Fix bug in pipetables with a trailing | on the header row separator (issue #10) 2016-06-19 11:37:12 +09:00
102 changed files with 6909 additions and 4391 deletions

View File

@@ -2,7 +2,7 @@
<img align="right" width="160px" height="160px" src="img/markdig.png">
Markdig is a fast, powerfull, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
Markdig is a fast, powerful, [CommonMark](http://commonmark.org/) compliant, extensible Markdown processor for .NET.
> NOTE: The repository is under construction. There will be a dedicated website and proper documentation at some point!
@@ -10,12 +10,13 @@ You can **try Markdig online** and compare it to other implementations on [babel
## Features
- **Very fast parser** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree**
- **Very fast parser and html renderer** (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- **Abstract Syntax Tree** with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout [MarkdownEditor for Visual Studio](https://visualstudiogallery.msdn.microsoft.com/eaab33c3-437b-4918-8354-872dfe5d1bfe) powered by Markdig!
- Converter to **HTML**
- Passing more than **600+ tests** from the latest [CommonMark specs](http://spec.commonmark.org/)
- Includes all the core elements of CommonMark:
- including GFM fenced code blocks.
- including **GFM fenced code blocks**.
- **Extensible** architecture
- Even the core Markdown/CommonMark parsing is pluggable, so it allows to disable builtin Markdown/Commonmark parsing (e.g [Disable HTML parsing](https://github.com/lunet-io/markdig/blob/7964bd0160d4c18e4155127a4c863d61ebd8944a/src/Markdig/MarkdownExtensions.cs#L306)) or change behaviour (e.g change matching `#` of a headers with `@`)
- Built-in with **20+ extensions**, including:
@@ -79,6 +80,10 @@ var result = Markdown.ToHtml("This is a text with some *emphasis*", pipeline);
You can have a look at the [MarkdownExtensions](https://github.com/lunet-io/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) that describes all actionable extensions (by modifying the MarkdownPipeline)
## Build
In order to build Markdig, you need to install [.NET Core RTM](https://www.microsoft.com/net/core)
## License
This software is released under the [BSD-Clause 2 license](https://github.com/lunet-io/markdig/blob/master/license.txt).
@@ -110,7 +115,7 @@ This is an early preview of the benchmarking against various implementations:
### Analysis of the results:
- Markdig is roughly **x100 times faster than MarkdownSharp**, **30x times faster than docfx**
- **Among the best in CPU**, Extremelly competitive and often faster than other implementations (not feature wise equivalent)
- **Among the best in CPU**, Extremely competitive and often faster than other implementations (not feature wise equivalent)
- **15% to 30% less allocations** and GC pressure
Because Marked.NET, MarkdownSharp and DocAsCode.MarkdownLite are way too slow, they are not included in the following charts:

View File

@@ -24,6 +24,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
@@ -59,9 +60,12 @@
<DesignTime>True</DesignTime>
<DependentUpon>Specs.tt</DependentUpon>
</Compile>
<Compile Include="Specs\TestEmphasisPlus.cs" />
<Compile Include="TestHtmlAttributes.cs" />
<Compile Include="TestHtmlHelper.cs" />
<Compile Include="TestLineReader.cs" />
<Compile Include="TestLinkHelper.cs" />
<Compile Include="TestPragmaLines.cs" />
<Compile Include="TestSourcePosition.cs" />
<Compile Include="TestStringSliceList.cs" />
<Compile Include="TestPlayParser.cs" />
@@ -83,6 +87,8 @@
<None Include="Specs\GridTableSpecs.md" />
<None Include="Specs\HardlineBreakSpecs.md" />
<None Include="Specs\BootstrapSpecs.md" />
<None Include="Specs\DiagramsSpecs.md" />
<None Include="Specs\NoHtmlSpecs.md" />
<None Include="Specs\TaskListSpecs.md" />
<None Include="Specs\SmartyPantsSpecs.md" />
<None Include="Specs\MediaSpecs.md" />
@@ -98,6 +104,7 @@
</ItemGroup>
<ItemGroup>
<Service Include="{508349B6-6B84-4DF5-91F0-309BEEBAD82D}" />
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@@ -45,3 +45,15 @@ This is a 😃 HTML document
.
<p>This is a <abbr title="Hypertext Markup Language">😃 HTML</abbr> document</p>
````````````````````````````````
Abbreviations may be similar:
```````````````````````````````` example
*[1A]: First
*[1A1]: Second
*[1A2]: Third
We can abbreviate 1A, 1A1 and 1A2!
.
<p>We can abbreviate <abbr title="First">1A</abbr>, <abbr title="Second">1A1</abbr> and <abbr title="Third">1A2</abbr>!</p>
````````````````````````````````

View File

@@ -105,3 +105,28 @@ Term 1
<pre><code>: Not valid
</code></pre>
````````````````````````````````
Definition lists can be nested inside list items
```````````````````````````````` example
1. First
2. Second
Term 1
: Definition
Term 2
: Second Definition
.
<ol>
<li><p>First</p></li>
<li><p>Second</p>
<dl>
<dt>Term 1</dt>
<dd>Definition</dd>
<dt>Term 2</dt>
<dd>Second Definition</dd>
</dl></li>
</ol>
````````````````````````````````

View File

@@ -0,0 +1,26 @@
# Extensions
Adds support for diagrams extension:
## Mermaid diagrams
Using a fenced code block with the `mermaid` language info will output a `<div class='mermaid'>` instead of a `pre/code` block:
```````````````````````````````` example
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
.
<div class="mermaid">graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
</div>
````````````````````````````````
TODO: Add other text diagram languages

View File

@@ -11,3 +11,11 @@ This is a test with a :) and a :angry: smiley
.
<p>This is a test with a 😃 and a 😠 smiley</p>
````````````````````````````````
An emoji needs to be preceded by a space and followed by a space:
```````````````````````````````` example
These are not:) an :)emoji with a:) x:angry:x
.
<p>These are not:) an :)emoji with a:) x:angry:x</p>
````````````````````````````````

View File

@@ -21,6 +21,17 @@ H~2~O is a liquid. 2^10^ is 1024
.
<p>H<sub>2</sub>O is a liquid. 2<sup>10</sup> is 1024</p>
````````````````````````````````
Certain punctuation characters are exempted from the rule forbidding them within inline delimiters
```````````````````````````````` example
One quintillionth can be expressed as 10^-18^
Daggers^†^ and double-daggers^‡^ can be used to denote notes.
.
<p>One quintillionth can be expressed as 10<sup>-18</sup></p>
<p>Daggers<sup>†</sup> and double-daggers<sup>‡</sup> can be used to denote notes.</p>
````````````````````````````````
## Inserted

View File

@@ -61,3 +61,60 @@ multi-paragraph list items.<a href="#fnref:3" class="footnote-back-ref">&#8617;<
</div>
````````````````````````````````
Check with mulitple consecutive footnotes:
```````````````````````````````` example
Here is a footnote[^1]. And another one[^2]. And a third one[^3]. And a fourth[^4].
[^1]: Footnote 1 text
[^2]: Footnote 2 text
a
[^3]: Footnote 3 text
[^4]: Footnote 4 text
.
<p>Here is a footnote<a id="fnref:1" href="#fn:1" class="footnote-ref"><sup>1</sup></a>. And another one<a id="fnref:2" href="#fn:2" class="footnote-ref"><sup>2</sup></a>. And a third one<a id="fnref:3" href="#fn:3" class="footnote-ref"><sup>3</sup></a>. And a fourth<a id="fnref:4" href="#fn:4" class="footnote-ref"><sup>4</sup></a>.</p>
<p>a</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:1">
<p>Footnote 1 text<a href="#fnref:1" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:2">
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:3">
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:4">
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">&#8617;</a></p></li>
</ol>
</div>
````````````````````````````````
Another test with consecutive footnotes without a blank line separator:
```````````````````````````````` example
Here is a footnote[^1]. And another one[^2]. And a third one[^3]. And a fourth[^4].
[^1]: Footnote 1 text
[^2]: Footnote 2 text
[^3]: Footnote 3 text
[^4]: Footnote 4 text
.
<p>Here is a footnote<a id="fnref:1" href="#fn:1" class="footnote-ref"><sup>1</sup></a>. And another one<a id="fnref:2" href="#fn:2" class="footnote-ref"><sup>2</sup></a>. And a third one<a id="fnref:3" href="#fn:3" class="footnote-ref"><sup>3</sup></a>. And a fourth<a id="fnref:4" href="#fn:4" class="footnote-ref"><sup>4</sup></a>.</p>
<div class="footnotes">
<hr />
<ol>
<li id="fn:1">
<p>Footnote 1 text<a href="#fnref:1" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:2">
<p>Footnote 2 text<a href="#fnref:2" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:3">
<p>Footnote 3 text<a href="#fnref:3" class="footnote-back-ref">&#8617;</a></p></li>
<li id="fn:4">
<p>Footnote 4 text<a href="#fnref:4" class="footnote-back-ref">&#8617;</a></p></li>
</ol>
</div>
````````````````````````````````

View File

@@ -70,8 +70,8 @@ A regular row can continue a previous regular row when column separator `|` are
+---------+---------+---------+
| Col1 | Col2 | Col3 |
| Col1a | Col2a | Col3a |
| Col12 | Col3b |
| Col123 |
| Col1b | Col3b |
| Col1c |
.
<table>
<col style="width:33.33%">
@@ -87,11 +87,11 @@ Col2a</td>
Col3a</td>
</tr>
<tr>
<td colspan="2">Col12</td>
<td></td>
<td colspan="2">Col1b</td>
<td>Col3b</td>
</tr>
<tr>
<td colspan="3">Col123</td>
<td colspan="3">Col1c</td>
</tr>
</tbody>
</table>
@@ -184,3 +184,92 @@ Alignment might be specified on the first row using the character `:`:
</tbody>
</table>
````````````````````````````````
A grid table may have cells spanning both columns and rows:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+---+---+ B +
| D | E | B |
+ D +---+---+
| D | CCCCC |
+---+---+---+
.
<table>
<col style="width:33.33%">
<col style="width:33.33%">
<col style="width:33.33%">
<tbody>
<tr>
<td colspan="2">AAAAA</td>
<td rowspan="2">B
B
B</td>
</tr>
<tr>
<td rowspan="2">D
D
D</td>
<td>E</td>
</tr>
<tr>
<td colspan="2">CCCCC</td>
</tr>
</tbody>
</table>
````````````````````````````````
A grid table may have cells with both colspan and rowspan:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+ AAAAA +---+
| AAAAA | C |
+---+---+---+
| D | E | F |
+---+---+---+
.
<table>
<col style="width:33.33%">
<col style="width:33.33%">
<col style="width:33.33%">
<tbody>
<tr>
<td colspan="2" rowspan="2">AAAAA
AAAAA
AAAAA</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
<tr>
<td>D</td>
<td>E</td>
<td>F</td>
</tr>
</tbody>
</table>
````````````````````````````````
A grid table may not have irregularly shaped cells:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+ A +---+ B +
| A | C | B |
+---+---+---+
| DDDDD | E |
+---+---+---+
.
<p>+---+---+---+
| AAAAA | B |
+ A +---+ B +
| A | C | B |
+---+---+---+
| DDDDD | E |
+---+---+---+</p>
````````````````````````````````

View File

@@ -38,7 +38,7 @@ Like for numbered list, a list can start with a different letter
b. First item
c. Second item
.
<ol type="a" start="b">
<ol type="a" start="2">
<li>First item</li>
<li>Second item</li>
</ol>
@@ -100,8 +100,26 @@ Like for numbered list, a list can start with a different letter
ii. First item
iii. Second item
.
<ol type="i" start="ii">
<ol type="i" start="2">
<li>First item</li>
<li>Second item</li>
</ol>
````````````````````````````````
Lists can be restarted, specifying the start point.
```````````````````````````````` example
1. First item
Some text
2. Second item
.
<ol>
<li>First item</li>
</ol>
<p>Some text</p>
<ol start="2">
<li>Second item</li>
</ol>
````````````````````````````````

View File

@@ -68,6 +68,13 @@ This is a $$$math block$$$
<p>This is a <span class="math">$math block$</span></p>
````````````````````````````````
Regular text can come both before and after the math inline
```````````````````````````````` example
This is a $math block$ with text on both sides.
.
<p>This is a <span class="math">math block</span> with text on both sides.</p>
````````````````````````````````
A mathematic block takes precedence over standard emphasis `*` `_`:
```````````````````````````````` example

View File

@@ -0,0 +1,27 @@
# Extensions
## NoHTML
The extension DisableHtml allows to disable the parsing of HTML:
For inline HTML:
```````````````````````````````` example
this is some text</td></tr>
.
<p>this is some text&lt;/td&gt;&lt;/tr&gt;</p>
````````````````````````````````
For Block HTML:
```````````````````````````````` example
<div>
this is some text
</div>
.
<p>&lt;div&gt;
this is some text
&lt;/div&gt;</p>
````````````````````````````````

View File

@@ -122,7 +122,7 @@ c no d
c no d</p>
````````````````````````````````
The number of columns in the first row determine the number of columns for the whole table. Any extra columns delimiter `|` for sub-sequent lines are converted to literal strings instead:
If a row contains more column than the header row, it will still be added as a column:
```````````````````````````````` example
a | b
@@ -141,7 +141,8 @@ a | b
<tbody>
<tr>
<td>0</td>
<td>1 | 2</td>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
@@ -214,6 +215,76 @@ Column delimiters `|` at the very beginning of a line or just before a line endi
</tbody>
</table>
````````````````````````````````
A pipe may be present at both the beginning/ending of each line:
```````````````````````````````` example
|a|b|
|-|-|
|0|1|
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
Or may be ommitted on one side:
```````````````````````````````` example
a|b|
-|-|
0|1|
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
```````````````````````````````` example
|a|b
|-|-
|0|1
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
Single column table can be declared with lines starting only by a column delimiter:
```````````````````````````````` example
@@ -307,6 +378,31 @@ The text alignment can be changed by using the character `:` with the header col
</table>
````````````````````````````````
Test alignment with starting and ending pipes:
```````````````````````````````` example
| abc | def | ghi |
|:---:|-----|----:|
| 1 | 2 | 3 |
.
<table>
<thead>
<tr>
<th style="text-align: center;">abc</th>
<th>def</th>
<th style="text-align: right;">ghi</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;">1</td>
<td>2</td>
<td style="text-align: right;">3</td>
</tr>
</tbody>
</table>
````````````````````````````````
The following example shows a non matching header column separator:
```````````````````````````````` example
@@ -412,3 +508,42 @@ a | b
</table>
````````````````````````````````
** Tests **
Tests trailing spaces after pipes
```````````````````````````````` example
| abc | def |
|---|:---|
| cde| ddd|
| eee| fff|
| fff | fffff |
|gggg | ffff |
.
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
<tbody>
<tr>
<td>cde</td>
<td>ddd</td>
</tr>
<tr>
<td>eee</td>
<td>fff</td>
</tr>
<tr>
<td>fff</td>
<td>fffff</td>
</tr>
<tr>
<td>gggg</td>
<td>ffff</td>
</tr>
</tbody>
</table>
````````````````````````````````

View File

@@ -83,29 +83,6 @@ They are' not matching 'quotes
.
<p>They are' not matching 'quotes</p>
````````````````````````````````
Double quotes using ``` `` ``` are working if they match another `''` pair, and there is no other double quotes on the line (otherwise they would be parsed as a code span):
```````````````````````````````` example
This is ``a double quote''
.
<p>This is &ldquo;a double quote&rdquo;</p>
````````````````````````````````
```````````````````````````````` example
This is ``a code span''``
.
<p>This is <code>a code span''</code></p>
````````````````````````````````
```````````````````````````````` example
hello ``there```
test
.
<p>hello &ldquo;there&rdquo;`
test</p>
````````````````````````````````
An emphasis starting inside left/right quotes will span over the right quote:
```````````````````````````````` example
@@ -133,3 +110,36 @@ This is a en ellipsis...
.
<p>This is a en ellipsis&hellip;</p>
````````````````````````````````
Check that a smartypants are not breaking pipetable parsing:
```````````````````````````````` example
a | b
-- | --
0 | 1
.
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
````````````````````````````````
Check quotes and dash:
```````````````````````````````` example
A "quote" with a ---
.
<p>A &ldquo;quote&rdquo; with a &mdash;</p>
````````````````````````````````

File diff suppressed because it is too large Load Diff

View File

@@ -38,25 +38,28 @@ SOFTWARE.
<#@ import namespace="System.CodeDom.Compiler" #>
<#@ output extension=".cs" #><#
var specFiles = new KeyValuePair<string, string>[] {
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables"),
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes"),
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes"),
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras"),
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak"),
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables"),
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes"),
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes"),
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis"),
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations"),
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras"),
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations"),
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics"),
// new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/91e045ca370258903ed138450373043a496ec64b/spec.txt", string.Empty), // 0.26 specs
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak|advanced+hardlinebreak"),
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis|advanced+emojis"),
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("BootstrapSpecs.md"), "bootstrap+pipetables+figures+attributes"),
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "medialinks"),
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "smartypants"),
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers"),
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists"),
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "medialinks|advanced+medialinks"),
new KeyValuePair<string, string>(Host.ResolvePath("SmartyPantsSpecs.md"), "pipetables+smartypants|advanced+smartypants"), // Check with smartypants to make sure that it doesn't break pipetables
new KeyValuePair<string, string>(Host.ResolvePath("AutoIdentifierSpecs.md"), "autoidentifiers|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("TaskListSpecs.md"), "tasklists|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("DiagramsSpecs.md"), "diagrams|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("NoHtmlSpecs.md"), "nohtml"),
};
var emptyLines = false;
var displayEmptyLines = false;

View File

@@ -12,10 +12,10 @@ A task list item consist of `[ ]` or `[x]` or `[X]` inside a list item (ordered
- [ ] Item3
- Item4
.
<ul>
<li><input disabled="disabled" type="checkbox" /> Item1</li>
<li><input disabled="disabled" type="checkbox" checked="checked" /> Item2</li>
<li><input disabled="disabled" type="checkbox" /> Item3</li>
<ul class="contains-task-list">
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item1</li>
<li class="task-list-item"><input disabled="disabled" type="checkbox" checked="checked" /> Item2</li>
<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Item3</li>
<li>Item4</li>
</ul>
````````````````````````````````

View File

@@ -0,0 +1,25 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using NUnit.Framework;
namespace Markdig.Tests
{
[TestFixture]
public partial class TestEmphasisPlus
{
[Test]
public void StrongNormal()
{
TestParser.TestSpec("***Strong emphasis*** normal", "<p><strong><em>Strong emphasis</em></strong> normal</p>", "");
}
[Test]
public void NormalStrongNormal()
{
TestParser.TestSpec("normal ***Strong emphasis*** normal", "<p>normal <strong><em>Strong emphasis</em></strong> normal</p>", "");
}
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers.Html;
using NUnit.Framework;
using System.Collections.Generic;
namespace Markdig.Tests
{
[TestFixture()]
public class TestHtmlAttributes
{
[Test]
public void TestAddClass()
{
var attributes = new HtmlAttributes();
attributes.AddClass("test");
Assert.NotNull(attributes.Classes);
Assert.AreEqual(new List<string>() { "test" }, attributes.Classes);
attributes.AddClass("test");
Assert.AreEqual(1, attributes.Classes.Count);
attributes.AddClass("test1");
Assert.AreEqual(new List<string>() { "test", "test1" }, attributes.Classes);
}
[Test]
public void TestAddProperty()
{
var attributes = new HtmlAttributes();
attributes.AddProperty("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
attributes.AddPropertyIfNotExist("key1", "1");
Assert.NotNull(attributes.Properties);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, attributes.Properties);
attributes.AddPropertyIfNotExist("key2", "2");
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, attributes.Properties);
}
[Test]
public void TestCopyTo()
{
var from = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
var to = new HtmlAttributes();
from.CopyTo(to);
Assert.True(ReferenceEquals(from.Classes, to.Classes));
Assert.True(ReferenceEquals(from.Properties, to.Properties));
// From: Classes From: Properties To: Classes To: Properties
// test1: null null null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.CopyTo(to, false, false);
Assert.Null(to.Classes);
Assert.Null(to.Properties);
// test2: ["test"] ["key1", "1"] null null
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test");
from.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1")}, to.Properties);
// test3: null null ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1") }, to.Properties);
// test4: ["test1"] ["key2", "2"] ["test"] ["key1", "1"]
from = new HtmlAttributes();
to = new HtmlAttributes();
from.AddClass("test1");
from.AddProperty("key2", "2");
to.AddClass("test");
to.AddProperty("key1", "1");
from.CopyTo(to, false, false);
Assert.AreEqual(new List<string>() { "test", "test1" }, to.Classes);
Assert.AreEqual(new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("key1", "1"), new KeyValuePair<string, string>("key2", "2") }, to.Properties);
}
}
}

View File

@@ -297,5 +297,102 @@ namespace Markdig.Tests
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<ab"), out text, out isEmail));
Assert.False(LinkHelper.TryParseAutolink(new StringSlice(@"<user@>"), out text, out isEmail));
}
[TestCase("Header identifiers in HTML", "header-identifiers-in-html")]
[TestCase("* Dogs*?--in *my* house?", "dogs-in-my-house")] // Not Pandoc equivalent: dogs--in...
[TestCase("[HTML], [S5], or [RTF]?", "html-s5-or-rtf")]
[TestCase("3. Applications", "applications")]
[TestCase("33", "")]
public void TestUrilizeNonAscii_Pandoc(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("abc", "abc")]
[TestCase("a-c", "a-c")]
[TestCase("a c", "a-c")]
[TestCase("a_c", "a_c")]
[TestCase("a.c", "a.c")]
[TestCase("a,c", "ac")]
[TestCase("a--", "a")] // Not Pandoc-equivalent: a--
[TestCase("a__", "a")] // Not Pandoc-equivalent: a__
[TestCase("a..", "a")] // Not Pandoc-equivalent: a..
[TestCase("a??", "a")]
[TestCase("a ", "a")]
[TestCase("a--d", "a-d")]
[TestCase("a__d", "a_d")]
[TestCase("a??d", "ad")]
[TestCase("a d", "a-d")]
[TestCase("a..d", "a.d")]
[TestCase("-bc", "bc")]
[TestCase("_bc", "bc")]
[TestCase(" bc", "bc")]
[TestCase("?bc", "bc")]
[TestCase(".bc", "bc")]
[TestCase("a-.-", "a")] // Not Pandoc equivalent: a-.-
public void TestUrilizeOnlyAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bær", "br")]
[TestCase("bør", "br")]
[TestCase("bΘr", "br")]
[TestCase("四五", "")]
public void TestUrilizeOnlyAscii_NonAscii(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("bár", "bar")]
[TestCase("àrrivé", "arrive")]
public void TestUrilizeOnlyAscii_Normalization(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("123", "")]
[TestCase("1,-b", "b")]
[TestCase("b1,-", "b1")] // Not Pandoc equivalent: b1-
[TestCase("ab3", "ab3")]
[TestCase("ab3de", "ab3de")]
public void TestUrilizeOnlyAscii_Numeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, true));
}
[TestCase("一二三四五", "一二三四五")]
[TestCase("一,-b", "一-b")]
public void TestUrilizeNonAscii_NonAsciiNumeric(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
[TestCase("bær", "bær")]
[TestCase("æ5el", "æ5el")]
[TestCase("-æ5el", "æ5el")]
[TestCase("-frø-", "frø")]
[TestCase("-fr-ø", "fr-ø")]
public void TestUrilizeNonAscii_Simple(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
// Just to be sure, test for characters expressly forbidden in URI fragments:
[TestCase("b#r", "br")]
[TestCase("b%r", "br")] // Invalid except as an escape character
[TestCase("b^r", "br")]
[TestCase("b[r", "br")]
[TestCase("b]r", "br")]
[TestCase("b{r", "br")]
[TestCase("b}r", "br")]
[TestCase("b<r", "br")]
[TestCase("b>r", "br")]
[TestCase(@"b\r", "br")]
[TestCase(@"b""r", "br")]
public void TestUrilizeNonAscii_NonValidCharactersForFragments(string input, string expectedResult)
{
Assert.AreEqual(expectedResult, LinkHelper.Urilize(input, false));
}
}
}

View File

@@ -64,7 +64,9 @@ namespace Markdig.Tests
var extensionGroups = extensionsGroupText.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var extensionsText in extensionGroups)
{
var pipeline = new MarkdownPipelineBuilder().Configure(extensionsText);
var builder = new MarkdownPipelineBuilder();
builder.DebugLog = Console.Out;
var pipeline = extensionsText == "self" ? builder.UseSelfPipeline() : builder.Configure(extensionsText);
yield return new KeyValuePair<string, MarkdownPipeline>(extensionsText, pipeline.Build());
}
}
@@ -78,7 +80,7 @@ namespace Markdig.Tests
private static string Compact(string html)
{
// Normalize the output to make it compatible with CommonMark specs
html = html.Replace("\r", "").Trim();
html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim();
html = Regex.Replace(html, @"\s+</li>", "</li>");
html = Regex.Replace(html, @"<li>\s+", "<li>");
html = html.Normalize(NormalizationForm.FormKD);

View File

@@ -27,6 +27,85 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
Console.WriteLine(result);
}
[Test]
public void TestSelfPipeline1()
{
var text = @" <!--markdig:pipetables-->
a | b
- | -
0 | 1
";
TestParser.TestSpec(text, @"<!--markdig:pipetables-->
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
", "self");
}
[Test]
public void TestListBug()
{
// TODO: Add this test back to the CommonMark specs
var text = @"- item1
- item2
- item3
- item4";
TestParser.TestSpec(text, @"<ul>
<li>item1
<ul>
<li>item2
<ul>
<li>item3
<ul>
<li>item4</li>
</ul></li>
</ul></li>
</ul></li>
</ul>
");
}
[Test]
public void TestHtmlBug()
{
TestParser.TestSpec(@" # header1
<pre class='copy'>
blabla
</pre>
# header2
", @"<h1>header1</h1>
<pre class='copy'>
blabla
</pre>
<h1>header2</h1>");
}
[Test]
public void TestBugAdvancaed()
{
TestParser.TestSpec(@"`https://{domain}/callbacks`
#### HEADING
Paragraph
", "<p><code>https://{domain}/callbacks</code></p>\n<h4 id=\"heading\">HEADING</h4>\n<p>Paragraph</p>", "advanced");
}
[Test]
public void TestSamePipelineAllExtensions()
{

View File

@@ -0,0 +1,81 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
{
[TestFixture]
public class TestPragmaLines
{
[Test]
public void TestFindClosest()
{
var doc = Markdown.Parse(
"test1\n" + // 0
"\n" + // 1
"test2\n" + // 2
"\n" + // 3
"test3\n" + // 4
"\n" + // 5
"test4\n" + // 6
"\n" + // 7
"# Heading\n" + // 8
"\n" + // 9
"Long para\n" + // 10
"on multiple\n" + // 11
"lines\n" + // 12
"to check that\n" + // 13
"lines are\n" + // 14
"correctly \n" + // 15
"found\n" + // 16
"\n" + // 17
"- item1\n" + // 18
"- item2\n" + // 19
"- item3\n" + // 20
"\n" + // 21
"This is a last paragraph\n" // 22
, new MarkdownPipelineBuilder().UsePragmaLines().Build());
foreach (var exact in new int[] {0, 2, 4, 6, 8, 10, 18, 19, 20, 22})
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
Assert.AreEqual(22, doc.FindClosestLine(23));
Assert.AreEqual(10, doc.FindClosestLine(11));
Assert.AreEqual(10, doc.FindClosestLine(12));
Assert.AreEqual(10, doc.FindClosestLine(13));
Assert.AreEqual(18, doc.FindClosestLine(14)); // > 50% of the paragraph, we switch to next
Assert.AreEqual(18, doc.FindClosestLine(15));
Assert.AreEqual(18, doc.FindClosestLine(16));
}
[Test]
public void TestFindClosest1()
{
var text =
"- item1\n" + // 0
" - item11\n" + // 1
" - item12\n" + // 2
" - item121\n" + // 3
" - item13\n" + // 4
" - item131\n" + // 5
" - item1311\n"; // 6
var pipeline = new MarkdownPipelineBuilder().UsePragmaLines().Build();
var doc = Markdown.Parse(text, pipeline);
for (int exact = 0; exact < 7; exact++)
{
Assert.AreEqual(exact, doc.FindClosestLine(exact));
}
Assert.AreEqual(6, doc.FindClosestLine(50));
}
}
}

View File

@@ -88,9 +88,9 @@ literal ( 0, 5) 5-11
Check("01*2**3*", @"
paragraph ( 0, 0) 0-7
literal ( 0, 0) 0-1
emphasis ( 0, 2) 2-4
emphasis ( 0, 2) 2-7
literal ( 0, 3) 3-3
emphasis ( 0, 5) 5-7
literal ( 0, 4) 4-5
literal ( 0, 6) 6-6
");
}
@@ -206,19 +206,6 @@ literal ( 0, 4) 4-5
Assert.AreEqual(SourceSpan.Empty, link.TitleSpan);
}
[Test]
public void TestHtmlInline()
{
// 0123456789
Check("01<b>4</b>", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-1
html ( 0, 2) 2-4
literal ( 0, 5) 5-5
html ( 0, 6) 6-9
");
}
[Test]
public void TestAutolinkInline()
{
@@ -252,6 +239,60 @@ literal ( 4, 0) 16-16
");
}
[Test]
public void TestHtmlBlock1()
{
// 0 1
// 01 2 345678901 23
Check("0\n\n<!--A-->\n1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
html ( 2, 0) 3-10
paragraph ( 3, 0) 12-12
literal ( 3, 0) 12-12
");
}
[Test]
public void TestHtmlComment()
{
// 0 1 2
// 012345678901 234567890 1234
Check("# 012345678\n<!--0-->\n123\n", @"
heading ( 0, 0) 0-10
literal ( 0, 2) 2-10
html ( 1, 0) 12-19
paragraph ( 2, 0) 21-23
literal ( 2, 0) 21-23
");
}
[Test]
public void TestHtmlInline()
{
// 0123456789
Check("01<b>4</b>", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-1
html ( 0, 2) 2-4
literal ( 0, 5) 5-5
html ( 0, 6) 6-9
");
}
[Test]
public void TestHtmlInline1()
{
// 0
// 0123456789
Check("0<!--A-->1", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-0
html ( 0, 1) 1-8
literal ( 0, 9) 9-9
");
}
[Test]
public void TestThematicBreak()
{
@@ -328,12 +369,13 @@ literal ( 0, 2) 2-3
[Test]
public void TestHtmlEntityInline()
{
// 01234567
Check("0&nbsp;1", @"
paragraph ( 0, 0) 0-7
// 01 23456789
Check("0\n&nbsp; 1", @"
paragraph ( 0, 0) 0-9
literal ( 0, 0) 0-0
htmlentity ( 0, 1) 1-6
literal ( 0, 7) 7-7
linebreak ( 0, 1) 1-1
htmlentity ( 1, 0) 2-7
literal ( 1, 6) 8-9
");
}
@@ -613,7 +655,7 @@ literal ( 4, 2) 13-13
Check("0\n\n 0\n 1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 4) 7-13
code ( 2, 0) 3-13
");
}
@@ -624,7 +666,7 @@ code ( 2, 4) 7-13
Check("0\n\n\t0\n\t1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 4) 4-7
code ( 2, 0) 3-7
");
}
@@ -635,7 +677,7 @@ code ( 2, 4) 4-7
Check("0\n\n \t0\n \t1\n", @"
paragraph ( 0, 0) 0-0
literal ( 0, 0) 0-0
code ( 2, 4) 5-9
code ( 2, 0) 3-9
");
}

View File

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

View File

@@ -1,25 +1,25 @@
{
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0-rc2-3002702",
"version": "1.0.0",
"type": "platform"
},
"Microsoft.ApplicationInsights.AspNetCore": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-rc2-final",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final",
"Microsoft.Extensions.Logging": "1.0.0-rc2-final",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final",
"Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-final",
"Markdig": "0.2.1"
"Microsoft.ApplicationInsights.AspNetCore": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.0",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Markdig": "0.7.2"
},
"tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": {
"version": "1.0.0-preview1-final",
"version": "1.0.0-preview2-final",
"imports": "portable-net45+win8+dnxcore50"
}
},

View File

@@ -126,12 +126,13 @@ namespace Markdig.Extensions.Abbreviations
// If we don't have a container, create a new one
if (container == null)
{
container = new ContainerInline()
{
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
container = literal.Parent ??
new ContainerInline
{
Span = originalLiteral.Span,
Line = originalLiteral.Line,
Column = originalLiteral.Column,
};
}
int line;
@@ -150,7 +151,10 @@ namespace Markdig.Extensions.Abbreviations
// Append the previous literal
if (i > content.Start)
{
container.AppendChild(literal);
if (literal.Parent == null)
{
container.AppendChild(literal);
}
literal.Span.End = abbrInline.Span.Start - 1;
// Truncate it before the abbreviation

View File

@@ -132,6 +132,13 @@ namespace Markdig.Extensions.AutoIdentifiers
return;
}
// If id is already set, don't try to modify it
var attributes = processor.Block.GetAttributes();
if (attributes.Id != null)
{
return;
}
// Use a HtmlRenderer with
stripRenderer.Render(headingBlock.Inline);
var headingText = headingWriter.ToString();
@@ -152,7 +159,7 @@ namespace Markdig.Extensions.AutoIdentifiers
headingBuffer.Length = 0;
}
processor.Block.GetAttributes().Id = headingId;
attributes.Id = headingId;
}
}
}

View File

@@ -51,8 +51,7 @@ namespace Markdig.Extensions.DefinitionLists
}
var previousParent = paragraphBlock.Parent;
var indexOfParagraph = previousParent.IndexOf(paragraphBlock);
var currentDefinitionList = indexOfParagraph - 1 >= 0 ? previousParent[indexOfParagraph - 1] as DefinitionList : null;
var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent);
processor.Discard(paragraphBlock);
@@ -98,11 +97,24 @@ namespace Markdig.Extensions.DefinitionLists
processor.Open(definitionItem);
// Update the end position
currentDefinitionList.Span.End = processor.Line.End;
currentDefinitionList.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}
private static DefinitionList GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent)
{
var index = previousParent.IndexOf(paragraphBlock) - 1;
if (index < 0) return null;
var lastBlock = previousParent[index];
if (lastBlock is BlankLineBlock)
{
lastBlock = previousParent[index - 1];
previousParent.RemoveAt(index);
}
return lastBlock as DefinitionList;
}
public override BlockState TryContinue(BlockProcessor processor, Block block)
{
var definitionItem = (DefinitionItem)block;

View File

@@ -0,0 +1,31 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Diagrams
{
/// <summary>
/// Extension to allow diagrams.
/// </summary>
/// <seealso cref="Markdig.IMarkdownExtension" />
public class DiagramExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
var codeRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>();
// TODO: Add other well known diagram languages
codeRenderer.BlocksAsDiv.Add("mermaid");
}
}
}
}

View File

@@ -41,7 +41,7 @@ namespace Markdig.Extensions.Emoji
/// </summary>
public Dictionary<string, string> SmileyToEmoji { get; }
public override void Initialize(InlineProcessor processor)
public override void Initialize()
{
var firstChars = new HashSet<char>();
var textToMatch = new HashSet<string>();
@@ -67,18 +67,34 @@ namespace Markdig.Extensions.Emoji
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
string match;
// Previous char must be a space
if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero())
{
return false;
}
// Try to match an existing emoji
var startPosition = slice.Start;
if (!textMatchHelper.TryMatch(slice.Text, slice.Start, slice.Length, out match))
{
return false;
}
// Following char must be a space
if (!slice.PeekCharExtra(match.Length).IsWhiteSpaceOrZero())
{
return false;
}
// If we have a smiley, we decode it to emoji
string emoji;
if (!SmileyToEmoji.TryGetValue(match, out emoji))
{
emoji = match;
}
// Decode the eomji to unicode
string unicode;
if (!EmojiToUnicode.TryGetValue(emoji, out unicode))
{

View File

@@ -114,7 +114,7 @@ namespace Markdig.Extensions.Figures
figure.Add(caption);
}
figure.Span.End = line.End;
figure.UpdateSpanEnd(line.End);
// Don't keep the last line
return BlockState.BreakDiscard;
@@ -123,7 +123,7 @@ namespace Markdig.Extensions.Figures
// Reset the indentation to the column before the indent
processor.GoToColumn(processor.ColumnBeforeIndent);
figure.Span.End = line.End;
figure.UpdateSpanEnd(line.End);
return BlockState.Continue;
}

View File

@@ -80,7 +80,7 @@ namespace Markdig.Extensions.Footers
{
processor.NextChar(); // Skip following space
}
block.Span.End = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
}
return result;
}

View File

@@ -26,9 +26,14 @@ namespace Markdig.Extensions.Footnotes
}
public override BlockState TryOpen(BlockProcessor processor)
{
return TryOpen(processor, false);
}
private BlockState TryOpen(BlockProcessor processor, bool isContinue)
{
// We expect footnote to appear only at document level and not indented more than a code indent block
if (processor.IsCodeIndent || processor.CurrentContainer.GetType() != typeof(MarkdownDocument) )
if (processor.IsCodeIndent || (!isContinue && processor.CurrentContainer.GetType() != typeof(MarkdownDocument)) || (isContinue && !(processor.CurrentContainer is Footnote)))
{
return BlockState.None;
}
@@ -42,7 +47,7 @@ namespace Markdig.Extensions.Footnotes
processor.GoToColumn(saved);
return BlockState.None;
}
// Advance the column
int deltaColumn = processor.Start - start;
processor.Column = processor.Column + deltaColumn;
@@ -88,9 +93,23 @@ namespace Markdig.Extensions.Footnotes
return BlockState.ContinueDiscard;
}
if (footnote.IsLastLineEmpty && processor.Column == 0)
if (processor.Column == 0)
{
return BlockState.Break;
if (footnote.IsLastLineEmpty)
{
// Close the current footnote
processor.Close(footnote);
// Parse any opening footnote
return TryOpen(processor);
}
// Make sure that consecutive footnotes without a blanklines are parsed correctly
if (TryOpen(processor, true) == BlockState.Continue)
{
processor.Close(footnote);
return BlockState.Continue;
}
}
}
footnote.IsLastLineEmpty = false;

View File

@@ -46,7 +46,7 @@ namespace Markdig.Extensions.GenericAttributes
// Try to find if there is any attributes { in the info string on the first line of a FencedCodeBlock
if (line.Start < line.End)
{
var indexOfAttributes = line.Text.LastIndexOf('{', line.End);
int indexOfAttributes = line.IndexOf('{');
if (indexOfAttributes >= 0)
{
// Work on a copy

View File

@@ -64,7 +64,7 @@ namespace Markdig.Extensions.GenericAttributes
}
var currentHtmlAttributes = objectToAttach.GetAttributes();
attributes.CopyTo(currentHtmlAttributes);
attributes.CopyTo(currentHtmlAttributes, true, false);
// Update the position of the attributes
int line;

View File

@@ -7,6 +7,8 @@ using Markdig.Parsers;
namespace Markdig.Extensions.ListExtras
{
using System;
/// <summary>
/// Parser that adds supports for parsing alpha/roman list items (e.g: `a)` or `a.` or `ii.` or `II.`)
/// </summary>
@@ -52,7 +54,7 @@ namespace Markdig.Extensions.ListExtras
c = state.NextChar();
}
result.OrderedStart = state.Line.Text.Substring(startChar, endChar - startChar + 1);
result.OrderedStart = CharHelper.RomanToArabic(state.Line.Text.Substring(startChar, endChar - startChar + 1)).ToString();
result.BulletType = isRomanLow ? 'i' : 'I';
result.DefaultOrderedStart = isRomanLow ? "i" : "I";
}
@@ -60,7 +62,7 @@ namespace Markdig.Extensions.ListExtras
{
// otherwise we expect a regular alpha lettered list with a single character.
var isUpper = c.IsAlphaUpper();
result.OrderedStart = c.ToString();
result.OrderedStart = (Char.ToUpper(c) - 64).ToString();
result.BulletType = isUpper ? 'A' : 'a';
result.DefaultOrderedStart = isUpper ? "A" : "a";
state.NextChar();

View File

@@ -13,7 +13,7 @@ namespace Markdig.Extensions.Mathematics
/// An inline parser for <see cref="MathInline"/>.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
/// <seealso cref="IPostInlineProcessor" />
public class MathInlineParser : InlineParser
{
/// <summary>
@@ -63,6 +63,7 @@ namespace Markdig.Extensions.Mathematics
int closeDollars = 0;
var start = slice.Start;
var end = 0;
pc = match;
while (c != '\0')
{
@@ -101,7 +102,7 @@ namespace Markdig.Extensions.Mathematics
{
return false;
}
end = slice.Start - 1;
// Create a new MathInline
int line;
int column;
@@ -116,7 +117,7 @@ namespace Markdig.Extensions.Mathematics
};
inline.Content.Start = start;
// We substract the end to the number of opening $ to keep inside the block the additionals $
inline.Content.End = inline.Content.End - openDollars;
inline.Content.End = end - openDollars;
// Add the default class if necessary
if (DefaultClass != null)

View File

@@ -0,0 +1,34 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;
namespace Markdig.Extensions.NoRefLinks
{
/// <summary>
/// Extension to automatically render rel=nofollow to all links in an HTML output.
/// </summary>
public class NoFollowLinksExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(IMarkdownRenderer renderer)
{
var linkRenderer = renderer.ObjectRenderers.Find<LinkInlineRenderer>();
if (linkRenderer != null)
{
linkRenderer.AutoRelNoFollow = true;
}
var autolinkRenderer = renderer.ObjectRenderers.Find<AutolinkInlineRenderer>();
if (autolinkRenderer != null)
{
autolinkRenderer.AutoRelNoFollow = true;
}
}
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.PragmaLines
{
/// <summary>
/// Extension to a span for each line containing the original line id (using id = pragma-line#line_number_zero_based)
/// </summary>
/// <seealso cref="Markdig.IMarkdownExtension" />
public class PragmaLineExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.DocumentProcessed -= PipelineOnDocumentProcessed;
pipeline.DocumentProcessed += PipelineOnDocumentProcessed;
}
public void Setup(IMarkdownRenderer renderer)
{
}
private static void PipelineOnDocumentProcessed(MarkdownDocument document)
{
int index = 0;
AddPragmas(document, ref index);
}
private static void AddPragmas(Block block, ref int index)
{
var attribute = block.GetAttributes();
var pragmaId = GetPragmaId(block);
if ( attribute.Id == null)
{
attribute.Id = pragmaId;
}
else if (block.Parent != null)
{
var heading = block as HeadingBlock;
// If we have a heading, we will try to add the tag inside it
// otherwise we will add it just before
var tag = $"<a id=\"{pragmaId}\"></a>";
if (heading?.Inline?.FirstChild != null)
{
heading.Inline.FirstChild.InsertBefore(new HtmlInline() { Tag = tag });
}
else
{
block.Parent.Insert(index, new HtmlBlock(null) { Lines = new StringLineGroup(tag) });
index++;
}
}
var container = block as ContainerBlock;
if (container != null)
{
for (int i = 0; i < container.Count; i++)
{
var subBlock = container[i];
AddPragmas(subBlock, ref i);
}
}
}
private static string GetPragmaId(Block block)
{
return $"pragma-line-{block.Line}";
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using Markdig.Renderers;
namespace Markdig.Extensions.SelfPipeline
{
/// <summary>
/// Extension to enable SelfPipeline, to configure a Markdown parsing/convertion to HTML automatically
/// from an embedded special tag in the input text <code>&lt;!--markdig:extensions--&gt;</code> where extensions is a string
/// that specifies the extensions to use for the pipeline as exposed by <see cref="MarkdownExtensions.Configure"/> extension method
/// on the <see cref="MarkdownPipelineBuilder"/>. This extension will invalidate all other extensions and will override them.
/// </summary>
public sealed class SelfPipelineExtension : IMarkdownExtension
{
public const string DefaultTag = "markdig";
/// <summary>
/// Initializes a new instance of the <see cref="SelfPipelineExtension"/> class.
/// </summary>
/// <param name="tag">The matching start tag.</param>
/// <param name="defaultExtensions">The default extensions.</param>
/// <exception cref="System.ArgumentException">Tag cannot contain `<` or `>` characters</exception>
public SelfPipelineExtension(string tag = null, string defaultExtensions = null)
{
tag = tag?.Trim();
tag = string.IsNullOrEmpty(tag) ? DefaultTag : tag;
if (tag.IndexOfAny(new []{'<', '>'}) >= 0)
{
throw new ArgumentException("Tag cannot contain `<` or `>` characters", nameof(tag));
}
if (defaultExtensions != null)
{
// Check that this default pipeline is supported
// Will throw an ArgumentInvalidException if not
new MarkdownPipelineBuilder().Configure(defaultExtensions);
}
DefaultExtensions = defaultExtensions;
SelfPipelineHintTagStart = "<!--" + tag + ":";
}
/// <summary>
/// Gets the default pipeline to configure if no tag was found in the input text. Default is <c>null</c> (core pipeline).
/// </summary>
public string DefaultExtensions { get; }
/// <summary>
/// Gets the self pipeline hint tag start that will be matched.
/// </summary>
public string SelfPipelineHintTagStart { get; }
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Make sure that this pipeline has only one extension (itself)
if (pipeline.Extensions.Count > 1)
{
throw new InvalidOperationException(
"The SelfPipeline extension cannot be configured with other extensions");
}
}
public void Setup(IMarkdownRenderer renderer)
{
}
/// <summary>
/// Creates a pipeline automatically configured from an input markdown based on the presence of the configuration tag.
/// </summary>
/// <param name="inputText">The input text.</param>
/// <returns>The pipeline configured from the input</returns>
/// <exception cref="System.ArgumentNullException"></exception>
public MarkdownPipeline CreatePipelineFromInput(string inputText)
{
if (inputText == null) throw new ArgumentNullException(nameof(inputText));
var builder = new MarkdownPipelineBuilder();
string defaultConfig = DefaultExtensions;
var indexOfSelfPipeline = inputText.IndexOf(SelfPipelineHintTagStart, StringComparison.OrdinalIgnoreCase);
if (indexOfSelfPipeline >= 0)
{
var optionStart = indexOfSelfPipeline + SelfPipelineHintTagStart.Length;
var endOfTag = inputText.IndexOf("-->", optionStart, StringComparison.OrdinalIgnoreCase);
if (endOfTag >= 0)
{
defaultConfig = inputText.Substring(optionStart, endOfTag - optionStart).Trim();
}
}
if (!string.IsNullOrEmpty(defaultConfig))
{
builder.Configure(defaultConfig);
}
return builder.Build();
}
}
}

View File

@@ -28,10 +28,10 @@ namespace Markdig.Extensions.SmartyPants
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<SmaryPantsInlineParser>())
if (!pipeline.InlineParsers.Contains<SmartyPantsInlineParser>())
{
// Insert the parser after the code span parser
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(new SmaryPantsInlineParser());
pipeline.InlineParsers.InsertAfter<CodeInlineParser>(new SmartyPantsInlineParser());
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.SmartyPants
@@ -11,14 +12,14 @@ namespace Markdig.Extensions.SmartyPants
/// <summary>
/// The inline parser for SmartyPants.
/// </summary>
public class SmaryPantsInlineParser : InlineParser
public class SmartyPantsInlineParser : InlineParser, IPostInlineProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="SmaryPantsInlineParser"/> class.
/// Initializes a new instance of the <see cref="SmartyPantsInlineParser"/> class.
/// </summary>
public SmaryPantsInlineParser()
public SmartyPantsInlineParser()
{
OpeningCharacters = new[] {'`', '\'', '"', '<', '>', '.', '-'};
OpeningCharacters = new[] {'\'', '"', '<', '>', '.', '-'};
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
@@ -30,6 +31,8 @@ namespace Markdig.Extensions.SmartyPants
// " “ ” &ldquo; &rdquo; 'left-double-quote', 'right-double-quote'
// << >> « » &laquo; &raquo; 'left-angle-quote', 'right-angle-quote'
// ... … &hellip; 'ellipsis'
// Special case: &ndash; and &mdash; are handle as a PostProcess step to avoid conflicts with pipetables header separator row
// -- &ndash; 'ndash'
// --- — &mdash; 'mdash'
@@ -44,13 +47,6 @@ namespace Markdig.Extensions.SmartyPants
switch (c)
{
case '`':
if (slice.PeekChar(1) == '`')
{
slice.NextChar();
type = SmartyPantType.DoubleQuote; // We will resolve them at the end of parsing all inlines
}
break;
case '\'':
type = SmartyPantType.Quote; // We will resolve them at the end of parsing all inlines
if (slice.PeekChar(1) == '\'')
@@ -83,12 +79,9 @@ namespace Markdig.Extensions.SmartyPants
case '-':
if (slice.NextChar() == '-')
{
type = SmartyPantType.Dash2;
if (slice.PeekChar(1) == '-')
{
slice.NextChar();
type = SmartyPantType.Dash3;
}
var quotePants = GetOrCreateState(processor);
quotePants.HasDash = true;
return false;
}
break;
}
@@ -178,11 +171,8 @@ namespace Markdig.Extensions.SmartyPants
// We will check in a post-process step for balanaced open/close quotes
if (postProcess)
{
var quotePants = processor.ParserStates[Index] as List<SmartyPant>;
if (quotePants == null)
{
processor.ParserStates[Index] = quotePants = new List<SmartyPant>();
}
var quotePants = GetOrCreateState(processor);
// Register only if we don't have yet any quotes
if (quotePants.Count == 0)
{
@@ -195,11 +185,21 @@ namespace Markdig.Extensions.SmartyPants
return true;
}
private ListSmartyPants GetOrCreateState(InlineProcessor processor)
{
var quotePants = processor.ParserStates[Index] as ListSmartyPants;
if (quotePants == null)
{
processor.ParserStates[Index] = quotePants = new ListSmartyPants();
}
return quotePants;
}
private void BlockOnProcessInlinesEnd(InlineProcessor processor, Inline inline)
{
processor.Block.ProcessInlinesEnd -= BlockOnProcessInlinesEnd;
var pants = (List<SmartyPant>) processor.ParserStates[Index];
var pants = (ListSmartyPants) processor.ParserStates[Index];
// We only change quote into left or right quotes if we find proper balancing
var previousIndices = new int[3] {-1, -1, -1};
@@ -289,5 +289,92 @@ namespace Markdig.Extensions.SmartyPants
pants.Clear();
}
bool IPostInlineProcessor.PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex,
bool isFinalProcessing)
{
// Don't try to process anything if there are no dash
var quotePants = state.ParserStates[Index] as ListSmartyPants;
if (quotePants == null || !quotePants.HasDash)
{
return true;
}
var child = root;
var pendingContainers = new Stack<Inline>();
while (true)
{
while (child != null)
{
var next = child.NextSibling;
if (child is LiteralInline)
{
var literal = (LiteralInline) child;
var startIndex = 0;
var indexOfDash = literal.Content.IndexOf("--", startIndex);
if (indexOfDash >= 0)
{
var type = SmartyPantType.Dash2;
if (literal.Content.PeekCharAbsolute(indexOfDash + 2) == '-')
{
type = SmartyPantType.Dash3;
}
var nextContent = literal.Content;
var originalSpan = literal.Span;
literal.Span.End -= literal.Content.End - indexOfDash + 1;
literal.Content.End = indexOfDash - 1;
nextContent.Start = indexOfDash + (type == SmartyPantType.Dash2 ? 2 : 3);
var pant = new SmartyPant()
{
Span = new SourceSpan(literal.Content.End + 1, nextContent.Start - 1),
Line = literal.Line,
Column = literal.Column,
OpeningCharacter = '-',
Type = type
};
literal.InsertAfter(pant);
var postLiteral = new LiteralInline()
{
Span = new SourceSpan(pant.Span.End + 1, originalSpan.End),
Line = literal.Line,
Column = literal.Column,
Content = nextContent
};
pant.InsertAfter(postLiteral);
// Use the pending literal to proceed further
next = postLiteral;
}
}
else if (child is ContainerInline)
{
pendingContainers.Push(((ContainerInline)child).FirstChild);
}
child = next;
}
if (pendingContainers.Count > 0)
{
child = pendingContainers.Pop();
}
else
{
break;
}
}
return true;
}
private class ListSmartyPants : List<SmartyPant>
{
public bool HasDash { get; set; }
}
}
}

View File

@@ -1,6 +1,9 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Syntax;
@@ -10,7 +13,7 @@ namespace Markdig.Extensions.Tables
{
public GridTableParser()
{
OpeningCharacters = new[] {'+'};
OpeningCharacters = new[] { '+' };
}
public override BlockState TryOpen(BlockProcessor processor)
@@ -22,65 +25,47 @@ namespace Markdig.Extensions.Tables
}
var line = processor.Line;
// A grid table must start with a line like this:
// + ------------- + ------------ + ---------------------------------------- +
// Spaces are optional
GridTableState tableState = null;
var c = line.CurrentChar;
var startPosition = processor.Start;
while (true)
var lineStart = line.Start;
int startPosition = -1;
bool hasLeft = false,
hasRight = false;
while (line.Length > 0)
{
if (c == '+')
{
var startCharacter = line.Start;
line.NextChar();
if (line.IsEmptyOrWhitespace())
tableState = tableState ?? new GridTableState { Start = processor.Start, ExpectRow = true };
if (startPosition != -1)
{
if (tableState == null)
{
return BlockState.None;
}
break;
}
TableColumnAlign align;
if (TableHelper.ParseColumnHeader(ref line, '-', out align))
{
if (tableState == null)
{
tableState = new GridTableState()
{
Start = processor.Start,
ExpectRow = true,
};
}
tableState.AddColumn(startCharacter - startPosition, line.Start - 1 - startPosition, align);
c = line.CurrentChar;
continue;
hasRight = line.PeekCharAbsolute(line.Start - 1) == ':';
tableState.AddColumn(startPosition - lineStart, line.Start - lineStart, GetAlignment(hasLeft, hasRight));
}
hasLeft = line.PeekChar(1) == ':';
startPosition = line.Start;
}
// If we have any other characters, this is an invalid line
else if (c != ':' && c != '-')
{
return BlockState.None;
}
c = line.NextChar();
}
if (tableState == null || tableState.ColumnSlices.Count == (processor.Line.Length - 1))
{
return BlockState.None;
}
// Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid)
tableState.AddLine(ref processor.Line);
// Create the grid table
var table = new Table(this);
table.SetData(typeof(GridTableState), tableState);
// Calculate the total width of all columns
int totalWidth = 0;
foreach (var columnSlice in tableState.ColumnSlices)
{
totalWidth += columnSlice.End - columnSlice.Start;
totalWidth += columnSlice.End - columnSlice.Start - 1;
}
// Store the column width and alignment
@@ -89,8 +74,8 @@ namespace Markdig.Extensions.Tables
var columnDefinition = new TableColumnDefinition
{
// Column width proportional to the total width
Width = (float)(columnSlice.End - columnSlice.Start) * 100.0f / totalWidth,
Alignment = columnSlice.Align,
Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth,
Alignment = columnSlice.Align
};
table.ColumnDefinitions.Add(columnDefinition);
}
@@ -100,255 +85,285 @@ namespace Markdig.Extensions.Tables
return BlockState.ContinueDiscard;
}
private static TableColumnAlign GetAlignment(bool hasLeft, bool hasRight)
{
return hasLeft && hasRight
? TableColumnAlign.Center
: hasRight ? TableColumnAlign.Right : TableColumnAlign.Left;
}
public override BlockState TryContinue(BlockProcessor processor, Block block)
{
var gridTable = (Table) block;
var gridTable = (Table)block;
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
// We expect to start at the same
//if (processor.Start == tableState.Start)
tableState.AddLine(ref processor.Line);
if (processor.CurrentChar == '+')
{
var columns = tableState.ColumnSlices;
foreach (var columnSlice in columns)
{
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
columnSlice.CurrentColumnSpan = 0;
}
if (processor.CurrentChar == '+')
{
var result = ParseRowSeparator(processor, tableState, gridTable);
if (result != BlockState.None)
{
return result;
}
}
else if (processor.CurrentChar == '|')
{
var line = processor.Line;
// | ------------- | ------------ | ---------------------------------------- |
// Calculate the colspan for the new row
int columnIndex = -1;
foreach (var columnSlice in columns)
{
if (line.PeekCharExtra(columnSlice.Start) == '|')
{
columnIndex++;
}
if (columnIndex >= 0)
{
columns[columnIndex].CurrentColumnSpan++;
}
}
// Check if the colspan of the current row is the same than the previous row
bool continueRow = true;
foreach (var columnSlice in columns)
{
if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
{
continueRow = false;
break;
}
}
// If the current row doesn't continue the previous row (col span are different)
// Close the previous row
if (!continueRow)
{
TerminateLastRow(processor, tableState, gridTable, false);
}
for (int i = 0; i < columns.Count;)
{
var column = columns[i];
var nextColumnIndex = i + column.CurrentColumnSpan;
// If the span is 0, we exit
if (nextColumnIndex == i)
{
break;
}
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
var sliceForCell = line;
sliceForCell.Start = line.Start + column.Start + 1;
if (nextColumn != null)
{
sliceForCell.End = line.Start + nextColumn.Start - 1;
}
else
{
var columnEnd = columns[columns.Count - 1].End;
// If there is a `|` exactly at the expected end of the table row, we cut the line
// otherwise we allow to have the last cell of a row to be open for longer cell content
if (line.PeekCharExtra(columnEnd + 1) == '|')
{
sliceForCell.End = line.Start + columnEnd;
}
}
sliceForCell.TrimEnd();
// Process the content of the cell
column.BlockProcessor.LineIndex = processor.LineIndex;
column.BlockProcessor.ProcessLine(sliceForCell);
// Go to next column
i = nextColumnIndex;
}
return BlockState.ContinueDiscard;
}
return HandleNewRow(processor, tableState, gridTable);
}
TerminateLastRow(processor, tableState, gridTable, true);
// If we don't have a row, it means that only the header was valid
// So we need to remove the grid table, and create a ParagraphBlock
// with the 2 slices
if (gridTable.Count == 0)
if (processor.CurrentChar == '|')
{
var parser = processor.Parsers.FindExact<ParagraphBlockParser>();
// Discard the grid table
var parent = gridTable.Parent;
processor.Discard(gridTable);
var paragraphBlock = new ParagraphBlock(parser)
{
Lines = tableState.Lines,
};
parent.Add(paragraphBlock);
processor.Open(paragraphBlock);
return HandleContents(processor, tableState, gridTable);
}
TerminateCurrentRow(processor, tableState, gridTable, true);
// If the table is not valid we need to remove the grid table,
// and create a ParagraphBlock with the slices
if (!gridTable.IsValid())
{
Undo(processor, tableState, gridTable);
}
return BlockState.Break;
}
private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable)
{
bool isHeaderRow, hasRowSpan;
var columns = tableState.ColumnSlices;
SetRowSpanState(columns, processor.Line, out isHeaderRow, out hasRowSpan);
SetColumnSpanState(columns, processor.Line);
TerminateCurrentRow(processor, tableState, gridTable, false);
if (isHeaderRow)
{
for (int i = 0; i < gridTable.Count; i++)
{
var row = (TableRow)gridTable[i];
row.IsHeader = true;
}
}
tableState.StartRowGroup = gridTable.Count;
if (hasRowSpan)
{
HandleContents(processor, tableState, gridTable);
}
return BlockState.ContinueDiscard;
}
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
{
var lineStart = line.Start;
isHeaderRow = line.PeekChar(1) == '=' || line.PeekChar(2) == '=';
hasRowSpan = false;
foreach (var columnSlice in columns)
{
if (columnSlice.CurrentCell != null)
{
line.Start = lineStart + columnSlice.Start + 1;
line.End = lineStart + columnSlice.End - 1;
line.Trim();
if (line.IsEmptyOrWhitespace() || !IsRowSeperator(line))
{
hasRowSpan = true;
columnSlice.CurrentCell.RowSpan++;
columnSlice.CurrentCell.AllowClose = false;
}
else
{
columnSlice.CurrentCell.AllowClose = true;
}
}
}
}
private static bool IsRowSeperator(StringSlice slice)
{
while (slice.Length > 0)
{
if (slice.CurrentChar != '-' && slice.CurrentChar != '=' && slice.CurrentChar != ':')
{
return false;
}
slice.NextChar();
}
return true;
}
private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow)
{
var columns = tableState.ColumnSlices;
TableRow currentRow = null;
for (int i = 0; i < columns.Count; i++)
{
var columnSlice = columns[i];
if (columnSlice.CurrentCell != null)
{
if (currentRow == null)
{
currentRow = new TableRow();
}
// If this cell does not already belong to a row
if (columnSlice.CurrentCell.Parent == null)
{
currentRow.Add(columnSlice.CurrentCell);
}
// If the cell is not going to span through to the next row
if (columnSlice.CurrentCell.AllowClose)
{
columnSlice.BlockProcessor.Close(columnSlice.CurrentCell);
}
}
// Renew the block parser processor (or reset it for the last row)
if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell == null || columnSlice.CurrentCell.AllowClose))
{
columnSlice.BlockProcessor.ReleaseChild();
columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild();
}
// Create or erase the cell
if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose))
{
// We don't need the cell anymore if we have a last row
// Or the cell has a columnspan == 0
// And the cell does not have to be kept open to span rows
columnSlice.CurrentCell = null;
}
}
if (currentRow != null && currentRow.Count > 0)
{
gridTable.Add(currentRow);
}
}
private BlockState HandleContents(BlockProcessor processor, GridTableState tableState, Table gridTable)
{
var isRowLine = processor.CurrentChar == '+';
var columns = tableState.ColumnSlices;
var line = processor.Line;
SetColumnSpanState(columns, line);
if (!isRowLine && !CanContinueRow(columns))
{
TerminateCurrentRow(processor, tableState, gridTable, false);
}
for (int i = 0; i < columns.Count;)
{
var columnSlice = columns[i];
var nextColumnIndex = i + columnSlice.CurrentColumnSpan;
// If the span is 0, we exit
if (nextColumnIndex == i)
{
break;
}
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
var sliceForCell = line;
sliceForCell.Start = line.Start + columnSlice.Start + 1;
if (nextColumn != null)
{
sliceForCell.End = line.Start + nextColumn.Start - 1;
}
else
{
var columnEnd = columns[columns.Count - 1].End;
var columnEndChar = line.PeekCharExtra(columnEnd);
// If there is a `|` (or a `+` in the case that we are dealing with a row line
// with spanned contents) exactly at the expected end of the table row, we cut the line
// otherwise we allow to have the last cell of a row to be open for longer cell content
if (columnEndChar == '|' || (isRowLine && columnEndChar == '+'))
{
sliceForCell.End = line.Start + columnEnd - 1;
}
else if (line.PeekCharExtra(line.End) == '|')
{
sliceForCell.End = line.End - 1;
}
}
sliceForCell.TrimEnd();
if (!isRowLine || !IsRowSeperator(sliceForCell))
{
if (columnSlice.CurrentCell == null)
{
columnSlice.CurrentCell = new TableCell(this)
{
ColumnSpan = columnSlice.CurrentColumnSpan,
ColumnIndex = i
};
if (columnSlice.BlockProcessor == null)
{
columnSlice.BlockProcessor = processor.CreateChild();
}
// Ensure that the BlockParser is aware that the TableCell is the top-level container
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
}
// Process the content of the cell
columnSlice.BlockProcessor.LineIndex = processor.LineIndex;
columnSlice.BlockProcessor.ProcessLine(sliceForCell);
}
// Go to next column
i = nextColumnIndex;
}
return BlockState.ContinueDiscard;
}
private static void SetColumnSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line)
{
foreach (var columnSlice in columns)
{
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
columnSlice.CurrentColumnSpan = 0;
}
// | ------------- | ------------ | ---------------------------------------- |
// Calculate the colspan for the new row
int columnIndex = -1;
for (int i = 0; i < columns.Count; i++)
{
var columnSlice = columns[i];
var peek = line.PeekChar(columnSlice.Start);
if (peek == '|' || peek == '+')
{
columnIndex = i;
}
if (columnIndex >= 0)
{
columns[columnIndex].CurrentColumnSpan++;
}
}
}
private static bool CanContinueRow(List<GridTableState.ColumnSlice> columns)
{
foreach (var columnSlice in columns)
{
if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
{
return false;
}
}
return true;
}
private static void Undo(BlockProcessor processor, GridTableState tableState, Table gridTable)
{
var parser = processor.Parsers.FindExact<ParagraphBlockParser>();
// Discard the grid table
var parent = gridTable.Parent;
processor.Discard(gridTable);
var paragraphBlock = new ParagraphBlock(parser)
{
Lines = tableState.Lines,
};
parent.Add(paragraphBlock);
processor.Open(paragraphBlock);
}
public override bool Close(BlockProcessor processor, Block block)
{
// Work only on Table, not on TableCell
var gridTable = block as Table;
if (gridTable != null)
{
var tableState = (GridTableState) block.GetData(typeof (GridTableState));
TerminateLastRow(processor, tableState, gridTable, true);
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
TerminateCurrentRow(processor, tableState, gridTable, true);
if (!gridTable.IsValid())
{
Undo(processor, tableState, gridTable);
}
}
return true;
}
private BlockState ParseRowSeparator(BlockProcessor state, GridTableState tableState, Table gridTable)
{
// A grid table must start with a line like this:
// + ------------- + ------------ + ---------------------------------------- +
// Spaces are optional
var line = state.Line;
var c = line.CurrentChar;
bool isFirst = true;
var delimiterChar = '\0';
while (true)
{
if (c == '+')
{
line.NextChar();
if (line.IsEmptyOrWhitespace())
{
if (isFirst)
{
return BlockState.None;
}
break;
}
TableColumnAlign align;
if (TableHelper.ParseColumnHeaderDetect(ref line, ref delimiterChar, out align))
{
isFirst = false;
c = line.CurrentChar;
continue;
}
}
// If we have any other characters, this is an invalid line
return BlockState.None;
}
// If we have an header row
var isHeader = delimiterChar == '=';
// Terminate the current row
TerminateLastRow(state, tableState, gridTable, false);
// If we had a header row separator, we can mark all rows since last row separator
// to be header rows
if (isHeader)
{
for (int i = tableState.StartRowGroup; i < gridTable.Count; i++)
{
var row = (TableRow) gridTable[i];
row.IsHeader = true;
}
}
// Makr the next start row group continue on the next row
tableState.StartRowGroup = gridTable.Count;
// We don't keep the line
return BlockState.ContinueDiscard;
}
private void TerminateLastRow(BlockProcessor state, GridTableState tableState, Table gridTable, bool isLastRow)
{
var columns = tableState.ColumnSlices;
TableRow currentRow = null;
foreach (var columnSlice in columns)
{
if (columnSlice.CurrentCell != null)
{
if (currentRow == null)
{
currentRow = new TableRow();
}
currentRow.Add(columnSlice.CurrentCell);
columnSlice.BlockProcessor.Close(columnSlice.CurrentCell);
}
// Renew the block parser processor (or reset it for the last row)
if (columnSlice.BlockProcessor != null)
{
columnSlice.BlockProcessor.ReleaseChild();
columnSlice.BlockProcessor = isLastRow ? null : state.CreateChild();
}
// Create or erase the cell
if (isLastRow || columnSlice.CurrentColumnSpan == 0)
{
// We don't need the cell anymore if we have a last row
// Or the cell has a columnspan == 0
columnSlice.CurrentCell = null;
}
else
{
// Else we can create a new cell
columnSlice.CurrentCell = new TableCell(this)
{
ColumnSpan = columnSlice.CurrentColumnSpan
};
if (columnSlice.BlockProcessor == null)
{
columnSlice.BlockProcessor = state.CreateChild();
}
// Ensure that the BlockParser is aware that the TableCell is the top-level container
columnSlice.BlockProcessor.Open(columnSlice.CurrentCell);
}
}
if (currentRow != null)
{
gridTable.Add(currentRow);
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.Globalization;
using Markdig.Renderers;
using Markdig.Renderers.Html;
@@ -38,7 +39,9 @@ namespace Markdig.Extensions.Tables
{
foreach (var tableColumnDefinition in table.ColumnDefinitions)
{
renderer.WriteLine($"<col style=\"width:{Math.Round(tableColumnDefinition.Width*100)/100}%\">");
var width = Math.Round(tableColumnDefinition.Width*100)/100;
var widthValue = string.Format(CultureInfo.InvariantCulture, "{0:0.##}", width);
renderer.WriteLine($"<col style=\"width:{widthValue}%\">");
}
}
@@ -78,10 +81,17 @@ namespace Markdig.Extensions.Tables
{
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
}
if (table.ColumnDefinitions != null && i < table.ColumnDefinitions.Count)
if (cell.RowSpan != 1)
{
switch (table.ColumnDefinitions[i].Alignment)
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
}
if (table.ColumnDefinitions != null)
{
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
switch (table.ColumnDefinitions[columnIndex].Alignment)
{
case TableColumnAlign.Center:
renderer.Write(" style=\"text-align: center;\"");
@@ -101,7 +111,7 @@ namespace Markdig.Extensions.Tables
}
renderer.Write(cell);
renderer.ImplicitParagraph = previousImplicitParagraph;
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
}
renderer.WriteLine("</tr>");

View File

@@ -10,9 +10,9 @@ namespace Markdig.Extensions.Tables
/// The delimiter used to separate the columns of a pipe table.
/// </summary>
/// <seealso cref="Markdig.Syntax.Inlines.DelimiterInline" />
public class PiprTableDelimiterInline : DelimiterInline
public class PipeTableDelimiterInline : DelimiterInline
{
public PiprTableDelimiterInline(InlineParser parser) : base(parser)
public PipeTableDelimiterInline(InlineParser parser) : base(parser)
{
}

View File

@@ -28,13 +28,16 @@ namespace Markdig.Extensions.Tables
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Pipe tables require precise source location
pipeline.PreciseSourceLocation = true;
if (!pipeline.BlockParsers.Contains<PipeTableBlockParser>())
{
pipeline.BlockParsers.Insert(0, new PipeTableBlockParser());
}
var lineBreakParser = pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
{
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(Options));
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(lineBreakParser, Options));
}
}

View File

@@ -1,7 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Parsers.Inlines;
@@ -15,17 +18,20 @@ namespace Markdig.Extensions.Tables
/// The inline parser used to transform a <see cref="ParagraphBlock"/> into a <see cref="Table"/> at inline parsing time.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
public class PipeTableParser : InlineParser, IDelimiterProcessor
/// <seealso cref="IPostInlineProcessor" />
public class PipeTableParser : InlineParser, IPostInlineProcessor
{
private LineBreakInlineParser lineBreakParser;
private readonly LineBreakInlineParser lineBreakParser;
/// <summary>
/// Initializes a new instance of the <see cref="PipeTableParser" /> class.
/// </summary>
/// <param name="lineBreakParser">The linebreak parser to use</param>
/// <param name="options">The options.</param>
public PipeTableParser(PipeTableOptions options = null)
public PipeTableParser(LineBreakInlineParser lineBreakParser, PipeTableOptions options = null)
{
if (lineBreakParser == null) throw new ArgumentNullException(nameof(lineBreakParser));
this.lineBreakParser = lineBreakParser;
OpeningCharacters = new[] { '|', '\n' };
Options = options ?? new PipeTableOptions();
}
@@ -35,12 +41,6 @@ namespace Markdig.Extensions.Tables
/// </summary>
public PipeTableOptions Options { get; }
public override void Initialize(InlineProcessor processor)
{
// We are using the linebreak parser
lineBreakParser = processor.Parsers.Find<LineBreakInlineParser>() ?? new LineBreakInlineParser();
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// Only working on Paragraph block
@@ -96,11 +96,12 @@ namespace Markdig.Extensions.Tables
if (!isFirstLineEmpty)
{
tableState.ColumnAndLineDelimiters.Add(processor.Inline);
tableState.EndOfLines.Add(processor.Inline);
}
}
else
{
processor.Inline = new PiprTableDelimiterInline(this)
processor.Inline = new PipeTableDelimiterInline(this)
{
Span = new SourceSpan(position, position),
Line = globalLineIndex,
@@ -123,7 +124,7 @@ namespace Markdig.Extensions.Tables
return true;
}
public bool ProcessDelimiters(InlineProcessor state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing)
public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
{
var container = root as ContainerInline;
var tableState = state.ParserStates[Index] as TableState;
@@ -137,16 +138,16 @@ namespace Markdig.Extensions.Tables
}
var child = container.LastChild;
List<PiprTableDelimiterInline> delimitersToRemove = null;
List<PipeTableDelimiterInline> delimitersToRemove = null;
while (child != null)
{
var pipeDelimiter = child as PiprTableDelimiterInline;
var pipeDelimiter = child as PipeTableDelimiterInline;
if (pipeDelimiter != null)
{
if (delimitersToRemove == null)
{
delimitersToRemove = new List<PiprTableDelimiterInline>();
delimitersToRemove = new List<PipeTableDelimiterInline>();
}
delimitersToRemove.Add(pipeDelimiter);
}
@@ -168,8 +169,7 @@ namespace Markdig.Extensions.Tables
for (int i = 0; i < delimitersToRemove.Count; i++)
{
var pipeDelimiter = delimitersToRemove[i];
var literalInline = new LiteralInline() {Content = new StringSlice("|"), IsClosed = true};
pipeDelimiter.ReplaceBy(literalInline);
pipeDelimiter.ReplaceByLiteral();
// Check that the pipe that is being removed is not going to make a line without pipe delimiters
var tableDelimiters = tableState.ColumnAndLineDelimiters;
@@ -177,12 +177,12 @@ namespace Markdig.Extensions.Tables
if (i == 0)
{
leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PiprTableDelimiterInline;
leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PipeTableDelimiterInline;
}
else if (i + 1 == delimitersToRemove.Count)
{
rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count &&
tableDelimiters[delimiterIndex + 1] is PiprTableDelimiterInline;
tableDelimiters[delimiterIndex + 1] is PipeTableDelimiterInline;
}
// Remove this delimiter from the table processor
tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter);
@@ -198,14 +198,18 @@ namespace Markdig.Extensions.Tables
return true;
}
// Remove previous state
state.ParserStates[Index] = null;
// Continue
if (tableState == null || container == null || tableState.IsInvalidTable || !tableState.LineHasPipe ) //|| tableState.LineIndex != state.LocalLineIndex)
{
return true;
}
// Detect the header row
var delimiters = tableState.ColumnAndLineDelimiters;
delimiters.Add(null);
// TODO: we could optimize this by merging FindHeaderRow and the cell loop
var aligns = FindHeaderRow(delimiters);
if (Options.RequireHeaderSeparator && aligns == null)
@@ -223,153 +227,210 @@ namespace Markdig.Extensions.Tables
}
state.BlockNew = table;
TableRow firstRow = null;
int maxColumn = 0;
var cells = tableState.Cells;
cells.Clear();
Inline column = container.FirstChild;
if (column is PiprTableDelimiterInline)
//delimiters[0].DumpTo(state.DebugLog);
// delimiters contain a list of `|` and `\n` delimiters
// The `|` delimiters are created as child containers.
// So the following:
// | a | b \n
// | d | e \n
//
// Will generate a tree of the following node:
// |
// a
// |
// b
// \n
// |
// d
// |
// e
// \n
// When parsing delimiters, we need to recover whether a row is of the following form:
// 0) | a | b | \n
// 1) | a | b \n
// 2) a | b \n
// 3) a | b | \n
// If the last element is not a line break, add a line break to homogenize parsing in the next loop
var lastElement = delimiters[delimiters.Count - 1];
if (!(lastElement is LineBreakInline))
{
column = ((PiprTableDelimiterInline)column).FirstChild;
while (true)
{
if (lastElement is ContainerInline)
{
var nextElement = ((ContainerInline) lastElement).LastChild;
if (nextElement != null)
{
lastElement = nextElement;
continue;
}
}
break;
}
var endOfTable = new LineBreakInline();
lastElement.InsertAfter(endOfTable);
delimiters.Add(endOfTable);
tableState.EndOfLines.Add(endOfTable);
}
// TODO: This is not accurate for the table
table.Span.Start = column.Span.Start;
table.Span.End = column.Span.End;
table.Line = column.Line;
table.Column = column.Column;
int lastIndex = 0;
// Cell loop
// Reconstruct the table from the delimiters
TableRow row = null;
TableRow firstRow = null;
for (int i = 0; i < delimiters.Count; i++)
{
var delimiter = delimiters[i];
if (delimiter == null || IsLine(delimiter))
var pipeSeparator = delimiter as PipeTableDelimiterInline;
var isLine = delimiter is LineBreakInline;
if (row == null)
{
var beforeDelimiter = delimiter?.PreviousSibling;
var nextLineColumn = delimiter?.NextSibling;
TableRow row = null;
for (int j = lastIndex; j <= i; j++)
{
var columnSeparator = delimiters[j];
var pipeSeparator = columnSeparator as PiprTableDelimiterInline;
var endOfColumn = columnSeparator?.PreviousSibling;
// This is the first column empty
if (j == lastIndex && pipeSeparator != null && endOfColumn == null)
{
columnSeparator.Remove();
column = pipeSeparator.FirstChild;
continue;
}
if (pipeSeparator != null && IsTrailingColumnDelimiter(pipeSeparator))
{
TrimEnd(endOfColumn);
columnSeparator.Remove();
continue;
}
var cellContainer = new ContainerInline();
var item = column;
var isFirstItem = true;
TrimStart(item);
while (item != null && !IsLine(item) && !(item is PiprTableDelimiterInline))
{
var nextSibling = item.NextSibling;
item.Remove();
cellContainer.AppendChild(item);
if (isFirstItem)
{
cellContainer.Line = item.Line;
cellContainer.Column = item.Column;
cellContainer.Span.Start = item.Span.Start;
isFirstItem = false;
}
cellContainer.Span.End = item.Span.End;
item = nextSibling;
}
var tableParagraph = new ParagraphBlock()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
Inline = cellContainer
};
var tableCell = new TableCell()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
tableCell.Add(tableParagraph);
if (row == null)
{
row = new TableRow()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
}
row.Add(tableCell);
cells.Add(tableCell);
// If we have reached the end, we can add remaining delimiters as pure child of the current cell
if (row.Count == maxColumn && columnSeparator is PiprTableDelimiterInline)
{
columnSeparator.Remove();
tableParagraph.Inline.AppendChild(columnSeparator);
break;
}
TrimEnd(endOfColumn);
//TrimEnd(previousSibling);
if (columnSeparator != null)
{
if (pipeSeparator != null)
{
column = pipeSeparator.FirstChild;
}
columnSeparator.Remove();
}
}
if (row != null)
{
table.Add(row);
}
TrimEnd(beforeDelimiter);
if (delimiter != null)
{
delimiter.Remove();
}
if (nextLineColumn != null)
{
column = nextLineColumn;
}
row = new TableRow();
if (firstRow == null)
{
firstRow = row;
maxColumn = firstRow.Count;
}
lastIndex = i + 1;
// If the first delimiter is a pipe and doesn't have any parent or previous sibling, for cases like:
// 0) | a | b | \n
// 1) | a | b \n
if (pipeSeparator != null && (delimiter.PreviousSibling == null || delimiter.PreviousSibling is LineBreakInline))
{
delimiter.Remove();
continue;
}
}
// We need to find the beginning/ending of a cell from a right delimiter. From the delimiter 'x', we need to find a (without the delimiter start `|`)
// So we iterate back to the first pipe or line break
// x
// 1) | a | b \n
// 2) a | b \n
Inline endOfCell = null;
Inline beginOfCell = null;
var cellContentIt = delimiter;
while (true)
{
cellContentIt = cellContentIt.PreviousSibling ?? cellContentIt.Parent;
if (cellContentIt == null || cellContentIt is LineBreakInline)
{
break;
}
// The cell begins at the first effective child after a | or the top ContainerInline (which is not necessary to bring into the tree + it contains an invalid span calculation)
if (cellContentIt is PipeTableDelimiterInline || (cellContentIt.GetType() == typeof(ContainerInline) && cellContentIt.Parent == null ))
{
beginOfCell = ((ContainerInline)cellContentIt).FirstChild;
if (endOfCell == null)
{
endOfCell = beginOfCell;
}
break;
}
beginOfCell = cellContentIt;
if (endOfCell == null)
{
endOfCell = beginOfCell;
}
}
// If the current deilimiter is a pipe `|` OR
// the beginOfCell/endOfCell are not null and
// either they are :
// - different
// - they contain a single element, but it is not a line break (\n) or an empty/whitespace Literal.
// Then we can add a cell to the current row
if (!isLine || (beginOfCell != null && endOfCell != null && ( beginOfCell != endOfCell || !(beginOfCell is LineBreakInline || (beginOfCell is LiteralInline && ((LiteralInline)beginOfCell).Content.IsEmptyOrWhitespace())))))
{
if (!isLine)
{
// If the delimiter is a pipe, we need to remove it from the tree
// so that previous loop looking for a parent will not go further on subsequent cells
delimiter.Remove();
}
// We trim whitespace at the beginning and ending of the cell
TrimStart(beginOfCell);
TrimEnd(endOfCell);
var cellContainer = new ContainerInline();
// Copy elements from beginOfCell on the first level
var cellIt = beginOfCell;
while (cellIt != null && !IsLine(cellIt) && !(cellIt is PipeTableDelimiterInline))
{
var nextSibling = cellIt.NextSibling;
cellIt.Remove();
if (cellContainer.Span.IsEmpty)
{
cellContainer.Line = cellIt.Line;
cellContainer.Column = cellIt.Column;
cellContainer.Span = cellIt.Span;
}
cellContainer.AppendChild(cellIt);
cellContainer.Span.End = cellIt.Span.End;
cellIt = nextSibling;
}
// Create the cell and add it to the pending row
var tableParagraph = new ParagraphBlock()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
Inline = cellContainer
};
var tableCell = new TableCell()
{
Span = cellContainer.Span,
Line = cellContainer.Line,
Column = cellContainer.Column,
};
tableCell.Add(tableParagraph);
if (row.Span.IsEmpty)
{
row.Span = cellContainer.Span;
row.Line = cellContainer.Line;
row.Column = cellContainer.Column;
}
row.Add(tableCell);
cells.Add(tableCell);
}
// If we have a new line, we can add the row
if (isLine)
{
Debug.Assert(row != null);
if (table.Span.IsEmpty)
{
table.Span = row.Span;
table.Line = row.Line;
table.Column = row.Column;
}
table.Add(row);
row = null;
}
}
// Once we are done with the cells, we can remove all end of lines in the table tree
foreach (var endOfLine in tableState.EndOfLines)
{
endOfLine.Remove();
}
// If we have a header row, we can remove it
// TODO: we could optimize this by merging FindHeaderRow and the previous loop
if (aligns != null)
{
table.RemoveAt(1);
@@ -379,20 +440,12 @@ namespace Markdig.Extensions.Tables
}
// Perform delimiter processor that are coming after this processor
var delimiterProcessors = state.Parsers.DelimiterProcessors;
for (int i = 0; i < delimiterProcessors.Length; i++)
foreach (var cell in cells)
{
if (delimiterProcessors[i] == this)
{
foreach (var cell in cells)
{
var paragraph = (ParagraphBlock) cell[0];
state.ProcessDelimiters(i + 1, paragraph.Inline, null, true);
}
break;
}
var paragraph = (ParagraphBlock) cell[0];
state.PostProcessInlines(postInlineProcessorIndex + 1, paragraph.Inline, null, true);
}
// Clear cells when we are done
cells.Clear();
@@ -429,60 +482,62 @@ namespace Markdig.Extensions.Tables
List<TableColumnDefinition> aligns = null;
for (int i = 0; i < delimiters.Count; i++)
{
if (delimiters[i] != null && IsLine(delimiters[i]))
if (!IsLine(delimiters[i]))
{
// The last delimiter is always null,
for (int j = i + 1; j < delimiters.Count - 1; j++)
{
var delimiter = delimiters[j];
var nextDelimiter = delimiters[j + 1];
var columnDelimiter = delimiter as PiprTableDelimiterInline;
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
{
continue;
}
// Check the left side of a `|` delimiter
TableColumnAlign align;
if (!ParseHeaderString(delimiter.PreviousSibling, out align))
{
break;
}
// Create aligns until we may have a header row
if (aligns == null)
{
aligns = new List<TableColumnDefinition>();
}
aligns.Add(new TableColumnDefinition() { Alignment = align });
// If this is the last delimiter, we need to check the right side of the `|` delimiter
if (nextDelimiter == null)
{
var nextSibling = columnDelimiter != null
? columnDelimiter.FirstChild
: delimiter.NextSibling;
if (!ParseHeaderString(nextSibling, out align))
{
break;
}
isValidRow = true;
aligns.Add(new TableColumnDefinition() { Alignment = align });
break;
}
// If we are on a Line delimiter, exit
if (IsLine(delimiter))
{
isValidRow = true;
break;
}
}
break;
continue;
}
// The last delimiter is always null,
for (int j = i + 1; j < delimiters.Count; j++)
{
var delimiter = delimiters[j];
var nextDelimiter = j + 1 < delimiters.Count ? delimiters[j + 1] : null;
var columnDelimiter = delimiter as PipeTableDelimiterInline;
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
{
continue;
}
// Check the left side of a `|` delimiter
TableColumnAlign align = TableColumnAlign.Left;
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
{
break;
}
// Create aligns until we may have a header row
if (aligns == null)
{
aligns = new List<TableColumnDefinition>();
}
aligns.Add(new TableColumnDefinition() { Alignment = align });
// If this is the last delimiter, we need to check the right side of the `|` delimiter
if (nextDelimiter == null)
{
var nextSibling = columnDelimiter != null
? columnDelimiter.FirstChild
: delimiter.NextSibling;
if (!ParseHeaderString(nextSibling, out align))
{
break;
}
isValidRow = true;
aligns.Add(new TableColumnDefinition() { Alignment = align });
break;
}
// If we are on a Line delimiter, exit
if (IsLine(delimiter))
{
isValidRow = true;
break;
}
}
break;
}
return isValidRow ? aligns : null;
@@ -517,21 +572,6 @@ namespace Markdig.Extensions.Tables
return previous == null || IsLine(previous);
}
private static bool IsTrailingColumnDelimiter(PiprTableDelimiterInline inline)
{
var child = inline.FirstChild;
var literal = child as LiteralInline;
if (literal != null)
{
if (!literal.Content.IsEmptyOrWhitespace())
{
return false;
}
child = child.NextSibling;
}
return child == null || IsLine(child);
}
private static void TrimStart(Inline inline)
{
while (inline is ContainerInline && !(inline is DelimiterInline))
@@ -560,6 +600,7 @@ namespace Markdig.Extensions.Tables
{
ColumnAndLineDelimiters = new List<Inline>();
Cells = new List<TableCell>();
EndOfLines = new List<Inline>();
}
public bool IsInvalidTable { get; set; }
@@ -571,6 +612,8 @@ namespace Markdig.Extensions.Tables
public List<Inline> ColumnAndLineDelimiters { get; }
public List<TableCell> Cells { get; }
public List<Inline> EndOfLines { get; }
}
}
}

View File

@@ -34,5 +34,40 @@ namespace Markdig.Extensions.Tables
/// Gets or sets the column alignments. May be null.
/// </summary>
public List<TableColumnDefinition> ColumnDefinitions { get; private set; }
/// <summary>
/// Checks if the table structure is valid.
/// </summary>
/// <returns><c>True</c> if the table has rows and the number of cells per row is correct, other wise <c>false</c>.</returns>
public bool IsValid()
{
// A table with no rows is not valid.
if (Count == 0)
{
return false;
}
var columnCount = ColumnDefinitions.Count;
var rows = new int[Count];
for (int i = 0; i < Count; i++)
{
var row = (TableRow)this[i];
for (int j = 0; j < row.Count; j++)
{
var cell = (TableCell)row[j];
rows[i] += cell.ColumnSpan;
var rowSpan = cell.RowSpan - 1;
while (rowSpan > 0)
{
rows[i + rowSpan] += cell.ColumnSpan;
rowSpan--;
}
}
if (rows[i] > columnCount)
{
return false;
}
}
return true;
}
}
}

View File

@@ -26,12 +26,30 @@ namespace Markdig.Extensions.Tables
/// <param name="parser">The parser used to create this block.</param>
public TableCell(BlockParser parser) : base(parser)
{
AllowClose = true;
ColumnSpan = 1;
ColumnIndex = -1;
RowSpan = 1;
}
/// <summary>
/// Gets or sets the index of the column to which this cell belongs.
/// </summary>
public int ColumnIndex { get; set; }
/// <summary>
/// Gets or sets the column span this cell is covering. Default is 1.
/// </summary>
public int ColumnSpan { get; set; }
/// <summary>
/// Gets or sets the row span this cell is covering. Default is 1.
/// </summary>
public int RowSpan { get; set; }
/// <summary>
/// Gets or sets whether this cell can be closed.
/// </summary>
public bool AllowClose { get; set; }
}
}

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
namespace Markdig.Extensions.TaskLists
@@ -18,15 +19,29 @@ namespace Markdig.Extensions.TaskLists
public TaskListInlineParser()
{
OpeningCharacters = new[] {'['};
ListClass = "contains-task-list";
ListItemClass = "task-list-item";
}
/// <summary>
/// Gets or sets the list class used for a task list.
/// </summary>
public string ListClass { get; set; }
/// <summary>
/// Gets or sets the list item class used for a task list.
/// </summary>
public string ListItemClass { get; set; }
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// A tasklist is either
// [ ]
// or [x] or [X]
if (!(processor.Block.Parent is ListItemBlock))
var listItemBlock = processor.Block.Parent as ListItemBlock;
if (listItemBlock == null)
{
return false;
}
@@ -56,6 +71,19 @@ namespace Markdig.Extensions.TaskLists
};
taskItem.Span.End = taskItem.Span.Start + 2;
processor.Inline = taskItem;
// Add proper class for task list
if (!string.IsNullOrEmpty(ListItemClass))
{
listItemBlock.GetAttributes().AddClass(ListItemClass);
}
var listBlock = (ListBlock) listItemBlock.Parent;
if (!string.IsNullOrEmpty(ListClass))
{
listBlock.GetAttributes().AddClass(ListClass);
}
return true;
}
}

View File

@@ -7,6 +7,8 @@ using System.Runtime.CompilerServices;
namespace Markdig.Helpers
{
using System.Collections.Generic;
/// <summary>
/// Helper class for handling characters.
/// </summary>
@@ -18,6 +20,11 @@ namespace Markdig.Helpers
public const string ZeroSafeString = "\uFFFD";
// We don't support LCDM
private static IDictionary<char, int> romanMap = new Dictionary<char, int> { { 'I', 1 }, { 'V', 5 }, { 'X', 10 } };
private static readonly char[] punctuationExceptions = { '', '-', '†', '‡' };
public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose)
{
// A left-flanking delimiter run is a delimiter run that is
@@ -32,8 +39,11 @@ namespace Markdig.Helpers
pc.CheckUnicodeCategory(out prevIsWhiteSpace, out prevIsPunctuation);
c.CheckUnicodeCategory(out nextIsWhiteSpace, out nextIsPunctuation);
var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc);
var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c);
canOpen = !nextIsWhiteSpace &&
(!nextIsPunctuation || prevIsWhiteSpace || prevIsPunctuation);
((!nextIsPunctuation || nextIsExcepted) || prevIsWhiteSpace || prevIsPunctuation);
// A right-flanking delimiter run is a delimiter run that is
@@ -42,7 +52,7 @@ namespace Markdig.Helpers
// or a punctuation character.
// For purposes of this definition, the beginning and the end of the line count as Unicode whitespace.
canClose = !prevIsWhiteSpace &&
(!prevIsPunctuation || nextIsWhiteSpace || nextIsPunctuation);
((!prevIsPunctuation || prevIsExcepted) || nextIsWhiteSpace || nextIsPunctuation);
if (!enableWithinWord)
{
@@ -80,6 +90,26 @@ namespace Markdig.Helpers
return c == 'I' || c == 'V' || c == 'X';
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static int RomanToArabic(string text)
{
int result = 0;
for (int i = 0; i < text.Length; i++)
{
var character = Char.ToUpper(text[i]);
var candidate = romanMap[character];
if (i + 1 < text.Length && candidate < romanMap[Char.ToUpper(text[i + 1])])
{
result -= candidate;
}
else
{
result += candidate;
}
}
return result;
}
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public static int AddTab(int column)
{

View File

@@ -61,6 +61,11 @@ namespace Markdig.Helpers
}
previousIsSpace = false;
}
else if (c.IsDigit())
{
headingBuffer.Append(c);
previousIsSpace = false;
}
else if (!previousIsSpace && c.IsWhitespace())
{
var pc = headingBuffer[headingBuffer.Length - 1];
@@ -644,7 +649,7 @@ namespace Markdig.Helpers
}
text.NextChar(); // Skip ':'
// Skip any whitespaces before the url
// Skip any whitespace before the url
text.TrimStart();
urlSpan.Start = text.Start;

View File

@@ -101,19 +101,5 @@ namespace Markdig.Helpers
}
return false;
}
public bool ReplacyBy<TElement>(T element) where TElement : T
{
if (element == null) throw new ArgumentNullException(nameof(element));
for (int i = 0; i < Count; i++)
{
if (this[i] is TElement)
{
this[i] = element;
return true;
}
}
return false;
}
}
}

View File

@@ -194,52 +194,49 @@ namespace Markdig.Helpers
/// Searches the specified text within this slice.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <param name="ignoreCase">true if ignore case</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public bool Search(string text, out int index)
public int IndexOf(string text, int offset = 0, bool ignoreCase = false)
{
return Search(text, 0, out index);
var end = End - text.Length + 1;
if (ignoreCase)
{
for (int i = Start + offset; i <= end; i++)
{
if (MatchLowercase(text, End, i - Start))
{
return i; ;
}
}
}
else
{
for (int i = Start + offset; i <= end; i++)
{
if (Match(text, End, i - Start))
{
return i; ;
}
}
}
return -1;
}
/// <summary>
/// Searches the specified text within this slice.
/// Searches for the specified character within this slice.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public bool Search(string text, int offset, out int index)
/// <returns>A value >= 0 if the character was found, otherwise &lt; 0</returns>
public int IndexOf(char c)
{
var end = End - text.Length + 1;
index = Start + offset;
for (int i = index; i <= end; i ++)
for (int i = Start; i <= End; i++)
{
if (Match(text, End, i))
if (Text[i] == c)
{
index = i + text.Length;
return true;
return i;
}
}
return false;
}
/// <summary>
/// Searches the specified text within this slice (matching lowercase).
/// </summary>
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text was found; <c>false</c> otherwise</returns>
public bool SearchLowercase(string text, out int endOfIndex)
{
var end = End - text.Length + 1;
endOfIndex = 0;
for (int i = Start; i <= end; i++)
{
if (MatchLowercase(text, End, i))
{
endOfIndex = i + text.Length;
return true;
}
}
return false;
return -1;
}
/// <summary>

View File

@@ -53,17 +53,17 @@ namespace Markdig.Helpers
CharNode nextNode;
if (!node.TryGetValue(c, out nextNode))
{
return false;
break;
}
node = nextNode;
if (node.Content != null)
{
match = node.Content;
return true;
}
offset++;
length--;
}
if (node.Content != null)
{
match = node.Content;
return true;
}
return false;
}

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.IO;
using Markdig.Extensions.SelfPipeline;
using Markdig.Parsers;
using Markdig.Renderers;
using Markdig.Syntax;
@@ -30,17 +31,19 @@ namespace Markdig
}
/// <summary>
/// Converts a Markdown string to HTML.
/// Converts a Markdown string to HTML and output to the specified writer.
/// </summary>
/// <param name="markdown">A Markdown text.</param>
/// <param name="writer">The destination <see cref="TextWriter"/> that will receive the result of the conversion.</param>
/// <param name="pipeline">The pipeline used for the conversion.</param>
/// <returns>The Markdown document that has been parsed</returns>
/// <exception cref="System.ArgumentNullException">if reader or writer variable are null</exception>
public static void ToHtml(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
public static MarkdownDocument ToHtml(string markdown, TextWriter writer, MarkdownPipeline pipeline = null)
{
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
if (writer == null) throw new ArgumentNullException(nameof(writer));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
pipeline = CheckForSelfPipeline(pipeline, markdown);
// We override the renderer with our own writer
var renderer = new HtmlRenderer(writer);
@@ -49,6 +52,8 @@ namespace Markdig
var document = Parse(markdown, pipeline);
renderer.Render(document);
writer.Flush();
return document;
}
/// <summary>
@@ -64,6 +69,7 @@ namespace Markdig
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
pipeline = CheckForSelfPipeline(pipeline, markdown);
var document = Parse(markdown, pipeline);
pipeline.Setup(renderer);
return renderer.Render(document);
@@ -93,7 +99,18 @@ namespace Markdig
if (markdown == null) throw new ArgumentNullException(nameof(markdown));
pipeline = pipeline ?? new MarkdownPipelineBuilder().Build();
pipeline = CheckForSelfPipeline(pipeline, markdown);
return MarkdownParser.Parse(markdown, pipeline);
}
private static MarkdownPipeline CheckForSelfPipeline(MarkdownPipeline pipeline, string markdown)
{
var selfPipeline = pipeline.Extensions.Find<SelfPipelineExtension>();
if (selfPipeline != null)
{
return selfPipeline.CreatePipelineFromInput(markdown);
}
return pipeline;
}
}
}

View File

@@ -9,6 +9,7 @@ using Markdig.Extensions.Bootstrap;
using Markdig.Extensions.Citations;
using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.DefinitionLists;
using Markdig.Extensions.Diagrams;
using Markdig.Extensions.Emoji;
using Markdig.Extensions.EmphasisExtras;
using Markdig.Extensions.Figures;
@@ -19,6 +20,9 @@ using Markdig.Extensions.Hardlines;
using Markdig.Extensions.ListExtras;
using Markdig.Extensions.Mathematics;
using Markdig.Extensions.MediaLinks;
using Markdig.Extensions.NoRefLinks;
using Markdig.Extensions.PragmaLines;
using Markdig.Extensions.SelfPipeline;
using Markdig.Extensions.SmartyPants;
using Markdig.Extensions.Tables;
using Markdig.Extensions.TaskLists;
@@ -55,9 +59,50 @@ namespace Markdig
.UsePipeTables()
.UseListExtras()
.UseTaskLists()
.UseDiagrams()
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
}
/// <summary>
/// Uses the self pipeline extension that will detect the pipeline to use from the markdown input that contains a special tag. See <see cref="SelfPipelineExtension"/>
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <param name="defaultTag">The default tag to use to match the self pipeline configuration. By default, <see cref="SelfPipelineExtension.DefaultTag"/>, meaning that the HTML tag will be &lt;--markdig:extensions--&gt;</param>
/// <param name="defaultExtensions">The default extensions to configure if no pipeline setup was found from the Markdown document</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseSelfPipeline(this MarkdownPipelineBuilder pipeline, string defaultTag = SelfPipelineExtension.DefaultTag, string defaultExtensions = null)
{
if (pipeline.Extensions.Count != 0)
{
throw new InvalidOperationException("The SelfPipeline extension cannot be used with other extensions");
}
pipeline.Extensions.Add(new SelfPipelineExtension(defaultTag, defaultExtensions));
return pipeline;
}
/// <summary>
/// Uses pragma lines to output span with an id containing the line number (pragma-line#line_number_zero_based`)
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UsePragmaLines(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<PragmaLineExtension>();
return pipeline;
}
/// <summary>
/// Uses the diagrams extension
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseDiagrams(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<DiagramExtension>();
return pipeline;
}
/// <summary>
/// Uses precise source code location (useful for syntax highlighting).
/// </summary>
@@ -143,7 +188,7 @@ namespace Markdig
}
/// <summary>
/// Uses the boostrap extension.
/// Uses the bootstrap extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
@@ -322,6 +367,17 @@ namespace Markdig
return pipeline;
}
/// <summary>
/// Add rel=nofollow to all links rendered to HTML.
/// </summary>
/// <param name="pipeline"></param>
/// <returns></returns>
public static MarkdownPipelineBuilder UseNoFollowLinks(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<NoFollowLinksExtension>();
return pipeline;
}
/// <summary>
/// This will disable the HTML support in the markdown processor (for constraint/safe parsing).
/// </summary>
@@ -360,6 +416,8 @@ namespace Markdig
{
switch (extension.ToLowerInvariant())
{
case "common":
break;
case "advanced":
pipeline.UseAdvancedExtensions();
break;
@@ -423,11 +481,20 @@ namespace Markdig
case "tasklists":
pipeline.UseTaskLists();
break;
case "diagrams":
pipeline.UseDiagrams();
break;
case "nofollowlinks":
pipeline.UseNoFollowLinks();
break;
case "nohtml":
pipeline.DisableHtml();
break;
default:
throw new ArgumentException($"unknown extension {extension}");
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
}
}
return pipeline;
}
}
}
}

View File

@@ -47,7 +47,11 @@ namespace Markdig
internal ProcessDocumentDelegate DocumentProcessed;
internal void Setup(IMarkdownRenderer renderer)
/// <summary>
/// Allows to setup a <see cref="IMarkdownRenderer"/>.
/// </summary>
/// <param name="renderer">The markdown renderer to setup</param>
public void Setup(IMarkdownRenderer renderer)
{
if (renderer == null) throw new ArgumentNullException(nameof(renderer));
foreach (var extension in Extensions)

View File

@@ -24,7 +24,7 @@ namespace Markdig
public MarkdownPipelineBuilder()
{
// Add all default parsers
BlockParsers = new BlockParserList()
BlockParsers = new OrderedList<BlockParser>()
{
new ThematicBreakParser(),
new HeadingBlockParser(),
@@ -37,7 +37,7 @@ namespace Markdig
new ParagraphBlockParser(),
};
InlineParsers = new InlineParserList()
InlineParsers = new OrderedList<InlineParser>()
{
new HtmlEntityParser(),
new LinkInlineParser(),
@@ -56,12 +56,12 @@ namespace Markdig
/// <summary>
/// Gets the block parsers.
/// </summary>
public BlockParserList BlockParsers { get; private set; }
public OrderedList<BlockParser> BlockParsers { get; private set; }
/// <summary>
/// Gets the inline parsers.
/// </summary>
public InlineParserList InlineParsers { get; private set; }
public OrderedList<InlineParser> InlineParsers { get; private set; }
/// <summary>
/// Gets the register extensions.

View File

@@ -12,13 +12,6 @@ namespace Markdig.Parsers
/// <seealso cref="Markdig.Parsers.ParserList{Markdig.Parsers.BlockParser, Markdig.Parsers.BlockParserState}" />
public class BlockParserList : ParserList<BlockParser, BlockProcessor>
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockParserList"/> class.
/// </summary>
public BlockParserList()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="BlockParserList"/> class.
/// </summary>

View File

@@ -51,7 +51,6 @@ namespace Markdig.Parsers
Document = document;
document.IsOpen = true;
Parsers = parsers;
parsers.Initialize(this);
OpenedBlocks = new List<Block>();
NewBlocks = new Stack<Block>();
root = this;
@@ -402,6 +401,11 @@ namespace Markdig.Parsers
parserStateCache.Release(this);
}
internal bool IsOpen(Block block)
{
return OpenedBlocks.Contains(block);
}
/// <summary>
/// Closes a block at the specified index.
/// </summary>

View File

@@ -216,7 +216,7 @@ namespace Markdig.Parsers
// The line must contain only fence opening character followed only by whitespaces.
if (count <=0 && !processor.IsCodeIndent && (c == '\0' || c.IsWhitespace()) && line.TrimEnd())
{
block.Span.End = line.Start - 1;
block.UpdateSpanEnd(line.Start - 1);
// Don't keep the last line
return BlockState.BreakDiscard;

View File

@@ -12,13 +12,15 @@ namespace Markdig.Parsers
/// <seealso cref="Markdig.Parsers.BlockParser" />
public class FencedCodeBlockParser : FencedBlockParserBase<FencedCodeBlock>
{
public const string DefaultInfoPrefix = "language-";
/// <summary>
/// Initializes a new instance of the <see cref="FencedCodeBlockParser"/> class.
/// </summary>
public FencedCodeBlockParser()
{
OpeningCharacters = new[] {'`', '~'};
InfoPrefix = "language-";
InfoPrefix = DefaultInfoPrefix;
}
protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor)

View File

@@ -62,7 +62,7 @@ namespace Markdig.Parsers
}
// A space is required after leading #
if (leadingCount > 0 && leadingCount <= 6 && (c.IsSpace() || c == '\0'))
if (leadingCount > 0 && leadingCount <= 6 && (c.IsSpaceOrTab() || c == '\0'))
{
// Move to the content
var headingBlock = new HeadingBlock(this)
@@ -89,7 +89,7 @@ namespace Markdig.Parsers
c = processor.Line.Text[i];
if (endState == 0)
{
if (c.IsSpace()) // TODO: Not clear if it is a space or space+tab in the specs
if (c.IsSpaceOrTab())
{
continue;
}
@@ -105,7 +105,7 @@ namespace Markdig.Parsers
if (countClosingTags > 0)
{
if (c.IsSpace())
if (c.IsSpaceOrTab())
{
processor.Line.End = i - 1;
}

View File

@@ -171,51 +171,78 @@ namespace Markdig.Parsers
return CreateHtmlBlock(state, HtmlBlockType.InterruptingBlock, startColumn, startPosition);
}
private const string EndOfComment = "-->";
private const string EndOfCDATA = "]]>";
private const string EndOfProcessingInstruction = "?>";
private BlockState MatchEnd(BlockProcessor state, HtmlBlock htmlBlock)
{
state.GoToColumn(state.ColumnBeforeIndent);
// Early exit if it is not starting by an HTML tag
var line = state.Line;
var c = line.CurrentChar;
var result = BlockState.Continue;
int endof;
int index;
switch (htmlBlock.Type)
{
case HtmlBlockType.Comment:
if (line.Search("-->", out endof))
index = line.IndexOf(EndOfComment);
if (index >= 0)
{
htmlBlock.Span.End = endof - 1;
htmlBlock.UpdateSpanEnd(index + EndOfComment.Length);
result = BlockState.Break;
}
break;
case HtmlBlockType.CData:
if (line.Search("]]>", out endof))
index = line.IndexOf(EndOfCDATA);
if (index >= 0)
{
htmlBlock.Span.End = endof - 1;
htmlBlock.UpdateSpanEnd(index + EndOfCDATA.Length);
result = BlockState.Break;
}
break;
case HtmlBlockType.ProcessingInstruction:
if (line.Search("?>", out endof))
index = line.IndexOf(EndOfProcessingInstruction);
if (index >= 0)
{
htmlBlock.Span.End = endof - 1;
htmlBlock.UpdateSpanEnd(index + EndOfProcessingInstruction.Length);
result = BlockState.Break;
}
break;
case HtmlBlockType.DocumentType:
if (line.Search(">", out endof))
index = line.IndexOf('>');
if (index >= 0)
{
htmlBlock.Span.End = endof - 1;
htmlBlock.UpdateSpanEnd(index + 1);
result = BlockState.Break;
}
break;
case HtmlBlockType.ScriptPreOrStyle:
if (line.SearchLowercase("</script>", out endof) || line.SearchLowercase("</pre>", out endof) || line.SearchLowercase("</style>", out endof))
index = line.IndexOf("</script>", 0, true);
if (index >= 0)
{
htmlBlock.Span.End = endof - 1;
htmlBlock.UpdateSpanEnd(index + "</script>".Length);
result = BlockState.Break;
}
else
{
index = line.IndexOf("</pre>", 0, true);
if (index >= 0)
{
htmlBlock.UpdateSpanEnd(index + "</pre>".Length);
result = BlockState.Break;
}
else
{
index = line.IndexOf("</style>", 0, true);
if (index >= 0)
{
htmlBlock.UpdateSpanEnd(index + "</style>".Length);
result = BlockState.Break;
}
}
}
break;
case HtmlBlockType.InterruptingBlock:
if (state.IsBlankLine)

View File

@@ -17,8 +17,7 @@ namespace Markdig.Parsers
/// <summary>
/// Initializes this parser with the specified parser processor.
/// </summary>
/// <param name="processor">The parser processor.</param>
void Initialize(TProcessor processor);
void Initialize();
/// <summary>
/// Gets the index of this parser in <see cref="BlockParserList"/> or <see cref="InlineParserList"/>.

View File

@@ -6,9 +6,9 @@ using Markdig.Syntax.Inlines;
namespace Markdig.Parsers
{
/// <summary>
/// A procesor used for <see cref="DelimiterInline"/>.
/// A procesor called at the end of processing all inlines.
/// </summary>
public interface IDelimiterProcessor
public interface IPostInlineProcessor
{
/// <summary>
/// Processes the delimiters.
@@ -16,10 +16,10 @@ namespace Markdig.Parsers
/// <param name="state">The parser state.</param>
/// <param name="root">The root inline.</param>
/// <param name="lastChild">The last child.</param>
/// <param name="delimiterProcessorIndex">Index of this delimiter processor.</param>
/// <param name="postInlineProcessorIndex">Index of this delimiter processor.</param>
/// <param name="isFinalProcessing"></param>
/// <returns><c>true</c> to continue to the next delimiter processor;
/// <c>false</c> to stop the process (in case a processor is perfoming sub-sequent processor itself)</returns>
bool ProcessDelimiters(InlineProcessor state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing);
bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing);
}
}

View File

@@ -18,13 +18,14 @@ namespace Markdig.Parsers
public override BlockState TryOpen(BlockProcessor processor)
{
var startPosition = processor.Line.Start;
var startColumn = processor.ColumnBeforeIndent;
var startPosition = processor.StartBeforeIndent;
var result = TryContinue(processor, null);
if (result == BlockState.Continue)
{
processor.NewBlocks.Push(new CodeBlock(this)
{
Column = processor.Column,
Column = startColumn,
Span = new SourceSpan(startPosition, processor.Line.End)
});
}
@@ -48,7 +49,7 @@ namespace Markdig.Parsers
}
if (block != null)
{
block.Span.End = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
}
return BlockState.Continue;
}

View File

@@ -11,34 +11,24 @@ namespace Markdig.Parsers
/// <seealso cref="Markdig.Parsers.ParserList{Markdig.Parsers.InlineParser, Markdig.Parsers.InlineParserState}" />
public class InlineParserList : ParserList<InlineParser, InlineProcessor>
{
public InlineParserList()
{
}
public InlineParserList(IEnumerable<InlineParser> parsers) : base(parsers)
{
// Prepare the list of post inline processors
var postInlineProcessors = new List<IPostInlineProcessor>();
foreach (var parser in this)
{
var delimProcessor = parser as IPostInlineProcessor;
if (delimProcessor != null)
{
postInlineProcessors.Add(delimProcessor);
}
}
PostInlineProcessors = postInlineProcessors.ToArray();
}
/// <summary>
/// Gets the registered delimiter processors.
/// Gets the registered post inline processors.
/// </summary>
public IDelimiterProcessor[] DelimiterProcessors { get; private set; }
public override void Initialize(InlineProcessor initState)
{
// Prepare the list of delimiter processors
var delimiterProcessors = new List<IDelimiterProcessor>();
foreach (var parser in this)
{
var delimProcessor = parser as IDelimiterProcessor;
if (delimProcessor != null)
{
delimiterProcessors.Add(delimProcessor);
}
}
DelimiterProcessors = delimiterProcessors.ToArray();
base.Initialize(initState);
}
public IPostInlineProcessor[] PostInlineProcessors { get; private set; }
}
}

View File

@@ -46,7 +46,6 @@ namespace Markdig.Parsers
Parsers = parsers;
PreciseSourceLocation = preciseSourcelocation;
lineOffsets = new List<StringLineGroup.LineOffset>();
Parsers.Initialize(this);
ParserStates = new object[Parsers.Count];
LiteralInlineParser = new LiteralInlineParser();
}
@@ -239,7 +238,21 @@ namespace Markdig.Parsers
if (nextInline.Parent == null)
{
// Get deepest container
FindLastContainer().AppendChild(nextInline);
var container = FindLastContainer();
if (!ReferenceEquals(container, nextInline))
{
container.AppendChild(nextInline);
}
if (container == Root)
{
if (container.Span.IsEmpty)
{
container.Span = nextInline.Span;
}
container.Span.End = nextInline.Span.End;
}
}
}
else
@@ -254,39 +267,39 @@ namespace Markdig.Parsers
}
}
if (DebugLog != null)
{
DebugLog.WriteLine($"** Dump: char '{c}");
leafBlock.Inline.DumpTo(DebugLog);
}
//if (DebugLog != null)
//{
// DebugLog.WriteLine($"** Dump: char '{c}");
// leafBlock.Inline.DumpTo(DebugLog);
//}
}
Inline = null;
if (DebugLog != null)
{
DebugLog.WriteLine("** Dump before Emphasis:");
leafBlock.Inline.DumpTo(DebugLog);
}
//if (DebugLog != null)
//{
// DebugLog.WriteLine("** Dump before Emphasis:");
// leafBlock.Inline.DumpTo(DebugLog);
//}
// Process all delimiters
ProcessDelimiters(0, Root, null, true);
// PostProcess all inlines
PostProcessInlines(0, Root, null, true);
//TransformDelimitersToLiterals();
if (DebugLog != null)
{
DebugLog.WriteLine();
DebugLog.WriteLine("** Dump after Emphasis:");
leafBlock.Inline.DumpTo(DebugLog);
}
//if (DebugLog != null)
//{
// DebugLog.WriteLine();
// DebugLog.WriteLine("** Dump after Emphasis:");
// leafBlock.Inline.DumpTo(DebugLog);
//}
}
public void ProcessDelimiters(int startingIndex, Inline root, Inline lastChild, bool isFinalProcessing)
public void PostProcessInlines(int startingIndex, Inline root, Inline lastChild, bool isFinalProcessing)
{
for (int i = startingIndex; i < Parsers.DelimiterProcessors.Length; i++)
for (int i = startingIndex; i < Parsers.PostInlineProcessors.Length; i++)
{
var delimiterProcessor = Parsers.DelimiterProcessors[i];
if (!delimiterProcessor.ProcessDelimiters(this, root, lastChild, i, isFinalProcessing))
var postInlineProcessor = Parsers.PostInlineProcessors[i];
if (!postInlineProcessor.PostProcess(this, root, lastChild, i, isFinalProcessing))
{
break;
}

View File

@@ -62,6 +62,10 @@ namespace Markdig.Parsers.Inlines
Column = column
};
}
else
{
return false;
}
return true;
}

View File

@@ -15,8 +15,8 @@ namespace Markdig.Parsers.Inlines
/// An inline parser for <see cref="EmphasisInline"/>.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
/// <seealso cref="Markdig.Parsers.IDelimiterProcessor" />
public class EmphasisInlineParser : InlineParser, IDelimiterProcessor
/// <seealso cref="IPostInlineProcessor" />
public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
{
private CharacterMap<EmphasisDescriptor> emphasisMap;
private readonly DelimitersObjectCache inlinesCache;
@@ -63,7 +63,7 @@ namespace Markdig.Parsers.Inlines
/// </summary>
public CreateEmphasisInlineDelegate CreateEmphasisInline { get; set; }
public override void Initialize(InlineProcessor processor)
public override void Initialize()
{
OpeningCharacters = new char[EmphasisDescriptors.Count];
@@ -85,7 +85,7 @@ namespace Markdig.Parsers.Inlines
emphasisMap = new CharacterMap<EmphasisDescriptor>(tempMap);
}
public bool ProcessDelimiters(InlineProcessor state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing)
public bool PostProcess(InlineProcessor state, Inline root, Inline lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
{
var container = root as ContainerInline;
if (container == null)
@@ -225,9 +225,15 @@ namespace Markdig.Parsers.Inlines
for (int j = i - 1; j >= 0; j--)
{
var previousOpenDelimiter = delimiters[j];
var isOddMatch = ((closeDelimiter.Type & DelimiterType.Open) != 0 ||
(previousOpenDelimiter.Type & DelimiterType.Close) != 0) &&
previousOpenDelimiter.DelimiterCount != closeDelimiter.DelimiterCount &&
(previousOpenDelimiter.DelimiterCount + closeDelimiter.DelimiterCount) % 3 == 0;
if (previousOpenDelimiter.DelimiterChar == closeDelimiter.DelimiterChar &&
(previousOpenDelimiter.Type & DelimiterType.Open) != 0 &&
previousOpenDelimiter.DelimiterCount > 0)
previousOpenDelimiter.DelimiterCount > 0 && !isOddMatch)
{
openDelimiter = previousOpenDelimiter;
openDelimiterIndex = j;
@@ -313,7 +319,8 @@ namespace Markdig.Parsers.Inlines
if (closeDelimiter.DelimiterCount == 0)
{
closeDelimiter.MoveChildrenAfter(emphasis);
var newParent = openDelimiter.DelimiterCount > 0 ? emphasis : emphasis.Parent;
closeDelimiter.MoveChildrenAfter(newParent);
closeDelimiter.Remove();
delimiters.RemoveAt(i);
i--;

View File

@@ -45,14 +45,14 @@ namespace Markdig.Parsers.Inlines
if (literal != null)
{
var matched = slice;
matched.End = match - 1;
matched.End = slice.Start + match - 1;
int line;
int column;
processor.Inline = new HtmlEntityInline()
{
Original = matched,
Transcoded = new StringSlice(literal),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End + 1)),
Span = new SourceSpan(processor.GetSourcePosition(startPosition, out line, out column), processor.GetSourcePosition(matched.End)),
Line = line,
Column = column
};

View File

@@ -118,6 +118,7 @@ namespace Markdig.Parsers.Inlines
Title = HtmlHelper.Unescape(linkRef.Title),
Label = label,
LabelSpan = labelSpan,
UrlSpan = linkRef.UrlSpan,
IsImage = parent.IsImage,
Reference = linkRef,
Span = new SourceSpan(parent.Span.Start, endPosition),
@@ -159,7 +160,7 @@ namespace Markdig.Parsers.Inlines
link.IsClosed = true;
// Process emphasis delimiters
state.ProcessDelimiters(0, link, null, false);
state.PostProcessInlines(0, link, null, false);
state.Inline = link;
isValidLink = true;
@@ -238,7 +239,7 @@ namespace Markdig.Parsers.Inlines
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.ProcessDelimiters(0, link, null, false);
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.

View File

@@ -65,13 +65,24 @@ namespace Markdig.Parsers.Inlines
// The LiteralInlineParser is always matching (at least an empty string)
var endPosition = slice.Start + length - 1;
processor.Inline = new LiteralInline()
var previousInline = processor.Inline as LiteralInline;
if (previousInline != null && ReferenceEquals(previousInline.Content.Text, slice.Text) &&
previousInline.Content.End + 1 == slice.Start)
{
Content = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
Line = line,
Column = column,
};
previousInline.Content.End = endPosition;
previousInline.Span.End = processor.GetSourcePosition(endPosition);
}
else
{
processor.Inline = new LiteralInline()
{
Content = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
Line = line,
Column = column,
};
}
slice.Start = nextStart;

View File

@@ -33,7 +33,7 @@ namespace Markdig.Parsers
/// </summary>
public OrderedList<ListItemParser> ItemParsers { get; }
public override void Initialize(BlockProcessor processor)
public override void Initialize()
{
var tempMap = new Dictionary<char, ListItemParser>();
@@ -138,11 +138,6 @@ namespace Markdig.Parsers
list.CountBlankLinesReset++;
}
if (list.CountBlankLinesReset > 1)
{
return BlockState.BreakDiscard;
}
if (list.CountBlankLinesReset == 1 && listItem.ColumnWidth < 0)
{
state.Close(listItem);
@@ -153,7 +148,7 @@ namespace Markdig.Parsers
}
// Update list-item source end position
listItem.Span.End = state.Line.End;
listItem.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}
@@ -170,11 +165,11 @@ namespace Markdig.Parsers
{
if (state.Indent > columWidth && state.IsCodeIndent)
{
state.GoToColumn(columWidth);
state.GoToColumn(state.ColumnBeforeIndent + columWidth);
}
// Update list-item source end position
listItem.Span.End = state.Line.End;
listItem.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}
@@ -254,6 +249,20 @@ namespace Markdig.Parsers
columnWidth = (state.IsBlankLine ? columnBeforeIndent : state.Column) - initColumnBeforeIndent;
}
// Starts/continue the list unless:
// - an empty list item follows a paragraph
// - an ordered list is not starting by '1'
var isPreviousParagraph = (block ?? state.LastBlock) is ParagraphBlock;
if (isPreviousParagraph)
{
var isOpen = state.IsOpen(block ?? state.LastBlock);
if (state.IsBlankLine || (isOpen && listInfo.BulletType == '1' && listInfo.OrderedStart != "1"))
{
state.GoToColumn(initColumn);
return BlockState.None;
}
}
var newListItem = new ListItemBlock(this)
{
Column = initColumn,
@@ -352,11 +361,11 @@ namespace Markdig.Parsers
isLastListItem = false;
}
// Update end-position for the list
if (listBlock.Count > 0)
{
listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
}
//// Update end-position for the list
//if (listBlock.Count > 0)
//{
// listBlock.Span.End = listBlock[listBlock.Count - 1].Span.End;
//}
return true;
}

View File

@@ -51,14 +51,10 @@ namespace Markdig.Parsers
document = new MarkdownDocument();
// Initialize the block parsers
var blockParserList = new BlockParserList();
blockParserList.AddRange(pipeline.BlockParsers);
blockProcessor = new BlockProcessor(stringBuilderCache, document, blockParserList);
blockProcessor = new BlockProcessor(stringBuilderCache, document, pipeline.BlockParsers);
// Initialize the inline parsers
var inlineParserList = new InlineParserList();
inlineParserList.AddRange(pipeline.InlineParsers);
inlineProcessor = new InlineProcessor(stringBuilderCache, document, inlineParserList, pipeline.PreciseSourceLocation)
inlineProcessor = new InlineProcessor(stringBuilderCache, document, pipeline.InlineParsers, pipeline.PreciseSourceLocation)
{
DebugLog = pipeline.DebugLog
};

View File

@@ -40,7 +40,7 @@ namespace Markdig.Parsers
return TryParseSetexHeading(processor, block);
}
block.Span.End = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}
@@ -139,7 +139,7 @@ namespace Markdig.Parsers
return BlockState.BreakDiscard;
}
block.Span.End = state.Line.End;
block.UpdateSpanEnd(state.Line.End);
return BlockState.Continue;
}

View File

@@ -19,8 +19,7 @@ namespace Markdig.Parsers
/// <summary>
/// Initializes this parser with the specified parser processor.
/// </summary>
/// <param name="processor">The parser processor.</param>
public virtual void Initialize(TProcessor processor)
public virtual void Initialize()
{
}

View File

@@ -16,60 +16,10 @@ namespace Markdig.Parsers
/// <seealso cref="Markdig.Helpers.OrderedList{T}" />
public abstract class ParserList<T, TState> : OrderedList<T> where T : ParserBase<TState>
{
private CharacterMap<T[]> charMap;
private T[] globalParsers;
private readonly CharacterMap<T[]> charMap;
private readonly T[] globalParsers;
protected ParserList()
{
}
protected ParserList(IEnumerable<T> parsers) : base(parsers)
{
}
/// <summary>
/// Gets the list of global parsers (that don't have any opening characters defined)
/// </summary>
public T[] GlobalParsers => globalParsers;
/// <summary>
/// Gets all the opening characters defined.
/// </summary>
public char[] OpeningCharacters => charMap.OpeningCharacters;
/// <summary>
/// Gets the list of parsers valid for the specified opening character.
/// </summary>
/// <param name="openingChar">The opening character.</param>
/// <returns>A list of parsers valid for the specified opening character or null if no parsers registered.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public T[] GetParsersForOpeningCharacter(char openingChar)
{
return charMap[openingChar];
}
/// <summary>
/// Searches for an opening character from a registered parser in the specified string.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public int IndexOfOpeningCharacter(string text, int start, int end)
{
return charMap.IndexOfOpeningCharacter(text, start, end);
}
/// <summary>
/// Initializes this instance with specified parser state.
/// </summary>
/// <param name="initState">State of the initialize.</param>
/// <exception cref="System.InvalidOperationException">
/// Unexpected null parser found
/// or
/// </exception>
public virtual void Initialize(TState initState)
protected ParserList(IEnumerable<T> parsersArg) : base(parsersArg)
{
var charCounter = new Dictionary<char, int>();
int globalCounter = 0;
@@ -82,7 +32,7 @@ namespace Markdig.Parsers
throw new InvalidOperationException("Unexpected null parser found");
}
parser.Initialize(initState);
parser.Initialize();
parser.Index = i;
if (parser.OpeningCharacters != null && parser.OpeningCharacters.Length != 0)
{
@@ -134,5 +84,50 @@ namespace Markdig.Parsers
charMap = new CharacterMap<T[]>(tempCharMap);
}
/// <summary>
/// Gets the list of global parsers (that don't have any opening characters defined)
/// </summary>
public T[] GlobalParsers => globalParsers;
/// <summary>
/// Gets all the opening characters defined.
/// </summary>
public char[] OpeningCharacters => charMap.OpeningCharacters;
/// <summary>
/// Gets the list of parsers valid for the specified opening character.
/// </summary>
/// <param name="openingChar">The opening character.</param>
/// <returns>A list of parsers valid for the specified opening character or null if no parsers registered.</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public T[] GetParsersForOpeningCharacter(char openingChar)
{
return charMap[openingChar];
}
/// <summary>
/// Searches for an opening character from a registered parser in the specified string.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns>Index position within the string of the first opening character found in the specified text; if not found, returns -1</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public int IndexOfOpeningCharacter(string text, int start, int end)
{
return charMap.IndexOfOpeningCharacter(text, start, end);
}
/// <summary>
/// Initializes this instance with specified parser state.
/// </summary>
/// <exception cref="System.InvalidOperationException">
/// Unexpected null parser found
/// or
/// </exception>
private void Initialize()
{
}
}
}

View File

@@ -61,7 +61,6 @@ namespace Markdig.Parsers
var c = processor.CurrentChar;
if (c != quote.QuoteChar)
{
block.Span.End = processor.Start - 1;
return processor.IsBlankLine ? BlockState.BreakDiscard : BlockState.None;
}
@@ -71,18 +70,8 @@ namespace Markdig.Parsers
processor.NextChar(); // Skip following space
}
block.Span.End = processor.Line.End;
block.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}
public override bool Close(BlockProcessor processor, Block block)
{
var quoteBlock = block as QuoteBlock;
if (quoteBlock?.LastChild != null)
{
quoteBlock.Span.End = quoteBlock.LastChild.Span.End;
}
return true;
}
}
}

View File

@@ -54,7 +54,7 @@ namespace Markdig.Parsers
breakCharCount++;
}
else if (c.IsSpace())
else if (c.IsSpaceOrTab())
{
hasSpacesSinceLastMatch = true;
}

View File

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

View File

@@ -1,6 +1,10 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Collections.Generic;
using Markdig.Parsers;
using Markdig.Syntax;
namespace Markdig.Renderers.Html
@@ -11,24 +15,57 @@ namespace Markdig.Renderers.Html
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.CodeBlock}" />
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
/// </summary>
public CodeBlockRenderer()
{
BlocksAsDiv = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
public bool OutputAttributesOnPre { get; set; }
/// <summary>
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
/// </summary>
public HashSet<string> BlocksAsDiv { get; }
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
{
renderer.EnsureLine();
renderer.Write("<pre");
if (OutputAttributesOnPre)
var fencedCodeBlock = obj as FencedCodeBlock;
if (fencedCodeBlock?.Info != null && BlocksAsDiv.Contains(fencedCodeBlock.Info))
{
renderer.WriteAttributes(obj);
var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ??
FencedCodeBlockParser.DefaultInfoPrefix;
// We are replacing the HTML attribute `language-mylang` by `mylang` only for a div block
// NOTE that we are allocating a closure here
renderer.Write("<div")
.WriteAttributes(obj.TryGetAttributes(),
cls => cls.StartsWith(infoPrefix) ? cls.Substring(infoPrefix.Length) : cls)
.Write(">");
renderer.WriteLeafRawLines(obj, true, true, true);
renderer.WriteLine("</div>");
}
renderer.Write("><code");
if (!OutputAttributesOnPre)
else
{
renderer.WriteAttributes(obj);
renderer.Write("<pre");
if (OutputAttributesOnPre)
{
renderer.WriteAttributes(obj);
}
renderer.Write("><code");
if (!OutputAttributesOnPre)
{
renderer.WriteAttributes(obj);
}
renderer.Write(">");
renderer.WriteLeafRawLines(obj, true, true);
renderer.WriteLine("</code></pre>");
}
renderer.Write(">");
renderer.WriteLeafRawLines(obj, true, true);
renderer.WriteLine("</code></pre>");
}
}
}

View File

@@ -44,9 +44,14 @@ namespace Markdig.Renderers.Html
if (name == null) throw new ArgumentNullException(nameof(name));
if (Classes == null)
{
Classes = new List<string>(2); // Use half list compare to default capacity (4), as we don't expect lots of classes
Classes = new List<string>(2);
// Use half list compare to default capacity (4), as we don't expect lots of classes
}
if (!Classes.Contains(name))
{
Classes.Add(name);
}
Classes.Add(name);
}
/// <summary>
@@ -113,7 +118,7 @@ namespace Markdig.Renderers.Html
}
if (htmlAttributes.Classes == null)
{
htmlAttributes.Classes = shared ? Classes : new List<string>(Classes);
htmlAttributes.Classes = shared ? Classes : Classes != null ? new List<string>(Classes) : null;
}
else if (Classes != null)
{
@@ -122,7 +127,7 @@ namespace Markdig.Renderers.Html
if (htmlAttributes.Properties == null)
{
htmlAttributes.Properties = shared ? Properties : new List<KeyValuePair<string, string>>(Properties);
htmlAttributes.Properties = shared ? Properties : Properties != null ? new List<KeyValuePair<string, string>>(Properties) : null;
}
else if (Properties != null)
{

View File

@@ -11,6 +11,11 @@ namespace Markdig.Renderers.Html.Inlines
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.AutolinkInline}" />
public class AutolinkInlineRenderer : HtmlObjectRenderer<AutolinkInline>
{
/// <summary>
/// Gets or sets a value indicating whether to always add rel="nofollow" for links or not.
/// </summary>
public bool AutoRelNoFollow { get; set; }
protected override void Write(HtmlRenderer renderer, AutolinkInline obj)
{
if (renderer.EnableHtmlForInline)
@@ -22,6 +27,12 @@ namespace Markdig.Renderers.Html.Inlines
}
renderer.WriteEscapeUrl(obj.Url);
renderer.WriteAttributes(obj);
if (!obj.IsEmail && AutoRelNoFollow)
{
renderer.Write(" rel=\"nofollow\"");
}
renderer.Write("\">");
}

View File

@@ -11,6 +11,11 @@ namespace Markdig.Renderers.Html.Inlines
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{Markdig.Syntax.Inlines.LinkInline}" />
public class LinkInlineRenderer : HtmlObjectRenderer<LinkInline>
{
/// <summary>
/// Gets or sets a value indicating whether to always add rel="nofollow" for links or not.
/// </summary>
public bool AutoRelNoFollow { get; set; }
protected override void Write(HtmlRenderer renderer, LinkInline link)
{
if (renderer.EnableHtmlForInline)
@@ -54,6 +59,10 @@ namespace Markdig.Renderers.Html.Inlines
{
if (renderer.EnableHtmlForInline)
{
if (AutoRelNoFollow)
{
renderer.Write(" rel=\"nofollow\"");
}
renderer.Write(">");
}
renderer.WriteChildren(link);

View File

@@ -23,7 +23,7 @@ namespace Markdig.Renderers.Html
renderer.Write(" type=\"").Write(listBlock.BulletType).Write("\"");
}
if (listBlock.OrderedStart != null && (listBlock.DefaultOrderedStart != listBlock.OrderedStart))
if (listBlock.OrderedStart != null && (listBlock.OrderedStart != "1"))
{
renderer.Write(" start=\"").Write(listBlock.OrderedStart).Write("\"");
}
@@ -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

@@ -83,26 +83,28 @@ namespace Markdig.Renderers
/// Writes the content escaped for HTML.
/// </summary>
/// <param name="slice">The slice.</param>
/// <param name="softEscape">Only escape &lt; and &amp;</param>
/// <returns>This instance</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public HtmlRenderer WriteEscape(ref StringSlice slice)
public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false)
{
if (slice.Start > slice.End)
{
return this;
}
return WriteEscape(slice.Text, slice.Start, slice.Length);
return WriteEscape(slice.Text, slice.Start, slice.Length, softEscape);
}
/// <summary>
/// Writes the content escaped for HTML.
/// </summary>
/// <param name="slice">The slice.</param>
/// <param name="softEscape">Only escape &lt; and &amp;</param>
/// <returns>This instance</returns>
[MethodImpl(MethodImplOptionPortable.AggressiveInlining)]
public HtmlRenderer WriteEscape(StringSlice slice)
public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false)
{
return WriteEscape(ref slice);
return WriteEscape(ref slice, softEscape);
}
/// <summary>
@@ -111,8 +113,9 @@ namespace Markdig.Renderers
/// <param name="content">The content.</param>
/// <param name="offset">The offset.</param>
/// <param name="length">The length.</param>
/// <param name="softEscape">Only escape &lt; and &amp;</param>
/// <returns>This instance</returns>
public HtmlRenderer WriteEscape(string content, int offset, int length)
public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false)
{
if (string.IsNullOrEmpty(content) || length == 0)
return this;
@@ -132,12 +135,15 @@ namespace Markdig.Renderers
previousOffset = offset + 1;
break;
case '>':
Write(content, previousOffset, offset - previousOffset);
if (EnableHtmlEscape)
if (!softEscape)
{
Write("&gt;");
Write(content, previousOffset, offset - previousOffset);
if (EnableHtmlEscape)
{
Write("&gt;");
}
previousOffset = offset + 1;
}
previousOffset = offset + 1;
break;
case '&':
Write(content, previousOffset, offset - previousOffset);
@@ -148,12 +154,15 @@ namespace Markdig.Renderers
previousOffset = offset + 1;
break;
case '"':
Write(content, previousOffset, offset - previousOffset);
if (EnableHtmlEscape)
if (!softEscape)
{
Write("&quot;");
Write(content, previousOffset, offset - previousOffset);
if (EnableHtmlEscape)
{
Write("&quot;");
}
previousOffset = offset + 1;
}
previousOffset = offset + 1;
break;
}
}
@@ -233,8 +242,9 @@ namespace Markdig.Renderers
/// Writes the specified <see cref="HtmlAttributes"/>.
/// </summary>
/// <param name="attributes">The attributes to render.</param>
/// <param name="classFilter">A class filter used to transform a class into another class at writing time</param>
/// <returns>This instance</returns>
public HtmlRenderer WriteAttributes(HtmlAttributes attributes)
public HtmlRenderer WriteAttributes(HtmlAttributes attributes, Func<string, string> classFilter = null)
{
if (attributes == null)
{
@@ -256,7 +266,7 @@ namespace Markdig.Renderers
{
Write(" ");
}
WriteEscape(cssClass);
WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass);
}
Write("\"");
}
@@ -284,8 +294,9 @@ namespace Markdig.Renderers
/// <param name="leafBlock">The leaf block.</param>
/// <param name="writeEndOfLines">if set to <c>true</c> write end of lines.</param>
/// <param name="escape">if set to <c>true</c> escape the content for HTML</param>
/// <param name="softEscape">Only escape &lt; and &amp;</param>
/// <returns>This instance</returns>
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape)
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false)
{
if (leafBlock == null) throw new ArgumentNullException(nameof(leafBlock));
if (leafBlock.Lines.Lines != null)
@@ -300,7 +311,7 @@ namespace Markdig.Renderers
}
if (escape)
{
WriteEscape(ref slices[i].Slice);
WriteEscape(ref slices[i].Slice, softEscape);
}
else
{

View File

@@ -1,6 +1,8 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -11,6 +13,16 @@ namespace Markdig.Renderers
/// </summary>
public interface IMarkdownRenderer
{
/// <summary>
/// Occurs when before writing an object.
/// </summary>
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
/// <summary>
/// Occurs when after writing an object.
/// </summary>
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
/// <summary>
/// Gets the object renderers that will render <see cref="Block"/> and <see cref="Inline"/> elements.
/// </summary>

View File

@@ -35,6 +35,16 @@ namespace Markdig.Renderers
public bool IsLastInContainer { get; private set; }
/// <summary>
/// Occurs when before writing an object.
/// </summary>
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
/// <summary>
/// Occurs when after writing an object.
/// </summary>
public event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
/// <summary>
/// Writes the children of the specified <see cref="ContainerBlock"/>.
/// </summary>
@@ -105,6 +115,10 @@ namespace Markdig.Renderers
var objectType = obj.GetType();
// Calls before writing an object
var writeBefore = ObjectWriteBefore;
writeBefore?.Invoke(this, obj);
// Handle regular renderers
IMarkdownObjectRenderer renderer = previousObjectType == objectType ? previousRenderer : null;
if (renderer == null && !renderersPerType.TryGetValue(objectType, out renderer))
@@ -142,6 +156,10 @@ namespace Markdig.Renderers
previousObjectType = objectType;
previousRenderer = renderer;
// Calls after writing an object
var writeAfter = ObjectWriteAfter;
writeAfter?.Invoke(this, obj);
}
}
}

View File

@@ -75,5 +75,19 @@ namespace Markdig.Syntax
{
ProcessInlinesEnd?.Invoke(state, null);
}
public void UpdateSpanEnd(int spanEnd)
{
// Update parent spans
var parent = this;
while (parent != null)
{
if (spanEnd > parent.Span.End)
{
parent.Span.End = spanEnd;
}
parent = parent.Parent;
}
}
}
}

View File

@@ -0,0 +1,138 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
namespace Markdig.Syntax
{
/// <summary>
/// Extensions for <see cref="Block"/>
/// </summary>
public static class BlockExtensions
{
// TODO: Add test for this code
public static Block FindBlockAtPosition(this Block rootBlock, int position)
{
var contains = rootBlock.CompareToPosition(position) == 0;
var blocks = rootBlock as ContainerBlock;
if (blocks == null || blocks.Count == 0 || !contains)
{
return contains ? rootBlock : null;
}
var lowerIndex = 0;
var upperIndex = blocks.Count - 1;
// binary search on lines
Block block = null;
while (lowerIndex <= upperIndex)
{
int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex;
block = blocks[midIndex];
int comparison = block.CompareToPosition(position);
if (comparison == 0)
{
break;
}
block = null;
if (comparison < 0)
lowerIndex = midIndex + 1;
else
upperIndex = midIndex - 1;
}
if (block == null)
{
return rootBlock;
}
// Recursively go deep into the block
return FindBlockAtPosition(block, position);
}
public static int FindClosestLine(this MarkdownDocument root, int line)
{
var closestBlock = root.FindClosestBlock(line);
return closestBlock?.Line ?? 0;
}
public static Block FindClosestBlock(this Block rootBlock, int line)
{
var blocks = rootBlock as ContainerBlock;
if (blocks == null || blocks.Count == 0)
{
return rootBlock.Line == line ? rootBlock : null;
}
var lowerIndex = 0;
var upperIndex = blocks.Count - 1;
// binary search on lines
while (lowerIndex <= upperIndex)
{
int midIndex = (upperIndex - lowerIndex) / 2 + lowerIndex;
var block = blocks[midIndex];
int comparison = block.Line.CompareTo(line);
if (comparison == 0)
{
return block;
}
if (comparison < 0)
lowerIndex = midIndex + 1;
else
upperIndex = midIndex - 1;
}
// If we are between two lines, try to find the best spot
if (lowerIndex > 0 && lowerIndex < blocks.Count)
{
var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1];
var nextBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex];
if (prevBlock.Line == line)
{
return prevBlock;
}
if (nextBlock.Line == line)
{
return nextBlock;
}
// we calculate the position of the current line relative to the line found and previous line
var prevLine = prevBlock.Line;
var nextLine = nextBlock.Line;
var middle = (line - prevLine) * 1.0 / (nextLine - prevLine);
// If relative position < 0.5, we select the previous line, otherwise we select the line found
return middle < 0.5 ? prevBlock : nextBlock;
}
if (lowerIndex == 0)
{
var prevBlock = blocks[lowerIndex].FindClosestBlock(line) ?? blocks[lowerIndex];
return prevBlock;
}
if (lowerIndex == blocks.Count)
{
var prevBlock = blocks[lowerIndex - 1].FindClosestBlock(line) ?? blocks[lowerIndex - 1];
return prevBlock;
}
return null;
}
public static bool ContainsPosition(this Block block, int position)
{
return CompareToPosition(block, position) == 0;
}
public static int CompareToPosition(this Block block, int position)
{
return position < block.Span.Start ? 1 : position > block.Span.End + 1 ? -1 : 0;
}
}
}

View File

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

View File

@@ -3,6 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.Diagnostics;
using Markdig.Helpers;
using Markdig.Parsers;
namespace Markdig.Syntax.Inlines
@@ -41,5 +42,18 @@ namespace Markdig.Syntax.Inlines
/// </summary>
/// <returns>The string representation of this delimiter</returns>
public abstract string ToLiteral();
public void ReplaceByLiteral()
{
var literalInline = new LiteralInline()
{
Content = new StringSlice(ToLiteral()),
Span = Span,
Line = Line,
Column = Column,
IsClosed = true
};
ReplaceBy(literalInline);
}
}
}

View File

@@ -229,6 +229,24 @@ namespace Markdig.Syntax.Inlines
}
}
public Inline FindBestParent()
{
var current = this;
while (current.Parent != null || current.PreviousSibling != null)
{
if (current.Parent != null)
{
current = current.Parent;
continue;
}
current = current.PreviousSibling;
}
return current;
}
protected virtual void OnChildRemove(Inline child)
{

View File

@@ -47,5 +47,10 @@ namespace Markdig.Syntax.Inlines
/// The content as a <see cref="StringSlice"/>.
/// </summary>
public StringSlice Content;
public override string ToString()
{
return Content.ToString();
}
}
}

View File

@@ -59,7 +59,23 @@ namespace Markdig.Syntax
public string Title { get; set; }
/// <summary>
/// Gets or sets the create link inline calback for this instance.
/// The label span
/// </summary>
public SourceSpan LabelSpan;
/// <summary>
/// The URL span
/// </summary>
public SourceSpan UrlSpan;
/// <summary>
/// The title span
/// </summary>
public SourceSpan TitleSpan;
/// <summary>
/// Gets or sets the create link inline callback for this instance.
/// </summary>
/// <remarks>
/// This callback is called when an inline link is matching this reference definition.
@@ -72,20 +88,28 @@ namespace Markdig.Syntax
/// <typeparam name="T">Type of the text</typeparam>
/// <param name="text">The text.</param>
/// <param name="block">The block.</param>
/// <returns><c>true</c> if parsing is successfull; <c>false</c> otherwise</returns>
/// <returns><c>true</c> if parsing is successful; <c>false</c> otherwise</returns>
public static bool TryParse<T>(ref T text, out LinkReferenceDefinition block) where T : ICharIterator
{
block = null;
string label;
string url;
string title;
SourceSpan labelSpan;
SourceSpan urlSpan;
SourceSpan titleSpan;
if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title))
if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan, out titleSpan))
{
return false;
}
block = new LinkReferenceDefinition(label, url, title);
block = new LinkReferenceDefinition(label, url, title)
{
LabelSpan = labelSpan,
UrlSpan = urlSpan,
TitleSpan = titleSpan
};
return true;
}
}

View File

@@ -19,6 +19,8 @@ namespace Markdig.Syntax
/// <returns>An iteration over the descendant elements</returns>
public static IEnumerable<MarkdownObject> Descendants(this MarkdownObject markdownObject)
{
// TODO: implement a recursiveless method
var block = markdownObject as ContainerBlock;
if (block != null)
{

Some files were not shown because too many files have changed in this diff Show More