mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-12 05:44:48 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5980a485 | ||
|
|
19dd902519 | ||
|
|
f046a46275 | ||
|
|
25e9eafa8b | ||
|
|
f4ff981008 | ||
|
|
a1b48aff89 | ||
|
|
0aa34caa82 | ||
|
|
98ce9b1a06 | ||
|
|
ab157b21ea | ||
|
|
53c72d3031 | ||
|
|
165e2f97d0 | ||
|
|
6c577059ad | ||
|
|
0ea4dc769b | ||
|
|
a9b626e810 | ||
|
|
b90d6f9769 | ||
|
|
f0ea008c46 | ||
|
|
c43d5ccd63 | ||
|
|
d6d7b398e4 | ||
|
|
e1032e5094 | ||
|
|
a32ac298c5 | ||
|
|
2e6ab670cb | ||
|
|
72b7fce48c | ||
|
|
fa281f1ca1 | ||
|
|
1b92311aeb | ||
|
|
a593212f03 | ||
|
|
c4ec928953 | ||
|
|
e7df7fabeb | ||
|
|
1c187f2d81 | ||
|
|
da66cf90c3 | ||
|
|
0e8bd7407f | ||
|
|
d70f14addb | ||
|
|
e7b9eea3a5 | ||
|
|
97af9d822d | ||
|
|
cf7a09ab76 | ||
|
|
e755627421 | ||
|
|
f9be64a988 | ||
|
|
d65431e6cc | ||
|
|
86fb962fdb | ||
|
|
d003837b27 | ||
|
|
fd813e3c5a | ||
|
|
a3691c4423 | ||
|
|
9506f22025 | ||
|
|
06ae907949 | ||
|
|
555523b2af | ||
|
|
3b9772f772 | ||
|
|
891c80f48c | ||
|
|
105b09e1ec | ||
|
|
9fe7596a23 | ||
|
|
82af7cadc5 |
@@ -46,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
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<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" />
|
||||
@@ -87,6 +88,8 @@
|
||||
<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" />
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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
|
||||
|
||||
27
src/Markdig.Tests/Specs/NoHtmlSpecs.md
Normal file
27
src/Markdig.Tests/Specs/NoHtmlSpecs.md
Normal 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</td></tr></p>
|
||||
````````````````````````````````
|
||||
|
||||
For Block HTML:
|
||||
|
||||
```````````````````````````````` example
|
||||
<div>
|
||||
this is some text
|
||||
</div>
|
||||
.
|
||||
<p><div>
|
||||
this is some text
|
||||
</div></p>
|
||||
````````````````````````````````
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -507,3 +508,42 @@ a | b
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
** Tests **
|
||||
|
||||
Tests trailing spaces after pipes
|
||||
|
||||
```````````````````````````````` example
|
||||
| abc | def |
|
||||
|---|:---|
|
||||
| cde| ddd|
|
||||
| eee| fff|
|
||||
| fff | fffff |
|
||||
|gggg | ffff |
|
||||
.
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>cde</td>
|
||||
<td>ddd</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>eee</td>
|
||||
<td>fff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>fff</td>
|
||||
<td>fffff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>gggg</td>
|
||||
<td>ffff</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
````````````````````````````````
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ SOFTWARE.
|
||||
<#@ output extension=".cs" #><#
|
||||
var specFiles = new KeyValuePair<string, string>[] {
|
||||
// new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/master/spec.txt", string.Empty),
|
||||
new KeyValuePair<string, string>("https://raw.githubusercontent.com/jgm/CommonMark/91e045ca370258903ed138450373043a496ec64b/spec.txt", string.Empty), // 0.26 specs
|
||||
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"),
|
||||
@@ -59,6 +59,8 @@ SOFTWARE.
|
||||
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"),
|
||||
};
|
||||
var emptyLines = false;
|
||||
var displayEmptyLines = false;
|
||||
|
||||
25
src/Markdig.Tests/Specs/TestEmphasisPlus.cs
Normal file
25
src/Markdig.Tests/Specs/TestEmphasisPlus.cs
Normal 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>", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Markdig.Tests/Specs/YamlSpecs.md
Normal file
44
src/Markdig.Tests/Specs/YamlSpecs.md
Normal 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>
|
||||
````````````````````````````````
|
||||
|
||||
@@ -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!
|
||||
@@ -65,6 +70,7 @@ namespace Markdig.Tests
|
||||
foreach (var extensionsText in extensionGroups)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -28,30 +31,14 @@ Later in a text we are using HTML and it becomes an abbr tag HTML
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPipeTables()
|
||||
public void TestEmptyLiteral()
|
||||
{
|
||||
TestParser.TestSpec(@"
|
||||
| abc | def | ghi |
|
||||
|:---:|-----|----:|
|
||||
| 1 | 2 | 3 |
|
||||
", @"
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style=""text-align: center;"">abc</th>
|
||||
<th>def</th>
|
||||
<th style=""text-align: right;"">ghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style=""text-align: center;"">1</td>
|
||||
<td>2</td>
|
||||
<td style=""text-align: right;"">3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
", "advanced");
|
||||
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]
|
||||
@@ -123,6 +110,12 @@ blabla
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestStandardUriEscape()
|
||||
{
|
||||
TestParser.TestSpec(@"", "<p><img src=\"你好.png\" alt=\"你好\" /></p>", "nonascii-noescape");
|
||||
}
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestBugAdvancaed()
|
||||
|
||||
@@ -659,6 +659,20 @@ 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
|
||||
");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIndentedCodeWithTabs()
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -103,6 +102,19 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
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;
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Markdig.Extensions.DefinitionLists
|
||||
{
|
||||
if (!hasOpendd)
|
||||
{
|
||||
renderer.Write("<dd>");
|
||||
renderer.Write("<dd").WriteAttributes(definitionItem).Write(">");
|
||||
countdd = 0;
|
||||
hasOpendd = true;
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Markdig.Extensions.GenericAttributes
|
||||
}
|
||||
|
||||
var currentHtmlAttributes = objectToAttach.GetAttributes();
|
||||
attributes.CopyTo(currentHtmlAttributes, false, false);
|
||||
attributes.CopyTo(currentHtmlAttributes, true, false);
|
||||
|
||||
// Update the position of the attributes
|
||||
int line;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -100,205 +89,99 @@ namespace Markdig.Extensions.Tables
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
private static TableColumnAlign GetAlignment(bool hasLeft, bool hasRight)
|
||||
{
|
||||
return hasLeft && hasRight
|
||||
? TableColumnAlign.Center
|
||||
: hasRight ? TableColumnAlign.Right : TableColumnAlign.Left;
|
||||
}
|
||||
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var gridTable = (Table) block;
|
||||
var gridTable = (Table)block;
|
||||
var tableState = (GridTableState)block.GetData(typeof(GridTableState));
|
||||
|
||||
// We expect to start at the same
|
||||
//if (processor.Start == tableState.Start)
|
||||
tableState.AddLine(ref processor.Line);
|
||||
if (processor.CurrentChar == '+')
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
columnSlice.PreviousColumnSpan = columnSlice.CurrentColumnSpan;
|
||||
columnSlice.CurrentColumnSpan = 0;
|
||||
}
|
||||
|
||||
if (processor.CurrentChar == '+')
|
||||
{
|
||||
var result = ParseRowSeparator(processor, tableState, gridTable);
|
||||
if (result != BlockState.None)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else if (processor.CurrentChar == '|')
|
||||
{
|
||||
var line = processor.Line;
|
||||
|
||||
// | ------------- | ------------ | ---------------------------------------- |
|
||||
// Calculate the colspan for the new row
|
||||
int columnIndex = -1;
|
||||
for (int i = 0; i < columns.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
if (line.PeekCharExtra(columnSlice.Start) == '|')
|
||||
{
|
||||
columnIndex = i;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public override bool Close(BlockProcessor processor, Block block)
|
||||
private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
// Work only on Table, not on TableCell
|
||||
var gridTable = block as Table;
|
||||
if (gridTable != null)
|
||||
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)
|
||||
{
|
||||
var tableState = (GridTableState) block.GetData(typeof (GridTableState));
|
||||
TerminateLastRow(processor, tableState, gridTable, true);
|
||||
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 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)
|
||||
private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow)
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
TableRow currentRow = null;
|
||||
@@ -311,47 +194,180 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
currentRow = new TableRow();
|
||||
}
|
||||
currentRow.Add(columnSlice.CurrentCell);
|
||||
columnSlice.BlockProcessor.Close(columnSlice.CurrentCell);
|
||||
// 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)
|
||||
if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell == null || columnSlice.CurrentCell.AllowClose))
|
||||
{
|
||||
columnSlice.BlockProcessor.ReleaseChild();
|
||||
columnSlice.BlockProcessor = isLastRow ? null : state.CreateChild();
|
||||
columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild();
|
||||
}
|
||||
|
||||
// Create or erase the cell
|
||||
if (isLastRow || columnSlice.CurrentColumnSpan == 0)
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Else we can create a new cell
|
||||
columnSlice.CurrentCell = new TableCell(this)
|
||||
{
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan,
|
||||
ColumnIndex = i
|
||||
};
|
||||
|
||||
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)
|
||||
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));
|
||||
TerminateCurrentRow(processor, tableState, gridTable, true);
|
||||
if (!gridTable.IsValid())
|
||||
{
|
||||
Undo(processor, tableState, gridTable);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +81,16 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
renderer.Write($" colspan=\"{cell.ColumnSpan}\"");
|
||||
}
|
||||
var columnIndex = cell.ColumnIndex == -1 ? i : cell.ColumnIndex;
|
||||
if (table.ColumnDefinitions != null && columnIndex < table.ColumnDefinitions.Count)
|
||||
if (cell.RowSpan != 1)
|
||||
{
|
||||
renderer.Write($" rowspan=\"{cell.RowSpan}\"");
|
||||
}
|
||||
if (table.ColumnDefinitions != null)
|
||||
{
|
||||
var columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count
|
||||
? i
|
||||
: cell.ColumnIndex;
|
||||
columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex;
|
||||
switch (table.ColumnDefinitions[columnIndex].Alignment)
|
||||
{
|
||||
case TableColumnAlign.Center:
|
||||
@@ -104,7 +111,7 @@ namespace Markdig.Extensions.Tables
|
||||
}
|
||||
renderer.Write(cell);
|
||||
renderer.ImplicitParagraph = previousImplicitParagraph;
|
||||
|
||||
|
||||
renderer.WriteLine(row.IsHeader ? "</th>" : "</td>");
|
||||
}
|
||||
renderer.WriteLine("</tr>");
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -18,14 +21,17 @@ namespace Markdig.Extensions.Tables
|
||||
/// <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,
|
||||
@@ -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);
|
||||
@@ -421,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 = TableColumnAlign.Left;
|
||||
if (delimiter.PreviousSibling != null && !ParseHeaderString(delimiter.PreviousSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Create aligns until we may have a header row
|
||||
if (aligns == null)
|
||||
{
|
||||
aligns = new List<TableColumnDefinition>();
|
||||
}
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
|
||||
// If this is the last delimiter, we need to check the right side of the `|` delimiter
|
||||
if (nextDelimiter == null)
|
||||
{
|
||||
var nextSibling = columnDelimiter != null
|
||||
? columnDelimiter.FirstChild
|
||||
: delimiter.NextSibling;
|
||||
|
||||
if (!ParseHeaderString(nextSibling, out align))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
isValidRow = true;
|
||||
aligns.Add(new TableColumnDefinition() { Alignment = align });
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are on a Line delimiter, exit
|
||||
if (IsLine(delimiter))
|
||||
{
|
||||
isValidRow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return isValidRow ? aligns : null;
|
||||
@@ -509,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))
|
||||
@@ -552,6 +600,7 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
ColumnAndLineDelimiters = new List<Inline>();
|
||||
Cells = new List<TableCell>();
|
||||
EndOfLines = new List<Inline>();
|
||||
}
|
||||
|
||||
public bool IsInvalidTable { get; set; }
|
||||
@@ -563,6 +612,8 @@ namespace Markdig.Extensions.Tables
|
||||
public List<Inline> ColumnAndLineDelimiters { get; }
|
||||
|
||||
public List<TableCell> Cells { get; }
|
||||
|
||||
public List<Inline> EndOfLines { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,10 @@ 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>
|
||||
@@ -39,5 +41,15 @@ namespace Markdig.Extensions.Tables
|
||||
/// 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; }
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,6 @@ namespace Markdig.Extensions.Tables
|
||||
{
|
||||
align = TableColumnAlign.Left;
|
||||
|
||||
// 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;
|
||||
|
||||
31
src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs
Normal file
31
src/Markdig/Extensions/Yaml/YamlFrontMatterBlock.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
using Markdig.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; }
|
||||
}
|
||||
}
|
||||
33
src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs
Normal file
33
src/Markdig/Extensions/Yaml/YamlFrontMatterExtension.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs
Normal file
44
src/Markdig/Extensions/Yaml/YamlFrontMatterParser.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Markdig/Extensions/Yaml/YamlFrontMatterRenderer.cs
Normal file
19
src/Markdig/Extensions/Yaml/YamlFrontMatterRenderer.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -649,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ 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;
|
||||
|
||||
@@ -63,6 +65,28 @@ namespace Markdig
|
||||
.UseGenericAttributes(); // Must be last as it is one parser that is modifying other parsers
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@@ -412,6 +436,8 @@ 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())
|
||||
@@ -487,6 +513,15 @@ namespace Markdig
|
||||
case "nofollowlinks":
|
||||
pipeline.UseNoFollowLinks();
|
||||
break;
|
||||
case "nohtml":
|
||||
pipeline.DisableHtml();
|
||||
break;
|
||||
case "yaml":
|
||||
pipeline.UseYamlFrontMatter();
|
||||
break;
|
||||
case "nonascii-noescape":
|
||||
pipeline.UseNonAsciiNoEscape();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -18,16 +18,23 @@ namespace Markdig.Parsers
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
var startColumn = processor.ColumnBeforeIndent;
|
||||
var startPosition = processor.StartBeforeIndent;
|
||||
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 = startColumn,
|
||||
Span = new SourceSpan(startPosition, processor.Line.End)
|
||||
Column = processor.Column,
|
||||
Span = new SourceSpan(processor.Start, processor.Line.End)
|
||||
});
|
||||
|
||||
// Go back to the correct column
|
||||
processor.GoToColumn(column);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -11,20 +11,7 @@ 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)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registered post inline processors.
|
||||
/// </summary>
|
||||
public IPostInlineProcessor[] PostInlineProcessors { get; private set; }
|
||||
|
||||
public override void Initialize(InlineProcessor initState)
|
||||
{
|
||||
// Prepare the list of post inline processors
|
||||
var postInlineProcessors = new List<IPostInlineProcessor>();
|
||||
@@ -37,8 +24,11 @@ namespace Markdig.Parsers
|
||||
}
|
||||
}
|
||||
PostInlineProcessors = postInlineProcessors.ToArray();
|
||||
|
||||
base.Initialize(initState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registered post inline processors.
|
||||
/// </summary>
|
||||
public IPostInlineProcessor[] PostInlineProcessors { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -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,31 +267,31 @@ 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);
|
||||
//}
|
||||
|
||||
// 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 PostProcessInlines(int startingIndex, Inline root, Inline lastChild, bool isFinalProcessing)
|
||||
|
||||
@@ -62,6 +62,10 @@ namespace Markdig.Parsers.Inlines
|
||||
Column = column
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -319,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--;
|
||||
|
||||
@@ -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),
|
||||
@@ -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 '(':
|
||||
@@ -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;
|
||||
|
||||
@@ -75,21 +75,26 @@ namespace Markdig.Parsers.Inlines
|
||||
}
|
||||
else
|
||||
{
|
||||
processor.Inline = new LiteralInline()
|
||||
// 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)
|
||||
{
|
||||
Content = length > 0 ? new StringSlice(slice.Text, slice.Start, endPosition) : StringSlice.Empty,
|
||||
Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -253,10 +253,14 @@ namespace Markdig.Parsers
|
||||
// - an empty list item follows a paragraph
|
||||
// - an ordered list is not starting by '1'
|
||||
var isPreviousParagraph = (block ?? state.LastBlock) is ParagraphBlock;
|
||||
if (isPreviousParagraph && (state.IsBlankLine || (listInfo.BulletType == '1' && listInfo.OrderedStart != "1")))
|
||||
if (isPreviousParagraph)
|
||||
{
|
||||
state.GoToColumn(initColumn);
|
||||
return BlockState.None;
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@ namespace Markdig
|
||||
{
|
||||
public static partial class Markdown
|
||||
{
|
||||
public const string Version = "0.7.2";
|
||||
public const string Version = "0.8.4";
|
||||
}
|
||||
}
|
||||
@@ -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("\"");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -203,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the license.txt file in the project root for more information.
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
|
||||
namespace Markdig.Syntax.Inlines
|
||||
@@ -41,5 +42,18 @@ namespace Markdig.Syntax.Inlines
|
||||
/// </summary>
|
||||
/// <returns>The string representation of this delimiter</returns>
|
||||
public abstract string ToLiteral();
|
||||
|
||||
public void ReplaceByLiteral()
|
||||
{
|
||||
var literalInline = new LiteralInline()
|
||||
{
|
||||
Content = new StringSlice(ToLiteral()),
|
||||
Span = Span,
|
||||
Line = Line,
|
||||
Column = Column,
|
||||
IsClosed = true
|
||||
};
|
||||
ReplaceBy(literalInline);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,24 @@ namespace Markdig.Syntax.Inlines
|
||||
}
|
||||
}
|
||||
|
||||
public Inline FindBestParent()
|
||||
{
|
||||
var current = this;
|
||||
|
||||
while (current.Parent != null || current.PreviousSibling != null)
|
||||
{
|
||||
if (current.Parent != null)
|
||||
{
|
||||
current = current.Parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
current = current.PreviousSibling;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
protected virtual void OnChildRemove(Inline child)
|
||||
{
|
||||
|
||||
|
||||
@@ -47,5 +47,10 @@ namespace Markdig.Syntax.Inlines
|
||||
/// The content as a <see cref="StringSlice"/>.
|
||||
/// </summary>
|
||||
public StringSlice Content;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Content.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,23 @@ namespace Markdig.Syntax
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the create link inline calback for this instance.
|
||||
/// The label span
|
||||
/// </summary>
|
||||
public SourceSpan LabelSpan;
|
||||
|
||||
/// <summary>
|
||||
/// The URL span
|
||||
/// </summary>
|
||||
public SourceSpan UrlSpan;
|
||||
|
||||
/// <summary>
|
||||
/// The title span
|
||||
/// </summary>
|
||||
public SourceSpan TitleSpan;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the create link inline callback for this instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This callback is called when an inline link is matching this reference definition.
|
||||
@@ -72,20 +88,28 @@ namespace Markdig.Syntax
|
||||
/// <typeparam name="T">Type of the text</typeparam>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <param name="block">The block.</param>
|
||||
/// <returns><c>true</c> if parsing is successfull; <c>false</c> otherwise</returns>
|
||||
/// <returns><c>true</c> if parsing is successful; <c>false</c> otherwise</returns>
|
||||
public static bool TryParse<T>(ref T text, out LinkReferenceDefinition block) where T : ICharIterator
|
||||
{
|
||||
block = null;
|
||||
string label;
|
||||
string url;
|
||||
string title;
|
||||
SourceSpan labelSpan;
|
||||
SourceSpan urlSpan;
|
||||
SourceSpan titleSpan;
|
||||
|
||||
if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title))
|
||||
if (!LinkHelper.TryParseLinkReferenceDefinition(ref text, out label, out url, out title, out labelSpan, out urlSpan, out titleSpan))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
block = new LinkReferenceDefinition(label, url, title);
|
||||
block = new LinkReferenceDefinition(label, url, title)
|
||||
{
|
||||
LabelSpan = labelSpan,
|
||||
UrlSpan = urlSpan,
|
||||
TitleSpan = titleSpan
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Markdig",
|
||||
"version": "0.7.2",
|
||||
"version": "0.8.4",
|
||||
"authors": [ "Alexandre Mutel" ],
|
||||
"description": "A fast, powerfull, CommonMark compliant, extensible Markdown processor for .NET with 20+ builtin extensions (pipetables, footnotes, definition lists... etc.)",
|
||||
"copyright": "Alexandre Mutel",
|
||||
@@ -11,7 +11,7 @@
|
||||
"projectUrl": "https://github.com/lunet-io/markdig",
|
||||
"iconUrl": "https://raw.githubusercontent.com/lunet-io/markdig/master/img/markdig.png",
|
||||
"requireLicenseAcceptance": false,
|
||||
"releaseNotes": "> 0.7.2\n- Fix rendering of table colspan with non english locale\n- Fix grid table colspan parsing\n- Add nofollow extension for links\n> 0.7.1\n- Fix issue in smarty pants which could lead to an InvalidCastException\n- Update parsers to latest CommonMark specs\n>0.7.0\n- Update to latest NETStandard.Library 1.6.0\n- Fix issue with digits in auto-identifier extension\n- Fix incorrect start of span calculated for code indented blocks",
|
||||
"releaseNotes": "> 0.8.4\n- Fix issue when calculating the span of an indented code block within a list. Make sure to include first whitespace on the line\n> 0.8.3\n- fix NullReferenceException with Gridtables extension when a single `+` is entered on a line\n> 0.8.2\n- fix potential cast exception with Abreviation extension and empty literals\n> 0.8.1\n- new extension to disable URI escaping for non-US-ASCII characters to workaround a bug in Edge/IE\n- Fix an issue with abbreviations with left/right multiple non-punctuation/space characters\n> 0.8.0\n- Update to latest CommonMark specs\n- Fix empty literal\n- Add YAML frontmatter extension\n",
|
||||
"tags": [ "Markdown CommonMark md html md2html" ]
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">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.</s:String></wpf:ResourceDictionary>
|
||||
See the license.txt file in the project root for more information.</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></s:String></wpf:ResourceDictionary>
|
||||
Reference in New Issue
Block a user