mirror of
https://github.com/xoofx/markdig.git
synced 2026-07-08 18:16:21 +00:00
Add AST mutation helpers for block and inline trees
This commit is contained in:
@@ -183,4 +183,55 @@ public class TestContainerBlocks
|
||||
|
||||
Assert.Throws<IndexOutOfRangeException>(() => container.CopyTo(destination, 3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanTransferChildrenToAnotherContainer()
|
||||
{
|
||||
var source = new MockContainerBlock();
|
||||
var first = new ParagraphBlock { Column = 1 };
|
||||
var second = new ParagraphBlock { Column = 2 };
|
||||
source.Add(first);
|
||||
source.Add(second);
|
||||
|
||||
var destination = new MockContainerBlock();
|
||||
var existing = new ParagraphBlock { Column = 0 };
|
||||
destination.Add(existing);
|
||||
|
||||
source.TransferChildrenTo(destination);
|
||||
|
||||
Assert.That(source.Count, Is.EqualTo(0));
|
||||
Assert.That(destination.Count, Is.EqualTo(3));
|
||||
Assert.That(destination[0], Is.SameAs(existing));
|
||||
Assert.That(destination[1], Is.SameAs(first));
|
||||
Assert.That(destination[2], Is.SameAs(second));
|
||||
Assert.That(first.Parent, Is.SameAs(destination));
|
||||
Assert.That(second.Parent, Is.SameAs(destination));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BlockCanBeRemovedAndReplaced()
|
||||
{
|
||||
var root = new MockContainerBlock();
|
||||
var toRemove = new ParagraphBlock();
|
||||
root.Add(toRemove);
|
||||
|
||||
toRemove.Remove();
|
||||
Assert.That(root.Count, Is.EqualTo(0));
|
||||
Assert.That(toRemove.Parent, Is.Null);
|
||||
|
||||
var sourceContainer = new MockContainerBlock();
|
||||
sourceContainer.Add(new ParagraphBlock { Column = 3 });
|
||||
sourceContainer.Add(new ParagraphBlock { Column = 4 });
|
||||
root.Add(sourceContainer);
|
||||
|
||||
var replacement = new MockContainerBlock();
|
||||
sourceContainer.ReplaceBy(replacement);
|
||||
|
||||
Assert.That(root[0], Is.SameAs(replacement));
|
||||
Assert.That(sourceContainer.Parent, Is.Null);
|
||||
Assert.That(sourceContainer.Count, Is.EqualTo(0));
|
||||
Assert.That(replacement.Count, Is.EqualTo(2));
|
||||
Assert.That(replacement[0].Column, Is.EqualTo(3));
|
||||
Assert.That(replacement[1].Column, Is.EqualTo(4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,4 +35,29 @@ public class TestContainerInlines
|
||||
var leafBlock2 = new MockLeafBlock();
|
||||
Assert.Throws<ArgumentException>(() => leafBlock2.Inline = two);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanTransferChildrenToAnotherContainer()
|
||||
{
|
||||
var source = new ContainerInline();
|
||||
var first = new LiteralInline("a");
|
||||
var second = new LiteralInline("b");
|
||||
source.AppendChild(first);
|
||||
source.AppendChild(second);
|
||||
|
||||
var destination = new ContainerInline();
|
||||
var existing = new LiteralInline("x");
|
||||
destination.AppendChild(existing);
|
||||
|
||||
source.TransferChildrenTo(destination);
|
||||
|
||||
Assert.That(source.FirstChild, Is.Null);
|
||||
Assert.That(source.LastChild, Is.Null);
|
||||
Assert.That(destination.FirstChild, Is.SameAs(existing));
|
||||
Assert.That(existing.NextSibling, Is.SameAs(first));
|
||||
Assert.That(first.NextSibling, Is.SameAs(second));
|
||||
Assert.That(second.NextSibling, Is.Null);
|
||||
Assert.That(first.Parent, Is.SameAs(destination));
|
||||
Assert.That(second.Parent, Is.SameAs(destination));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,20 +113,7 @@ public class AlertInlineParser : InlineParser
|
||||
attributes.AddClass("markdown-alert");
|
||||
attributes.AddClass(s_alertTypeClassCache.Get(alertType.AsSpan()));
|
||||
|
||||
// Replace the quote block with the alert block
|
||||
var parentQuoteBlock = quoteBlock.Parent!;
|
||||
var indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock);
|
||||
parentQuoteBlock[indexOfQuoteBlock] = alertBlock;
|
||||
|
||||
while (quoteBlock.Count > 0)
|
||||
{
|
||||
var block = quoteBlock[0];
|
||||
quoteBlock.RemoveAt(0);
|
||||
alertBlock.Add(block);
|
||||
}
|
||||
|
||||
// Workaround to replace the parent container
|
||||
// Experimental API, so we are keeping it internal for now until we are sure it's the way we want to go
|
||||
quoteBlock.ReplaceBy(alertBlock);
|
||||
processor.ReplaceParentContainer(quoteBlock, alertBlock);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -154,6 +154,56 @@ public abstract class Block : MarkdownObject, IBlock
|
||||
ThrowHelper.CheckDepthLimit(depth, useLargeLimit: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes this block from its parent container.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
Parent?.Remove(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces this block with <paramref name="replacement"/> in its parent container.
|
||||
/// </summary>
|
||||
/// <param name="replacement">The replacement block.</param>
|
||||
/// <param name="moveChildren">
|
||||
/// <c>true</c> to transfer children when both this block and <paramref name="replacement"/> are containers.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="replacement"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown if <paramref name="replacement"/> is already attached to a parent container.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown if this block has no parent container.</exception>
|
||||
/// <remarks>
|
||||
/// This method does not recompute spans or trivia. Callers are responsible for updating those values
|
||||
/// if a transform requires exact source information after replacement.
|
||||
/// </remarks>
|
||||
public void ReplaceBy(Block replacement, bool moveChildren = true)
|
||||
{
|
||||
if (replacement is null) ThrowHelper.ArgumentNullException(nameof(replacement));
|
||||
if (replacement.Parent is not null)
|
||||
{
|
||||
ThrowHelper.ArgumentException("Cannot replace with a block that is already attached to another container (replacement.Parent != null)", nameof(replacement));
|
||||
}
|
||||
|
||||
var parent = Parent;
|
||||
if (parent is null)
|
||||
{
|
||||
ThrowHelper.InvalidOperationException("Cannot replace a block that has no parent");
|
||||
}
|
||||
|
||||
int index = parent.IndexOf(this);
|
||||
if (index < 0)
|
||||
{
|
||||
ThrowHelper.InvalidOperationException("Cannot replace a block that is not attached to its parent container");
|
||||
}
|
||||
|
||||
parent[index] = replacement;
|
||||
|
||||
if (moveChildren && this is ContainerBlock sourceContainer && replacement is ContainerBlock destinationContainer)
|
||||
{
|
||||
sourceContainer.TransferChildrenTo(destinationContainer);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Block FindRootMostContainerParent(Block block)
|
||||
{
|
||||
while (true)
|
||||
@@ -186,4 +236,4 @@ public abstract class Block : MarkdownObject, IBlock
|
||||
public List<StringSlice>? LinesBefore;
|
||||
public List<StringSlice>? LinesAfter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,39 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfers all children from this container to <paramref name="destination"/>.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination container.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
|
||||
/// <remarks>
|
||||
/// Child order is preserved. This method does not recompute source or destination spans/trivia.
|
||||
/// </remarks>
|
||||
public void TransferChildrenTo(ContainerBlock destination)
|
||||
{
|
||||
if (destination is null)
|
||||
{
|
||||
ThrowHelper.ArgumentNullException(nameof(destination));
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, destination))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var children = _children;
|
||||
int count = Count;
|
||||
for (int i = 0; i < count && i < children.Length; i++)
|
||||
{
|
||||
var child = children[i].Block;
|
||||
child.Parent = null;
|
||||
children[i] = default;
|
||||
destination.Add(child);
|
||||
}
|
||||
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
public bool Contains(Block item)
|
||||
{
|
||||
return IndexOf(item) >= 0;
|
||||
@@ -320,4 +353,4 @@ public abstract class ContainerBlock : Block, IList<Block>, IReadOnlyList<Block>
|
||||
return _comparer.Compare(x.Block, y.Block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,30 @@ public class ContainerInline : Inline, IEnumerable<Inline>
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transfers all children from this container to <paramref name="destination"/>.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination container.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
|
||||
public void TransferChildrenTo(ContainerInline destination)
|
||||
{
|
||||
if (destination is null) ThrowHelper.ArgumentNullException(nameof(destination));
|
||||
|
||||
if (ReferenceEquals(this, destination))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var child = FirstChild;
|
||||
while (child != null)
|
||||
{
|
||||
var next = child.NextSibling;
|
||||
child.Remove();
|
||||
destination.AppendChild(child);
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves all the children of this container after the specified inline.
|
||||
/// </summary>
|
||||
@@ -293,4 +317,4 @@ public class ContainerInline : Inline, IEnumerable<Inline>
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user