mirror of
https://github.com/xoofx/markdig.git
synced 2026-07-08 18:16:21 +00:00
Add span validation/update APIs and tests
This commit is contained in:
373
src/Markdig.Tests/TestExtensionSpanCoverage.cs
Normal file
373
src/Markdig.Tests/TestExtensionSpanCoverage.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
using Markdig.Extensions.Abbreviations;
|
||||
using Markdig.Extensions.Alerts;
|
||||
using Markdig.Extensions.DefinitionLists;
|
||||
using Markdig.Extensions.Emoji;
|
||||
using Markdig.Extensions.Figures;
|
||||
using Markdig.Extensions.Footers;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.JiraLinks;
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Extensions.SmartyPants;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Extensions.TaskLists;
|
||||
using Markdig.Extensions.Yaml;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TestExtensionSpanCoverage
|
||||
{
|
||||
public sealed class ExtensionSpanCase(
|
||||
string name,
|
||||
Action<MarkdownPipelineBuilder> configurePipeline,
|
||||
string markdown,
|
||||
Action<MarkdownDocument> validate,
|
||||
bool validateSpanTree = true)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public Action<MarkdownPipelineBuilder> ConfigurePipeline { get; } = configurePipeline;
|
||||
public string Markdown { get; } = markdown;
|
||||
public Action<MarkdownDocument> Validate { get; } = validate;
|
||||
public bool ValidateSpanTree { get; } = validateSpanTree;
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(GetExtensionSpanCases))]
|
||||
public void ExtensionSpanTreeIsValid(ExtensionSpanCase testCase)
|
||||
{
|
||||
var builder = new MarkdownPipelineBuilder
|
||||
{
|
||||
PreciseSourceLocation = true
|
||||
};
|
||||
testCase.ConfigurePipeline(builder);
|
||||
|
||||
var document = Markdown.Parse(testCase.Markdown.ReplaceLineEndings("\n"), builder.Build());
|
||||
|
||||
if (testCase.ValidateSpanTree)
|
||||
{
|
||||
Assert.That(document.HasValidSpan(recursive: true), Is.True, $"{testCase.Name} has invalid container spans");
|
||||
}
|
||||
|
||||
testCase.Validate?.Invoke(document);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void JiraLinkSpanMatchesToken()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
PreciseSourceLocation = true
|
||||
}.UseJiraLinks(new JiraLinkOptions("https://jira.example.com")).Build();
|
||||
|
||||
var document = Markdown.Parse("ABC-123", pipeline);
|
||||
var jiraLink = document.Descendants<JiraLink>().FirstOrDefault();
|
||||
Assert.That(jiraLink, Is.Not.Null);
|
||||
Assert.That(jiraLink!.Span, Is.EqualTo(new SourceSpan(0, 6)));
|
||||
|
||||
var literal = jiraLink.FirstChild as LiteralInline;
|
||||
Assert.That(literal, Is.Not.Null);
|
||||
Assert.That(literal!.Span, Is.EqualTo(new SourceSpan(0, 6)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TaskListSpanMatchesCheckboxToken()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
PreciseSourceLocation = true
|
||||
}.UseTaskLists().Build();
|
||||
|
||||
var document = Markdown.Parse("- [x] done", pipeline);
|
||||
var task = document.Descendants<TaskList>().FirstOrDefault();
|
||||
Assert.That(task, Is.Not.Null);
|
||||
Assert.That(task!.Span, Is.EqualTo(new SourceSpan(2, 4)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AlertBlockSpanCoversSourceQuote()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
PreciseSourceLocation = true
|
||||
}.UseAlertBlocks().Build();
|
||||
|
||||
var document = Markdown.Parse("> [!NOTE]\n> body", pipeline);
|
||||
var alert = document.Descendants<AlertBlock>().FirstOrDefault();
|
||||
Assert.That(alert, Is.Not.Null);
|
||||
Assert.That(alert!.Span.Start, Is.EqualTo(0));
|
||||
Assert.That(alert.Span.End, Is.GreaterThan(0));
|
||||
|
||||
var paragraph = alert.Descendants<ParagraphBlock>().FirstOrDefault();
|
||||
Assert.That(paragraph, Is.Not.Null);
|
||||
Assert.That(paragraph!.Span.Start, Is.GreaterThanOrEqualTo(alert.Span.Start));
|
||||
Assert.That(paragraph.Span.End, Is.LessThanOrEqualTo(alert.Span.End));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void YamlFrontMatterSpanCoversFrontMatterContent()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder
|
||||
{
|
||||
PreciseSourceLocation = true
|
||||
}.UseYamlFrontMatter().Build();
|
||||
|
||||
var document = Markdown.Parse("---\na: 1\n---\ntext", pipeline);
|
||||
var yaml = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
|
||||
Assert.That(yaml, Is.Not.Null);
|
||||
Assert.That(yaml!.Span.Start, Is.EqualTo(0));
|
||||
Assert.That(yaml.Span.End, Is.GreaterThanOrEqualTo(7));
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetExtensionSpanCases()
|
||||
{
|
||||
yield return Case(
|
||||
"AlertBlocks",
|
||||
builder => builder.UseAlertBlocks(),
|
||||
"> [!NOTE]\n> body",
|
||||
document => AssertNodesHaveNonEmptySpan<AlertBlock>(document));
|
||||
|
||||
yield return Case(
|
||||
"AutoLinks",
|
||||
builder => builder.UseAutoLinks(),
|
||||
"http://example.com",
|
||||
document =>
|
||||
{
|
||||
var autoLink = document.Descendants<LinkInline>().FirstOrDefault(link => link.IsAutoLink);
|
||||
Assert.That(autoLink, Is.Not.Null);
|
||||
Assert.That(autoLink!.Span.IsEmpty, Is.False);
|
||||
Assert.That(autoLink.UrlSpan, Is.EqualTo(autoLink.Span));
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"NonAsciiNoEscape",
|
||||
builder => builder.UseNonAsciiNoEscape(),
|
||||
"Café");
|
||||
|
||||
yield return Case(
|
||||
"YamlFrontMatter",
|
||||
builder => builder.UseYamlFrontMatter(),
|
||||
"---\na: 1\n---\ntext",
|
||||
document => AssertNodesHaveNonEmptySpan<YamlFrontMatterBlock>(document));
|
||||
|
||||
yield return Case(
|
||||
"SelfPipeline",
|
||||
builder => builder.UseSelfPipeline(),
|
||||
"<!--markdig:tasklists-->\n- [x] done",
|
||||
document => AssertNodesHaveNonEmptySpan<TaskList>(document),
|
||||
validateSpanTree: false);
|
||||
|
||||
yield return Case(
|
||||
"PragmaLines",
|
||||
builder => builder.UsePragmaLines(),
|
||||
"# Heading\n\nParagraph");
|
||||
|
||||
yield return Case(
|
||||
"Diagrams",
|
||||
builder => builder.UseDiagrams(),
|
||||
"```mermaid\ngraph TD;\n```");
|
||||
|
||||
yield return Case(
|
||||
"TaskLists",
|
||||
builder => builder.UseTaskLists(),
|
||||
"- [x] done",
|
||||
document => AssertNodesHaveNonEmptySpan<TaskList>(document));
|
||||
|
||||
yield return Case(
|
||||
"CustomContainers",
|
||||
builder => builder.UseCustomContainers(),
|
||||
":::\nvalue\n:::",
|
||||
document => AssertNodesHaveNonEmptySpan<Markdig.Extensions.CustomContainers.CustomContainer>(document));
|
||||
|
||||
yield return Case(
|
||||
"MediaLinks",
|
||||
builder => builder.UseMediaLinks(),
|
||||
"[video](https://www.youtube.com/watch?v=dQw4w9WgXcQ)",
|
||||
document => AssertNodesHaveNonEmptySpan<LinkInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"AutoIdentifiers",
|
||||
builder => builder.UseAutoIdentifiers(),
|
||||
"# Heading",
|
||||
document =>
|
||||
{
|
||||
var heading = document.Descendants<HeadingBlock>().FirstOrDefault();
|
||||
Assert.That(heading, Is.Not.Null);
|
||||
Assert.That(heading!.GetAttributes().Id, Is.Not.Null.And.Not.Empty);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"SmartyPants",
|
||||
builder => builder.UseSmartyPants(),
|
||||
"<<a>>",
|
||||
document => AssertNodesHaveNonEmptySpan<SmartyPant>(document));
|
||||
|
||||
yield return Case(
|
||||
"Bootstrap",
|
||||
builder => builder.UseBootstrap(),
|
||||
"");
|
||||
|
||||
yield return Case(
|
||||
"Mathematics",
|
||||
builder => builder.UseMathematics(),
|
||||
"$a$",
|
||||
document => AssertNodesHaveNonEmptySpan<MathInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"Figures",
|
||||
builder => builder.UseFigures(),
|
||||
"^^^\ntext\n^^^",
|
||||
document => AssertNodesHaveNonEmptySpan<Figure>(document));
|
||||
|
||||
yield return Case(
|
||||
"Abbreviations",
|
||||
builder => builder.UseAbbreviations(),
|
||||
"*[HTML]: HyperText Markup Language\n\nHTML",
|
||||
document => AssertNodesHaveNonEmptySpan<AbbreviationInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"DefinitionLists",
|
||||
builder => builder.UseDefinitionLists(),
|
||||
"a0\n: 1234",
|
||||
document => AssertNodesHaveNonEmptySpan<DefinitionList>(document));
|
||||
|
||||
yield return Case(
|
||||
"PipeTables",
|
||||
builder => builder.UsePipeTables(),
|
||||
"a|b\n-|-\n1|2",
|
||||
document => AssertNodesHaveNonEmptySpan<Table>(document));
|
||||
|
||||
yield return Case(
|
||||
"GridTables",
|
||||
builder => builder.UseGridTables(),
|
||||
"+-+-+\n|a|b|\n+=+=+\n|c|d|\n+-+-+",
|
||||
document => AssertNodesHaveNonEmptySpan<Table>(document));
|
||||
|
||||
yield return Case(
|
||||
"Citations",
|
||||
builder => builder.UseCitations(),
|
||||
"\"\"text\"\"",
|
||||
document =>
|
||||
{
|
||||
var citation = document.Descendants<EmphasisInline>().FirstOrDefault(x => x.DelimiterChar == '"' && x.DelimiterCount == 2);
|
||||
Assert.That(citation, Is.Not.Null);
|
||||
Assert.That(citation!.Span.IsEmpty, Is.False);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"Footers",
|
||||
builder => builder.UseFooters(),
|
||||
"^^ one\n^^ two",
|
||||
document => AssertNodesHaveNonEmptySpan<FooterBlock>(document));
|
||||
|
||||
yield return Case(
|
||||
"Footnotes",
|
||||
builder => builder.UseFootnotes(),
|
||||
"[^1]: note\n\nref[^1]",
|
||||
document =>
|
||||
{
|
||||
AssertNodesHaveNonEmptySpan<FootnoteGroup>(document);
|
||||
AssertNodesHaveNonEmptySpan<Footnote>(document);
|
||||
var links = document.Descendants<FootnoteLink>().ToList();
|
||||
Assert.That(links.Count, Is.GreaterThan(0));
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"Hardlines",
|
||||
builder => builder.UseSoftlineBreakAsHardlineBreak(),
|
||||
"a\nb",
|
||||
document =>
|
||||
{
|
||||
var lineBreak = document.Descendants<LineBreakInline>().FirstOrDefault();
|
||||
Assert.That(lineBreak, Is.Not.Null);
|
||||
Assert.That(lineBreak!.IsHard, Is.True);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"EmphasisExtras",
|
||||
builder => builder.UseEmphasisExtras(),
|
||||
"~~text~~",
|
||||
document =>
|
||||
{
|
||||
var emphasis = document.Descendants<EmphasisInline>().FirstOrDefault(x => x.DelimiterChar == '~' && x.DelimiterCount == 2);
|
||||
Assert.That(emphasis, Is.Not.Null);
|
||||
Assert.That(emphasis!.Span.IsEmpty, Is.False);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"ListExtras",
|
||||
builder => builder.UseListExtras(),
|
||||
"A. item",
|
||||
document =>
|
||||
{
|
||||
var list = document.Descendants<ListBlock>().FirstOrDefault();
|
||||
Assert.That(list, Is.Not.Null);
|
||||
Assert.That(list!.Span.IsEmpty, Is.False);
|
||||
Assert.That(list.IsOrdered, Is.True);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"GenericAttributes",
|
||||
builder => builder.UseGenericAttributes(),
|
||||
"text{#custom-id}",
|
||||
document =>
|
||||
{
|
||||
var paragraph = document.Descendants<ParagraphBlock>().FirstOrDefault();
|
||||
Assert.That(paragraph, Is.Not.Null);
|
||||
var attributes = paragraph!.TryGetAttributes();
|
||||
Assert.That(attributes, Is.Not.Null);
|
||||
Assert.That(attributes!.Span.IsEmpty, Is.False);
|
||||
});
|
||||
|
||||
yield return Case(
|
||||
"EmojiAndSmiley",
|
||||
builder => builder.UseEmojiAndSmiley(),
|
||||
":)",
|
||||
document => AssertNodesHaveNonEmptySpan<EmojiInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"NoFollowLinks",
|
||||
builder => builder.UseReferralLinks("nofollow"),
|
||||
"[link](https://example.com)",
|
||||
document => AssertNodesHaveNonEmptySpan<LinkInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"ReferralLinks",
|
||||
builder => builder.UseReferralLinks("nofollow", "noopener"),
|
||||
"[link](https://example.com)",
|
||||
document => AssertNodesHaveNonEmptySpan<LinkInline>(document));
|
||||
|
||||
yield return Case(
|
||||
"JiraLinks",
|
||||
builder => builder.UseJiraLinks(new JiraLinkOptions("https://jira.example.com")),
|
||||
"ABC-123",
|
||||
document => AssertNodesHaveNonEmptySpan<JiraLink>(document));
|
||||
|
||||
yield return Case(
|
||||
"Globalization",
|
||||
builder => builder.UseGlobalization(),
|
||||
"## Héllo");
|
||||
}
|
||||
|
||||
private static TestCaseData Case(
|
||||
string name,
|
||||
Action<MarkdownPipelineBuilder> configurePipeline,
|
||||
string markdown,
|
||||
Action<MarkdownDocument> validate = null,
|
||||
bool validateSpanTree = true)
|
||||
{
|
||||
return new TestCaseData(new ExtensionSpanCase(name, configurePipeline, markdown, validate, validateSpanTree))
|
||||
.SetName($"Extension_{name}_MaintainsValidSpans");
|
||||
}
|
||||
|
||||
private static void AssertNodesHaveNonEmptySpan<T>(MarkdownDocument document) where T : MarkdownObject
|
||||
{
|
||||
var nodes = document.Descendants<T>().ToList();
|
||||
Assert.That(nodes.Count, Is.GreaterThan(0), $"Expected at least one node of type `{typeof(T).Name}`");
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
Assert.That(node.Span.IsEmpty, Is.False, $"Node `{typeof(T).Name}` has an empty span");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,21 @@ public sealed class TestParserAuthoringHelpers
|
||||
Assert.That(processor.TryDiscard(document), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EmitUpdatesInlineAndLeafSpans()
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder();
|
||||
pipeline.InlineParsers.InsertBefore<AutolinkInlineParser>(new SpanEmittingInlineParser());
|
||||
|
||||
var document = Markdown.Parse("~", pipeline.Build());
|
||||
var paragraph = document[0] as ParagraphBlock;
|
||||
|
||||
Assert.That(paragraph, Is.Not.Null);
|
||||
Assert.That(paragraph!.Inline, Is.Not.Null);
|
||||
Assert.That(paragraph.Inline!.Span, Is.EqualTo(new SourceSpan(0, 0)));
|
||||
Assert.That(paragraph.Span, Is.EqualTo(new SourceSpan(0, 0)));
|
||||
}
|
||||
|
||||
private sealed class CountingInlineParser : InlineParser
|
||||
{
|
||||
public CountingInlineParser()
|
||||
@@ -75,4 +90,31 @@ public sealed class TestParserAuthoringHelpers
|
||||
{
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SpanEmittingInlineParser : InlineParser
|
||||
{
|
||||
public SpanEmittingInlineParser()
|
||||
{
|
||||
OpeningCharacters = ['~'];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
if (slice.CurrentChar != '~')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = processor.GetSourcePosition(slice.Start, out int line, out int column);
|
||||
processor.Emit(new LiteralInline("x")
|
||||
{
|
||||
Line = line,
|
||||
Column = column,
|
||||
Span = new SourceSpan(start, start)
|
||||
});
|
||||
|
||||
slice.SkipChar();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ literal ( 4, 0) 13-14
|
||||
// 012 3456789A
|
||||
Check("a0\n: 1234", @"
|
||||
definitionlist ( 0, 0) 0-10
|
||||
definitionitem ( 1, 0) 3-10
|
||||
definitionitem ( 1, 0) 0-10
|
||||
definitionterm ( 0, 0) 0-1
|
||||
literal ( 0, 0) 0-1
|
||||
paragraph ( 1, 4) 7-10
|
||||
@@ -594,7 +594,7 @@ literal ( 1, 4) 7-10
|
||||
// 012 3456789AB CDEF01234
|
||||
Check("a0\n: 1234\n: 5678", @"
|
||||
definitionlist ( 0, 0) 0-20
|
||||
definitionitem ( 1, 0) 3-10
|
||||
definitionitem ( 1, 0) 0-10
|
||||
definitionterm ( 0, 0) 0-1
|
||||
literal ( 0, 0) 0-1
|
||||
paragraph ( 1, 4) 7-10
|
||||
@@ -997,4 +997,4 @@ literal ( 8, 2) 77-92
|
||||
.Replace("block", string.Empty)
|
||||
.Replace("inline", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
src/Markdig.Tests/TestSpanConsistency.cs
Normal file
93
src/Markdig.Tests/TestSpanConsistency.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TestSpanConsistency
|
||||
{
|
||||
private sealed class MockContainerBlock : ContainerBlock
|
||||
{
|
||||
public MockContainerBlock() : base(null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BlockUpdateSpanToIncludePropagatesToParents()
|
||||
{
|
||||
var root = new MockContainerBlock { Span = new SourceSpan(30, 40) };
|
||||
var child = new MockContainerBlock { Span = new SourceSpan(32, 35) };
|
||||
root.Add(child);
|
||||
|
||||
child.UpdateSpanToInclude(new SourceSpan(10, 50));
|
||||
|
||||
Assert.That(child.Span, Is.EqualTo(new SourceSpan(10, 50)));
|
||||
Assert.That(root.Span, Is.EqualTo(new SourceSpan(10, 50)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainerBlockCanValidateAndUpdateSpansRecursively()
|
||||
{
|
||||
var document = new MarkdownDocument();
|
||||
var container = new MockContainerBlock { Span = new SourceSpan(30, 31) };
|
||||
var paragraph = new ParagraphBlock { Span = new SourceSpan(23, 24) };
|
||||
paragraph.Inline = new ContainerInline { Span = new SourceSpan(22, 24) };
|
||||
|
||||
container.Add(paragraph);
|
||||
document.Add(container);
|
||||
document.Span = new SourceSpan(0, 5);
|
||||
|
||||
Assert.That(document.HasValidSpan(recursive: true), Is.False);
|
||||
|
||||
Assert.That(document.UpdateSpanFromChildren(recursive: true), Is.True);
|
||||
Assert.That(document.HasValidSpan(recursive: true), Is.True);
|
||||
Assert.That(paragraph.Span, Is.EqualTo(new SourceSpan(22, 24)));
|
||||
Assert.That(container.Span, Is.EqualTo(new SourceSpan(22, 31)));
|
||||
Assert.That(document.Span, Is.EqualTo(new SourceSpan(0, 31)));
|
||||
|
||||
container.Span = new SourceSpan(0, 100);
|
||||
document.Span = new SourceSpan(0, 100);
|
||||
|
||||
Assert.That(document.UpdateSpanFromChildren(recursive: true, preserveSelfSpan: false), Is.True);
|
||||
Assert.That(container.Span, Is.EqualTo(new SourceSpan(22, 24)));
|
||||
Assert.That(document.Span, Is.EqualTo(new SourceSpan(22, 24)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainerInlineCanValidateAndUpdateSpansRecursively()
|
||||
{
|
||||
var root = new ContainerInline { Span = new SourceSpan(35, 36) };
|
||||
var nested = new EmphasisInline { Span = new SourceSpan(50, 51) };
|
||||
var literal = new LiteralInline("x") { Span = new SourceSpan(10, 12) };
|
||||
|
||||
nested.AppendChild(literal);
|
||||
root.AppendChild(nested);
|
||||
|
||||
Assert.That(root.HasValidSpan(recursive: true), Is.False);
|
||||
|
||||
Assert.That(root.UpdateSpanFromChildren(recursive: true), Is.True);
|
||||
Assert.That(root.HasValidSpan(recursive: true), Is.True);
|
||||
Assert.That(nested.Span, Is.EqualTo(new SourceSpan(10, 51)));
|
||||
Assert.That(root.Span, Is.EqualTo(new SourceSpan(10, 51)));
|
||||
|
||||
nested.Span = new SourceSpan(0, 100);
|
||||
root.Span = new SourceSpan(0, 100);
|
||||
|
||||
Assert.That(root.UpdateSpanFromChildren(recursive: true, preserveSelfSpan: false), Is.True);
|
||||
Assert.That(nested.Span, Is.EqualTo(new SourceSpan(10, 12)));
|
||||
Assert.That(root.Span, Is.EqualTo(new SourceSpan(10, 12)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ContainerBlockSpanCanBeExpandedAfterInsertions()
|
||||
{
|
||||
var container = new MockContainerBlock { Span = new SourceSpan(20, 25) };
|
||||
container.Insert(0, new ParagraphBlock { Span = new SourceSpan(10, 12) });
|
||||
container.Insert(1, new ParagraphBlock { Span = new SourceSpan(30, 35) });
|
||||
|
||||
Assert.That(container.HasValidSpan(), Is.False);
|
||||
Assert.That(container.UpdateSpanFromChildren(), Is.True);
|
||||
Assert.That(container.Span, Is.EqualTo(new SourceSpan(10, 35)));
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public class DefinitionListParser : BlockParser
|
||||
{
|
||||
Line = processor.LineIndex,
|
||||
Column = column,
|
||||
Span = new SourceSpan(startPosition, processor.Line.End),
|
||||
Span = new SourceSpan(paragraphBlock.Span.Start, processor.Line.End),
|
||||
OpeningCharacter = processor.CurrentChar,
|
||||
};
|
||||
|
||||
@@ -199,4 +199,4 @@ public class DefinitionListParser : BlockParser
|
||||
list.Span.End = list.LastChild!.Span.End;
|
||||
return BlockState.Break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Extensions.JiraLinks;
|
||||
@@ -80,18 +81,15 @@ public class JiraLinkInlineParser : InlineParser
|
||||
return false;
|
||||
}
|
||||
|
||||
int spanStart = processor.GetSourcePosition(startKey, out int line, out int column);
|
||||
var jiraLink = new JiraLink() //create the link at the relevant position
|
||||
{
|
||||
Span =
|
||||
{
|
||||
Start = processor.GetSourcePosition(slice.Start, out int line, out int column)
|
||||
},
|
||||
Span = new SourceSpan(spanStart, spanStart + (endIssue - startKey)),
|
||||
Line = line,
|
||||
Column = column,
|
||||
Issue = new StringSlice(slice.Text, startIssue, endIssue),
|
||||
ProjectKey = new StringSlice(slice.Text, startKey, endKey),
|
||||
};
|
||||
jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey);
|
||||
|
||||
// Builds the Url
|
||||
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
|
||||
@@ -107,7 +105,12 @@ public class JiraLinkInlineParser : InlineParser
|
||||
builder.Append(jiraLink.ProjectKey.AsSpan());
|
||||
builder.Append('-');
|
||||
builder.Append(jiraLink.Issue.AsSpan());
|
||||
jiraLink.AppendChild(new LiteralInline(builder.ToString()));
|
||||
jiraLink.AppendChild(new LiteralInline(builder.ToString())
|
||||
{
|
||||
Span = jiraLink.Span,
|
||||
Line = line,
|
||||
Column = column,
|
||||
});
|
||||
|
||||
if (_options.OpenInNewWindow)
|
||||
{
|
||||
|
||||
@@ -456,6 +456,29 @@ public class InlineProcessor
|
||||
|
||||
var container = FindLastContainer();
|
||||
container.AppendChild(inline);
|
||||
|
||||
if (ReferenceEquals(container, Root) && !inline.Span.IsEmpty)
|
||||
{
|
||||
if (container.Span.IsEmpty)
|
||||
{
|
||||
container.Span = inline.Span;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inline.Span.Start < container.Span.Start)
|
||||
{
|
||||
container.Span.Start = inline.Span.Start;
|
||||
}
|
||||
|
||||
if (inline.Span.End > container.Span.End)
|
||||
{
|
||||
container.Span.End = inline.Span.End;
|
||||
}
|
||||
}
|
||||
|
||||
Block?.UpdateSpanToInclude(inline.Span);
|
||||
}
|
||||
|
||||
Inline = inline;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,45 @@ public abstract class Block : MarkdownObject, IBlock
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this block span and all parent container spans to include the specified <paramref name="span"/>.
|
||||
/// </summary>
|
||||
/// <param name="span">The span to include.</param>
|
||||
public void UpdateSpanToInclude(SourceSpan span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int depth = 0;
|
||||
Block? current = this;
|
||||
while (current is not null)
|
||||
{
|
||||
if (current.Span.IsEmpty)
|
||||
{
|
||||
current.Span = span;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (span.Start < current.Span.Start)
|
||||
{
|
||||
current.Span.Start = span.Start;
|
||||
}
|
||||
|
||||
if (span.End > current.Span.End)
|
||||
{
|
||||
current.Span.End = span.End;
|
||||
}
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
depth++;
|
||||
}
|
||||
|
||||
ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true);
|
||||
}
|
||||
|
||||
public void UpdateSpanEnd(int spanEnd)
|
||||
{
|
||||
// Update parent spans
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace Markdig.Syntax;
|
||||
|
||||
@@ -271,6 +272,124 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this container span is valid with respect to child block spans.
|
||||
/// </summary>
|
||||
/// <param name="recursive">
|
||||
/// When <c>true</c>, validates descendant container blocks and inline containers recursively.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> when this container span contains all direct child spans and recursive checks (if enabled) succeed;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool HasValidSpan(bool recursive = false)
|
||||
{
|
||||
var children = _children;
|
||||
for (int i = 0; i < Count && i < children.Length; i++)
|
||||
{
|
||||
var child = children[i].Block;
|
||||
if (!ContainsSpan(Span, child.Span))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!recursive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child is ContainerBlock containerBlock)
|
||||
{
|
||||
if (!containerBlock.HasValidSpan(recursive: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (child is LeafBlock leafBlock && leafBlock.Inline is ContainerInline inline)
|
||||
{
|
||||
if (!ContainsSpan(leafBlock.Span, inline.Span))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!inline.HasValidSpan(recursive: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this container span from its child block spans.
|
||||
/// </summary>
|
||||
/// <param name="recursive">
|
||||
/// When <c>true</c>, updates descendant container blocks and inline containers recursively before updating this container.
|
||||
/// </param>
|
||||
/// <param name="preserveSelfSpan">
|
||||
/// When <c>true</c>, preserves this container current span and only expands it to include children.
|
||||
/// When <c>false</c>, recomputes from children only.
|
||||
/// </param>
|
||||
/// <returns><c>true</c> when this container span changed; otherwise, <c>false</c>.</returns>
|
||||
public bool UpdateSpanFromChildren(bool recursive = false, bool preserveSelfSpan = true)
|
||||
{
|
||||
var updatedSpan = SourceSpan.Empty;
|
||||
bool hasUpdatedSpan = false;
|
||||
|
||||
if (preserveSelfSpan && !Span.IsEmpty)
|
||||
{
|
||||
updatedSpan = Span;
|
||||
hasUpdatedSpan = true;
|
||||
}
|
||||
|
||||
var children = _children;
|
||||
for (int i = 0; i < Count && i < children.Length; i++)
|
||||
{
|
||||
var child = children[i].Block;
|
||||
|
||||
if (recursive)
|
||||
{
|
||||
if (child is ContainerBlock containerBlock)
|
||||
{
|
||||
containerBlock.UpdateSpanFromChildren(recursive: true, preserveSelfSpan: preserveSelfSpan);
|
||||
}
|
||||
else if (child is LeafBlock leafBlock && leafBlock.Inline is ContainerInline inline)
|
||||
{
|
||||
inline.UpdateSpanFromChildren(recursive: true, preserveSelfSpan: preserveSelfSpan);
|
||||
|
||||
if (!ContainsSpan(leafBlock.Span, inline.Span))
|
||||
{
|
||||
if (preserveSelfSpan && !leafBlock.Span.IsEmpty)
|
||||
{
|
||||
leafBlock.UpdateSpanToInclude(inline.Span);
|
||||
}
|
||||
else
|
||||
{
|
||||
leafBlock.Span = inline.Span;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppendSpan(ref updatedSpan, ref hasUpdatedSpan, child.Span);
|
||||
}
|
||||
|
||||
if (!hasUpdatedSpan)
|
||||
{
|
||||
updatedSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
if (updatedSpan == Span)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span = updatedSpan;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Sort(IComparer<Block> comparer)
|
||||
{
|
||||
if (comparer is null) ThrowHelper.ArgumentNullException(nameof(comparer));
|
||||
@@ -353,4 +472,36 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
return _comparer.Compare(x.Block, y.Block);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool ContainsSpan(in SourceSpan containerSpan, in SourceSpan childSpan)
|
||||
{
|
||||
return childSpan.IsEmpty || (!containerSpan.IsEmpty && childSpan.Start >= containerSpan.Start && childSpan.End <= containerSpan.End);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AppendSpan(ref SourceSpan destinationSpan, ref bool hasDestinationSpan, in SourceSpan spanToAppend)
|
||||
{
|
||||
if (spanToAppend.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasDestinationSpan)
|
||||
{
|
||||
destinationSpan = spanToAppend;
|
||||
hasDestinationSpan = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (spanToAppend.Start < destinationSpan.Start)
|
||||
{
|
||||
destinationSpan.Start = spanToAppend.Start;
|
||||
}
|
||||
|
||||
if (spanToAppend.End > destinationSpan.End)
|
||||
{
|
||||
destinationSpan.End = spanToAppend.End;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace Markdig.Syntax.Inlines;
|
||||
|
||||
@@ -264,6 +266,113 @@ public class ContainerInline : Inline, IEnumerable<Inline>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this container span is valid with respect to child inline spans.
|
||||
/// </summary>
|
||||
/// <param name="recursive">When <c>true</c>, validates descendant container inline spans recursively.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> when this container span contains all direct child spans and recursive checks (if enabled) succeed;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool HasValidSpan(bool recursive = false)
|
||||
{
|
||||
var child = FirstChild;
|
||||
while (child is not null)
|
||||
{
|
||||
if (!ContainsSpan(Span, child.Span))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recursive && child is ContainerInline childContainer && !childContainer.HasValidSpan(recursive: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
child = child.NextSibling;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates this container span from its child inline spans.
|
||||
/// </summary>
|
||||
/// <param name="recursive">When <c>true</c>, updates descendant container inline spans recursively before updating this container.</param>
|
||||
/// <param name="preserveSelfSpan">
|
||||
/// When <c>true</c>, preserves this container current span and only expands it to include children.
|
||||
/// When <c>false</c>, recomputes from children only.
|
||||
/// </param>
|
||||
/// <returns><c>true</c> when this container span changed; otherwise, <c>false</c>.</returns>
|
||||
public bool UpdateSpanFromChildren(bool recursive = false, bool preserveSelfSpan = true)
|
||||
{
|
||||
var updatedSpan = SourceSpan.Empty;
|
||||
bool hasUpdatedSpan = false;
|
||||
|
||||
if (preserveSelfSpan && !Span.IsEmpty)
|
||||
{
|
||||
updatedSpan = Span;
|
||||
hasUpdatedSpan = true;
|
||||
}
|
||||
|
||||
var child = FirstChild;
|
||||
while (child is not null)
|
||||
{
|
||||
if (recursive && child is ContainerInline childContainer)
|
||||
{
|
||||
childContainer.UpdateSpanFromChildren(recursive: true, preserveSelfSpan: preserveSelfSpan);
|
||||
}
|
||||
|
||||
AppendSpan(ref updatedSpan, ref hasUpdatedSpan, child.Span);
|
||||
child = child.NextSibling;
|
||||
}
|
||||
|
||||
if (!hasUpdatedSpan)
|
||||
{
|
||||
updatedSpan = SourceSpan.Empty;
|
||||
}
|
||||
|
||||
if (updatedSpan == Span)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span = updatedSpan;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool ContainsSpan(in SourceSpan containerSpan, in SourceSpan childSpan)
|
||||
{
|
||||
return childSpan.IsEmpty || (!containerSpan.IsEmpty && childSpan.Start >= containerSpan.Start && childSpan.End <= containerSpan.End);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AppendSpan(ref SourceSpan destinationSpan, ref bool hasDestinationSpan, in SourceSpan spanToAppend)
|
||||
{
|
||||
if (spanToAppend.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasDestinationSpan)
|
||||
{
|
||||
destinationSpan = spanToAppend;
|
||||
hasDestinationSpan = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (spanToAppend.Start < destinationSpan.Start)
|
||||
{
|
||||
destinationSpan.Start = spanToAppend.Start;
|
||||
}
|
||||
|
||||
if (spanToAppend.End > destinationSpan.End)
|
||||
{
|
||||
destinationSpan.End = spanToAppend.End;
|
||||
}
|
||||
}
|
||||
|
||||
public struct Enumerator : IEnumerator<Inline>
|
||||
{
|
||||
private readonly ContainerInline container;
|
||||
|
||||
Reference in New Issue
Block a user