Upstream xoofx/markdig state of the touched files before 93486bb52c
This commit is contained in:
1 parent
425c9f8a46
commit
1d93169fc0
2 files changed
+956
No files matched your search
@@ -0,0 +1,560 @@
|
||||
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 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,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.PeekCharExtra(columnEnd);
|
||||
// If there is a `|` (or a `+` in the case that we are dealing with a row line
|
||||
// with spanned contents) exactly at the expected end of the table row, we cut the line
|
||||
// otherwise we allow to have the last cell of a row to be open for longer cell content
|
||||
if (columnEndChar == '|' || (isRowLine && columnEndChar == '+'))
|
||||
{
|
||||
sliceForCell.End = line.Start + columnEnd - 1;
|
||||
}
|
||||
else if (line.PeekCharExtra(line.End - 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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user