How to Implement Custom Document Types in Quarkdown: Extending Beyond Plain, Paged, Slides, and Docs

You can implement custom document types in Quarkdown by adding a new enum constant to DocumentType.kt, exposing it via the .doctype function in the standard library, and wiring any rendering-specific logic such as page formats, numbering schemes, or third-party assets.

Quarkdown ships with four built-in document types—PLAIN, PAGED, SLIDES, and DOCS—that determine default page sizing, heading numbering, and which JavaScript libraries get injected during HTML rendering. Because DocumentType is defined as a Kotlin enum, adding support for custom types like magazine or book requires modifying the core source code and updating the pipeline that consumes this type.

Understanding the DocumentType Architecture

A document type in Quarkdown is an enum constant that lives in quarkdown-core/src/main/kotlin/com/quarkdown/core/document/DocumentType.kt. Each type influences four critical areas of the rendering pipeline:

  • Default page format – The defaultPageFormat parameter (a PageFormatInfo object) defines width, height, and orientation used by the HTML post-renderer and PDF generator.
  • Default numbering – The defaultNumbering parameter (a DocumentNumbering object) sets the initial formats for headings, figures, tables, and math expressions.
  • Function constraints – Native functions can be gated to specific types using the @OnlyForDocumentType and @NotForDocumentType annotations.
  • Third-party assets – The HTML post-renderer checks context.documentInfo.type to decide which CSS or JavaScript bundles to include, such as Reveal.js for slides.
// DocumentType enum signature (quarkdown-core/.../DocumentType.kt)
enum class DocumentType(
    val preferredOrientation: PageOrientation,
    val defaultPageFormat: PageFormatInfo? = null,
    val defaultNumbering: DocumentNumbering? = null,
)

Because this is a compiled enum, you cannot add types at runtime; you must extend the source and rebuild the compiler.

Step-by-Step Implementation Guide

1. Add the Enum Entry in DocumentType.kt

Create a new constant in quarkdown-core/src/main/kotlin/com/quarkdown/core/document/DocumentType.kt. Supply the preferredOrientation, and optionally defaultPageFormat and defaultNumbering. Insert the constant after the existing DOCS entry or alongside it.

MAGAZINE(
    preferredOrientation = PageOrientation.PORTRAIT,
    // 5.5" × 8.5" in points (1 inch = 72 points)
    defaultPageFormat = PageFormatInfo(
        pageWidth = 5.5 * 72,
        pageHeight = 8.5 * 72
    ),
    defaultNumbering = DocumentNumbering(
        headings = NumberingFormat.fromString("1."),
        figures = NumberingFormat.fromString("(1)"),
        tables = NumberingFormat.fromString("Table (1)"),
        math = NumberingFormat.fromString("(1)")
    )
)

2. Expose the Type via the .doctype Function

The user-facing .doctype function is implemented in quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Document.kt. Add a branch that maps a string identifier (e.g., "magazine") to your new enum constant.

@Name("doctype")
fun doctype(
    @Name("type") type: String,
    @Injected context: Context
): DocumentInfo {
    val docType = when (type.lowercase()) {
        "plain"    -> DocumentType.PLAIN
        "paged"    -> DocumentType.PAGED
        "slides"   -> DocumentType.SLIDES
        "docs"     -> DocumentType.DOCS
        "magazine" -> DocumentType.MAGAZINE  // new mapping
        else -> error("Unsupported document type: $type")
    }
    return DocumentInfo(type = docType)
}

3. Configure HTML Rendering and Third-Party Assets

If your type requires special styling or scripts, update quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/document/HtmlDocumentBuilder.kt to read context.documentInfo.type.defaultPageFormat. Additionally, create a subclass of ThirdPartyLibrary in quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/thirdparty/ThirdPartyLibrary.kt to conditionally inject assets.

class MagazineAssets : ThirdPartyLibrary {
    override fun isRequired(context: Context) =
        context.documentInfo.type == DocumentType.MAGAZINE

    override val scriptUrls = listOf("assets/magazine-layout.css")
}

4. Apply Function Constraints (Optional)

To restrict native functions to your new type, use the annotations defined in quarkdown-core/src/main/kotlin/com/quarkdown/core/function/reflect/annotation/. Follow the pattern used in quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Slides.kt.

@OnlyForDocumentType(DocumentType.MAGAZINE)
@Name("magazineheader")
fun magazineHeader(content: String) = // implementation

5. Add Tests and Documentation

Write unit tests in quarkdown-test/src/test/kotlin/com/quarkdown/test/DocumentTest.kt that compile a .qd file with .doctype {magazine} and assert that DocumentInfo.type equals DocumentType.MAGAZINE, that the default page format is applied, and that the correct third-party bundles are requested.

Complete Example: Adding a Magazine Document Type

Below is the full implementation of a MAGAZINE type that renders portrait documents at 5.5×8.5 inches with custom numbering.

Step A: Define the enum with custom sizing.

// quarkdown-core/src/main/kotlin/com/quarkdown/core/document/DocumentType.kt
enum class DocumentType(
    val preferredOrientation: PageOrientation,
    val defaultPageFormat: PageFormatInfo? = null,
    val defaultNumbering: DocumentNumbering? = null,
) {
    // ... existing types ...
    
    MAGAZINE(
        preferredOrientation = PageOrientation.PORTRAIT,
        defaultPageFormat = PageFormatInfo(
            pageWidth = 5.5 * 72,
            pageHeight = 8.5 * 72
        ),
        defaultNumbering = DocumentNumbering(
            headings = NumberingFormat.fromString("1."),
            figures = NumberingFormat.fromString("(1)"),
            tables = NumberingFormat.fromString("Table (1)"),
            math = NumberingFormat.fromString("(1)")
        )
    );
}

Step B: Map the user-facing string.

// quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Document.kt
"magazine" -> DocumentType.MAGAZINE

Step C: Register conditional assets.

// quarkdown-html/.../thirdparty/MagazineAssets.kt
class MagazineAssets : ThirdPartyLibrary {
    override fun isRequired(context: Context) =
        context.documentInfo.type == DocumentType.MAGAZINE
    override val styleUrls = listOf("magazine.css")
}

Step D: Use it in a Quarkdown file.

.doctype {magazine}
.title {The Quarkdown Quarterly}

.author {Jane Doe}
.date {2024}

## Feature Story

This content renders with a 5.5×8.5 inch page size and custom numbering.

Running quarkdown myfile.qd now produces output using the magazine specifications.

Summary

  • Add the enum constant in DocumentType.kt with orientation, page format, and numbering defaults.
  • Expose the type by adding a string mapping in the .doctype function inside Document.kt.
  • Wire rendering logic in HtmlDocumentBuilder.kt and ThirdPartyLibrary implementations to handle page sizes and assets.
  • Gate functions using @OnlyForDocumentType or @NotForDocumentType annotations when behavior should be type-specific.
  • Validate your changes with unit tests in DocumentTest.kt that assert correct type assignment and pipeline integration.

Frequently Asked Questions

Can I add a custom document type without modifying the Quarkdown source code?

No. Because DocumentType is implemented as a Kotlin enum in quarkdown-core/src/main/kotlin/com/quarkdown/core/document/DocumentType.kt, new types must be added at compile time. You cannot register custom types via plugins or configuration files; you must fork or extend the core repository and rebuild the compiler.

How do I restrict a native function to only work with my custom document type?

Annotate the function with @OnlyForDocumentType(DocumentType.YOUR_TYPE) as defined in quarkdown-core/src/main/kotlin/com/quarkdown/core/function/reflect/annotation/OnlyForDocumentType.kt. Conversely, use @NotForDocumentType to exclude your type from functions that should not support it. See quarkdown-stdlib/src/main/kotlin/com/quarkdown/stdlib/Slides.kt for a working example of slides-only functions.

What is the difference between a document type and a page format?

A document type is a high-level classification (e.g., SLIDES, MAGAZINE) that carries semantic meaning, default numbering rules, and asset requirements. A page format is a property of the document type that defines physical dimensions (width/height in points or millimeters) and orientation. The document type provides the default page format via its defaultPageFormat parameter, but the format itself is just the geometric specification used by the HTML and PDF renderers.

How do I add custom CSS or JavaScript for my new document type?

Create a class that implements ThirdPartyLibrary in the quarkdown-html module, overriding isRequired(context) to return true only when context.documentInfo.type == DocumentType.YOUR_TYPE. Return your asset URLs in scriptUrls or styleUrls. The HtmlDocumentBuilder will automatically include these resources when building the final HTML document for that type.

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 →