Replace a markdown node with MarkdownAST/AbstractTrees

Right, I suppose I knew that was the correct answer when I posted: you can’t safely iterate and mutate at the same time :wink:

At least on paper/pseudocode, building a mutated copy of the original AST is relatively straightforward. What I was getting hung up on – why I didn’t start with that in the first place – was how to “copy” a node without its children. Looking at the source code of copy_tree really clarified how to do that. So I think using the recursive rewrite_links function below (modeled after copy_tree) is a clean way to implement this:

using Pkg
Pkg.activate(temp=true)
Pkg.add(url="https://github.com/JuliaDocs/Documenter.jl", rev="master")
Pkg.add("MarkdownAST")
Pkg.add("AbstractTrees")

import Documenter
import MarkdownAST
import Markdown
import AbstractTrees

MD_MINIMAL = raw"""
Text with [rabiner_tutorial_1989](@cite).
"""

function parse_md_string(mdsrc)
    mdpage = Markdown.parse(mdsrc)
    return convert(MarkdownAST.Node, mdpage)
end

function rewrite_links(node::MarkdownAST.Node{M}) where M
    new_node = MarkdownAST.Node{M}(node.element, deepcopy(node.meta))
    for child in node.children
        if child.element == MarkdownAST.Link("@cite", "")
            new_text = "[Citation](https://github.com/JuliaDocs/DocumenterCitations.jl) for `key`"
            # In reality, new_text would be derived from the link node. Might
            # contain multiple links and arbitrary inline formatting.
            println("-> Doing transform to new text=$new_text")
            expanded = Documenter.mdparse(new_text; mode=:span)
            append!(new_node.children, expanded)
        else
            push!(new_node.children, rewrite_links(child))
        end
    end
    return new_node
end

mdast = parse_md_string(MD_MINIMAL)
println("====== IN =======")
println("AS AST:")
@show mdast
println("AS TEXT:")
print(string(convert(Markdown.MD, mdast)))
println("=== TRANSFORM ===")
mdast = rewrite_links(mdast)
println("====== OUT =======")
println("AS AST:")
@show mdast
println("AS TEXT:")
print(string(convert(Markdown.MD, mdast)))
println("====== END =======")

It doesn’t technically mutate the original mdast, but I can always replace the original root’s children with the new root’s children at the end.