Compare commits

...

119 Commits

Author SHA1 Message Date
Alexandre Mutel
2054c16662 Bump to 0.9.0 2016-10-11 13:10:25 +02:00
Alexandre Mutel
95641e562f Add support for the autolink extension (issue #64) 2016-10-11 13:10:06 +02:00
Alexandre Mutel
e4953931c7 Update donation logo 2016-09-29 20:00:08 +02:00
Alexandre Mutel
93b5b7a091 Add donation button 2016-09-29 18:23:30 +02:00
Alexandre Mutel
4f52e893ee Disable warning for comments as it slows down build on appveyor 2016-09-23 14:01:01 +02:00
Alexandre Mutel
23ede077a1 Bump version to 0.8.5 2016-09-23 13:48:15 +02:00
Alexandre Mutel
d5e6f17683 Allow to force table alignment to left (issue #62) 2016-09-23 13:43:49 +02:00
Alexandre Mutel
4d5980a485 Bump version to 0.8.4 2016-09-22 21:48:58 +02:00
Alexandre Mutel
19dd902519 Fix issue when calculating the span of an indented code block within a list. Make sure to include first whitespace on the line 2016-09-22 21:48:15 +02:00
Alexandre Mutel
f046a46275 Bump version to 0.8.3 2016-09-22 16:26:54 +02:00
Alexandre Mutel
25e9eafa8b Allow to pass a manual pipeline to TestParser.TestSpec 2016-09-22 16:25:46 +02:00
Alexandre Mutel
f4ff981008 Fix NullReferenceException with GridTable extension when a single + is entered on a line 2016-09-22 16:25:13 +02:00
Alexandre Mutel
a1b48aff89 Bump version to 0.8.2 2016-09-22 12:44:52 +02:00
Alexandre Mutel
0aa34caa82 Merge pull request #61 from christophano/bugfix/literal-inline-parser
Fixes issue where LiteralInlineParser calls PostMatch while processor.Inline is type other than LiteralInline
2016-09-22 12:40:16 +02:00
Chris Rodgers
98ce9b1a06 Fixes issue where LiteralInlineParser calls PostMatch while processor.Inline is type other than LiteralInline 2016-09-22 09:28:52 +01:00
Alexandre Mutel
ab157b21ea Update the readme with recent extensions added 2016-09-22 09:31:43 +02:00
Alexandre Mutel
53c72d3031 Bump version to 0.8.1 2016-09-21 13:06:51 +02:00
Alexandre Mutel
165e2f97d0 Output attached html attributes for dd in definition lists (fix issue #59) 2016-09-21 13:06:37 +02:00
Alexandre Mutel
6c577059ad Add extension NonAsciiNoEscape for URI to workaround a bug/singular behavior of EDGE/IE for local file links with non US-ASCII characters 2016-09-21 12:57:16 +02:00
Alexandre Mutel
0ea4dc769b Merge pull request #60 from christophano/bugfix/abbreviation-preceded-by-punctuation
Fixes issue where punctuation forms part of word, so abbreviation is not valid
2016-09-20 16:37:21 +02:00
Chris Rodgers
a9b626e810 Fixes issue where punctuation forms part of word, so abbreviation is not valid. 2016-09-20 15:22:31 +01:00
Alexandre Mutel
b90d6f9769 Bump to 0.8.0 2016-09-19 10:36:35 +02:00
Alexandre Mutel
f0ea008c46 Add support for YAML frontmatter parsing/discard (issue #37) 2016-09-19 10:11:38 +02:00
Alexandre Mutel
c43d5ccd63 Don't create an empty LiteralInline when trimming spaces at the end of a line (issue #42) 2016-09-19 09:10:36 +02:00
Alexandre Mutel
d6d7b398e4 Update to latest CommonMark specs. Fix new corner cases when parsing inline links 2016-09-18 13:24:19 +02:00
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
109 changed files with 7975 additions and 4880 deletions

View File

@@ -1,8 +1,8 @@
# Markdig [![Build status](https://ci.appveyor.com/api/projects/status/hk391x8jcskxt1u8?svg=true)](https://ci.appveyor.com/project/xoofx/markdig) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/)
# Markdig [![Build status](https://ci.appveyor.com/api/projects/status/hk391x8jcskxt1u8?svg=true)](https://ci.appveyor.com/project/xoofx/markdig) [![NuGet](https://img.shields.io/nuget/v/Markdig.svg)](https://www.nuget.org/packages/Markdig/) [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
<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:
@@ -45,6 +46,8 @@ You can **try Markdig online** and compare it to other implementations on [babel
- **Emoji** support (inspired from [Markdown-it](https://markdown-it.github.io/))
- **SmartyPants** (inspired from [Daring Fireball - SmartyPants](https://daringfireball.net/projects/smartypants/))
- **Bootstrap** class (to output bootstrap class)
- **Diagrams** extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports only for [`mermaid` diagrams](https://knsv.github.io/mermaid/))
- **YAML frontmatter** to parse without evaluating the frontmatter and to discard it from the HTML output (typically used for previewing without the frontmatter in MarkdownEditor)
- Compatible with .NET 3.5, 4.0+ and .NET Core (`netstandard1.1+`)
## Documentation
@@ -79,6 +82,10 @@ var result = Markdown.ToHtml("This is a text with some *emphasis*", pipeline);
You can have a look at the [MarkdownExtensions](https://github.com/lunet-io/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) that describes all actionable extensions (by modifying the MarkdownPipeline)
## Build
In order to build Markdig, you need to install [.NET Core RTM](https://www.microsoft.com/net/core)
## License
This software is released under the [BSD-Clause 2 license](https://github.com/lunet-io/markdig/blob/master/license.txt).
@@ -110,7 +117,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:
@@ -168,6 +175,12 @@ WarmupCount=2 TargetCount=10
TestMarkdownDeep | 7.4076 ms | 0.0617 ms | 318.00 | 186.00 | 84.00 | 2,576,728.69 |
```
## Donate
If you are using this library and find it useful for your project, please consider a donation for it!
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FRGHXBTP442JL)
## Credits
Thanks to the fantastic work done by [John Mac Farlane](http://johnmacfarlane.net/) for the CommonMark specs and all the people involved in making Markdown a better standard!

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" />
@@ -71,6 +75,7 @@
<ItemGroup>
<None Include="App.config" />
<None Include="project.json" />
<None Include="Specs\AutoLinks.md" />
<None Include="Specs\AutoIdentifierSpecs.md" />
<None Include="Specs\AbbreviationSpecs.md" />
<None Include="Specs\FigureFooterAndCiteSpecs.md" />
@@ -83,6 +88,9 @@
<None Include="Specs\GridTableSpecs.md" />
<None Include="Specs\HardlineBreakSpecs.md" />
<None Include="Specs\BootstrapSpecs.md" />
<None Include="Specs\DiagramsSpecs.md" />
<None Include="Specs\NoHtmlSpecs.md" />
<None Include="Specs\YamlSpecs.md" />
<None Include="Specs\TaskListSpecs.md" />
<None Include="Specs\SmartyPantsSpecs.md" />
<None Include="Specs\MediaSpecs.md" />
@@ -98,6 +106,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,25 @@ This is a 😃 HTML document
.
<p>This is a <abbr title="Hypertext Markup Language">😃 HTML</abbr> document</p>
````````````````````````````````
Abbreviations may be similar:
```````````````````````````````` example
*[1A]: First
*[1A1]: Second
*[1A2]: Third
We can abbreviate 1A, 1A1 and 1A2!
.
<p>We can abbreviate <abbr title="First">1A</abbr>, <abbr title="Second">1A1</abbr> and <abbr title="Third">1A2</abbr>!</p>
````````````````````````````````
Abbreviations should match whole word only:
```````````````````````````````` example
*[1A]: First
We should not abbreviate 1.1A or 11A!
.
<p>We should not abbreviate 1.1A or 11A!</p>
````````````````````````````````

View File

@@ -0,0 +1,46 @@
# Extensions
This section describes the different extensions supported:
## AutoLinks
Autolinks will format as a HTML link any string that starts by:
- `http://` or `https://`
- `ftp://`
- `mailto:`
- `www.`
```````````````````````````````` example
This is a http://www.google.com URL and https://www.google.com
This is a ftp://test.com
And a mailto:email@toto.com
And a plain www.google.com
.
<p>This is a <a href="http://www.google.com">http://www.google.com</a> URL and <a href="https://www.google.com">https://www.google.com</a>
This is a <a href="ftp://test.com">ftp://test.com</a>
And a <a href="mailto:email@toto.com">mailto:email@toto.com</a>
And a plain <a href="http://www.google.com">www.google.com</a></p>
````````````````````````````````
But incomplete links will not be matched:
```````````````````````````````` example
This is not a http:/www.google.com URL and https:/www.google.com
This is not a ftp:/test.com
And not a mailto:emailtoto.com
And not a plain www. or a www.x
.
<p>This is not a http:/www.google.com URL and https:/www.google.com
This is not a ftp:/test.com
And not a mailto:emailtoto.com
And not a plain www. or a www.x</p>
````````````````````````````````
Previous character must be a punctuation or a valid space (tab, space, new line):
```````````````````````````````` example
This is not a nhttp://www.google.com URL but this is (https://www.google.com)
.
<p>This is not a nhttp://www.google.com URL but this is (<a href="https://www.google.com">https://www.google.com</a>)</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,103 @@ Alignment might be specified on the first row using the character `:`:
</tbody>
</table>
````````````````````````````````
A grid table may have cells spanning both columns and rows:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+---+---+ B +
| D | E | B |
+ D +---+---+
| D | CCCCC |
+---+---+---+
.
<table>
<col style="width:33.33%">
<col style="width:33.33%">
<col style="width:33.33%">
<tbody>
<tr>
<td colspan="2">AAAAA</td>
<td rowspan="2">B
B
B</td>
</tr>
<tr>
<td rowspan="2">D
D
D</td>
<td>E</td>
</tr>
<tr>
<td colspan="2">CCCCC</td>
</tr>
</tbody>
</table>
````````````````````````````````
A grid table may have cells with both colspan and rowspan:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+ AAAAA +---+
| AAAAA | C |
+---+---+---+
| D | E | F |
+---+---+---+
.
<table>
<col style="width:33.33%">
<col style="width:33.33%">
<col style="width:33.33%">
<tbody>
<tr>
<td colspan="2" rowspan="2">AAAAA
AAAAA
AAAAA</td>
<td>B</td>
</tr>
<tr>
<td>C</td>
</tr>
<tr>
<td>D</td>
<td>E</td>
<td>F</td>
</tr>
</tbody>
</table>
````````````````````````````````
A grid table may not have irregularly shaped cells:
```````````````````````````````` example
+---+---+---+
| AAAAA | B |
+ A +---+ B +
| A | C | B |
+---+---+---+
| DDDDD | E |
+---+---+---+
.
<p>+---+---+---+
| AAAAA | B |
+ A +---+ B +
| A | C | B |
+---+---+---+
| DDDDD | E |
+---+---+---+</p>
````````````````````````````````
An empty `+` on a line should result in a simple empty list output:
```````````````````````````````` example
+
.
<ul>
<li></li>
</ul>
````````````````````````````````

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>
@@ -345,7 +346,8 @@ The first row is considered as a **header row** if it is separated from the regu
</table>
````````````````````````````````
The text alignment is defined by default to be left.
The text alignment is defined by default to be center for header and left for cells. If the left alignment is applied, it will force the column heading to be left aligned.
There is no way to define a different alignment for heading and cells (apart from the default).
The text alignment can be changed by using the character `:` with the header column separator:
```````````````````````````````` example
@@ -357,19 +359,19 @@ The text alignment can be changed by using the character `:` with the header col
<table>
<thead>
<tr>
<th>a</th>
<th style="text-align: left;">a</th>
<th style="text-align: center;">b</th>
<th style="text-align: right;">c</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td style="text-align: left;">0</td>
<td style="text-align: center;">1</td>
<td style="text-align: right;">2</td>
</tr>
<tr>
<td>3</td>
<td style="text-align: left;">3</td>
<td style="text-align: center;">4</td>
<td style="text-align: right;">5</td>
</tr>
@@ -377,6 +379,31 @@ The text alignment can be changed by using the character `:` with the header col
</table>
````````````````````````````````
Test alignment with starting and ending pipes:
```````````````````````````````` example
| abc | def | ghi |
|:---:|-----|----:|
| 1 | 2 | 3 |
.
<table>
<thead>
<tr>
<th style="text-align: center;">abc</th>
<th>def</th>
<th style="text-align: right;">ghi</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center;">1</td>
<td>2</td>
<td style="text-align: right;">3</td>
</tr>
</tbody>
</table>
````````````````````````````````
The following example shows a non matching header column separator:
```````````````````````````````` example
@@ -482,3 +509,42 @@ a | b
</table>
````````````````````````````````
** Tests **
Tests trailing spaces after pipes
```````````````````````````````` example
| abc | def |
|---|---|
| cde| ddd|
| eee| fff|
| fff | fffff |
|gggg | ffff |
.
<table>
<thead>
<tr>
<th>abc</th>
<th>def</th>
</tr>
</thead>
<tbody>
<tr>
<td>cde</td>
<td>ddd</td>
</tr>
<tr>
<td>eee</td>
<td>fff</td>
</tr>
<tr>
<td>fff</td>
<td>fffff</td>
</tr>
<tr>
<td>gggg</td>
<td>ffff</td>
</tr>
</tbody>
</table>
````````````````````````````````

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,30 @@ 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/cfc84164475d3bec8be9482c21a705adc93a54f5/spec.txt", string.Empty), // 0.26 specs
new KeyValuePair<string, string>(Host.ResolvePath("PipeTableSpecs.md"), "pipetables|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("FootnotesSpecs.md"), "footnotes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("GenericAttributesSpecs.md"), "attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("EmphasisExtraSpecs.md"), "emphasisextras|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("HardlineBreakSpecs.md"), "hardlinebreak|advanced+hardlinebreak"),
new KeyValuePair<string, string>(Host.ResolvePath("GridTableSpecs.md"), "gridtables|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("CustomContainerSpecs.md"), "customcontainers+attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("DefinitionListSpecs.md"), "definitionlists+attributes|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("EmojiSpecs.md"), "emojis|advanced+emojis"),
new KeyValuePair<string, string>(Host.ResolvePath("AbbreviationSpecs.md"), "abbreviations|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("ListExtraSpecs.md"), "listextras|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("FigureFooterAndCiteSpecs.md"), "figures+footers+citations|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("MathSpecs.md"), "mathematics|advanced"),
new KeyValuePair<string, string>(Host.ResolvePath("BootstrapSpecs.md"), "bootstrap+pipetables+figures+attributes"),
new KeyValuePair<string, string>(Host.ResolvePath("MediaSpecs.md"), "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"),
new KeyValuePair<string, string>(Host.ResolvePath("YamlSpecs.md"), "yaml"),
new KeyValuePair<string, string>(Host.ResolvePath("AutoLinks.md"), "autolinks|advanced"),
};
var emptyLines = false;
var displayEmptyLines = false;

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,44 @@
# Extensions
Adds support for YAML frontmatter parsing:
## YAML frontmatter discard
If a frontmatter is present, it will not be rendered:
```````````````````````````````` example
---
this: is a frontmatter
---
This is a text
.
<p>This is a text</p>
````````````````````````````````
But if a frontmatter doesn't happen on the first line, it will be parse as regular Markdown content
```````````````````````````````` example
This is a text1
---
this: is a frontmatter
---
This is a text2
.
<h2>This is a text1</h2>
<h2>this: is a frontmatter</h2>
<p>This is a text2</p>
````````````````````````````````
It expects an exact 3 dashes `---`:
```````````````````````````````` example
----
this: is a frontmatter
----
This is a text
.
<hr />
<h2>this: is a frontmatter</h2>
<p>This is a text</p>
````````````````````````````````

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

@@ -15,25 +15,30 @@ namespace Markdig.Tests
foreach (var pipeline in GetPipeline(extensions))
{
Console.WriteLine($"Pipeline configured with extensions: {pipeline.Key}");
// Uncomment this line to get more debug information for process inlines.
//pipeline.DebugLog = Console.Out;
var result = Markdown.ToHtml(inputText, pipeline.Value);
result = Compact(result);
expectedOutputText = Compact(expectedOutputText);
Console.WriteLine("```````````````````Source");
Console.WriteLine(DisplaySpaceAndTabs(inputText));
Console.WriteLine("```````````````````Result");
Console.WriteLine(DisplaySpaceAndTabs(result));
Console.WriteLine("```````````````````Expected");
Console.WriteLine(DisplaySpaceAndTabs(expectedOutputText));
Console.WriteLine("```````````````````");
Console.WriteLine();
TextAssert.AreEqual(expectedOutputText, result);
TestSpec(inputText, expectedOutputText, pipeline.Value);
}
}
public static void TestSpec(string inputText, string expectedOutputText, MarkdownPipeline pipeline)
{
// Uncomment this line to get more debug information for process inlines.
//pipeline.DebugLog = Console.Out;
var result = Markdown.ToHtml(inputText, pipeline);
result = Compact(result);
expectedOutputText = Compact(expectedOutputText);
Console.WriteLine("```````````````````Source");
Console.WriteLine(DisplaySpaceAndTabs(inputText));
Console.WriteLine("```````````````````Result");
Console.WriteLine(DisplaySpaceAndTabs(result));
Console.WriteLine("```````````````````Expected");
Console.WriteLine(DisplaySpaceAndTabs(expectedOutputText));
Console.WriteLine("```````````````````");
Console.WriteLine();
TextAssert.AreEqual(expectedOutputText, result);
}
private static IEnumerable<KeyValuePair<string, MarkdownPipeline>> GetPipeline(string extensionsGroupText)
{
// For the standard case, we make sure that both the CommmonMark core and Extra/Advanced are CommonMark compliant!
@@ -64,7 +69,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 +85,7 @@ namespace Markdig.Tests
private static string Compact(string html)
{
// Normalize the output to make it compatible with CommonMark specs
html = html.Replace("\r", "").Trim();
html = html.Replace("\r\n", "\n").Replace(@"\r", @"\n").Trim();
html = Regex.Replace(html, @"\s+</li>", "</li>");
html = Regex.Replace(html, @"<li>\s+", "<li>");
html = html.Normalize(NormalizationForm.FormKD);

View File

@@ -2,6 +2,9 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System;
using System.Linq;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using NUnit.Framework;
namespace Markdig.Tests
@@ -27,6 +30,102 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
Console.WriteLine(result);
}
[Test]
public void TestEmptyLiteral()
{
var text = @"> *some text*
> some other text";
var doc = Markdown.Parse(text);
Assert.True(doc.Descendants().OfType<LiteralInline>().All(x => !x.Content.IsEmpty),
"There should not have any empty literals");
}
[Test]
public void TestSelfPipeline1()
{
var text = @" <!--markdig:pipetables-->
a | b
- | -
0 | 1
";
TestParser.TestSpec(text, @"<!--markdig:pipetables-->
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>1</td>
</tr>
</tbody>
</table>
", "self");
}
[Test]
public void TestListBug()
{
// TODO: Add this test back to the CommonMark specs
var text = @"- item1
- item2
- item3
- item4";
TestParser.TestSpec(text, @"<ul>
<li>item1
<ul>
<li>item2
<ul>
<li>item3
<ul>
<li>item4</li>
</ul></li>
</ul></li>
</ul></li>
</ul>
");
}
[Test]
public void TestHtmlBug()
{
TestParser.TestSpec(@" # header1
<pre class='copy'>
blabla
</pre>
# header2
", @"<h1>header1</h1>
<pre class='copy'>
blabla
</pre>
<h1>header2</h1>");
}
[Test]
public void TestStandardUriEscape()
{
TestParser.TestSpec(@"![你好](你好.png)", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
}
[Test]
public void TestBugAdvancaed()
{
TestParser.TestSpec(@"`https://{domain}/callbacks`
#### HEADING
Paragraph
", "<p><code>https://{domain}/callbacks</code></p>\n<h4 id=\"heading\">HEADING</h4>\n<p>Paragraph</p>", "advanced");
}
[Test]
public void TestSamePipelineAllExtensions()
{

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
");
}
@@ -293,10 +293,6 @@ literal ( 0, 9) 9-9
");
}
[Test]
public void TestThematicBreak()
{
@@ -373,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
");
}
@@ -658,7 +655,21 @@ 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
");
}
[Test]
public void TestIndentedCodeAfterList()
{
// 0 1 2 3 4 5
// 012345678901234567 8 901234567890123456 789012345678901234 56789
Check("1) Some list item\n\n some code\n more code\n", @"
list ( 0, 0) 0-53
listitem ( 0, 0) 0-53
paragraph ( 0, 3) 3-16
literal ( 0, 3) 3-16
code ( 2, 0) 19-53
");
}
@@ -669,7 +680,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
");
}
@@ -680,7 +691,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

@@ -101,20 +101,9 @@ namespace Markdig.Extensions.Abbreviations
for (int i = content.Start; i < content.End; i++)
{
string match;
if (matcher.TryMatch(text, i, content.End - i + 1, out match))
if (matcher.TryMatch(text, i, content.End - i + 1, out match) && IsValidAbbreviation(match, content, i))
{
// The word matched must be embraced by punctuation or whitespace or \0.
var c = content.PeekCharAbsolute(i - 1);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
continue;
}
var indexAfterMatch = i + match.Length;
c = content.PeekCharAbsolute(indexAfterMatch);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
continue;
}
// We should have a match, but in case...
Abbreviation abbr;
@@ -126,12 +115,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 +140,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
@@ -192,5 +185,39 @@ namespace Markdig.Extensions.Abbreviations
}
};
}
private static bool IsValidAbbreviation(string match, StringSlice content, int matchIndex)
{
// The word matched must be embraced by punctuation or whitespace or \0.
var index = matchIndex - 1;
while (index > content.Start)
{
var c = content.PeekCharAbsolute(index);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
return false;
}
if (!c.IsAsciiPunctuation())
{
break;
}
index--;
}
index = matchIndex + match.Length;
while (index < content.End)
{
var c = content.PeekCharAbsolute(index);
if (!(c == '\0' || c.IsAsciiPunctuation() || c.IsWhitespace()))
{
return false;
}
if (!c.IsAsciiPunctuation())
{
break;
}
index++;
}
return true;
}
}
}

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

@@ -0,0 +1,29 @@
// 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.Inlines;
namespace Markdig.Extensions.AutoLinks
{
/// <summary>
/// Extension to automatically create <see cref="LinkInline"/> when a link url http: or mailto: is found.
/// </summary>
/// <seealso cref="Markdig.IMarkdownExtension" />
public class AutoLinkExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.InlineParsers.Contains<AutoLinkParser>())
{
// Insert the parser before any other parsers
pipeline.InlineParsers.Insert(0, new AutoLinkParser());
}
}
public void Setup(IMarkdownRenderer renderer)
{
}
}
}

View File

@@ -0,0 +1,143 @@
// 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.Parsers;
using Markdig.Syntax.Inlines;
namespace Markdig.Extensions.AutoLinks
{
/// <summary>
/// The inline parser used to for autolinks.
/// </summary>
/// <seealso cref="Markdig.Parsers.InlineParser" />
public class AutoLinkParser : InlineParser
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLinkParser"/> class.
/// </summary>
public AutoLinkParser()
{
OpeningCharacters = new char[]
{
'h', // for http:// and https://
'f', // for ftp://
'm', // for mailto:
'w', // for www.
};
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
string match;
// Previous char must be a whitespace or a punctuation
var previousChar = slice.PeekCharExtra(-1);
if (!previousChar.IsAsciiPunctuation() && !previousChar.IsWhiteSpaceOrZero())
{
return false;
}
var startPosition = slice.Start;
var c = slice.CurrentChar;
// Precheck URL
switch (c)
{
case 'h':
if (!slice.MatchLowercase("ttp://", 1) && !slice.MatchLowercase("ttps://", 1))
{
return false;
}
break;
case 'f':
if (!slice.MatchLowercase("tp://", 1))
{
return false;
}
break;
case 'm':
if (!slice.MatchLowercase("ailto:", 1))
{
return false;
}
break;
case 'w':
if (!slice.MatchLowercase("ww.", 1) || previousChar == '/') // We won't match http:/www. or /www.xxx
{
return false;
}
break;
}
// Parse URL
string link;
if (!LinkHelper.TryParseUrl(ref slice, out link))
{
return false;
}
// Post-check URL
switch (c)
{
case 'h':
if (string.Equals(link, "http://", StringComparison.OrdinalIgnoreCase) ||
string.Equals(link, "https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'f':
if (string.Equals(link, "ftp://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
break;
case 'm':
if (string.Equals(link, "mailto:", StringComparison.OrdinalIgnoreCase) || !link.Contains("@"))
{
return false;
}
break;
case 'w':
// We require at least two .
if (link.Length <= "www.x.y".Length || link.IndexOf(".", 4, StringComparison.Ordinal) < 0)
{
return false;
}
break;
}
int line;
int column;
var inline = new LinkInline()
{
Span =
{
Start = processor.GetSourcePosition(startPosition, out line, out column),
},
Line = line,
Column = column,
Url = c == 'w' ? "http://" + link : link,
IsClosed = true,
};
inline.Span.End = inline.Span.Start + link.Length - 1;
inline.UrlSpan = inline.Span;
inline.AppendChild(new LiteralInline()
{
Span = inline.Span,
Line = line,
Column = column,
Content = new StringSlice(slice.Text, startPosition, startPosition + link.Length - 1),
IsClosed = true
});
processor.Inline = inline;
return true;
}
}
}

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

@@ -49,7 +49,7 @@ namespace Markdig.Extensions.DefinitionLists
{
if (!hasOpendd)
{
renderer.Write("<dd>");
renderer.Write("<dd").WriteAttributes(definitionItem).Write(">");
countdd = 0;
hasOpendd = true;
}

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,27 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
namespace Markdig.Extensions.NonAsciiNoEscape
{
/// <summary>
/// Extension that will disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE.
/// </summary>
public class NonAsciiNoEscapeExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}
public void Setup(IMarkdownRenderer renderer)
{
var htmlRenderer = renderer as HtmlRenderer;
if (htmlRenderer != null)
{
htmlRenderer.UseNonAsciiNoEscape = true;
}
}
}
}

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,51 @@ namespace Markdig.Extensions.Tables
}
var line = processor.Line;
// A grid table must start with a line like this:
// + ------------- + ------------ + ---------------------------------------- +
// Spaces are optional
GridTableState tableState = null;
// Match the first row that should be of the minimal form: +---------------
var c = line.CurrentChar;
var startPosition = processor.Start;
while (true)
var lineStart = line.Start;
while (c == '+')
{
if (c == '+')
var columnStart = line.Start;
line.NextChar();
line.TrimStart();
// if we have reached the end of the line, exit
c = line.CurrentChar;
if (c == 0)
{
var startCharacter = line.Start;
line.NextChar();
if (line.IsEmptyOrWhitespace())
{
if (tableState == null)
{
return BlockState.None;
}
break;
}
TableColumnAlign align;
if (TableHelper.ParseColumnHeader(ref line, '-', out align))
{
if (tableState == null)
{
tableState = new GridTableState()
{
Start = processor.Start,
ExpectRow = true,
};
}
tableState.AddColumn(startCharacter - startPosition, line.Start - 1 - startPosition, align);
c = line.CurrentChar;
continue;
}
break;
}
// If we have any other characters, this is an invalid line
return BlockState.None;
// Parse a column alignment
TableColumnAlign? columnAlign;
if (!TableHelper.ParseColumnHeader(ref line, '-', out columnAlign))
{
return BlockState.None;
}
tableState = tableState ?? new GridTableState { Start = processor.Start, ExpectRow = true };
tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign);
c = line.CurrentChar;
}
if (c != 0 || tableState == null)
{
return BlockState.None;
}
// Store the line (if we need later to build a ParagraphBlock because the GridTable was in fact invalid)
tableState.AddLine(ref processor.Line);
// Create the grid table
var table = new Table(this);
table.SetData(typeof(GridTableState), tableState);
// Calculate the total width of all columns
int totalWidth = 0;
foreach (var columnSlice in tableState.ColumnSlices)
{
totalWidth += columnSlice.End - columnSlice.Start;
totalWidth += columnSlice.End - columnSlice.Start - 1;
}
// Store the column width and alignment
@@ -89,8 +78,8 @@ namespace Markdig.Extensions.Tables
var columnDefinition = new TableColumnDefinition
{
// Column width proportional to the total width
Width = (float)(columnSlice.End - columnSlice.Start) * 100.0f / totalWidth,
Alignment = columnSlice.Align,
Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth,
Alignment = columnSlice.Align
};
table.ColumnDefinitions.Add(columnDefinition);
}
@@ -102,253 +91,276 @@ namespace Markdig.Extensions.Tables
public override BlockState TryContinue(BlockProcessor processor, Block block)
{
var gridTable = (Table) block;
var gridTable = (Table)block;
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
// We expect to start at the same
//if (processor.Start == tableState.Start)
tableState.AddLine(ref processor.Line);
if (processor.CurrentChar == '+')
{
var columns = tableState.ColumnSlices;
foreach (var columnSlice in columns)
{
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
columnSlice.CurrentColumnSpan = 0;
}
if (processor.CurrentChar == '+')
{
var result = ParseRowSeparator(processor, tableState, gridTable);
if (result != BlockState.None)
{
return result;
}
}
else if (processor.CurrentChar == '|')
{
var line = processor.Line;
// | ------------- | ------------ | ---------------------------------------- |
// Calculate the colspan for the new row
int columnIndex = -1;
foreach (var columnSlice in columns)
{
if (line.PeekCharExtra(columnSlice.Start) == '|')
{
columnIndex++;
}
if (columnIndex >= 0)
{
columns[columnIndex].CurrentColumnSpan++;
}
}
// Check if the colspan of the current row is the same than the previous row
bool continueRow = true;
foreach (var columnSlice in columns)
{
if (columnSlice.PreviousColumnSpan != columnSlice.CurrentColumnSpan)
{
continueRow = false;
break;
}
}
// If the current row doesn't continue the previous row (col span are different)
// Close the previous row
if (!continueRow)
{
TerminateLastRow(processor, tableState, gridTable, false);
}
for (int i = 0; i < columns.Count;)
{
var column = columns[i];
var nextColumnIndex = i + column.CurrentColumnSpan;
// If the span is 0, we exit
if (nextColumnIndex == i)
{
break;
}
var nextColumn = nextColumnIndex < columns.Count ? columns[nextColumnIndex] : null;
var sliceForCell = line;
sliceForCell.Start = 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

@@ -31,7 +31,7 @@ namespace Markdig.Extensions.Tables
Lines.Add(line);
}
public void AddColumn(int start, int end, TableColumnAlign align)
public void AddColumn(int start, int end, TableColumnAlign? align)
{
if (ColumnSlices == null)
{
@@ -60,7 +60,7 @@ namespace Markdig.Extensions.Tables
public int End { get; set; }
public TableColumnAlign Align { get; set; }
public TableColumnAlign? Align { get; set; }
public int CurrentColumnSpan { get; set; }

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,17 +81,31 @@ namespace Markdig.Extensions.Tables
{
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
}
if (table.ColumnDefinitions != null && i < table.ColumnDefinitions.Count)
if (cell.RowSpan != 1)
{
switch (table.ColumnDefinitions[i].Alignment)
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
}
if (table.ColumnDefinitions != null)
{
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
? i
: cell.ColumnIndex;
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
var alignment = table.ColumnDefinitions[columnIndex].Alignment;
if (alignment.HasValue)
{
case TableColumnAlign.Center:
renderer.Write(" style=\"text-align: center;\"");
break;
case TableColumnAlign.Right:
renderer.Write(" style=\"text-align: right;\"");
break;
switch (alignment)
{
case TableColumnAlign.Center:
renderer.Write(" style=\"text-align: center;\"");
break;
case TableColumnAlign.Right:
renderer.Write(" style=\"text-align: right;\"");
break;
case TableColumnAlign.Left:
renderer.Write(" style=\"text-align: left;\"");
break;
}
}
}
renderer.WriteAttributes(cell);
@@ -101,7 +118,7 @@ namespace Markdig.Extensions.Tables
}
renderer.Write(cell);
renderer.ImplicitParagraph = previousImplicitParagraph;
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
}
renderer.WriteLine("</tr>");

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

@@ -34,9 +34,10 @@ namespace Markdig.Extensions.Tables
{
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();
@@ -400,7 +453,7 @@ namespace Markdig.Extensions.Tables
return false;
}
private static bool ParseHeaderString(Inline inline, out TableColumnAlign align)
private static bool ParseHeaderString(Inline inline, out TableColumnAlign? align)
{
align = 0;
var literal = inline as LiteralInline;
@@ -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 = 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;
continue;
}
// The last delimiter is always null,
for (int j = i + 1; j < delimiters.Count; j++)
{
var delimiter = delimiters[j];
var nextDelimiter = j + 1 < delimiters.Count ? delimiters[j + 1] : null;
var columnDelimiter = delimiter as PipeTableDelimiterInline;
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
{
continue;
}
// Check the left side of a `|` delimiter
TableColumnAlign? align = null;
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
{
break;
}
// Create aligns until we may have a header row
if (aligns == null)
{
aligns = new List<TableColumnDefinition>();
}
aligns.Add(new TableColumnDefinition() { Alignment = align });
// If this is the last delimiter, we need to check the right side of the `|` delimiter
if (nextDelimiter == null)
{
var nextSibling = columnDelimiter != null
? columnDelimiter.FirstChild
: delimiter.NextSibling;
if (!ParseHeaderString(nextSibling, out align))
{
break;
}
isValidRow = true;
aligns.Add(new TableColumnDefinition() { Alignment = align });
break;
}
// If we are on a Line delimiter, exit
if (IsLine(delimiter))
{
isValidRow = true;
break;
}
}
break;
}
return isValidRow ? aligns : null;
@@ -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

@@ -17,6 +17,6 @@ namespace Markdig.Extensions.Tables
/// <summary>
/// Gets or sets the column alignment.
/// </summary>
public TableColumnAlign Alignment { get; set; }
public TableColumnAlign? Alignment { get; set; }
}
}

View File

@@ -20,7 +20,7 @@ namespace Markdig.Extensions.Tables
/// <returns>
/// <c>true</c> if parsing was successfull
/// </returns>
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign align)
public static bool ParseColumnHeader(ref StringSlice slice, char delimiterChar, out TableColumnAlign? align)
{
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
}
@@ -34,7 +34,7 @@ namespace Markdig.Extensions.Tables
/// <returns>
/// <c>true</c> if parsing was successfull
/// </returns>
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign align)
public static bool ParseColumnHeaderAuto(ref StringSlice slice, out char delimiterChar, out TableColumnAlign? align)
{
delimiterChar = '\0';
return ParseColumnHeaderDetect(ref slice, ref delimiterChar, out align);
@@ -49,11 +49,10 @@ namespace Markdig.Extensions.Tables
/// <returns>
/// <c>true</c> if parsing was successfull
/// </returns>
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign align)
public static bool ParseColumnHeaderDetect(ref StringSlice slice, ref char delimiterChar, out TableColumnAlign? align)
{
align = TableColumnAlign.Left;
align = null;
// Work on a copy of the slice
slice.TrimStart();
var c = slice.CurrentChar;
bool hasLeft = false;
@@ -87,6 +86,7 @@ namespace Markdig.Extensions.Tables
count++;
}
// We expect at least one `-` delimiter char
if (count == 0)
{
return false;
@@ -104,7 +104,7 @@ namespace Markdig.Extensions.Tables
align = hasLeft && hasRight
? TableColumnAlign.Center
: hasRight ? TableColumnAlign.Right : TableColumnAlign.Left;
: hasRight ? TableColumnAlign.Right : hasLeft ? TableColumnAlign.Left : (TableColumnAlign?) null;
return true;
}

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.Parsers;
using Markdig.Syntax;
namespace Markdig.Extensions.Yaml
{
/// <summary>
/// A YAML frontmatter block.
/// </summary>
/// <seealso cref="Markdig.Syntax.CodeBlock" />
public class YamlFrontMatterBlock : CodeBlock, IFencedBlock
{
/// <summary>
/// Initializes a new instance of the <see cref="YamlFrontMatterBlock"/> class.
/// </summary>
/// <param name="parser">The parser.</param>
public YamlFrontMatterBlock(BlockParser parser) : base(parser)
{
}
public string Info { get; set; }
public string Arguments { get; set; }
public int FencedCharCount { get; set; }
public char FencedChar { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Yaml
{
/// <summary>
/// Extension to discard a YAML frontmatter at the beginning of a Markdown document.
/// </summary>
public class YamlFrontMatterExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
if (!pipeline.BlockParsers.Contains<YamlFrontMatterParser>())
{
// Insert the YAML parser before the thematic break parser, as it is also triggered on a --- dash
pipeline.BlockParsers.InsertBefore<ThematicBreakParser>(new YamlFrontMatterParser());
}
}
public void Setup(IMarkdownRenderer renderer)
{
if (!renderer.ObjectRenderers.Contains<YamlFrontMatterRenderer>())
{
renderer.ObjectRenderers.InsertBefore<CodeBlockRenderer>(new YamlFrontMatterRenderer());
}
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Parsers;
namespace Markdig.Extensions.Yaml
{
/// <summary>
/// Block parser for a YAML frontmatter.
/// </summary>
/// <seealso cref="Markdig.Parsers.FencedBlockParserBase{YamlFrontMatterBlock}" />
public class YamlFrontMatterParser : FencedBlockParserBase<YamlFrontMatterBlock>
{
// We reuse a FencedCodeBlock parser to grab a frontmatter, only active if it happens on the first line of the document.
/// <summary>
/// Initializes a new instance of the <see cref="FencedCodeBlockParser"/> class.
/// </summary>
public YamlFrontMatterParser()
{
OpeningCharacters = new[] { '-' };
InfoPrefix = null;
// We expect only 3 --- at the beginning of the file no more, no less
MinimumMatchCount = 3;
MaximumMatchCount = 3;
}
protected override YamlFrontMatterBlock CreateFencedBlock(BlockProcessor processor)
{
return new YamlFrontMatterBlock(this);
}
public override BlockState TryOpen(BlockProcessor processor)
{
// Only accept a frontmatter at the beginning of the file
if (processor.LineIndex != 0)
{
return BlockState.None;
}
return base.TryOpen(processor);
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Markdig.Extensions.Yaml
{
/// <summary>
/// Empty renderer for a <see cref="YamlFrontMatterBlock"/>
/// </summary>
/// <seealso cref="Markdig.Renderers.Html.HtmlObjectRenderer{YamlFrontMatterBlock}" />
public class YamlFrontMatterRenderer : HtmlObjectRenderer<YamlFrontMatterBlock>
{
protected override void Write(HtmlRenderer renderer, YamlFrontMatterBlock obj)
{
}
}
}

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

@@ -168,6 +168,17 @@ namespace Markdig.Helpers
return i == text.Length;
}
/// <summary>
/// Matches the specified text using lowercase comparison.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="offset">The offset.</param>
/// <returns><c>true</c> if the text matches; <c>false</c> otherwise</returns>
public bool MatchLowercase(string text, int offset = 0)
{
return MatchLowercase(text, End, offset);
}
/// <summary>
/// Matches the specified text using lowercase comparison.
/// </summary>
@@ -194,52 +205,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 - Start))
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;
@@ -42,6 +43,7 @@ namespace Markdig
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);
@@ -67,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);
@@ -96,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

@@ -5,10 +5,12 @@
using System;
using Markdig.Extensions.Abbreviations;
using Markdig.Extensions.AutoIdentifiers;
using Markdig.Extensions.AutoLinks;
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,10 +21,14 @@ 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.NonAsciiNoEscape;
using Markdig.Extensions.Tables;
using Markdig.Extensions.TaskLists;
using Markdig.Extensions.Yaml;
using Markdig.Parsers;
using Markdig.Parsers.Inlines;
@@ -56,9 +62,63 @@ namespace Markdig
.UsePipeTables()
.UseListExtras()
.UseTaskLists()
.UseDiagrams()
.UseAutoLinks()
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
}
/// <summary>
/// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy`
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseAutoLinks(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<AutoLinkExtension>();
return pipeline;
}
/// <summary>
/// Uses this extension to disable URI escape with % characters for non-US-ASCII characters in order to workaround a bug under IE/Edge with local file links containing non US-ASCII chars. DO NOT USE OTHERWISE.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseNonAsciiNoEscape(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<NonAsciiNoEscapeExtension>();
return pipeline;
}
/// <summary>
/// Uses YAML frontmatter extension that will parse a YAML frontmatter into the MarkdownDocument. Note that they are not rendered by any default HTML renderer.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
public static MarkdownPipelineBuilder UseYamlFrontMatter(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<YamlFrontMatterExtension>();
return pipeline;
}
/// <summary>
/// Uses the self pipeline extension that will detect the pipeline to use from the markdown input that contains a special tag. See <see cref="SelfPipelineExtension"/>
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <param name="defaultTag">The default tag to use to match the self pipeline configuration. By default, <see cref="SelfPipelineExtension.DefaultTag"/>, meaning that the HTML tag will be &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>
@@ -70,6 +130,17 @@ namespace Markdig
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>
@@ -155,7 +226,7 @@ namespace Markdig
}
/// <summary>
/// Uses the boostrap extension.
/// Uses the bootstrap extension.
/// </summary>
/// <param name="pipeline">The pipeline.</param>
/// <returns>The modified pipeline</returns>
@@ -334,6 +405,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>
@@ -368,10 +450,14 @@ namespace Markdig
return pipeline;
}
// TODO: the extension string should come from the extension itself instead of this hardcoded switch case.
foreach (var extension in extensions.Split(new[] { '+' }, StringSplitOptions.RemoveEmptyEntries))
{
switch (extension.ToLowerInvariant())
{
case "common":
break;
case "advanced":
pipeline.UseAdvancedExtensions();
break;
@@ -435,11 +521,29 @@ namespace Markdig
case "tasklists":
pipeline.UseTaskLists();
break;
case "diagrams":
pipeline.UseDiagrams();
break;
case "nofollowlinks":
pipeline.UseNoFollowLinks();
break;
case "nohtml":
pipeline.DisableHtml();
break;
case "yaml":
pipeline.UseYamlFrontMatter();
break;
case "nonascii-noescape":
pipeline.UseNonAsciiNoEscape();
break;
case "autolinks":
pipeline.UseAutoLinks();
break;
default:
throw new ArgumentException($"unknown extension {extension}");
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
}
}
return pipeline;
}
}
}
}

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;
@@ -305,6 +304,64 @@ namespace Markdig.Parsers
}
}
/// <summary>
/// Unwind any previous indent from the current character back to the first space.
/// </summary>
public void UnwindAllIndents()
{
// Find the previous first space on the current line
var previousStart = Line.Start;
for (; Line.Start > originalLineStart; Line.Start--)
{
var c = Line.PeekCharAbsolute(Line.Start - 1);
if (c == 0)
{
break;
}
if (!c.IsSpaceOrTab())
{
break;
}
}
var targetStart = Line.Start;
// Nothing changed? Early exit
if (previousStart == targetStart)
{
return;
}
// TODO: factorize the following code with what is done with GoToColumn
// If we have found the first space, we need to recalculate the correct column
Line.Start = originalLineStart;
Column = 0;
ColumnBeforeIndent = 0;
StartBeforeIndent = originalLineStart;
for (; Line.Start < targetStart; Line.Start++)
{
var c = Line.Text[Line.Start];
if (c == '\t')
{
Column = CharHelper.AddTab(Column);
}
else
{
if (!c.IsSpaceOrTab())
{
ColumnBeforeIndent = Column + 1;
StartBeforeIndent = Line.Start + 1;
}
Column++;
}
}
// Reset the indent
ColumnBeforeIndent = Column;
StartBeforeIndent = Start;
}
/// <summary>
/// Moves to the position to the code indent (<see cref="ColumnBeforeIndent"/> + 4 spaces).
/// </summary>
@@ -402,6 +459,11 @@ namespace Markdig.Parsers
parserStateCache.Release(this);
}
internal bool IsOpen(Block block)
{
return OpenedBlocks.Contains(block);
}
/// <summary>
/// Closes a block at the specified index.
/// </summary>

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,15 +18,23 @@ namespace Markdig.Parsers
public override BlockState TryOpen(BlockProcessor processor)
{
var startPosition = processor.Line.Start;
var result = TryContinue(processor, null);
if (result == BlockState.Continue)
{
// Save the column where we need to go back
var column = processor.Column;
// Unwind all indents all spaces before in order to calculate correct span
processor.UnwindAllIndents();
processor.NewBlocks.Push(new CodeBlock(this)
{
Column = processor.Column,
Span = new SourceSpan(startPosition, processor.Line.End)
Span = new SourceSpan(processor.Start, processor.Line.End)
});
// Go back to the correct column
processor.GoToColumn(column);
}
return result;
}
@@ -48,7 +56,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;
@@ -210,6 +211,7 @@ namespace Markdig.Parsers.Inlines
// compact reference link/image,
// or shortcut reference link/image
var parentDelimiter = openParent.Parent;
var savedText = text;
switch (text.CurrentChar)
{
case '(':
@@ -238,7 +240,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.
@@ -252,9 +254,10 @@ namespace Markdig.Parsers.Inlines
return true;
}
break;
default:
text = savedText;
goto default;
default:
var labelSpan = SourceSpan.Empty;
string label = null;
bool isLabelSpanLocal = true;

View File

@@ -65,20 +65,36 @@ 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
{
// Create a new LiteralInline only if it is not empty
var newSlice = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty;
if (!newSlice.IsEmpty)
{
processor.Inline = new LiteralInline()
{
Content = length > 0 ? newSlice : StringSlice.Empty,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
Line = line,
Column = column,
};
}
}
slice.Start = nextStart;
// Call only PostMatch if necessary
if (PostMatch != null)
if (processor.Inline is LiteralInline)
{
PostMatch(processor, ref slice);
PostMatch?.Invoke(processor, ref slice);
}
return true;

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.6";
public const string Version = "0.9.0";
}
}

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

@@ -118,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)
{
@@ -127,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("\"");
}

View File

@@ -64,6 +64,8 @@ namespace Markdig.Renderers
/// </summary>
public bool ImplicitParagraph { get; set; }
public bool UseNonAsciiNoEscape { get; set; }
/// <summary>
/// Writes the content escaped for HTML.
/// </summary>
@@ -83,26 +85,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 +115,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 +137,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 +156,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;
}
}
@@ -194,22 +205,29 @@ namespace Markdig.Renderers
Write(content, previousPosition, i - previousPosition);
previousPosition = i + 1;
byte[] bytes;
if (c >= '\ud800' && c <= '\udfff' && previousPosition < length)
// Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working
if (UseNonAsciiNoEscape)
{
bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] });
// Skip next char as it is decoded above
i++;
previousPosition = i + 1;
Write(c);
}
else
{
bytes = Encoding.UTF8.GetBytes(new[] { c });
}
for (var j = 0; j < bytes.Length; j++)
{
Write($"%{bytes[j]:X2}");
byte[] bytes;
if (c >= '\ud800' && c <= '\udfff' && previousPosition < length)
{
bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] });
// Skip next char as it is decoded above
i++;
previousPosition = i + 1;
}
else
{
bytes = Encoding.UTF8.GetBytes(new[] { c });
}
for (var j = 0; j < bytes.Length; j++)
{
Write($"%{bytes[j]:X2}");
}
}
}
}
@@ -233,8 +251,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 +275,7 @@ namespace Markdig.Renderers
{
Write(" ");
}
WriteEscape(cssClass);
WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass);
}
Write("\"");
}
@@ -284,8 +303,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 +320,7 @@ namespace Markdig.Renderers
}
if (escape)
{
WriteEscape(ref slices[i].Slice);
WriteEscape(ref slices[i].Slice, softEscape);
}
else
{

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

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