# How to Extend Quarkdown with New AST Nodes and NodeVisitor Implementations

> Learn to extend Quarkdown by adding new AST nodes and NodeVisitor implementations in Kotlin. Enhance your Quarkdown renderer with custom components.

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

---

**You can extend Quarkdown by creating a Kotlin class that implements the `Node` interface, adding a `visit()` method to the `NodeVisitor<T>` interface, and implementing that method in concrete renderers such as the HTML renderer.**

Quarkdown is an extensible Markdown compiler built in Kotlin that processes documents through an abstract syntax tree (AST). To add custom syntax elements, you must extend the AST hierarchy with new node types and teach the rendering pipeline how to convert them into output formats. This guide walks through the exact steps required to extend Quarkdown with new AST nodes and NodeVisitor implementations, using concrete file paths and working code examples from the `iamgio/quarkdown` repository.


## Step 1: Define a New AST Node Class

Create a Kotlin class in `quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/` that implements the `Node` interface (or `NestableNode` / `SingleChildNestableNode` if it contains children).

For inline elements that wrap content, implement `Node` directly and store children in a property. Here is a concrete example that adds a `Highlight` node for marking text:

```kotlin
package com.quarkdown.core.ast.base.inline

import com.quarkdown.core.ast.Node
import com.quarkdown.core.visitor.node.NodeVisitor

/**
 * Inline node that emphasizes its children with a `<mark>` element.
 *
 * @property children the inline nodes that should be highlighted.
 */
class Highlight(
    override val children: List<Node>
) : Node {
    /** Accepts a visitor that knows how to render a Highlight node. */
    override fun <T> accept(visitor: NodeVisitor<T>) = visitor.visit(this)
}

```

The `accept` method forwards the call to a `visit(Highlight)` overload that you will add to the visitor in the next step. This follows the visitor pattern used throughout the codebase, as defined in [`quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/Node.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/Node.kt).


## Step 2: Extend the NodeVisitor Interface

Update [`quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt) to declare a new abstract method. Place it alongside existing inline node visitors for consistency:

```kotlin
// Inside interface NodeVisitor<T> (after the existing inline methods)
fun visit(node: Highlight): T

```

This change creates a contract that every concrete renderer must fulfill. The interface uses a generic type `T` so that different renderers can return their specific output types (e.g., `CharSequence` for HTML, `String` for plain text).


## Step 3: Implement Rendering in Concrete Visitors

Implement the `visit` method in every renderer you want to support. For HTML output, edit [`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/node/QuarkdownHtmlNodeRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/node/QuarkdownHtmlNodeRenderer.kt):

```kotlin
override fun visit(node: Highlight): CharSequence =
    // Wrap the highlighted content in a <mark> element.
    buildTag("mark") {
        // Render all children recursively.
        +node.children
    }

```

The HTML renderer provides utility methods like `buildTag` and `tagBuilder` to simplify tag generation. For plain-text support, add a similar implementation to the plaintext renderer in the `quarkdown-plaintext` module:

```kotlin
override fun visit(node: Highlight): String = node.children.joinToString("") { it.accept(this) }

```

If a renderer does not need special markup, you can delegate to the generic visitor to traverse children automatically.


## Step 4: Expose the Node via the Standard Library (Optional)

To make your custom syntax available in Quarkdown documents without manual AST construction, expose a standard-library function that instantiates your node. Create a file in `quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/`:

```kotlin
package com.quarkdown.stdlib

import com.quarkdown.core.ast.base.inline.Highlight
import com.quarkdown.core.ast.Node

/**
 * .highlight {text}
 *
 * Returns a Highlight node that marks its body.
 */
fun highlight(body: List<Node>): Highlight = Highlight(body)

```

After recompiling, writers can use the new syntax directly:

```markdown
.highlight {
    This text will be highlighted.
}

```

The parser automatically registers this function during the `FunctionCallExpansionStage`, creating `Highlight` instances during compilation.


## Step 5: Test Your Extension

Add integration tests in `quarkdown-test` to verify end-to-end behavior:

```kotlin
@Test
fun `highlight renders as mark`() {
    val source = ".highlight { hello }"
    val html = compileToHtml(source)
    assertTrue(html.contains("<mark>hello</mark>"))
}

```

Run `./gradlew :quarkdown-test:test` to validate that the new AST node converts correctly to HTML and other output formats.


## Summary

- **Create** a Kotlin class implementing `Node` (or `NestableNode`) in `quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/`.
- **Declare** a `visit(node: YourNode)` method in [`quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt).
- **Implement** that method in concrete renderers like [`QuarkdownHtmlNodeRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/QuarkdownHtmlNodeRenderer.kt) to generate output.
- **Expose** a standard-library function to make the syntax available in Quarkdown source files.
- **Test** the extension using the `quarkdown-test` module to ensure correct rendering.


## Frequently Asked Questions

### What is the difference between Node, NestableNode, and SingleChildNestableNode?

`Node` is the base interface for all AST elements. `NestableNode` extends `Node` for elements that contain multiple children, while `SingleChildNestableNode` is a convenience interface for nodes that wrap exactly one child. Choose the interface in [`quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/Node.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/Node.kt) that matches your element's structure.

### How do I add HTML rendering for a custom AST node?

Add an override of the `visit` method in [`quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/node/QuarkdownHtmlNodeRenderer.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/node/QuarkdownHtmlNodeRenderer.kt). Use the `buildTag` helper to generate HTML tags wrapping the node's children, as shown in the `Highlight` example above.

### Can I extend Quarkdown without modifying the core parser?

Yes. By defining new AST nodes and exposing them through the standard library functions, you avoid touching the lexer or parser. The `FunctionCallExpansionStage` handles the conversion from function calls to AST nodes automatically.

### Which file contains the NodeVisitor interface?

The `NodeVisitor<T>` interface is defined in [`quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt`](https://github.com/iamgio/quarkdown/blob/main/quarkdown-core/src/main/kotlin/com/quarkdown/core/visitor/node/NodeVisitor.kt). Every renderer in the ecosystem implements this interface to provide polymorphic rendering behavior.