How to Create Custom Native Functions in Quarkdown’s Stdlib Module
To create a custom native function in Quarkdown, write a Kotlin function that returns OutputValue<*> with the @Name annotation, export it via moduleOf in a QuarkdownModule, and register that module in Stdlib.kt using MultiFunctionLibraryLoader.
Quarkdown extends Markdown through a standard library (stdlib) of native Kotlin functions exposed to the document syntax. Unlike runtime functions created via the .function command, native functions are compiled into the library and available globally without parsing overhead. This guide walks through the three-step process to add your own native functions to iamgio/quarkdown.
Step 1 – Define the Native Kotlin Function
Create a Kotlin file under quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/. Each native function must return an OutputValue<*> (or a concrete subclass like StringValue or NumberValue) and use annotations to control how Quarkdown maps the function name and parameters.
package com.quarkdown.stdlib
import com.quarkdown.core.function.value.NumberValue
import com.quarkdown.core.function.value.OutputValue
import com.quarkdown.core.function.value.wrappedAsValue
import com.quarkdown.core.function.reflect.annotation.Name
import com.quarkdown.core.function.reflect.annotation.LikelyNamed
/**
* Returns the square of a numeric argument.
*
* Quarkdown usage:
* .square {5} → 25
* .square number:{7} → 49
*/
@Name("square") // Exposes the function as `.square` in Quarkdown
fun square(
@LikelyNamed number: NumberValue // Accepts a numeric value, can be passed positionally or by name
): OutputValue<*> =
// NumberValue already contains the Kotlin number; we simply multiply it.
NumberValue(number.value * number.value).wrappedAsValue()
Key implementation details:
@Namespecifies the callable name in Quarkdown (e.g.,.square). Without it, the Kotlin function name is used.@LikelyNamedindicates the parameter is usually passed by name (e.g.,number:{5}) but also works positionally..wrappedAsValue()converts the raw Kotlin result back into a Quarkdown-compatibleOutputValue.
Step 2 – Export the Function in a QuarkdownModule
Functions must be bundled into a QuarkdownModule to be discoverable by the stdlib loader. In the same file (or a separate one), create a module using moduleOf:
package com.quarkdown.stdlib
import com.quarkdown.core.function.library.module.QuarkdownModule
import com.quarkdown.core.function.library.module.moduleOf
/**
* Module that contains math-related native functions.
*/
val MyMath: QuarkdownModule = moduleOf(
::square // List every function you want to expose from this file
)
The moduleOf function accepts function references and returns a module object that the stdlib can import.
Step 3 – Register the Module with the Stdlib Loader
Open quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Stdlib.kt. Import your new module and add it to the MultiFunctionLibraryLoader chain:
// Inside quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Stdlib.kt
...
import com.quarkdown.stdlib.MyMath // ← NEW IMPORT
object Stdlib : LibraryExporter {
override val library: Library
get() =
MultiFunctionLibraryLoader(name = "stdlib")
.load(
Document,
Layout,
Text,
Primitives,
MiscElements,
Math,
Logical,
String,
Icon,
Emoji,
Collection,
Dictionary,
Optionality,
Logger,
Flow,
TableComputation,
Data,
Localization,
Library,
Slides,
Ecosystem,
Html,
Mermaid,
Reference,
Bibliography,
MyMath, // ← NEW MODULE
)...
After registering, rebuild the project to compile the new function into the stdlib:
./gradlew installDist
Once built, the function is available in any Quarkdown document:
.square {6} <!-- yields 36 -->
.square number:{8} <!-- yields 64 -->
Understanding the Runtime Alternative
The Flow module (quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Flow.kt, lines 93-104) demonstrates how the .function command creates user-defined functions at runtime using SimpleFunction. This approach wraps expressions dynamically but incurs parsing overhead. Native functions defined in Kotlin bypass this runtime machinery entirely, offering better performance and type safety by leveraging compile-time registration via the stdlib loader.
Summary
- Define the function in
quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/using Kotlin and the@Nameannotation, ensuring it returnsOutputValue<*>or a concrete value type. - Export the function in a
QuarkdownModuleusingmoduleOf(::yourFunction)to make it discoverable. - Register the module in
Stdlib.ktby adding it to theMultiFunctionLibraryLoaderlist and rebuilding with./gradlew installDist.
Frequently Asked Questions
How do I change the callable name of a native function in Quarkdown?
Use the @Name annotation on the Kotlin function. For example, @Name("pow") fun power(...) exposes the function as .pow in Quarkdown documents while keeping power as the internal Kotlin name.
What return types are valid for native functions?
Native functions must return OutputValue<*> or a concrete subclass such as StringValue, NumberValue, or BooleanValue. Use .wrappedAsValue() to convert raw Kotlin types into Quarkdown values that the interpreter can handle.
Where is the central stdlib loader defined?
The MultiFunctionLibraryLoader is instantiated in quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Stdlib.kt. This file aggregates all modules (Document, Layout, Flow, etc.) into the standard library that is shipped with the application.
Do I need to rebuild the project after adding a native function?
Yes. Unlike runtime functions created with .function, native functions are compiled Kotlin code. Run ./gradlew installDist to recompile the stdlib module and make the new function available to the Quarkdown interpreter.
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 →