//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 08 Oct 2024 Matthew Giannini Creation
//
**
** Base class for all CommonMark AST nodes.
**
** The CommonMark AST is a tree of nodes where each node
** can have any number of children and one parent - except
** the root node which has no parent.
**
@Js
abstract class Node
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make()
{
}
** The parent node or null if this is the root of the AST
virtual Node? parent() { this.p }
** Used by sub-classes to set or clear this node's parent
protected virtual Void setParent(Node? p) { this.p = p }
** Private storage for parent
private Node? p
Node? next { private set }
Node? prev { private set }
Node? firstChild { private set }
Node? lastChild { private set }
SourceSpan[]? sourceSpans := null
{
get
{
&sourceSpans == null ? SourceSpan#.emptyList : &sourceSpans.toImmutable
}
private set
}
** Walk the AST using the given visitor. By default, we use reflection
** to call 'visitor.visit${this.typeof.name}'
virtual Void walk(Visitor visitor)
{
// This allows visitor sub-classes to have custom 'visit<CustomBlockorNode>()' methods
method := visitor.typeof.method("visit${this.typeof.name}", false)
if (method != null) method.callOn(visitor, [this])
else
{
// otherwise default back to calling generic visitors for custom nodes
if (this is CustomNode) visitor.visitCustomNode(this)
else if (this is CustomBlock) visitor.visitCustomBlock(this)
else throw ArgErr("no visit method found for ${this.typeof}")
}
}
//////////////////////////////////////////////////////////////////////////
// Tree modification
//////////////////////////////////////////////////////////////////////////
** Insert the child node as the last child node of this node.
This appendChild(Node child)
{
child.unlink
child.setParent(this)
if (this.lastChild != null)
{
this.lastChild.next = child
child.prev = this.lastChild
this.lastChild = child
}
else
{
this.firstChild = child
this.lastChild = child
}
return this
}
** Completely detach this node from the AST
Void unlink()
{
if (this.prev != null)
this.prev.next = this.next
else if (this.parent != null)
this.parent.firstChild = this.next
if (this.next != null)
this.next.prev = this.prev
else if (this.parent != null)
this.parent.lastChild = this.prev
this.p = null
this.next = null
this.prev = null
}
** Inserts the sibling node after this node
Void insertAfter(Node sibling)
{
sibling.unlink
sibling.next = this.next
if (sibling.next != null) sibling.next.prev = sibling
sibling.prev = this
this.next = sibling
sibling.p = this.parent
if (sibling.next == null) sibling.parent.lastChild = sibling
}
** Inserts the sibiling node before this node
Void insertBefore(Node sibling)
{
sibling.unlink
sibling.prev = this.prev
if (sibling.prev != null) sibling.prev.next = sibling
sibling.next = this
this.prev = sibling
sibling.p = this.parent
if (sibling.prev == null) sibling.parent.firstChild = sibling
}
** Add a source span to the end of the list. If it is null, this is a no-op
Void addSourceSpan(SourceSpan? sourceSpan)
{
if (sourceSpan == null) return
if (sourceSpans == null) sourceSpans = SourceSpan[,]
sourceSpans.add(sourceSpan)
}
** Replace the current source spans with the provided list
Void setSourceSpans(SourceSpan[] sourceSpans)
{
this.sourceSpans = sourceSpans.isEmpty ? null : sourceSpans.dup
}
//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////
** Get nodes between start (exclusive) and end (exclusive)
static Void eachBetween(Node start, Node end, |Node| f)
{
Node? node := start.next
while (node != null && node !== end)
{
// need to stash next in case it gets modified by f()
next := node.next
f(node)
node = next
}
}
** Get all the children of the given parent node
static Node[] children(Node parent)
{
acc := Node[,]
for (child := parent.firstChild; child != null; child = child.next) acc.add(child)
return acc
}
** Recursively try to find a node with the given type within the children
** of the specified node. Throw if node could not be found
static Node find(Node parent, Type nodeType)
{
tryFind(parent, nodeType) ?: throw Err("${nodeType} not found")
}
** Recursively try to find a node with the given type within the children of the
** specified node.
static Node? tryFind(Node parent, Type nodeType)
{
node := parent.firstChild
while (node != null)
{
next := node.next
if (node.typeof.fits(nodeType)) return node
result := tryFind(node, nodeType)
if (result != null) return result
node = next
}
return null
}
@NoDoc static Void eachChild(Node parent, |Node| f)
{
node := parent.firstChild
while (node != null)
{
saveNext := node.next
f(node)
eachChild(node, f)
node = saveNext
}
}
** Dump the node tree to given output stream
@NoDoc static Void dumpTree(Node node, OutStream out := Env.cur.out, Int indent := 0)
{
sp := " " * indent
out.writeChars("${sp}${node}\n")
child := node.firstChild
while (child != null)
{
dumpTree(child, out, indent + 2)
child = child.next
}
}
/* JAVA version
private void tree(Node node) { tree(node, 0); }
private void tree(Node node, int indent)
{
String sp = " ".repeat(indent);
System.out.println(sp + node);
Node child = node.getFirstChild();
while (child != null) {
tree(child, indent + 2);
child = child.getNext();
}
}
*/
//////////////////////////////////////////////////////////////////////////
// Obj
//////////////////////////////////////////////////////////////////////////
override Str toStr() { "${typeof.name}{${toStrAttributes}}" }
virtual protected Str toStrAttributes() { "" }
}
**************************************************************************
** HardLineBreak
**************************************************************************
** Hard line break
@Js
class HardLineBreak : Node { }
**************************************************************************
** SoftLineBreak
**************************************************************************
** Soft line break
@Js
class SoftLineBreak : Node { }
**************************************************************************
** Delimited
**************************************************************************
** A node that uses delimiters in the source form, e.g. '*bold*'
@Js
mixin Delimited
{
** Return the opening (beginning) delimiter, e.g. '*'
abstract Str openingDelim()
** Return the closing (ending) delimiter, e.g. '*'
abstract Str closingDelim()
}
**************************************************************************
** StrongEmphasis
**************************************************************************
** Strong emphasis
@Js
class StrongEmphasis : Node, Delimited
{
new make(Str delimiter) { this.delimiter = delimiter }
const Str delimiter
override Str openingDelim() { delimiter }
override Str closingDelim() { delimiter }
}
**************************************************************************
** Emphasis
**************************************************************************
** Emphasis
@Js
class Emphasis : Node, Delimited
{
new make(Str delimiter) { this.delimiter = delimiter }
const Str delimiter
override Str openingDelim() { delimiter }
override Str closingDelim() { delimiter }
}
**************************************************************************
** Text
**************************************************************************
** Text
@Js
class Text : Node
{
new make(Str literal) { this.literal = literal }
Str literal
override protected Str toStrAttributes() { "literal=${literal}" }
}
**************************************************************************
** Code
**************************************************************************
** Code
@Js
class Code : Node
{
new make(Str literal) { this.literal = literal }
const Str literal
}
**************************************************************************
** HtmlInline
**************************************************************************
** HTML inline
@Js
class HtmlInline : Node
{
new make (Str? literal := null) { this.literal = literal }
Str? literal
}
**************************************************************************
** CustomNode
**************************************************************************
** Custom node
@Js
class CustomNode : Node { }