How NodeOutputValueVisitor Converts Function Outputs to AST Nodes
NodeOutputValueVisitor implements the visitor pattern to recursively transform OutputValue subtypes into concrete AST nodes, with separate handling for block and inline contexts via BlockNodeOutputValueVisitor and InlineNodeOutputValueVisitor.
The conversion of function return values into document nodes is a critical stage in the iamgio/quarkdown compilation pipeline. When a native or user-defined function executes, its result must become a Node that fits into the abstract syntax tree. This transformation is handled by the NodeOutputValueVisitor hierarchy, which maps every OutputValue subtype to its corresponding AST representation.
Visitor Factory and Context Selection
When a function call is expanded by FunctionCallNodeExpander, the compiler requests a visitor from NodeOutputValueVisitorFactory that matches the call’s rendering style. The factory produces two distinct implementations based on whether the function was invoked in block or inline context.
// NodeOutputValueVisitorFactory.kt
override fun block(): OutputValueVisitor<Node> = BlockNodeOutputValueVisitor(context)
override fun inline(): OutputValueVisitor<Node> = InlineNodeOutputValueVisitor(context)
BlockNodeOutputValueVisitor delegates scalar value conversion to an InlineNodeOutputValueVisitor and wraps the result in a Paragraph node. InlineNodeOutputValueVisitor directly creates inline nodes such as Text or CheckBox. Both subclasses inherit the generic type-specific conversion logic from the abstract NodeOutputValueVisitor base class.
Core Conversion Logic in NodeOutputValueVisitor
The base visitor defines overloaded visit methods for each concrete OutputValue subtype, allowing Kotlin’s type system to dispatch to the correct handler automatically. The mapping between return types and AST nodes is as follows:
- OrderedCollectionValue → Creates an
OrderedListwhere each element becomes aListItemchild (lines 41‑47 inNodeOutputValueVisitor.kt) - UnorderedCollectionValue → Creates an
UnorderedListwithListItemchildren (lines 48‑52) - GeneralCollectionValue → Wraps child nodes in a
MarkdownContentcontainer (line 55) - PairValue → Treated as a two‑element ordered collection and processed recursively (lines 58‑59)
- DictionaryValue → Generates a table with Key and Value columns, with each cell rendered recursively (lines 61‑75)
- NodeValue → Returns the wrapped
Nodeunchanged without modification (line 77) - VoidValue → Produces a
BlankNodeas a no‑op placeholder (line 79)
Both list handlers utilize a private helper that recursively converts collection elements into ListItem nodes:
private fun createListItems(value: IterableValue<*>) = value.map {
ListItem(children = listOf(it.accept(this)))
}
This helper ensures nested structures are traversed depth‑first, with each element accepting the visitor to produce its corresponding node.
Handling Dynamic Values and Raw Markdown
A DynamicValue represents runtime values produced by stdlib .function calls or user‑defined lambdas. Its handling serves as a catch‑all mechanism that inspects the unwrapped runtime type:
when (value.unwrappedValue) {
is OutputValue<*> -> value.unwrappedValue.accept(this)
is Iterable<*> -> GeneralCollectionValue(value.unwrappedValue as Iterable<OutputValue<*>>).accept(this)
is Node -> value.unwrappedValue
else -> this.visit(parseRaw(value.unwrappedValue.toString(),
value.evaluationContext))
}
The logic follows three distinct paths:
- Nested OutputValue – Recursively visits the value to handle nested structures.
- Iterable – Wraps the collection in a
GeneralCollectionValueso each item becomes a node. - Raw Node – Returns the object directly if it is already an AST fragment.
- Fallback – Parses the string representation as Markdown via the abstract
parseRawmethod.
The parseRaw implementation differs between block and inline contexts. In BlockNodeOutputValueVisitor, it delegates to ValueFactory.blockMarkdown(...).asNodeValue() (lines 41‑46), while InlineNodeOutputValueVisitor uses ValueFactory.inlineMarkdown(...).asNodeValue() (lines 32‑36). This ensures raw markdown strings retain their contextual semantics.
Block-to-Inline Promotion Strategy
When a block‑style function returns a scalar value such as StringValue, the block visitor forwards conversion to its inline counterpart, then promotes the result to block level by wrapping it in a Paragraph:
override fun visit(value: StringValue) = inline.visit(value).inParagraph()
The inParagraph helper (lines 25‑28 in BlockNodeOutputValueVisitor.kt) creates a new Paragraph node containing the inline child, ensuring block context requirements are satisfied without duplicating scalar conversion logic.
Practical Conversion Examples
The following patterns demonstrate how different return types materialize in the AST:
// Ordered collection becomes a numbered list
fun myList() = OrderedCollectionValue(listOf(1, 2, 3))
// Produces: <ol><li>1</li><li>2</li><li>3</li></ol>
// Lambda returning raw markdown is parsed dynamically
val mk = function { "**Bold** and _italic_." }
// DynamicValue triggers parseRaw → creates Paragraph with Strong and Emphasis nodes
// Dictionary renders as a two-column table
fun meta() = DictionaryValue(mapOf("Author" to "Alice"))
// Produces: Table with headers "Key" and "Value", row: Author | Alice
Complete Execution Flow
The end‑to‑end transformation follows this pipeline:
- Function Resolution –
FunctionCallNodeExpanderexecutes the native or user function. - Value Emission – The function returns a concrete
OutputValuesubtype. - Visitor Acquisition – The expander requests a block or inline visitor from
NodeOutputValueVisitorFactory. - Recursive Conversion –
visitor.visit(outputValue)traverses the value hierarchy, building nodes. - AST Insertion – The resulting
Nodeis grafted into the document tree at the call site.
Summary
- NodeOutputValueVisitor uses the visitor pattern to map each
OutputValuesubtype to specific AST node constructors. NodeOutputValueVisitorFactoryprovides context‑aware visitors that differentiate between block and inline rendering modes.- DynamicValue handling accommodates raw markdown strings, iterables, and pre‑constructed nodes through recursive delegation.
- BlockNodeOutputValueVisitor automatically promotes inline nodes to block level by wrapping them in
Paragraphelements. - All conversion paths are centralized in
quarkdown-core/src/main/kotlin/com/quarkdown/core/function/value/output/node/, ensuring type safety and extensibility.
Frequently Asked Questions
What is the difference between BlockNodeOutputValueVisitor and InlineNodeOutputValueVisitor?
BlockNodeOutputValueVisitor handles top‑level block contexts by wrapping scalar results in Paragraph nodes and parsing raw markdown as block‑level content. InlineNodeOutputValueVisitor generates inline nodes directly (such as Text or CheckBox) and parses strings as inline markdown. Both inherit the core conversion logic from NodeOutputValueVisitor but differ in their treatment of leaf values.
How does Quarkdown handle functions that return raw markdown strings?
When a DynamicValue contains a non‑node, non‑iterable object, its toString() representation is passed to the abstract parseRaw method. The block visitor invokes ValueFactory.blockMarkdown() to produce a block‑level AST fragment, while the inline visitor uses ValueFactory.inlineMarkdown(), ensuring the generated nodes match the calling context.
What happens when a function returns a DictionaryValue?
The visitor maps DictionaryValue to a Table node with two columns labeled Key and Value. Each entry in the dictionary becomes a table row, with keys and values recursively processed through accept(this) to ensure consistent node generation, allowing nested structures within cells.
Why does NodeOutputValueVisitor use the visitor pattern?
The visitor pattern leverages Kotlin’s compile‑time type dispatch to route each OutputValue subtype to its specific conversion method without casting. This approach keeps conversion logic cohesive within NodeOutputValueVisitor while allowing subclasses to override specific behaviors, maintaining the open/closed principle for future value types.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →