Configuration
Configure parsers, renderers, source spans, and extensions.
A Markdown engine
Kotlin Multiplatform
CommonMark parser & renderer for Kotlin
A CommonMark parser and renderer written in Kotlin. Keep the syntax, inspect the tree, and add only the extensions your application needs.
[1] The core stays CommonMark-shaped; extensions are opt-in.
## Field notes
A tree you can **walk**.Document
├─ Heading(level = 2)
│ └─ Text("Field notes")
└─ Paragraph
├─ Text("A tree you can ")
├─ StrongEmphasis
│ └─ Text("walk")
└─ Text(".")<h2>Field notes</h2>
<p>A tree you can <strong>walk</strong>.</p>Getting started
Add Kotmark to your Kotlin project.
For Kotlin Multiplatform, put the dependency in commonMain.dependencies. For a JVM project, use the module’s dependencies block. JVM applications require Java 17 or newer.
implementation("io.heapy.kotmark:kotmark:0.1.0")The default parser and HTML renderer are plain Kotlin factories. Start with CommonMark; add an extension to both sides only when its syntax and output are wanted.
import io.heapy.kotmark.parser.parser
import io.heapy.kotmark.renderer.html.htmlRenderer
fun renderMarkdown(markdown: String): String {
val document = parser().parse(markdown)
return htmlRenderer().render(document)
}Specification support
Kotmark implements CommonMark 0.31.2. The specification tests cover the complete current example set and the corpus bundled with commonmark-java.
GFM tables, strikethrough, task lists, and alerts are available as extensions. Kotmark supports selected GFM features; it does not claim full GFM conformance. Bare URL detection follows autolink-java rules, with documented differences from GFM.
Extension index / opt-in syntax
Add the extension module you need and register it with the parser and each renderer you use. The autolink module also provides a standalone URL and email extractor.
| Module | Adds | Dialect note |
|---|---|---|
| autolink | Bare URL and email links | Uses upstream autolink-java rules |
| emoji | Unicode emoji from shortcodes | GitHub gemoji aliases and custom mappings |
| footnotes | Footnote references and definitions | Opt-in syntax |
| gfm-alerts | GitHub-style alert blocks | Supported alert fixtures; not full GFM |
| gfm-strikethrough | Struck-through text | Configurable delimiter rules |
| gfm-tables | Pipe tables | Supported GFM table grammar |
| heading-anchor | Stable heading IDs | Customizable ID generation |
| highlight | ==highlighted text== | Own inline extension syntax |
| image-attributes | Extra image attributes | Parser and HTML renderer hooks |
| ins | Inserted text | Opt-in inline syntax |
| math | Inline and fenced formula source | Opaque content; no TeX engine |
| task-list-items | Checkbox list items | Opt-in list rendering |
| yaml-front-matter | YAML document metadata | Separate front-matter node |
Every module has its own page with syntax, options, and a compiled example: start from the extension index. Registration is covered in the configuration guide.
Extension workshop / custom blocks
A custom block has two jobs: recognize its source lines and put a node in the tree. The example uses :::memo through :::, stores the body as literal text, and escapes it on HTML output.
CustomBlock; expose the content your renderers need.AbstractBlockParser and a factory to recognize the opener, continue, and finish at the closer.Parser.ParserExtension.HtmlWriter.text.import io.heapy.kotmark.Extension
import io.heapy.kotmark.extensions
import io.heapy.kotmark.node.CustomBlock
import io.heapy.kotmark.parser.Parser
import io.heapy.kotmark.parser.SourceLine
import io.heapy.kotmark.parser.block.AbstractBlockParser
import io.heapy.kotmark.parser.block.AbstractBlockParserFactory
import io.heapy.kotmark.parser.block.BlockContinue
import io.heapy.kotmark.parser.block.BlockStart
import io.heapy.kotmark.parser.block.MatchedBlockParser
import io.heapy.kotmark.parser.block.ParserState
import io.heapy.kotmark.parser.parser
import io.heapy.kotmark.renderer.html.HtmlRenderer
import io.heapy.kotmark.renderer.html.HtmlRenderer.HtmlRendererExtension
import io.heapy.kotmark.renderer.html.htmlRenderer
import io.heapy.kotmark.renderer.html.nodeRenderer
class MemoBlock : CustomBlock() {
var literal: String = ""
}
private class MemoBlockParser : AbstractBlockParser() {
override val block = MemoBlock()
private val content = StringBuilder()
private var openingLine = true
override fun tryContinue(parserState: ParserState): BlockContinue {
val line = parserState.line.content
.subSequence(parserState.nextNonSpaceIndex, parserState.line.content.length)
.toString()
.trimEnd(' ', '\t')
return if (line == ":::") {
BlockContinue.Finished
} else {
BlockContinue.AtIndex(parserState.nextNonSpaceIndex)
}
}
override fun addLine(line: SourceLine) {
if (openingLine) {
openingLine = false
} else {
content.append(line.content).append('\n')
}
}
override fun closeBlock() {
block.literal = content.toString()
}
}
private class MemoBlockParserFactory : AbstractBlockParserFactory() {
override fun tryStart(
state: ParserState,
matchedBlockParser: MatchedBlockParser,
): BlockStart? {
val line = state.line.content
.subSequence(state.nextNonSpaceIndex, state.line.content.length)
.toString()
if (state.indent >= 4 || line != ":::memo") return null
return BlockStart.of(MemoBlockParser()).atIndex(state.line.content.length)
}
}
class MemoExtension private constructor() : Parser.ParserExtension, HtmlRendererExtension {
override fun extend(parserConfiguration: Parser.Configuration) {
parserConfiguration.customBlockParserFactory(MemoBlockParserFactory())
}
override fun extend(rendererConfiguration: HtmlRenderer.Configuration) {
rendererConfiguration.nodeRenderer<MemoBlock> { memo ->
writer.line()
writer.tag("aside", mapOf("class" to "memo"))
writer.text(memo.literal)
writer.tag("/aside")
writer.line()
}
}
companion object {
fun create(): Extension = MemoExtension()
}
}
fun renderMemo(markdown: String): String {
val extensions = extensions { add(MemoExtension.create()) }
val document = parser { extensions(extensions) }.parse(markdown)
return htmlRenderer { extensions(extensions) }.render(document)
}[5] This is an intentionally small teaching grammar. Production extensions should define container behavior, indentation, unterminated input, and precedence explicitly.
Extension workshop / custom renderers
Register a typed handler with nodeRenderer<Heading>. The heading is already typed, and the context is the receiver. Delegate children with renderChildren so emphasis and other inline nodes still use their normal renderers.
import io.heapy.kotmark.node.Heading
import io.heapy.kotmark.renderer.html.HtmlRenderer
import io.heapy.kotmark.renderer.html.htmlRenderer
import io.heapy.kotmark.renderer.html.nodeRenderer
import io.heapy.kotmark.renderer.html.renderChildren
fun chapterRenderer(): HtmlRenderer = htmlRenderer {
nodeRenderer<Heading> { heading ->
val tag = "h${heading.level}"
writer.line()
writer.tag(tag, extendAttributes(heading, tag, mapOf("class" to "chapter-title")))
renderChildren(heading)
writer.tag("/$tag")
writer.line()
}
}WHAT THIS PRESERVES
Factories are consulted in registration order. A custom renderer registered before another renderer for the same node type takes precedence.
For data attributes on core-rendered nodes, prefer an AttributeProvider over replacing the renderer.
Under the paper / parse to output
The parser builds a document tree; renderers walk that same tree into HTML, Markdown, or plain text. Extensions join at the parse or render boundary, not by rewriting a string after the fact.
Supported platforms
JVM + AndroidJava 17+ on the JVM
JS + WasmJavaScript, Wasm-JS, and Wasm-WASI
Desktop NativeLinux, macOS, and Windows
Apple platformsiOS, tvOS, and watchOS
Exact targets and their runtime verification are listed in the platform matrix.
Guides & API
Configure parsers, renderers, source spans, and extensions.
Resolve relative links and images against a base URL or your application’s routes.
Render documents straight into the kotlinx.html DSL or a DOM consumer, with an adapter for each extension.
Explore the nodes, parser options, rendering hooks, and extension APIs.
Migration / Java lineage, Kotlin API
Moving from commonmark-java or an earlier builder-based API? Kotmark uses Kotlin configuration factories and the io.heapy.kotmark packages. Existing integrations need source changes and recompilation.
Parser.builder().build()parser { includeSourceSpans = … }parser { } and htmlRenderer { }.AtIndex, AtColumn, Finished, or null.Migration guide → · Block continuation → · Upstream record →