How to Create Custom QuarkdownModule Definitions with Function Registrations in Quarkdown

Creating custom QuarkdownModule definitions involves writing Kotlin functions annotated with Quarkdown-specific decorators, grouping them via the moduleOf helper into a QuarkdownModule, and loading the module through a MultiFunctionLibraryLoader to generate a Library that the compiler registers.

Quarkdown, the Markdown-based content generation system from iamgio/quarkdown, exposes its standard library through modular Kotlin collections called QuarkdownModules. Understanding how to create custom QuarkdownModule definitions with function registrations allows developers to extend the language with domain-specific commands that integrate seamlessly with the built-in .function {arg} syntax.

Understanding the QuarkdownModule Architecture

At the core of Quarkdown’s extensibility lies the QuarkdownModule class located in quarkdown-core/src/main/kotlin/com/quarkdown/core/function/library/module/QuarkdownModule.kt. This class is essentially a mutable HashSet of ExportableFunction objects that can be created from Kotlin function references using the moduleOf helper (line 30), or composed from other modules using the overloaded + operator.

The MultiFunctionLibraryLoader in quarkdown-core/src/main/kotlin/com/quarkdown/core/function/library/loader/MultiFunctionLibraryLoader.kt serves as the bridge between modules and the compiler. It transforms one or more QuarkdownModule instances into a Library object that the compiler makes available to all Quarkdown scripts during resolution.

For global availability, the Stdlib object in quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Stdlib.kt aggregates all built-in modules. Adding custom modules here makes their functions available to every Quarkdown document without additional configuration.

Step-by-Step Guide to Creating a Custom Module

Define Native Functions with Annotations

Functions must use Quarkdown’s value types (such as NodeValue or OutputValue) and may include annotations like @Name to control the callable name and @LikelyBody to indicate which parameter should receive the brace-delimited body in Quarkdown syntax.

// src/main/kotlin/com/example/stdlib/Custom.kt
package com.example.stdlib

import com.quarkdown.core.function.value.NodeValue
import com.quarkdown.core.function.value.wrappedAsValue
import com.quarkdown.core.function.reflect.annotation.Name

/** Simple greeting function callable as .greet {World} */
@Name("greet")
fun greet(name: String): NodeValue =
    "Hello, $name!".wrappedAsValue()

Group Functions into a QuarkdownModule

Use the moduleOf factory function to collect your function references into a module. This creates the actual QuarkdownModule instance that tracks your ExportableFunction entries.

import com.quarkdown.core.function.library.module.QuarkdownModule
import com.quarkdown.core.function.library.module.moduleOf

/** Exporter for the custom stdlib module */
val Custom: QuarkdownModule = moduleOf(::greet)

Load the Module into a Library

Create a LibraryExporter implementation that uses MultiFunctionLibraryLoader to convert your module into a compiler-recognized Library. This approach keeps your module separate from the global stdlib, requiring explicit imports in Quarkdown documents.

import com.quarkdown.core.function.library.Library
import com.quarkdown.core.function.library.LibraryExporter
import com.quarkdown.core.function.library.loader.MultiFunctionLibraryLoader

object CustomLib : LibraryExporter {
    override val library: Library
        get() = MultiFunctionLibraryLoader(name = "custom")
            .load(Custom)
}

Register in the Global Stdlib (Optional)

To make your functions available globally without explicit library imports, extend the built-in Stdlib object by editing quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Stdlib.kt:

// Inside Stdlib.object
import com.example.stdlib.Custom

object Stdlib : LibraryExporter {
    override val library: Library
        get() = MultiFunctionLibraryLoader(name = "stdlib")
            .load(
                Document,
                Layout,
                Text,
                Custom          // <-- Include your module here
            )
}

After recompiling the project, commands like .greet {World} become available in any Quarkdown document.

Composing Multiple Modules

Because QuarkdownModule implements the + operator, you can combine multiple modules before loading. This is particularly useful for organizing large codebases into logical groups while exposing them through a single library.

val AllMyStuff = Custom + AnotherModule + Text

// Load the composed module
val library = MultiFunctionLibraryLoader(name = "combined")
    .load(AllMyStuff)

The resulting module behaves as a unified set of functions, with the MultiFunctionLibraryLoader registering all contained exportable functions into the same namespace.

Execution Flow and Value Mapping

When Quarkdown encounters a call like .greet {World}, the execution proceeds through four stages:

  1. Parsing: The parser creates a FunctionCallNode representing the invocation.
  2. Resolution: The node's Context queries the Library (built by MultiFunctionLibraryLoader) to resolve the name "greet" to your ExportableFunction.
  3. Execution: The function receives arguments as Kotlin types, computes the result, and returns a NodeValue.
  4. Mapping: A NodeOutputValueVisitor converts the returned value into an AST node consumable by rendering stages (HTML, PDF, or plain-text).

Summary

  • QuarkdownModule acts as a container for ExportableFunction instances, implemented as a HashSet in QuarkdownModule.kt.
  • Use moduleOf to instantiate modules from Kotlin function references, or the + operator to compose existing modules.
  • MultiFunctionLibraryLoader transforms modules into Library objects that the compiler registers during script compilation.
  • The Stdlib object in Stdlib.kt provides the central aggregation point for globally available modules.
  • Functions must return Quarkdown value types (e.g., NodeValue) and can use annotations like @Name to control their external callable names.

Frequently Asked Questions

What return types are required for functions registered in a QuarkdownModule?

Functions must return Quarkdown-specific value types such as NodeValue or OutputValue. The core library provides utility extensions like .wrappedAsValue() to convert standard Kotlin types (Strings, Ints) into the required wrapper types before returning.

How can I make a custom QuarkdownModule available without modifying the global Stdlib?

Create a standalone LibraryExporter implementation that loads your module via MultiFunctionLibraryLoader, then reference this library explicitly in your Quarkdown documents or CLI configuration. This keeps your custom functions isolated from the global namespace while still making them callable.

Can I combine multiple QuarkdownModules into a single library registration?

Yes. The QuarkdownModule class overloads the + operator to allow composition. You can merge modules using val Combined = ModuleA + ModuleB and pass the result to a single MultiFunctionLibraryLoader.load() call, registering all functions under one library name.

What annotations are available for controlling function behavior in Quarkdown?

The reflection system supports annotations like @Name (to specify the callable name) and @LikelyBody (to indicate which parameter receives the brace-delimited content in Quarkdown syntax). These annotations are processed when the MultiFunctionLibraryLoader converts Kotlin functions into ExportableFunction instances.

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 →