Compare commits
10
Commits
10ebd55fd4
...
5444164847
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5444164847 | ||
|
|
463eec7085 | ||
|
|
5a326ef5b7 | ||
|
|
2b9fbfb252 | ||
|
|
1d93169fc0 | ||
|
|
425c9f8a46 | ||
|
|
49e145437f | ||
|
|
9006f0b4a2 | ||
|
|
40eebc0cd8 | ||
|
|
cf9456728c |
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# Contributions to xoofx/markdig
|
||||
|
||||
A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET
|
||||
|
||||
This repository holds only my commits to the upstream project, with their original author, date and diff.
|
||||
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
|
||||
|
||||
## Merged pull requests
|
||||
|
||||
- [Fix definition lists requiring 3 spaces after the marker](https://github.com/xoofx/markdig/pull/959) (2026-10-15)
|
||||
- [Fix grid table open last cell taking a | from the next line (#494)](https://github.com/xoofx/markdig/pull/958) (2026-10-15)
|
||||
- [Fix: keep trailing content out of an unmatched emphasis closer (#743)](https://github.com/xoofx/markdig/pull/950) (2026-09-19)
|
||||
- [Fix heading auto-link hijacking nested link labels (#668)](https://github.com/xoofx/markdig/pull/949) (2026-09-19)
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Auto-identifiers
|
||||
---
|
||||
|
||||
# Auto-identifiers
|
||||
|
||||
Enable with `.UseAutoIdentifiers()` (included in `UseAdvancedExtensions()`).
|
||||
|
||||
This extension automatically generates `id` attributes for all headings, similar to [Pandoc auto identifiers](https://pandoc.org/MANUAL.html#extension-auto_identifiers). This is essential for linking to specific sections.
|
||||
|
||||
## How it works
|
||||
|
||||
Every heading gets an `id` attribute derived from its text content:
|
||||
|
||||
```markdown
|
||||
## Getting Started
|
||||
```
|
||||
|
||||
Renders as:
|
||||
|
||||
```html
|
||||
<h2 id="getting-started">Getting Started</h2>
|
||||
```
|
||||
|
||||
You can then link to it:
|
||||
|
||||
```markdown
|
||||
See [Getting Started](#getting-started).
|
||||
```
|
||||
|
||||
With the default `AutoLink` option, `[Getting Started]` also links to the heading.
|
||||
These implicit heading references do not resolve inside another still-open link or
|
||||
image bracket. For example, `[Read [Getting Started]](/guide)` links the entire
|
||||
label `Read [Getting Started]` to `/guide`. Explicit reference definitions such as
|
||||
`[Getting Started]: /guide` retain normal CommonMark reference-link behavior.
|
||||
|
||||
## ID generation rules
|
||||
|
||||
1. Convert to lowercase
|
||||
2. Remove anything that is not a letter, number, space, or hyphen
|
||||
3. Replace spaces with hyphens
|
||||
4. Remove leading/trailing hyphens
|
||||
|
||||
**Examples:**
|
||||
|
||||
{.table}
|
||||
| Heading | Generated ID |
|
||||
|---|---|
|
||||
| `# Hello World` | `hello-world` |
|
||||
| `## C# Tips & Tricks` | `c-tips--tricks` |
|
||||
| `### 2. Installation` | `2-installation` |
|
||||
|
||||
## Duplicate handling
|
||||
|
||||
If two headings produce the same ID, a numeric suffix is appended:
|
||||
|
||||
```markdown
|
||||
## Section
|
||||
## Section
|
||||
## Section
|
||||
```
|
||||
|
||||
Produces: `section`, `section-1`, `section-2`.
|
||||
|
||||
## Options
|
||||
|
||||
`UseAutoIdentifiers` accepts an `AutoIdentifierOptions` flags enum:
|
||||
|
||||
```csharp
|
||||
using Markdig.Extensions.AutoIdentifiers;
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
|
||||
.Build();
|
||||
```
|
||||
|
||||
Available options:
|
||||
|
||||
{.table}
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `AutoIdentifierOptions.Default` | Standard auto-identifier behavior |
|
||||
| `AutoIdentifierOptions.GitHub` | GitHub-compatible ID generation |
|
||||
| `AutoIdentifierOptions.AutoLink` | Also create a self-link anchor |
|
||||
| `AutoIdentifierOptions.AllowOnlyAscii` | Strip non-ASCII characters from IDs |
|
||||
|
||||
## Combining with generic attributes
|
||||
|
||||
You can override the auto-generated ID using [Generic attributes](generic-attributes.md):
|
||||
|
||||
```markdown
|
||||
## My Heading {#custom-id}
|
||||
```
|
||||
|
||||
The explicit `#custom-id` takes precedence over the auto-generated one.
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: Emphasis extras
|
||||
---
|
||||
|
||||
# Emphasis extras
|
||||
|
||||
Enable with `.UseEmphasisExtras()` (included in `UseAdvancedExtensions()`).
|
||||
|
||||
This extension adds several emphasis styles beyond standard bold and italic.
|
||||
|
||||
If you need improved emphasis parsing for Chinese/Japanese/Korean (CJK) text, see [CJK friendly emphasis](cjk-friendly-emphasis.md).
|
||||
|
||||
## Strikethrough
|
||||
|
||||
Wrap text with `~~` for strikethrough:
|
||||
|
||||
```markdown
|
||||
This is ~~deleted~~ text.
|
||||
```
|
||||
|
||||
This is ~~deleted~~ text.
|
||||
|
||||
## Subscript
|
||||
|
||||
Wrap text with `~` for subscript:
|
||||
|
||||
```markdown
|
||||
H~2~O is water.
|
||||
```
|
||||
|
||||
H~2~O is water.
|
||||
|
||||
## Superscript
|
||||
|
||||
Wrap text with `^` for superscript:
|
||||
|
||||
```markdown
|
||||
2^10^ is 1024.
|
||||
```
|
||||
|
||||
2^10^ is 1024.
|
||||
|
||||
## Inserted text
|
||||
|
||||
Wrap text with `++` for inserted/underlined text:
|
||||
|
||||
```markdown
|
||||
This text has been ++inserted++.
|
||||
```
|
||||
|
||||
This text has been ++inserted++.
|
||||
|
||||
## Marked/highlighted text
|
||||
|
||||
Wrap text with `==` for marked/highlighted text:
|
||||
|
||||
```markdown
|
||||
This is ==highlighted== text.
|
||||
```
|
||||
|
||||
This is ==highlighted== text.
|
||||
|
||||
An unmatched remainder of a closing delimiter run must not pull subsequent text
|
||||
into the emphasis. For example, `===a===== tail` renders as
|
||||
`=<mark>a===</mark> tail`. This example also round-trips unchanged with
|
||||
`EnableTrackTrivia()` and `RoundtripRenderer`.
|
||||
|
||||
## HTML output
|
||||
|
||||
{.table}
|
||||
| Syntax | HTML output |
|
||||
|---|---|
|
||||
| `~~text~~` | `<del>text</del>` |
|
||||
| `~text~` | `<sub>text</sub>` |
|
||||
| `^text^` | `<sup>text</sup>` |
|
||||
| `++text++` | `<ins>text</ins>` |
|
||||
| `==text==` | `<mark>text</mark>` |
|
||||
|
||||
## Selective activation
|
||||
|
||||
By default, all emphasis extras are enabled. Use `EmphasisExtraOptions` to enable only specific ones:
|
||||
|
||||
```csharp
|
||||
using Markdig.Extensions.EmphasisExtras;
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Superscript)
|
||||
.Build();
|
||||
```
|
||||
|
||||
Available options:
|
||||
|
||||
- `EmphasisExtraOptions.Strikethrough`
|
||||
- `EmphasisExtraOptions.Subscript`
|
||||
- `EmphasisExtraOptions.Superscript`
|
||||
- `EmphasisExtraOptions.Inserted`
|
||||
- `EmphasisExtraOptions.Marked`
|
||||
- `EmphasisExtraOptions.Default` (all of the above)
|
||||
@@ -0,0 +1,585 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Markdig.Extensions.AutoLinks;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
public class MiscTests
|
||||
{
|
||||
[Test]
|
||||
public void LinkWithInvalidNonAsciiDomainNameIsIgnored()
|
||||
{
|
||||
// Url from https://github.com/lunet-io/markdig/issues/438
|
||||
_ = Markdown.ToHtml("[minulém díle](http://V%20minulém%20díle%20jsme%20nainstalovali%20SQL%20Server,%20který%20je%20nutný%20pro%20běh%20Configuration%20Manageru.%20Dnes%20nás%20čeká%20instalace%20WSUS,%20což%20je%20produkt,%20jež%20je%20možné%20používat%20i%20jako%20samostatnou%20funkci%20ve%20Windows%20Serveru,%20který%20se%20stará%20o%20stažení%20a%20instalaci%20aktualizací%20z%20Microsoft%20Update%20na%20klientské%20počítače.%20Stejně%20jako%20v%20předchozích%20dílech,%20tak%20i%20v%20tomto%20si%20ukážeme%20obě%20varianty%20instalace%20–%20a%20to%20jak%20instalaci%20z%20PowerShellu,%20tak%20instalaci%20pomocí%20GUI.) ");
|
||||
|
||||
// Valid IDN
|
||||
TestParser.TestSpec("[foo](http://ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.com\">foo</a></p>");
|
||||
TestParser.TestSpec("[foo](http://ünicode.ünicode.com)", "<p><a href=\"http://xn--nicode-2ya.xn--nicode-2ya.com\">foo</a></p>");
|
||||
|
||||
// Invalid IDN
|
||||
TestParser.TestSpec("[foo](http://ünicode..com)", "<p><a href=\"http://%C3%BCnicode..com\">foo</a></p>");
|
||||
}
|
||||
|
||||
[TestCase("link [foo [bar]]")] // https://spec.commonmark.org/0.29/#example-508
|
||||
[TestCase("link [foo][bar]")]
|
||||
[TestCase("link [][foo][bar][]")]
|
||||
[TestCase("link [][foo][bar][[]]")]
|
||||
[TestCase("link [foo] [bar]")]
|
||||
[TestCase("link [[foo] [] [bar] [[abc]def]]")]
|
||||
[TestCase("[]")]
|
||||
[TestCase("[ ]")]
|
||||
[TestCase("[bar][]")]
|
||||
[TestCase("[bar][ foo]")]
|
||||
[TestCase("[bar][foo ][]")]
|
||||
[TestCase("[bar][fo[ ]o ][][]")]
|
||||
[TestCase("[a]b[c[d[e]f]g]h")]
|
||||
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o")]
|
||||
[TestCase("a[b[c[d]e]f[g]h]i[] [][foo][bar][] foo [j]k[l[m]n]o")]
|
||||
[TestCase("a[b[c[d]e]f[g]h]i foo [j]k[l[m]n]o[][]")]
|
||||
public void LinkTextMayContainBalancedBrackets(string linkText)
|
||||
{
|
||||
string markdown = $"[{linkText}](/uri)";
|
||||
string expected = $@"<p><a href=""/uri"">{linkText}</a></p>";
|
||||
|
||||
TestParser.TestSpec(markdown, expected);
|
||||
|
||||
// Make the link text unbalanced
|
||||
foreach (var bracketIndex in linkText
|
||||
.Select((c, i) => new Tuple<char, int>(c, i))
|
||||
.Where(t => t.Item1 == '[' || t.Item1 == ']')
|
||||
.Select(t => t.Item2))
|
||||
{
|
||||
string brokenLinkText = linkText.Remove(bracketIndex, 1);
|
||||
|
||||
markdown = $"[{brokenLinkText}](/uri)";
|
||||
expected = $@"<p><a href=""/uri"">{brokenLinkText}</a></p>";
|
||||
|
||||
string actual = Markdown.ToHtml(markdown);
|
||||
Assert.AreNotEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[TestCase('[', 9 * 1024, true, false)]
|
||||
[TestCase('[', 11 * 1024, true, true)]
|
||||
[TestCase('[', 100, false, false)]
|
||||
[TestCase('[', 150, false, true)]
|
||||
[TestCase('>', 100, true, false)]
|
||||
[TestCase('>', 150, true, true)]
|
||||
public void GuardsAgainstHighlyNestedNodes(char c, int count, bool parseOnly, bool shouldThrow)
|
||||
{
|
||||
var markdown = new string(c, count);
|
||||
Action test = parseOnly ? () => Markdown.Parse(markdown) : () => Markdown.ToHtml(markdown);
|
||||
|
||||
if (shouldThrow)
|
||||
{
|
||||
Exception e = Assert.Throws<ArgumentException>(test);
|
||||
Assert.True(e.Message.Contains("depth limit"));
|
||||
}
|
||||
else
|
||||
{
|
||||
test();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MaximumNestingDepthCanBeRaisedForDeepListExtras()
|
||||
{
|
||||
var markdown = "Krankenhaus\nD. " + string.Join(" ", Enumerable.Repeat("M.", 160));
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
MaximumNestingDepth = 512,
|
||||
}.UseListExtras().Build();
|
||||
|
||||
Assert.DoesNotThrow(() => Markdown.ToHtml(markdown, pipeline));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MaximumNestingDepthCanBeLowered()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
MaximumNestingDepth = 16,
|
||||
}.Build();
|
||||
|
||||
Exception e = Assert.Throws<ArgumentException>(() => Markdown.ToHtml(new string('>', 20), pipeline));
|
||||
Assert.True(e.Message.Contains("depth limit"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsIssue356Corrected()
|
||||
{
|
||||
string input = @"https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97";
|
||||
string expected = @"<p><a href=""https://foo.bar/path/%5C#m4mv5W0GYKZpGvfA.97"">https://foo.bar/path/\#m4mv5W0GYKZpGvfA.97</a></p>";
|
||||
|
||||
TestParser.TestSpec($"<{input}>", expected);
|
||||
TestParser.TestSpec(input, expected, "autolinks|advanced");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsIssue365Corrected()
|
||||
{
|
||||
// The scheme must be escaped too...
|
||||
string input = "";
|
||||
string expected = "<p><img src=\"%22onclick=%22alert&#40;%27click%27&#41;%22://\" alt=\"image\" /></p>";
|
||||
|
||||
TestParser.TestSpec(input, expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAltTextIsCorrectlyEscaped()
|
||||
{
|
||||
TestParser.TestSpec(
|
||||
@"",
|
||||
@"<p><img src=""girl.png"" alt=""This is image alt text with quotation ' and double quotation "hello" world"" /></p>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFixHang()
|
||||
{
|
||||
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "hang.md"));
|
||||
_ = Markdown.ToHtml(input);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInvalidHtmlEntity()
|
||||
{
|
||||
var input = "9&ddr;&*&ddr;&de��__";
|
||||
TestParser.TestSpec(input, "<p>9&ddr;&*&ddr;&de��__</p>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInvalidCharacterHandling()
|
||||
{
|
||||
var input = File.ReadAllText(Path.Combine(TestParser.TestsDirectory, "ArgumentOutOfRangeException.md"));
|
||||
_ = Markdown.ToHtml(input);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInvalidCodeEscape()
|
||||
{
|
||||
var input = "```**Header** ";
|
||||
_ = Markdown.ToHtml(input);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmphasisAndHtmlEntity()
|
||||
{
|
||||
var markdownText = "*Unlimited-Fun®*®";
|
||||
TestParser.TestSpec(markdownText, "<p><em>Unlimited-Fun®</em>®</p>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestThematicInsideCodeBlockInsideList()
|
||||
{
|
||||
var input = @"1. In the :
|
||||
|
||||
```
|
||||
Id DisplayName Description
|
||||
-- ----------- -----------
|
||||
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
|
||||
```";
|
||||
TestParser.TestSpec(input, @"<ol>
|
||||
<li><p>In the :</p>
|
||||
<pre><code>Id DisplayName Description
|
||||
-- ----------- -----------
|
||||
62375ab9-6b52-47ed-826b-58e47e0e304b Group.Unified ...
|
||||
</code></pre></li>
|
||||
</ol>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VisualizeMathExpressions()
|
||||
{
|
||||
string math = @"Math expressions
|
||||
|
||||
$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
|
||||
|
||||
$$\frac{n!}{k!(n-k)!} = \binom{n}{k}$$
|
||||
|
||||
$$
|
||||
\frac{n!}{k!(n-k)!} = \binom{n}{k}
|
||||
$$
|
||||
|
||||
<div class=""math"">
|
||||
\begin{align}
|
||||
\sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\
|
||||
& = \sqrt{\frac{73^2}{12^2}\cdot\frac{73^2-1}{73^2}} \\
|
||||
& = \sqrt{\frac{73^2}{12^2}}\sqrt{\frac{73^2-1}{73^2}} \\
|
||||
& = \frac{73}{12}\sqrt{1 - \frac{1}{73^2}} \\
|
||||
& \approx \frac{73}{12}\left(1 - \frac{1}{2\cdot73^2}\right)
|
||||
\end{align}
|
||||
</div>
|
||||
";
|
||||
//Console.WriteLine("Math Expressions:\n");
|
||||
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
|
||||
var html = Markdown.ToHtml(math, pl);
|
||||
//Console.WriteLine(html);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InlineMathExpression()
|
||||
{
|
||||
string math = @"Math expressions
|
||||
|
||||
$\frac{n!}{k!(n-k)!} = \binom{n}{k}$
|
||||
";
|
||||
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
|
||||
|
||||
var html = Markdown.ToHtml(math, pl);
|
||||
|
||||
var test1 = html.Contains("<p><span class=\"math\">\\(");
|
||||
var test2 = html.Contains("\\)</span></p>");
|
||||
if (!test1 || !test2)
|
||||
{
|
||||
Console.WriteLine(html);
|
||||
}
|
||||
|
||||
Assert.IsTrue(test1, "Leading bracket missing");
|
||||
Assert.IsTrue(test2, "Trailing bracket missing");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BlockMathExpression()
|
||||
{
|
||||
string math = @"Math expressions
|
||||
|
||||
$$
|
||||
\frac{n!}{k!(n-k)!} = \binom{n}{k}
|
||||
$$
|
||||
";
|
||||
var pl = new MarkdownPipelineBuilder().UseMathematics().Build(); // UseEmphasisExtras(EmphasisExtraOptions.Subscript).Build()
|
||||
|
||||
var html = Markdown.ToHtml(math, pl);
|
||||
var test1 = html.Contains("<div class=\"math\">\n\\[");
|
||||
var test2 = html.Contains("\\]</div>");
|
||||
if (!test1 || !test2)
|
||||
{
|
||||
Console.WriteLine(html);
|
||||
}
|
||||
|
||||
Assert.IsTrue(test1, "Leading bracket missing");
|
||||
Assert.IsTrue(test2, "Trailing bracket missing");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanDisableParsingHeadings()
|
||||
{
|
||||
var noHeadingsPipeline = new MarkdownPipelineBuilder().DisableHeadings().Build();
|
||||
|
||||
TestParser.TestSpec("Foo\n===", "<h1>Foo</h1>");
|
||||
TestParser.TestSpec("Foo\n===", "<p>Foo\n===</p>", noHeadingsPipeline);
|
||||
|
||||
TestParser.TestSpec("# Heading 1", "<h1>Heading 1</h1>");
|
||||
TestParser.TestSpec("# Heading 1", "<p># Heading 1</p>", noHeadingsPipeline);
|
||||
|
||||
// Does not also disable link reference definitions
|
||||
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>");
|
||||
TestParser.TestSpec("[Foo]\n\n[Foo]: bar", "<p><a href=\"bar\">Foo</a></p>", noHeadingsPipeline);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanOpenAutoLinksInNewWindow()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
|
||||
var newWindowPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { OpenInNewWindow = true }).Build();
|
||||
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\" target=\"_blank\">www.foo.bar</a></p>", newWindowPipeline);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanUseHttpsPrefixForWWWAutoLinks()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().Build();
|
||||
var httpsPipeline = new MarkdownPipelineBuilder().UseAutoLinks(new AutoLinkOptions() { UseHttpsForWWWLinks = true }).Build();
|
||||
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"http://www.foo.bar\">www.foo.bar</a></p>", pipeline);
|
||||
TestParser.TestSpec("www.foo.bar", "<p><a href=\"https://www.foo.bar\">www.foo.bar</a></p>", httpsPipeline);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RootInlineHasCorrectSourceSpan()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().Build();
|
||||
pipeline.TrackTrivia = true;
|
||||
|
||||
var document = Markdown.Parse("0123456789\n", pipeline);
|
||||
|
||||
var expectedSourceSpan = new SourceSpan(0, 10);
|
||||
Assert.That(((LeafBlock)document.LastChild).Inline.Span == expectedSourceSpan);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RootInlineInTableCellHasCorrectSourceSpan()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UsePreciseSourceLocation().UseAdvancedExtensions().Build();
|
||||
pipeline.TrackTrivia = true;
|
||||
|
||||
var document = Markdown.Parse("| a | b |\n| --- | --- |\n| <span id=\"dest\"></span><span id=\"DEST\"></span>*dest*<br/> | \\[in\\] The address of the result of the operation.<br/> |", pipeline);
|
||||
|
||||
var paragraph = (ParagraphBlock)((TableCell)((TableRow)((Table)document.LastChild).LastChild).First()).LastChild;
|
||||
Assert.That(paragraph.Inline.Span.Start == paragraph.Inline.FirstChild.Span.Start);
|
||||
Assert.That(paragraph.Inline.Span.End == paragraph.Inline.LastChild.Span.End);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGridTableShortLine()
|
||||
{
|
||||
var input = @"
|
||||
+--+
|
||||
| |
|
||||
+-";
|
||||
|
||||
var expected = @"<table>
|
||||
<col style=""width:100%"" />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseGridTables().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGridTableLastCellWithoutClosingPipe()
|
||||
{
|
||||
// The last cell is open and shorter than the column, and the next line has
|
||||
// a `|` where this one would end: https://github.com/xoofx/markdig/issues/494
|
||||
var input = @"
|
||||
+-----+----------+
|
||||
| 6 | 1
|
||||
| |
|
||||
+-----+----------+";
|
||||
|
||||
var expected = @"<table>
|
||||
<col style=""width:33.33%"" />
|
||||
<col style=""width:66.67%"" />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseGridTables().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefinitionListInListItemWithBlankLine()
|
||||
{
|
||||
var input = @"
|
||||
-
|
||||
|
||||
term
|
||||
: definition
|
||||
";
|
||||
|
||||
var expected = @"<ul>
|
||||
<li>
|
||||
<dl>
|
||||
<dt>term</dt>
|
||||
<dd>definition</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseDefinitionLists().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlertWithinAlertOrNestedBlock()
|
||||
{
|
||||
var input = @"
|
||||
>[!NOTE]
|
||||
[!NOTE]
|
||||
The second one is not a note.
|
||||
|
||||
>>[!NOTE]
|
||||
Also not a note.
|
||||
";
|
||||
|
||||
var expected = @"<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p>[!NOTE]
|
||||
The second one is not a note.</p>
|
||||
</div>
|
||||
<blockquote>
|
||||
<blockquote>
|
||||
<p>[!NOTE]
|
||||
Also not a note.</p>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks().Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedAlertInsideBlockquote()
|
||||
{
|
||||
// >>[!NOTE] should become a blockquote wrapping an alert when AllowNestedAlerts is enabled
|
||||
var input = @">>[!NOTE]
|
||||
Also a note.
|
||||
";
|
||||
|
||||
var expected = @"<blockquote>
|
||||
<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p>Also a note.</p>
|
||||
</div>
|
||||
</blockquote>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedAlertInsideAlert()
|
||||
{
|
||||
// An alert inside another alert is never recognized, even with AllowNestedAlerts
|
||||
var input = @">[!NOTE]
|
||||
> >[!TIP]
|
||||
> > A tip inside a note
|
||||
";
|
||||
|
||||
var expected = @"<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p></p>
|
||||
<blockquote>
|
||||
<p>[!TIP]
|
||||
A tip inside a note</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlertInsideListItem()
|
||||
{
|
||||
// Alerts inside list items require AllowNestedAlerts
|
||||
var input = @"- > [!NOTE]
|
||||
> A note inside a list item
|
||||
";
|
||||
|
||||
var expected = @"<ul>
|
||||
<li>
|
||||
<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p>A note inside a list item</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAlertInsideNestedListItem()
|
||||
{
|
||||
// Alert inside a nested list item (list item indented under another list item) requires AllowNestedAlerts
|
||||
var input = @"- list item 1
|
||||
- list item 2
|
||||
- > [!NOTE]
|
||||
> A note inside a nested list item
|
||||
";
|
||||
|
||||
var expected = @"<ul>
|
||||
<li>list item 1</li>
|
||||
<li>list item 2
|
||||
<ul>
|
||||
<li>
|
||||
<div class=""markdown-alert markdown-alert-note"">
|
||||
<p class=""markdown-alert-title""><svg viewBox=""0 0 16 16"" version=""1.1"" width=""16"" height=""16"" aria-hidden=""true""><path d=""M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z""></path></svg>Note</p>
|
||||
<p>A note inside a nested list item</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
";
|
||||
TestParser.TestSpec(input, expected, new MarkdownPipelineBuilder().UseAlertBlocks(allowNestedAlerts: true).Build());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIssue887ListAfterNestedBlockQuote()
|
||||
{
|
||||
var input = @"> >* _Unordered 1_ QL2 level 1 no space sep
|
||||
> > Continued 1
|
||||
> > * Unordered 2 level 1
|
||||
> > 1. **Ordered 1** QL2 level 1
|
||||
> > Continued 1
|
||||
> > > Not continued QL3
|
||||
> > 2. Ordered 2 QL2 level 1
|
||||
> > Continued 2a
|
||||
> >
|
||||
> > Continued 2b
|
||||
> >
|
||||
> > Continued 2c
|
||||
> >
|
||||
> > 3. Ordered 3 QL2 level 2
|
||||
> > Continued 3a
|
||||
> >
|
||||
> > Continued 3b
|
||||
";
|
||||
|
||||
var expected = @"<blockquote>
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li><em>Unordered 1</em> QL2 level 1 no space sep
|
||||
Continued 1</li>
|
||||
<li>Unordered 2 level 1</li>
|
||||
</ul>
|
||||
<ol>
|
||||
<li><strong>Ordered 1</strong> QL2 level 1
|
||||
Continued 1</li>
|
||||
</ol>
|
||||
<blockquote>
|
||||
<p>Not continued QL3</p>
|
||||
</blockquote>
|
||||
<ol start=""2"">
|
||||
<li><p>Ordered 2 QL2 level 1
|
||||
Continued 2a</p>
|
||||
<p>Continued 2b</p>
|
||||
<p>Continued 2c</p>
|
||||
<ol start=""3"">
|
||||
<li><p>Ordered 3 QL2 level 2
|
||||
Continued 3a</p>
|
||||
<p>Continued 3b</p>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
</blockquote>
|
||||
</blockquote>";
|
||||
|
||||
TestParser.TestSpec(input, expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIssue845ListItemBlankLine()
|
||||
{
|
||||
TestParser.TestSpec("-\n\n foo",@"
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
<p>foo</p>");
|
||||
TestParser.TestSpec("-\n-\n\n foo",@"
|
||||
<ul>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
<p>foo</p>");
|
||||
TestParser.TestSpec("-\n\n-\n\n foo",@"
|
||||
<ul>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
<p>foo</p>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
|
||||
// --------------------------------
|
||||
// Definition Lists
|
||||
// --------------------------------
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Markdig.Tests.Specs.DefinitionLists
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestExtensionsDefinitionLists
|
||||
{
|
||||
// # Extensions
|
||||
//
|
||||
// This section describes the different extensions supported:
|
||||
//
|
||||
// ## Definition lists
|
||||
//
|
||||
// A custom container is similar to a fenced code block, but it is using the character `:` to declare a block (with at least 3 characters), and instead of generating a `<pre><code>...</code></pre>` it will generate a `<div>...</div>` block.
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example001()
|
||||
{
|
||||
// Example 1
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
//
|
||||
// Term 1
|
||||
// : This is a definition item
|
||||
// With a paragraph
|
||||
// > This is a block quote
|
||||
//
|
||||
// - This is a list
|
||||
// - with an item2
|
||||
//
|
||||
// ```java
|
||||
// Test
|
||||
//
|
||||
//
|
||||
// ```
|
||||
//
|
||||
// And a last line
|
||||
// : This ia another definition item
|
||||
//
|
||||
// Term2
|
||||
// Term3 *with some inline*
|
||||
// : This is another definition for term2
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <dl>
|
||||
// <dt>Term 1</dt>
|
||||
// <dd><p>This is a definition item
|
||||
// With a paragraph</p>
|
||||
// <blockquote>
|
||||
// <p>This is a block quote</p>
|
||||
// </blockquote>
|
||||
// <ul>
|
||||
// <li>This is a list</li>
|
||||
// <li>with an item2</li>
|
||||
// </ul>
|
||||
// <pre><code class="language-java">Test
|
||||
//
|
||||
//
|
||||
// </code></pre>
|
||||
// <p>And a last line</p>
|
||||
// </dd>
|
||||
// <dd>This ia another definition item</dd>
|
||||
// <dt>Term2</dt>
|
||||
// <dt>Term3 <em>with some inline</em></dt>
|
||||
// <dd>This is another definition for term2</dd>
|
||||
// </dl>
|
||||
|
||||
TestParser.TestSpec("\nTerm 1\n: This is a definition item\n With a paragraph\n > This is a block quote\n\n - This is a list\n - with an item2\n\n ```java\n Test\n\n\n ```\n\n And a last line\n: This ia another definition item\n\nTerm2\nTerm3 *with some inline*\n: This is another definition for term2", "<dl>\n<dt>Term 1</dt>\n<dd><p>This is a definition item\nWith a paragraph</p>\n<blockquote>\n<p>This is a block quote</p>\n</blockquote>\n<ul>\n<li>This is a list</li>\n<li>with an item2</li>\n</ul>\n<pre><code class=\"language-java\">Test\n\n\n</code></pre>\n<p>And a last line</p>\n</dd>\n<dd>This ia another definition item</dd>\n<dt>Term2</dt>\n<dt>Term3 <em>with some inline</em></dt>\n<dd>This is another definition for term2</dd>\n</dl>", "definitionlists+attributes|advanced", context: "Example 1\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// A definition term can be followed at most by one blank line. Lazy continuations are supported for definitions:
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example002()
|
||||
{
|
||||
// Example 2
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// Term 1
|
||||
//
|
||||
// : Definition
|
||||
// with lazy continuation.
|
||||
//
|
||||
// Second paragraph of the definition.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <dl>
|
||||
// <dt>Term 1</dt>
|
||||
// <dd><p>Definition
|
||||
// with lazy continuation.</p>
|
||||
// <p>Second paragraph of the definition.</p>
|
||||
// </dd>
|
||||
// </dl>
|
||||
|
||||
TestParser.TestSpec("Term 1\n\n: Definition\nwith lazy continuation.\n\n Second paragraph of the definition.", "<dl>\n<dt>Term 1</dt>\n<dd><p>Definition\nwith lazy continuation.</p>\n<p>Second paragraph of the definition.</p>\n</dd>\n</dl>", "definitionlists+attributes|advanced", context: "Example 2\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// The `:` or `~` must be followed by at least one space or tab. A single space is enough:
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example003()
|
||||
{
|
||||
// Example 3
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// First Term
|
||||
// : This is the definition of the first term.
|
||||
//
|
||||
// Second Term
|
||||
// : This is one definition of the second term.
|
||||
// ~ This is another definition of the second term.
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <dl>
|
||||
// <dt>First Term</dt>
|
||||
// <dd>This is the definition of the first term.</dd>
|
||||
// <dt>Second Term</dt>
|
||||
// <dd>This is one definition of the second term.</dd>
|
||||
// <dd>This is another definition of the second term.</dd>
|
||||
// </dl>
|
||||
|
||||
TestParser.TestSpec("First Term\n: This is the definition of the first term.\n\nSecond Term\n: This is one definition of the second term.\n~ This is another definition of the second term.", "<dl>\n<dt>First Term</dt>\n<dd>This is the definition of the first term.</dd>\n<dt>Second Term</dt>\n<dd>This is one definition of the second term.</dd>\n<dd>This is another definition of the second term.</dd>\n</dl>", "definitionlists+attributes|advanced", context: "Example 3\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// Without a space or tab after the `:`, the line is not a definition:
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example004()
|
||||
{
|
||||
// Example 4
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// Term 1
|
||||
// :Invalid without a space after the colon
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>Term 1
|
||||
// :Invalid without a space after the colon</p>
|
||||
|
||||
TestParser.TestSpec("Term 1\n:Invalid without a space after the colon", "<p>Term 1\n:Invalid without a space after the colon</p>", "definitionlists+attributes|advanced", context: "Example 4\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// The `:` can be indented up to 3 spaces:
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example005()
|
||||
{
|
||||
// Example 5
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// Term 1
|
||||
// : Valid even if `:` starts at most 3 spaces
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <dl>
|
||||
// <dt>Term 1</dt>
|
||||
// <dd>Valid even if <code>:</code> starts at most 3 spaces</dd>
|
||||
// </dl>
|
||||
|
||||
TestParser.TestSpec("Term 1\n : Valid even if `:` starts at most 3 spaces", "<dl>\n<dt>Term 1</dt>\n<dd>Valid even if <code>:</code> starts at most 3 spaces</dd>\n</dl>", "definitionlists+attributes|advanced", context: "Example 5\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// But more than 3 spaces before `:` will trigger an indented code block:
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example006()
|
||||
{
|
||||
// Example 6
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// Term 1
|
||||
//
|
||||
// : Not valid
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <p>Term 1</p>
|
||||
// <pre><code>: Not valid
|
||||
// </code></pre>
|
||||
|
||||
TestParser.TestSpec("Term 1\n\n : Not valid", "<p>Term 1</p>\n<pre><code>: Not valid\n</code></pre>", "definitionlists+attributes|advanced", context: "Example 6\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
|
||||
// Definition lists can be nested inside list items
|
||||
[Test]
|
||||
public void ExtensionsDefinitionLists_Example007()
|
||||
{
|
||||
// Example 7
|
||||
// Section: Extensions / Definition lists
|
||||
//
|
||||
// The following Markdown:
|
||||
// 1. First
|
||||
//
|
||||
// 2. Second
|
||||
//
|
||||
// Term 1
|
||||
// : Definition
|
||||
//
|
||||
// Term 2
|
||||
// : Second Definition
|
||||
//
|
||||
// Should be rendered as:
|
||||
// <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>
|
||||
|
||||
TestParser.TestSpec("1. First\n \n2. Second\n \n Term 1\n : Definition\n \n Term 2\n : Second Definition", "<ol>\n<li><p>First</p></li>\n<li><p>Second</p>\n<dl>\n<dt>Term 1</dt>\n<dd>Definition</dd>\n<dt>Term 2</dt>\n<dd>Second Definition</dd>\n</dl></li>\n</ol>", "definitionlists+attributes|advanced", context: "Example 7\nSection Extensions / Definition lists\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
# Extensions
|
||||
|
||||
This section describes the different extensions supported:
|
||||
|
||||
## Definition lists
|
||||
|
||||
A custom container is similar to a fenced code block, but it is using the character `:` to declare a block (with at least 3 characters), and instead of generating a `<pre><code>...</code></pre>` it will generate a `<div>...</div>` block.
|
||||
|
||||
```````````````````````````````` example
|
||||
|
||||
Term 1
|
||||
: This is a definition item
|
||||
With a paragraph
|
||||
> This is a block quote
|
||||
|
||||
- This is a list
|
||||
- with an item2
|
||||
|
||||
```java
|
||||
Test
|
||||
|
||||
|
||||
```
|
||||
|
||||
And a last line
|
||||
: This ia another definition item
|
||||
|
||||
Term2
|
||||
Term3 *with some inline*
|
||||
: This is another definition for term2
|
||||
.
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd><p>This is a definition item
|
||||
With a paragraph</p>
|
||||
<blockquote>
|
||||
<p>This is a block quote</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>This is a list</li>
|
||||
<li>with an item2</li>
|
||||
</ul>
|
||||
<pre><code class="language-java">Test
|
||||
|
||||
|
||||
</code></pre>
|
||||
<p>And a last line</p>
|
||||
</dd>
|
||||
<dd>This ia another definition item</dd>
|
||||
<dt>Term2</dt>
|
||||
<dt>Term3 <em>with some inline</em></dt>
|
||||
<dd>This is another definition for term2</dd>
|
||||
</dl>
|
||||
````````````````````````````````
|
||||
|
||||
A definition term can be followed at most by one blank line. Lazy continuations are supported for definitions:
|
||||
|
||||
```````````````````````````````` example
|
||||
Term 1
|
||||
|
||||
: Definition
|
||||
with lazy continuation.
|
||||
|
||||
Second paragraph of the definition.
|
||||
.
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd><p>Definition
|
||||
with lazy continuation.</p>
|
||||
<p>Second paragraph of the definition.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
````````````````````````````````
|
||||
|
||||
The `:` or `~` must be followed by at least one space or tab. A single space is enough:
|
||||
|
||||
```````````````````````````````` example
|
||||
First Term
|
||||
: This is the definition of the first term.
|
||||
|
||||
Second Term
|
||||
: This is one definition of the second term.
|
||||
~ This is another definition of the second term.
|
||||
.
|
||||
<dl>
|
||||
<dt>First Term</dt>
|
||||
<dd>This is the definition of the first term.</dd>
|
||||
<dt>Second Term</dt>
|
||||
<dd>This is one definition of the second term.</dd>
|
||||
<dd>This is another definition of the second term.</dd>
|
||||
</dl>
|
||||
````````````````````````````````
|
||||
|
||||
Without a space or tab after the `:`, the line is not a definition:
|
||||
|
||||
```````````````````````````````` example
|
||||
Term 1
|
||||
:Invalid without a space after the colon
|
||||
.
|
||||
<p>Term 1
|
||||
:Invalid without a space after the colon</p>
|
||||
````````````````````````````````
|
||||
|
||||
The `:` can be indented up to 3 spaces:
|
||||
|
||||
```````````````````````````````` example
|
||||
Term 1
|
||||
: Valid even if `:` starts at most 3 spaces
|
||||
.
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd>Valid even if <code>:</code> starts at most 3 spaces</dd>
|
||||
</dl>
|
||||
````````````````````````````````
|
||||
|
||||
But more than 3 spaces before `:` will trigger an indented code block:
|
||||
|
||||
```````````````````````````````` example
|
||||
Term 1
|
||||
|
||||
: Not valid
|
||||
.
|
||||
<p>Term 1</p>
|
||||
<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,4 +21,60 @@ public class TestAutoLinks
|
||||
|
||||
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
|
||||
}
|
||||
|
||||
// https://github.com/xoofx/markdig/issues/668
|
||||
// A heading's implicit reference must not resolve inside another still-open
|
||||
// link bracket, which would break the outer link.
|
||||
[Test]
|
||||
public void TestAutoIdentifierHeadingLinkDoesNotHijackNestedLinkLabel()
|
||||
{
|
||||
var markdown = "# Testing Markdown\n\n" +
|
||||
"### Header\n\n" +
|
||||
"[Testing [Header]](https://www.bing.com)\n\n" +
|
||||
"[Testing [test]](https://www.google.com)\n";
|
||||
var expected = "<h1 id=\"testing-markdown\">Testing Markdown</h1>\n" +
|
||||
"<h3 id=\"header\">Header</h3>\n" +
|
||||
"<p><a href=\"https://www.bing.com\">Testing [Header]</a></p>\n" +
|
||||
"<p><a href=\"https://www.google.com\">Testing [test]</a></p>";
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAutoLinks()
|
||||
.UseAutoIdentifiers()
|
||||
.Build();
|
||||
var html = Markdown.ToHtml(markdown, pipeline);
|
||||
|
||||
Assert.That(html, Is.EqualTo(expected).IgnoreWhiteSpace);
|
||||
}
|
||||
|
||||
[TestCase("[Header]", "<a href=\"#header\">Header</a>")]
|
||||
[TestCase("[Header][]", "<a href=\"#header\">Header</a>")]
|
||||
[TestCase("[label][Header]", "<a href=\"#header\">label</a>")]
|
||||
[TestCase("[Testing [Header]](/target)", "<a href=\"/target\">Testing [Header]</a>")]
|
||||
[TestCase("[Testing [Header][]](/target)", "<a href=\"/target\">Testing [Header][]</a>")]
|
||||
[TestCase("[Testing [label][Header]](/target)", "<a href=\"/target\">Testing [label][Header]</a>")]
|
||||
[TestCase("![Testing [Header]](/image.png)", "<img src=\"/image.png\" alt=\"Testing [Header]\" />")]
|
||||
public void TestAutoIdentifierReferenceResolution(string markdown, string expected)
|
||||
{
|
||||
foreach (var trackTrivia in new[] { false, true })
|
||||
{
|
||||
var builder = new MarkdownPipelineBuilder().UseAutoLinks().UseAutoIdentifiers();
|
||||
if (trackTrivia)
|
||||
{
|
||||
builder.EnableTrackTrivia();
|
||||
}
|
||||
|
||||
var html = Markdown.ToHtml("# Header\n\n" + markdown, builder.Build());
|
||||
|
||||
Assert.That(html, Is.EqualTo("<h1 id=\"header\">Header</h1>\n<p>" + expected + "</p>").IgnoreWhiteSpace, $"TrackTrivia: {trackTrivia}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExplicitReferenceStillResolvesInsideOpenLink()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAutoLinks().UseAutoIdentifiers().Build();
|
||||
var html = Markdown.ToHtml("[Header]: /explicit\n\n# Header\n\n[Testing [Header]](/target)", pipeline);
|
||||
|
||||
Assert.That(html, Is.EqualTo("<h1 id=\"header\">Header</h1>\n<p>[Testing <a href=\"/explicit\">Header</a>](/target)</p>").IgnoreWhiteSpace);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,21 @@ public class TestEmphasisRoundtrip
|
||||
Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown));
|
||||
}
|
||||
|
||||
[TestCase("===a===== tail\n", "<p>=<mark>a===</mark> tail</p>")]
|
||||
[TestCase("===a==== tail\n", "<p>=<mark>a==</mark> tail</p>")]
|
||||
[TestCase("=====a======= tail\n", "<p>=<mark><mark>a===</mark></mark> tail</p>")]
|
||||
[TestCase("+++a+++++ tail\n", "<p>+<ins>a+++</ins> tail</p>")]
|
||||
[TestCase("*===a===== tail*\n", "<p><em>=<mark>a===</mark> tail</em></p>")]
|
||||
[TestCase("===a===== tail ==next==\n", "<p>=<mark>a===</mark> tail <mark>next</mark></p>")]
|
||||
public void UnmatchedCloserPreservesTrailingContent(string markdown, string expectedHtml)
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UseEmphasisExtras().EnableTrackTrivia().Build();
|
||||
|
||||
Assert.That(RoundTrip(markdown, pipeline), Is.EqualTo(markdown));
|
||||
Assert.That(Markdown.ToHtml(markdown, pipeline).Trim(), Is.EqualTo(expectedHtml));
|
||||
Assert.That(Markdown.ToHtml(markdown, new MarkdownPipelineBuilder().UseEmphasisExtras().Build()).Trim(), Is.EqualTo(expectedHtml));
|
||||
}
|
||||
|
||||
// The fix must not prevent balanced "marked" emphasis from being detected.
|
||||
[Test]
|
||||
public void BalancedMarkedEmphasisStillParses()
|
||||
|
||||
@@ -18,6 +18,9 @@ public class HeadingLinkReferenceDefinition : LinkReferenceDefinition
|
||||
public HeadingLinkReferenceDefinition(HeadingBlock headling)
|
||||
{
|
||||
Heading = headling;
|
||||
// Created implicitly, so it must not resolve inside another still-open
|
||||
// link bracket, e.g. [Some text [Heading]](url).
|
||||
AllowResolutionInsideOpenLink = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Extensions.DefinitionLists;
|
||||
|
||||
/// <summary>
|
||||
/// The block parser for a <see cref="DefinitionList"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="BlockParser" />
|
||||
public class DefinitionListParser : BlockParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefinitionListParser"/> class.
|
||||
/// </summary>
|
||||
public DefinitionListParser()
|
||||
{
|
||||
OpeningCharacters = [':', '~'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to open a block at the current parser position.
|
||||
/// </summary>
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
var paragraphBlock = processor.LastBlock as ParagraphBlock;
|
||||
if (processor.IsCodeIndent || paragraphBlock is null || paragraphBlock.LastLine - processor.LineIndex > 1)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
var startPosition = processor.Start;
|
||||
|
||||
var column = processor.ColumnBeforeIndent;
|
||||
var c = processor.NextChar();
|
||||
processor.ParseIndent();
|
||||
var delta = processor.Column - column;
|
||||
|
||||
// The marker must be followed by at least one space or tab
|
||||
if (!c.IsSpaceOrTab())
|
||||
{
|
||||
// Return back to original position
|
||||
processor.GoToColumn(column);
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
if (delta > 4)
|
||||
{
|
||||
processor.GoToColumn(column + 4);
|
||||
}
|
||||
|
||||
var previousParent = paragraphBlock.Parent!;
|
||||
var currentDefinitionList = GetCurrentDefinitionList(paragraphBlock, previousParent);
|
||||
|
||||
processor.Discard(paragraphBlock);
|
||||
|
||||
// If the paragraph block was not part of the opened blocks, we need to remove it manually from its parent container
|
||||
if (paragraphBlock.Parent != null)
|
||||
{
|
||||
paragraphBlock.Parent.Remove(paragraphBlock);
|
||||
}
|
||||
|
||||
if (currentDefinitionList is null)
|
||||
{
|
||||
currentDefinitionList = new DefinitionList(this)
|
||||
{
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
|
||||
Column = paragraphBlock.Column,
|
||||
Line = paragraphBlock.Line,
|
||||
};
|
||||
previousParent.Add(currentDefinitionList);
|
||||
}
|
||||
|
||||
var definitionItem = new DefinitionItem(this)
|
||||
{
|
||||
Line = processor.LineIndex,
|
||||
Column = column,
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
|
||||
OpeningCharacter = processor.CurrentChar,
|
||||
};
|
||||
|
||||
for (int i = 0; i < paragraphBlock.Lines.Count; i++)
|
||||
{
|
||||
var line = paragraphBlock.Lines.Lines[i];
|
||||
var term = new DefinitionTerm(this)
|
||||
{
|
||||
Column = paragraphBlock.Column,
|
||||
Line = line.Line,
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, paragraphBlock.Span.End),
|
||||
IsOpen = false
|
||||
};
|
||||
term.AppendLine(ref line.Slice, line.Column, line.Line, line.Position, processor.TrackTrivia);
|
||||
definitionItem.Add(term);
|
||||
}
|
||||
currentDefinitionList.Add(definitionItem);
|
||||
processor.Open(definitionItem);
|
||||
|
||||
// Update the end position
|
||||
currentDefinitionList.UpdateSpanEnd(processor.Line.End);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
private static DefinitionList? GetCurrentDefinitionList(ParagraphBlock paragraphBlock, ContainerBlock previousParent)
|
||||
{
|
||||
var index = previousParent.IndexOf(paragraphBlock) - 1;
|
||||
if (index < 0) return null;
|
||||
switch (previousParent[index])
|
||||
{
|
||||
case DefinitionList definitionList:
|
||||
return definitionList;
|
||||
|
||||
case BlankLineBlock:
|
||||
if (index > 0 && previousParent[index - 1] is DefinitionList definitionList2)
|
||||
{
|
||||
previousParent.RemoveAt(index);
|
||||
return definitionList2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to continue parsing the specified block.
|
||||
/// </summary>
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var definitionItem = (DefinitionItem)block;
|
||||
if (processor.IsCodeIndent)
|
||||
{
|
||||
processor.GoToCodeIndent();
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
var list = (DefinitionList)definitionItem.Parent!;
|
||||
var lastBlankLine = definitionItem.LastChild as BlankLineBlock;
|
||||
|
||||
// Check if we have another definition list
|
||||
if (Array.IndexOf(OpeningCharacters!, processor.CurrentChar) >= 0)
|
||||
{
|
||||
var startPosition = processor.Start;
|
||||
var column = processor.ColumnBeforeIndent;
|
||||
var c = processor.NextChar();
|
||||
processor.ParseIndent();
|
||||
var delta = processor.Column - column;
|
||||
|
||||
// The marker must be followed by at least one space or tab
|
||||
if (!c.IsSpaceOrTab())
|
||||
{
|
||||
// Remove the blankline before breaking this definition item
|
||||
if (lastBlankLine != null)
|
||||
{
|
||||
definitionItem.RemoveAt(definitionItem.Count - 1);
|
||||
}
|
||||
|
||||
list.Span.End = list.LastChild!.Span.End;
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
if (delta > 4)
|
||||
{
|
||||
processor.GoToColumn(column + 4);
|
||||
}
|
||||
|
||||
processor.Close(definitionItem);
|
||||
var nextDefinitionItem = new DefinitionItem(this)
|
||||
{
|
||||
Span = new SourceSpan(startPosition, processor.Line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column,
|
||||
OpeningCharacter = processor.CurrentChar,
|
||||
};
|
||||
list.Add(nextDefinitionItem);
|
||||
processor.Open(nextDefinitionItem);
|
||||
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
var isBreakable = definitionItem.LastChild?.IsBreakable ?? true;
|
||||
if (processor.IsBlankLine)
|
||||
{
|
||||
if (lastBlankLine is null && isBreakable)
|
||||
{
|
||||
definitionItem.Add(new BlankLineBlock());
|
||||
}
|
||||
return isBreakable ? BlockState.ContinueDiscard : BlockState.Continue;
|
||||
}
|
||||
|
||||
var paragraphBlock = definitionItem.LastChild as ParagraphBlock;
|
||||
if (lastBlankLine is null && paragraphBlock != null)
|
||||
{
|
||||
return BlockState.Continue;
|
||||
}
|
||||
|
||||
// Remove the blankline before breaking this definition item
|
||||
if (lastBlankLine != null)
|
||||
{
|
||||
definitionItem.RemoveAt(definitionItem.Count - 1);
|
||||
}
|
||||
|
||||
list.Span.End = list.LastChild!.Span.End;
|
||||
return BlockState.Break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
// Copyright (c) Alexandre Mutel. All rights reserved.
|
||||
// This file is licensed under the BSD-Clause 2 license.
|
||||
// See the license.txt file in the project root for more information.
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax;
|
||||
using System.Linq;
|
||||
|
||||
namespace Markdig.Extensions.Tables;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the GridTableParser type.
|
||||
/// </summary>
|
||||
public class GridTableParser : BlockParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the GridTableParser class.
|
||||
/// </summary>
|
||||
public GridTableParser()
|
||||
{
|
||||
OpeningCharacters = ['+'];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to open a block at the current parser position.
|
||||
/// </summary>
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
// A grid table cannot start more than an indent
|
||||
if (processor.IsCodeIndent)
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
var line = processor.Line;
|
||||
GridTableState? tableState = null;
|
||||
|
||||
// Match the first row that should be of the minimal form: +---------------
|
||||
var c = line.CurrentChar;
|
||||
var lineStart = line.Start;
|
||||
while (c == '+')
|
||||
{
|
||||
var columnStart = line.Start;
|
||||
line.SkipChar();
|
||||
line.TrimStart();
|
||||
|
||||
// if we have reached the end of the line, exit
|
||||
c = line.CurrentChar;
|
||||
if (c == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse a column alignment
|
||||
if (!TableHelper.ParseColumnHeader(ref line, '-', out TableColumnAlign? columnAlign, out _))
|
||||
{
|
||||
return BlockState.None;
|
||||
}
|
||||
|
||||
tableState ??= new GridTableState(start: processor.Start, expectRow: true);
|
||||
tableState.AddColumn(columnStart - lineStart, line.Start - lineStart, columnAlign);
|
||||
|
||||
c = line.CurrentChar;
|
||||
}
|
||||
|
||||
if (c != 0 || tableState is 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);
|
||||
var table = new Table(this)
|
||||
{
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column,
|
||||
Span = { Start = lineStart }
|
||||
};
|
||||
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 - 1;
|
||||
}
|
||||
|
||||
// Store the column width and alignment
|
||||
foreach (var columnSlice in tableState.ColumnSlices)
|
||||
{
|
||||
var columnDefinition = new TableColumnDefinition
|
||||
{
|
||||
// Column width proportional to the total width
|
||||
Width = (float)(columnSlice.End - columnSlice.Start - 1) * 100.0f / totalWidth,
|
||||
Alignment = columnSlice.Align
|
||||
};
|
||||
table.ColumnDefinitions.Add(columnDefinition);
|
||||
}
|
||||
|
||||
processor.NewBlocks.Push(table);
|
||||
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to continue parsing the specified block.
|
||||
/// </summary>
|
||||
public override BlockState TryContinue(BlockProcessor processor, Block block)
|
||||
{
|
||||
var gridTable = (Table)block;
|
||||
var tableState = (GridTableState)block.GetData(typeof(GridTableState))!;
|
||||
tableState.AddLine(ref processor.Line);
|
||||
if (processor.CurrentChar == '+')
|
||||
{
|
||||
gridTable.UpdateSpanEnd(processor.Line.End);
|
||||
return HandleNewRow(processor, tableState, gridTable);
|
||||
}
|
||||
if (processor.CurrentChar == '|')
|
||||
{
|
||||
gridTable.UpdateSpanEnd(processor.Line.End);
|
||||
return HandleContents(processor, tableState, gridTable);
|
||||
}
|
||||
TerminateCurrentRow(processor, tableState, gridTable, true);
|
||||
// If the table is not valid we need to remove the grid table,
|
||||
// and create a ParagraphBlock with the slices
|
||||
if (!gridTable.IsValid())
|
||||
{
|
||||
Undo(processor, tableState, gridTable);
|
||||
}
|
||||
return BlockState.Break;
|
||||
}
|
||||
|
||||
private BlockState HandleNewRow(BlockProcessor processor, GridTableState tableState, Table gridTable)
|
||||
{
|
||||
var columns = tableState.ColumnSlices!;
|
||||
SetRowSpanState(columns, processor.Line, out bool isHeaderRow, out bool hasRowSpan);
|
||||
SetColumnSpanState(columns, processor.Line);
|
||||
TerminateCurrentRow(processor, tableState, gridTable, false);
|
||||
if (isHeaderRow)
|
||||
{
|
||||
for (int i = 0; i < gridTable.Count; i++)
|
||||
{
|
||||
var row = (TableRow)gridTable[i];
|
||||
row.IsHeader = true;
|
||||
}
|
||||
}
|
||||
tableState.StartRowGroup = gridTable.Count;
|
||||
if (hasRowSpan)
|
||||
{
|
||||
HandleContents(processor, tableState, gridTable);
|
||||
}
|
||||
return BlockState.ContinueDiscard;
|
||||
}
|
||||
|
||||
private static void SetRowSpanState(List<GridTableState.ColumnSlice> columns, StringSlice line, out bool isHeaderRow, out bool hasRowSpan)
|
||||
{
|
||||
var lineStart = line.Start;
|
||||
var lineEnd = line.End;
|
||||
isHeaderRow = line.PeekChar() == '=' || line.PeekChar(2) == '=';
|
||||
hasRowSpan = false;
|
||||
foreach (var columnSlice in columns)
|
||||
{
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
line.Start = lineStart + columnSlice.Start + 1;
|
||||
line.End = Math.Min(lineStart + columnSlice.End - 1, lineEnd);
|
||||
line.Trim();
|
||||
if (line.IsEmptyOrWhitespace() || !IsRowSeparator(line))
|
||||
{
|
||||
hasRowSpan = true;
|
||||
columnSlice.CurrentCell.RowSpan++;
|
||||
columnSlice.CurrentCell.AllowClose = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
columnSlice.CurrentCell.AllowClose = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsRowSeparator(StringSlice slice)
|
||||
{
|
||||
char c = slice.CurrentChar;
|
||||
do
|
||||
{
|
||||
if (c != '-' && c != '=' && c != ':')
|
||||
{
|
||||
return c == '\0';
|
||||
}
|
||||
c = slice.NextChar();
|
||||
}
|
||||
while (true);
|
||||
}
|
||||
|
||||
private static void TerminateCurrentRow(BlockProcessor processor, GridTableState tableState, Table gridTable, bool isLastRow)
|
||||
{
|
||||
var columns = tableState.ColumnSlices;
|
||||
TableRow? currentRow = null;
|
||||
for (int i = 0; i < columns!.Count; i++)
|
||||
{
|
||||
var columnSlice = columns[i];
|
||||
if (columnSlice.CurrentCell != null)
|
||||
{
|
||||
if (currentRow == null)
|
||||
{
|
||||
TableCell firstCell = columns.First(c => c.CurrentCell != null).CurrentCell!;
|
||||
TableCell lastCell = columns.Last(c => c.CurrentCell != null).CurrentCell!;
|
||||
|
||||
currentRow ??= new TableRow()
|
||||
{
|
||||
Span = new SourceSpan(firstCell.Span.Start, lastCell.Span.End),
|
||||
Line = firstCell.Line
|
||||
};
|
||||
}
|
||||
|
||||
// If this cell does not already belong to a row
|
||||
if (columnSlice.CurrentCell.Parent is null)
|
||||
{
|
||||
currentRow.Add(columnSlice.CurrentCell);
|
||||
}
|
||||
// If the cell is not going to span through to the next row
|
||||
if (columnSlice.CurrentCell.AllowClose)
|
||||
{
|
||||
columnSlice.BlockProcessor!.Close(columnSlice.CurrentCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Renew the block parser processor (or reset it for the last row)
|
||||
if (columnSlice.BlockProcessor != null && (columnSlice.CurrentCell is null || columnSlice.CurrentCell.AllowClose))
|
||||
{
|
||||
columnSlice.BlockProcessor.ReleaseChild();
|
||||
columnSlice.BlockProcessor = isLastRow ? null : processor.CreateChild();
|
||||
}
|
||||
|
||||
// Create or erase the cell
|
||||
if (isLastRow || columnSlice.CurrentColumnSpan == 0 || (columnSlice.CurrentCell != null && columnSlice.CurrentCell.AllowClose))
|
||||
{
|
||||
// We don't need the cell anymore if we have a last row
|
||||
// Or the cell has a columnspan == 0
|
||||
// And the cell does not have to be kept open to span rows
|
||||
columnSlice.CurrentCell = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow is { 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.PeekChar(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 - line.Start) == '|')
|
||||
{
|
||||
sliceForCell.End = line.End - 1;
|
||||
}
|
||||
}
|
||||
sliceForCell.TrimEnd();
|
||||
|
||||
if (!isRowLine || !IsRowSeparator(sliceForCell))
|
||||
{
|
||||
if (columnSlice.CurrentCell is null)
|
||||
{
|
||||
columnSlice.CurrentCell = new TableCell(this)
|
||||
{
|
||||
ColumnSpan = columnSlice.CurrentColumnSpan,
|
||||
ColumnIndex = i,
|
||||
Column = columnSlice.Start,
|
||||
Line = processor.LineIndex,
|
||||
Span = new SourceSpan(line.Start + columnSlice.Start, line.Start + columnSlice.End)
|
||||
};
|
||||
|
||||
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.ProcessLinePart(sliceForCell, sliceForCell.Start - line.Start);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the close operation.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -407,15 +407,10 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
i--;
|
||||
}
|
||||
}
|
||||
else if ((closeDelimiter.Type & DelimiterType.Open) == 0)
|
||||
{
|
||||
closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline());
|
||||
delimiters.RemoveAt(i);
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep unmatched closers attached until their trailing content
|
||||
// has been moved out below.
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -432,6 +427,13 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
||||
|
||||
closeDelimiter.MoveChildrenAfter(outermostEmphasis);
|
||||
}
|
||||
|
||||
if (closeDelimiter.DelimiterCount > 0 && (closeDelimiter.Type & DelimiterType.Open) == 0)
|
||||
{
|
||||
closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline());
|
||||
delimiters.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,13 @@ public class LinkInlineParser : InlineParser
|
||||
return false;
|
||||
}
|
||||
|
||||
// An implicit definition must not hijack a bracket nested inside a
|
||||
// still-open link, which would break the outer one.
|
||||
if (!linkRef.AllowResolutionInsideOpenLink && HasActiveAncestorLink(parent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Inline? link = null;
|
||||
// Try to use a callback directly defined on the LinkReferenceDefinition
|
||||
if (linkRef.CreateLinkInline != null)
|
||||
@@ -430,6 +437,24 @@ public class LinkInlineParser : InlineParser
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the delimiter is nested inside another link/image
|
||||
/// delimiter that may still resolve.
|
||||
/// </summary>
|
||||
private static bool HasActiveAncestorLink(LinkDelimiterInline delimiter)
|
||||
{
|
||||
var inline = delimiter.Parent;
|
||||
while (inline != null)
|
||||
{
|
||||
if (inline is LinkDelimiterInline { IsActive: true })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
inline = inline.Parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void MarkParentAsInactive(Inline? inline)
|
||||
{
|
||||
while (inline != null)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
#if !NET5_0_OR_GREATER
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
// Required by the compiler for init-only properties on older target frameworks.
|
||||
internal static class IsExternalInit
|
||||
{
|
||||
}
|
||||
#endif
|
||||
@@ -34,6 +34,7 @@ public class LinkReferenceDefinition : LeafBlock
|
||||
public LinkReferenceDefinition() : base(null)
|
||||
{
|
||||
IsOpen = false;
|
||||
AllowResolutionInsideOpenLink = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +137,16 @@ public class LinkReferenceDefinition : LeafBlock
|
||||
/// </remarks>
|
||||
public CreateLinkInlineDelegate? CreateLinkInline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this definition may resolve into a link while its label is
|
||||
/// nested inside another still-open link or image bracket.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>true</c> for user-authored definitions, preserving CommonMark's "links
|
||||
/// cannot contain links". Implicitly generated ones can initialize it to <c>false</c>.
|
||||
/// </remarks>
|
||||
internal bool AllowResolutionInsideOpenLink { get; private protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tries to the parse the specified text into a definition.
|
||||
/// </summary>
|
||||
|
||||
Reference in new issue
Block a user