How to Implement Custom Post‑Renderers for HTML Output in Quarkdown
To implement custom post‑renderers for HTML output in Quarkdown, create a class that implements the PostRenderer interface (or delegates to HtmlPostRenderer), supply custom PostRendererResource implementations for additional assets, and register the renderer via a RendererFactory extension.
Quarkdown separates rendering (converting the AST to plain HTML) from post‑rendering (wrapping that HTML in a complete document structure and aggregating auxiliary resources). This architecture, defined in the iamgio/quarkdown repository, allows you to customize the final HTML output without modifying the core rendering logic. By extending the post‑renderer pipeline, you can inject custom meta tags, add footer fragments, bundle additional scripts, or modify the document wrapper while retaining Quarkdown’s built‑in media handling and theming capabilities.
Understanding the Post‑Rendering Architecture
The post‑rendering stage is governed by the PostRenderer interface located in quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/PostRenderer.kt. This contract defines three core operations: wrap() (envelops rendered HTML in a full document), generateResources() (produces auxiliary output files), and wrapResources() (groups resources for final output).
Quarkdown provides two primary implementations in the quarkdown-html module:
HtmlOnlyPostRenderer(quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt): A minimal wrapper that generates only the mainindex.htmlwithout extra assets.HtmlPostRenderer(quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt): The full‑featured default that delegates document wrapping toHtmlOnlyPostRendererand aggregates a configurable set ofPostRendererResourceobjects (themes, scripts, static assets).
The PostRendererResource interface (quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/resources/PostRendererResource.kt) defines pluggable resources that know how to copy files or generate content. These resources are automatically collected by HtmlPostRenderer during the generateResources() phase.
Finally, the RenderingComponents class (quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/RenderingComponents.kt) bundles the node renderer (AST → HTML) with the post‑renderer, created by the factory extension in HtmlRendererExtension.kt.
Creating a Custom Post‑Renderer Resource
To inject additional files into the output directory—such as a custom footer HTML snippet—implement the PostRendererResource sealed interface. Each resource receives a mutable Set<OutputResource> collector and the rendered HTML content, allowing conditional generation based on the document.
Here is an implementation that generates a static footer file:
// src/main/kotlin/com/example/quarkdown/CustomFooterResource.kt
package com.example.quarkdown
import com.quarkdown.rendering.html.post.resources.PostRendererResource
import com.quarkdown.core.pipeline.output.OutputResource
import com.quarkdown.core.pipeline.output.TextOutputArtifact
import com.quarkdown.core.pipeline.output.ArtifactType
class CustomFooterResource : PostRendererResource {
// Include this resource even in preview mode
override val runsInPreviewMode: Boolean = true
override fun includeTo(
collector: MutableSet<OutputResource>,
rendered: CharSequence
) {
val footer = """
<footer class="qd-custom-footer">
<p>Generated by a custom Quarkdown post‑renderer.</p>
</footer>
""".trimIndent()
collector += TextOutputArtifact(
name = "footer.html",
content = footer,
type = ArtifactType.HTML
)
}
}
This resource creates footer.html in the output directory alongside Quarkdown’s standard assets.
Implementing the Custom Post‑Renderer
Rather than implementing PostRenderer from scratch, the recommended pattern is delegation. Create a class that delegates to HtmlPostRenderer but overrides the resourcesProvider lambda to include your custom resources.
The following example wraps the standard behavior and adds CustomFooterResource to the resource set:
// src/main/kotlin/com/example/quarkdown/CustomHtmlPostRenderer.kt
package com.example.quarkdown
import com.quarkdown.core.context.Context
import com.quarkdown.rendering.html.post.HtmlOnlyPostRenderer
import com.quarkdown.rendering.html.post.HtmlPostRenderer
import com.quarkdown.core.rendering.PostRenderer
import com.quarkdown.rendering.html.post.resources.PostRendererResource
class CustomHtmlPostRenderer(
context: Context,
resourcesLayout: com.quarkdown.installlayout.InstallLayout.Html? = null,
relativePathToRoot: String = "."
) : PostRenderer by HtmlPostRenderer(
context = context,
resourcesLayout = resourcesLayout,
relativePathToRoot = relativePathToRoot,
base = HtmlOnlyPostRenderer(context, relativePathToRoot = relativePathToRoot),
resourcesProvider = {
// Retain default resources and append custom ones
HtmlPostRenderer(context, resourcesLayout, relativePathToRoot).resourcesProvider()
.plus(CustomFooterResource())
}
)
Key implementation details:
- The
baseparameter receivesHtmlOnlyPostRendererto handle standard document wrapping. - The
resourcesProviderlambda returns the union of default resources and your custom implementations. - By using Kotlin delegation (
by), all otherPostRenderermethods (wrap,generateResources,wrapResources) retain their default behavior.
Wiring the Renderer into the Pipeline
To activate your custom post‑renderer, extend RendererFactory with a new factory function that returns RenderingComponents configured with your implementation. This integrates your renderer into the Quarkdown pipeline while preserving the standard AST-to-HTML node renderer.
// src/main/kotlin/com/example/quarkdown/Extension.kt
package com.example.quarkdown
import com.quarkdown.core.context.Context
import com.quarkdown.core.rendering.RenderingComponents
import com.quarkdown.core.flavor.RendererFactory
import com.quarkdown.rendering.html.HtmlExportOptions
/**
* Registers a custom HTML renderer using [CustomHtmlPostRenderer].
*/
fun RendererFactory.customHtml(
context: Context,
options: HtmlExportOptions = HtmlExportOptions()
) = RenderingComponents(
// Standard AST visitor for HTML generation
nodeRenderer = accept(com.quarkdown.rendering.html.HtmlRendererFactoryVisitor(context)),
// Custom post‑renderer injection
postRenderer = CustomHtmlPostRenderer(context, options.resourcesLayout)
)
If you are building a CLI tool, override the default renderer selection in your pipeline initialization:
// src/main/kotlin/com/example/quarkdown/CLIWrapper.kt
package com.example.quarkdown
import com.quarkdown.cli.PipelineInitialization
import com.quarkdown.core.flavor.RendererFactory
fun main(args: Array<String>) {
PipelineInitialization(
rendererProvider = { factory: RendererFactory, ctx -> factory.customHtml(ctx) }
).run(args)
}
Execute your custom pipeline with Gradle:
./gradlew :quarkdown-cli:installDist
./quarkdown-cli/build/install/quarkdown-cli/bin/quarkdown-cli \
--renderer customHtml \
-i document.qd -o output/
The generated output/index.html will contain the standard Quarkdown content plus the resources defined in your custom post‑renderer.
Summary
- Post‑rendering is pluggable via the
PostRendererinterface inquarkdown-core, allowing customization of the final HTML document and its auxiliary files. - Delegate to existing implementations like
HtmlOnlyPostRendererandHtmlPostRendererto retain standard document structures while extending functionality. - Resources are modular—implement
PostRendererResourceto generate or copy additional files (scripts, styles, HTML fragments) into the output directory. - Registration occurs through
RendererFactoryextensions, which produceRenderingComponentspairing your custom post‑renderer with Quarkdown’s standard node renderer.
Frequently Asked Questions
How do I modify the HTML <head> section when using a custom post‑renderer?
Extend HtmlOnlyPostRenderer and override the wrap() method. This method receives the rendered body HTML and returns the complete document string, allowing you to inject custom <meta> tags, <link> elements, or CDN scripts before returning the final HTML. Delegate to super.wrap() first to obtain the base document, then use string manipulation or a template engine to insert your elements.
Can I conditionally include resources based on document content?
Yes. The PostRendererResource.includeTo() method receives the fully rendered HTML as a CharSequence parameter. You can parse this content—checking for specific CSS classes, data attributes, or Quarkdown-specific markers—and conditionally add resources to the collector set. For example, you might only include a math library script if the rendered content contains LaTeX delimiters.
What is the difference between HtmlOnlyPostRenderer and HtmlPostRenderer?
HtmlOnlyPostRenderer (quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlOnlyPostRenderer.kt) is a minimal implementation that only wraps the rendered HTML in a basic <html> structure and outputs a single index.html file. HtmlPostRenderer (quarkdown-html/src/main/kotlin/com/quarkdown/rendering/html/post/HtmlPostRenderer.kt) extends this functionality by managing a collection of PostRendererResource objects, enabling automatic generation of theme files, script bundles, media directories, and other auxiliary assets alongside the main document.
How do I prevent my custom resource from being included in preview mode?
Set the runsInPreviewMode property to false in your PostRendererResource implementation. When this property is false, Quarkdown’s pipeline excludes the resource during live preview operations, reducing overhead while developing. The resource will still be generated during full export builds.
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 →