Compare commits

...
10 Commits
Author SHA1 Message Date
dualfroz 5444164847 Describe this repository 2026-10-22 21:08:13 +02:00
dualfroz 463eec7085 Fix definition lists requiring 3 spaces after the marker (#384) 2026-10-10 11:42:13 +02:00
upstream 5a326ef5b7 Upstream xoofx/markdig state of the touched files before a9eea55dcf 2026-10-10 11:42:13 +02:00
dualfroz 2b9fbfb252 Fix grid table open last cell taking a | from the next line (#494)
To find a closing `|` at the end of the last column, GridTableParser used
PeekCharExtra, which ignores the end of the slice. When the line was
shorter than the table, it could read the next line instead, and a `|`
there made the cell run past the end of the line, so the next line's
leading `|` ended up in the cell. Use PeekChar, which stays within the
current line.
2026-10-03 02:46:01 +02:00
upstream 1d93169fc0 Upstream xoofx/markdig state of the touched files before 93486bb52c 2026-10-03 02:46:01 +02:00
dualfrozandAlexandre Mutel 425c9f8a46 Fix: keep trailing content out of an unmatched emphasis closer (#743) (#950)
* fix: keep trailing content out of unmatched emphasis closer (#743)

When EnableTrackTrivia() is enabled, an emphasis closer whose delimiter
run is only partially consumed (its leftover dropping below the minimum
count) kept the inlines that follow it nested inside the emphasis. The
roundtrip renderer then emitted the closing delimiters after that
trailing content, corrupting the output, for example a grid table
separator row using the "marked" (==) delimiter from EmphasisExtras.

Move the trailing content out to the outermost enclosing emphasis when
the closer can no longer be matched, mirroring the existing handling for
a fully consumed opener. The leftover closer keeps its position, so
cases without trailing content are unaffected. Adds a regression test.

* Fix unmatched emphasis closer cleanup order (#743)

---------

Co-authored-by: Alexandre Mutel <alexandre_mutel@live.com>
2026-09-19 11:52:40 +02:00
upstream 49e145437f Upstream xoofx/markdig state of the touched files before 27b8c9eb56 2026-09-19 11:52:40 +02:00
dualfrozandAlexandre Mutel 9006f0b4a2 Fix heading auto-link hijacking nested link labels (#668) (#949)
* Fix heading auto-link hijacking nested link labels (#668)

* Refactor heading reference resolution to init-only state (#949)

---------

Co-authored-by: Alexandre Mutel <alexandre_mutel@live.com>
2026-09-19 11:41:48 +02:00
upstream 40eebc0cd8 Upstream xoofx/markdig state of the touched files before baa95a9ca3 2026-09-19 11:41:48 +02:00
dualfroz cf9456728c Fix heading auto-link hijacking nested link labels (#668) 2026-09-04 03:24:13 +02:00
15 changed files with 1901 additions and 8 deletions

No files matched your search

+13
View File
@@ -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)
+95
View File
@@ -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.
+98
View File
@@ -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)
+585
View File
@@ -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 = "![image](\"onclick=\"alert&amp;#40;'click'&amp;#41;\"://)";
string expected = "<p><img src=\"%22onclick=%22alert&amp;#40;%27click%27&amp;#41;%22://\" alt=\"image\" /></p>";
TestParser.TestSpec(input, expected);
}
[Test]
public void TestAltTextIsCorrectlyEscaped()
{
TestParser.TestSpec(
@"![This is image alt text with quotation ' and double quotation ""hello"" world](girl.png)",
@"<p><img src=""girl.png"" alt=""This is image alt text with quotation ' and double quotation &quot;hello&quot; 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&amp;ddr;&amp;*&amp;ddr;&amp;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&#174;*&#174;";
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>
````````````````````````````````
+56
View File
@@ -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)
+12
View File
@@ -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
+12 -1
View File
@@ -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>
@@ -241,4 +252,4 @@ public class LinkReferenceDefinition : LeafBlock
public StringSlice UnescapedTitle;
public char TitleEnclosingCharacter;
}
}
}