How to Extend Quarkdown with New AST Nodes and NodeVisitor Implementations

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:

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.

Step 2: Extend the NodeVisitor Interface

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

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

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:

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

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:

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

@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

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 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. 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. Every renderer in the ecosystem implements this interface to provide polymorphic rendering behavior.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →