diff --git a/src/Markdig.Tests/Markdig.Tests.csproj b/src/Markdig.Tests/Markdig.Tests.csproj index 23801f21d..c461cd769 100644 --- a/src/Markdig.Tests/Markdig.Tests.csproj +++ b/src/Markdig.Tests/Markdig.Tests.csproj @@ -58,6 +58,7 @@ + diff --git a/src/Markdig.Tests/TestDescendantsOrder.cs b/src/Markdig.Tests/TestDescendantsOrder.cs new file mode 100644 index 000000000..643bb903e --- /dev/null +++ b/src/Markdig.Tests/TestDescendantsOrder.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using System.Linq; +using System.Collections.Generic; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestDescendantsOrder + { + [Test] + public void TestSchemas() + { + foreach (var markdown in TestParser.SpecsMarkdown) + { + AssertSameDescendantsOrder(markdown); + } + } + + private void AssertSameDescendantsOrder(string markdown) + { + var syntaxTree = Markdown.Parse(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); + + var descendants_legacy = Descendants_Legacy(syntaxTree).ToList(); + var descendants_new = syntaxTree.Descendants().ToList(); + + Assert.AreEqual(descendants_legacy.Count, descendants_new.Count); + + for (int i = 0; i < descendants_legacy.Count; i++) + { + Assert.AreSame(descendants_legacy[i], descendants_new[i]); + } + } + + private static IEnumerable Descendants_Legacy(MarkdownObject markdownObject) + { + // TODO: implement a recursiveless method + + var block = markdownObject as ContainerBlock; + if (block != null) + { + foreach (var subBlock in block) + { + yield return subBlock; + + foreach (var sub in Descendants_Legacy(subBlock)) + { + yield return sub; + } + + // Visit leaf block that have inlines + var leafBlock = subBlock as LeafBlock; + if (leafBlock?.Inline != null) + { + foreach (var subInline in Descendants_Legacy(leafBlock.Inline)) + { + yield return subInline; + } + } + } + } + else + { + var inline = markdownObject as ContainerInline; + if (inline != null) + { + var child = inline.FirstChild; + while (child != null) + { + var next = child.NextSibling; + yield return child; + + foreach (var sub in Descendants_Legacy(child)) + { + yield return sub; + } + + child = next; + } + } + } + } + } +} diff --git a/src/Markdig.Tests/TestParser.cs b/src/Markdig.Tests/TestParser.cs index dbe24d405..237bd005f 100644 --- a/src/Markdig.Tests/TestParser.cs +++ b/src/Markdig.Tests/TestParser.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.RegularExpressions; using Markdig.Extensions.JiraLinks; @@ -155,5 +156,20 @@ private static string Compact(string html) html = html.Normalize(NormalizationForm.FormKD); return html; } + + public static readonly string[] SpecsMarkdown; + static TestParser() + { + string assemblyDir = Path.GetDirectoryName(typeof(TestParser).Assembly.Location); + string specsDir = Path.GetFullPath(Path.Combine(assemblyDir, "../../Specs")); + + var files = Directory.GetFiles(specsDir).Where(file => file.EndsWith(".md", StringComparison.Ordinal)).ToList(); + SpecsMarkdown = new string[files.Count]; + + for (int i = 0; i < files.Count; i++) + { + SpecsMarkdown[i] = File.ReadAllText(files[i]); + } + } } } \ No newline at end of file diff --git a/src/Markdig/Syntax/Inlines/ContainerInline.cs b/src/Markdig/Syntax/Inlines/ContainerInline.cs index f7535a016..cb954a16e 100644 --- a/src/Markdig/Syntax/Inlines/ContainerInline.cs +++ b/src/Markdig/Syntax/Inlines/ContainerInline.cs @@ -94,25 +94,36 @@ public bool ContainsChild(Inline childToFind) /// An enumeration of T public IEnumerable FindDescendants() where T : Inline { - var child = FirstChild; - while (child != null) - { - var next = child.NextSibling; + // Fast-path an empty container to avoid allocating a Stack + if (LastChild == null) yield break; - if (child is T) - { - yield return (T)child; - } + Stack stack = new Stack(); - if (child is ContainerInline) - { - foreach (var subChild in ((ContainerInline) child).FindDescendants()) - { - yield return subChild; - } - } - - child = next; + var child = LastChild; + while (child != null) + { + stack.Push(child); + child = child.PreviousSibling; + } + + while (stack.Count > 0) + { + child = stack.Pop(); + + if (child is T childT) + { + yield return childT; + } + + if (child is ContainerInline containerInline) + { + child = containerInline.LastChild; + while (child != null) + { + stack.Push(child); + child = child.PreviousSibling; + } + } } } diff --git a/src/Markdig/Syntax/MarkdownObjectExtensions.cs b/src/Markdig/Syntax/MarkdownObjectExtensions.cs index c7fac83fb..c47b36882 100644 --- a/src/Markdig/Syntax/MarkdownObjectExtensions.cs +++ b/src/Markdig/Syntax/MarkdownObjectExtensions.cs @@ -1,131 +1,119 @@ -// 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 System.Collections.Generic; -using Markdig.Syntax.Inlines; - -namespace Markdig.Syntax -{ - /// - /// Extensions for visiting or - /// - public static class MarkdownObjectExtensions - { - /// - /// Iterates over the descendant elements for the specified markdown element, including and . - /// - /// The markdown object. - /// An iteration over the descendant elements - public static IEnumerable Descendants(this MarkdownObject markdownObject) - { - // TODO: implement a recursiveless method - - var block = markdownObject as ContainerBlock; - if (block != null) - { - foreach (var subBlock in block) - { - yield return subBlock; - - foreach (var sub in subBlock.Descendants()) - { - yield return sub; - } - - // Visit leaf block that have inlines - var leafBlock = subBlock as LeafBlock; - if (leafBlock?.Inline != null) - { - foreach (var subInline in Descendants(leafBlock.Inline)) - { - yield return subInline; - } - } - } - } - else - { - var inline = markdownObject as ContainerInline; - if (inline != null) - { - var child = inline.FirstChild; - while (child != null) - { - var next = child.NextSibling; - yield return child; - - foreach (var sub in child.Descendants()) - { - yield return sub; - } - - child = next; - } - } - } - } - - /// - /// Iterates over the descendant elements for the specified markdown element and filters by the type {T}. - /// - /// Type to use for filtering the descendants - /// The inline markdown object. - /// - /// An iteration over the descendant elements - /// - public static IEnumerable Descendants(this ContainerInline inline) where T : Inline - { - var child = inline.FirstChild; - while (child != null) - { - var next = child.NextSibling; - var childT = child as T; - if (childT != null) - { - yield return childT; - } - - var subContainer = child as ContainerInline; - if (subContainer != null) - { - foreach (var sub in subContainer.Descendants()) - { - yield return sub; - } - } - - child = next; - } - } - - /// - /// Iterates over the descendant elements for the specified markdown element and filters by the type {T}. - /// - /// Type to use for filtering the descendants - /// The markdown object. - /// - /// An iteration over the descendant elements - /// - public static IEnumerable Descendants(this ContainerBlock block) where T : Block - { - foreach (var subBlock in block) - { - var subBlockT = subBlock as T; - if (subBlockT != null) - { - yield return subBlockT; - } - - var subBlockContainer = subBlock as ContainerBlock; - if (subBlockContainer != null) - { - foreach (var sub in subBlockContainer.Descendants()) - { - yield return sub; - } - } - } - } - } +// 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 System.Collections.Generic; +using Markdig.Syntax.Inlines; + +namespace Markdig.Syntax +{ + /// + /// Extensions for visiting or + /// + public static class MarkdownObjectExtensions + { + /// + /// Iterates over the descendant elements for the specified markdown element, including and . + /// The descendant elements are returned in DFS-like order. + /// + /// The markdown object. + /// An iteration over the descendant elements + public static IEnumerable Descendants(this MarkdownObject markdownObject) + { + // Fast-path an object with no children to avoid allocating Stack objects + if (!(markdownObject is ContainerBlock) && !(markdownObject is ContainerInline)) yield break; + + // TODO: A single Stack<(MarkdownObject block, bool push)> when ValueTuples are available + Stack stack = new Stack(); + Stack pushStack = new Stack(); + + stack.Push(markdownObject); + pushStack.Push(false); + + while (stack.Count > 0) + { + var block = stack.Pop(); + if (pushStack.Pop()) yield return block; + if (block is ContainerBlock containerBlock) + { + int subBlockIndex = containerBlock.Count; + while (subBlockIndex-- > 0) + { + var subBlock = containerBlock[subBlockIndex]; + if (subBlock is LeafBlock leafBlock) + { + if (leafBlock.Inline != null) + { + stack.Push(leafBlock.Inline); + pushStack.Push(false); + } + } + stack.Push(subBlock); + pushStack.Push(true); + } + } + else if (block is ContainerInline containerInline) + { + var child = containerInline.LastChild; + while (child != null) + { + stack.Push(child); + pushStack.Push(true); + child = child.PreviousSibling; + } + } + } + } + + /// + /// Iterates over the descendant elements for the specified markdown element and filters by the type {T}. + /// + /// Type to use for filtering the descendants + /// The inline markdown object. + /// + /// An iteration over the descendant elements + /// + public static IEnumerable Descendants(this ContainerInline inline) where T : Inline + => inline.FindDescendants(); + + /// + /// Iterates over the descendant elements for the specified markdown element and filters by the type {T}. + /// + /// Type to use for filtering the descendants + /// The markdown object. + /// + /// An iteration over the descendant elements + /// + public static IEnumerable Descendants(this ContainerBlock block) where T : Block + { + // Fast-path an empty container to avoid allocating a Stack + if (block.Count == 0) yield break; + + Stack stack = new Stack(); + + int childrenCount = block.Count; + while (childrenCount-- > 0) + { + stack.Push(block[childrenCount]); + } + + while (stack.Count > 0) + { + var subBlock = stack.Pop(); + if (subBlock is T subBlockT) + { + yield return subBlockT; + } + + if (subBlock is ContainerBlock subBlockContainer) + { + childrenCount = subBlockContainer.Count; + while (childrenCount-- > 0) + { + stack.Push(subBlockContainer[childrenCount]); + } + } + } + } + } } \ No newline at end of file