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);
|
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));
|
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.
|
// The fix must not prevent balanced "marked" emphasis from being detected.
|
||||||
[Test]
|
[Test]
|
||||||
public void BalancedMarkedEmphasisStillParses()
|
public void BalancedMarkedEmphasisStillParses()
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public class HeadingLinkReferenceDefinition : LinkReferenceDefinition
|
|||||||
public HeadingLinkReferenceDefinition(HeadingBlock headling)
|
public HeadingLinkReferenceDefinition(HeadingBlock headling)
|
||||||
{
|
{
|
||||||
Heading = 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>
|
/// <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--;
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((closeDelimiter.Type & DelimiterType.Open) == 0)
|
|
||||||
{
|
|
||||||
closeDelimiter.ReplaceBy(closeDelimiter.AsLiteralInline());
|
|
||||||
delimiters.RemoveAt(i);
|
|
||||||
i--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Keep unmatched closers attached until their trailing content
|
||||||
|
// has been moved out below.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +427,13 @@ public class EmphasisInlineParser : InlineParser, IPostInlineProcessor
|
|||||||
|
|
||||||
closeDelimiter.MoveChildrenAfter(outermostEmphasis);
|
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;
|
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;
|
Inline? link = null;
|
||||||
// Try to use a callback directly defined on the LinkReferenceDefinition
|
// Try to use a callback directly defined on the LinkReferenceDefinition
|
||||||
if (linkRef.CreateLinkInline != null)
|
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)
|
private static void MarkParentAsInactive(Inline? inline)
|
||||||
{
|
{
|
||||||
while (inline != null)
|
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)
|
public LinkReferenceDefinition() : base(null)
|
||||||
{
|
{
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
|
AllowResolutionInsideOpenLink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -136,6 +137,16 @@ public class LinkReferenceDefinition : LeafBlock
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public CreateLinkInlineDelegate? CreateLinkInline { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Tries to the parse the specified text into a definition.
|
/// Tries to the parse the specified text into a definition.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in new issue
Block a user