# How NodeOutputValueVisitor Converts Function Outputs to AST Nodes

> Explore how NodeOutputValueVisitor transforms OutputValue subtypes into AST nodes. Discover the distinct handling for block and inline contexts in Quarkdown's iamgio/quarkdown repository.

- Repository: [Giorgio Garofalo/quarkdown](https://github.com/iamgio/quarkdown)
- Tags: internals
- Published: 2026-04-29

---

**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.

```kotlin
// 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 `OrderedList` where each element becomes a `ListItem` child (lines 41‑47 in [`NodeOutputValueVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/NodeOutputValueVisitor.kt))
- **UnorderedCollectionValue** → Creates an `UnorderedList` with `ListItem` children (lines 48‑52)
- **GeneralCollectionValue** → Wraps child nodes in a `MarkdownContent` container (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 `Node` unchanged without modification (line 77)
- **VoidValue** → Produces a `BlankNode` as a no‑op placeholder (line 79)

Both list handlers utilize a private helper that recursively converts collection elements into `ListItem` nodes:

```kotlin
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:

```kotlin
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:

1. **Nested OutputValue** – Recursively visits the value to handle nested structures.
2. **Iterable** – Wraps the collection in a `GeneralCollectionValue` so each item becomes a node.
3. **Raw Node** – Returns the object directly if it is already an AST fragment.
4. **Fallback** – Parses the string representation as Markdown via the abstract `parseRaw` method.

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`:

```kotlin
override fun visit(value: StringValue) = inline.visit(value).inParagraph()

```

The `inParagraph` helper (lines 25‑28 in [`BlockNodeOutputValueVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/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:

```kotlin
// 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>

```

```kotlin
// Lambda returning raw markdown is parsed dynamically
val mk = function { "**Bold** and _italic_." }
// DynamicValue triggers parseRaw → creates Paragraph with Strong and Emphasis nodes

```

```kotlin
// 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:

1. **Function Resolution** – `FunctionCallNodeExpander` executes the native or user function.
2. **Value Emission** – The function returns a concrete `OutputValue` subtype.
3. **Visitor Acquisition** – The expander requests a block or inline visitor from `NodeOutputValueVisitorFactory`.
4. **Recursive Conversion** – `visitor.visit(outputValue)` traverses the value hierarchy, building nodes.
5. **AST Insertion** – The resulting `Node` is grafted into the document tree at the call site.

## Summary

- **NodeOutputValueVisitor** uses the visitor pattern to map each `OutputValue` subtype to specific AST node constructors.
- **`NodeOutputValueVisitorFactory`** provides 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 `Paragraph` elements.
- 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.