Keep your Kotlin documentation snippets compiled, testable, and in sync with real source code.
Inspired by kotlinx-knit.
Korro embeds Kotlin sample snippets into Markdown and MDX documents. The snippets are ordinary functions in your Kotlin source tree. Korro then extracts the function bodies and writes them into your docs, so the code readers see is the same code your CI builds and tests.
plugins {
id("io.github.devcrocod.korro") version "0.2.1"
}Note
The plugin is also published to Maven Central as io.github.devcrocod:korro-gradle-plugin, so you can apply it from
buildSrc or a convention plugin — add the artifact as a regular dependency there and apply the
io.github.devcrocod.korro id from your convention. The classic buildscript { classpath … } form works too.
| Requirement | Version |
|---|---|
| Gradle | 8.5+ |
| JDK (build + runtime) | 17+ |
| Kotlin Analysis API (bundled) | 2.3.20 |
The bundled Kotlin version is pinned inside the plugin. Your consumer project's own org.jetbrains.kotlin.* plugin
version is irrelevant — Korro runs the Analysis API inside a worker with an isolated classloader. Your sample code can
be authored against any Kotlin version that the 2.3.20 Analysis API can parse.
| Task | Purpose |
|---|---|
korroGenerate |
Regenerates markdown into build/korro/docs/. Cacheable. Never touches source files. |
korro |
Applies generated output from build/korro/docs/ onto docs.baseDir. Depends on korroGenerate, so one command regenerates and copies. |
korroCheck |
Regenerates docs into a temp directory and fails the build if the committed source tree is out of date. Run this in CI. |
There is no korroClean — use ./gradlew clean or delete build/korro/. There is no korroTest.
Important
korro is the only task that writes into your source tree. korroGenerate and korroCheck stay entirely under
build/korro/ and never mutate docs — safe to run in any environment, including CI.
Typical workflow:
# Local authoring:
./gradlew korro # regenerate and update source markdown in one step
# CI:
./gradlew korroCheck # fail if docs drift from sampleskorro {
docs {
from(fileTree("docs") { include("**/*.md", "**/*.mdx") })
baseDir = layout.projectDirectory.dir("docs") // REQUIRED
}
samples {
from(fileTree("src/test/samples"))
outputs.from(fileTree("build/sampleOutputs")) // optional
}
behavior {
rewriteAsserts = false
ignoreMissing = false
}
}docs.from(...)is the set of markdown files to process.docs.baseDiris mandatory. Output files land at<buildDir>/korro/docs/<path-relative-to-baseDir>, and thekorrotask mirrors that tree back ontobaseDir. Set it to whichever directory the paths indocs.fromare rooted under — typicallylayout.projectDirectoryorlayout.projectDirectory.dir("docs").samples.from(...)is the set of Kotlin source files scanned forFUN/FUNStargets.samples.outputs.from(...)is optional. A file in this collection whose name exactly equals a resolvedFUNfully-qualified name is appended verbatim after the generated snippet.
rewriteAsserts(defaultfalse) — whentrue, sample bodies have theirassertPrints/assertTrue/assertFalse/assertFails/assertFailsWithcalls rewritten into a commentedprintln. Enable this only if your samples usekotlin.testidioms.ignoreMissing(defaultfalse) — strict by default. UnresolvedFUN/FUNS, unclosed//SampleStart, and non-function targets fail the task with a collected diagnostic list. Settrueto degrade those errors to warnings and keep the old snippet lines in the output.
Tip
Keep strict mode on outside of staged migrations — it surfaces broken references the moment a sample is renamed or
removed. Leaving ignoreMissing on permanently lets doc drift slip through unnoticed.
Use groupSamples to wrap multiple related snippets (for example, HTML tabs).
korro {
groupSamples {
beforeGroup = "<tabs>\n"
afterGroup = "</tabs>"
beforeSample = "<tab title=\"NAME\">\n"
afterSample = "\n</tab>"
funSuffix("_v1") { replaceText("NAME", "Version 1") }
funSuffix("_v2") { replaceText("NAME", "Version 2") }
}
}Tip
For new docs, prefer a single FUNS myFun_v* directive over two FUN myFun_v1 / FUN myFun_v2 directives.
Korro does not parse markdown; it recognizes directives only. A directive:
- starts at column 0 after
String.trim(), - opens with three dashes after an HTML- or MDX-comment prefix —
<!---for.mdor{/*---for.mdx(standard HTML comments<!--and standard MDX comments{/*are deliberately not recognized), - closes on the same line with
-->(in.md) or--*/}(in.mdx); multi-line directives are an error, - has a name matching
[_a-zA-Z.]+.
The syntax is selected per file by extension — .mdx uses the JSX-expression form, everything else uses the
HTML-comment form. Both encode the same four directives (IMPORT, FUN, FUNS, END) with identical semantics;
examples below show the .md form.
MDX equivalents (for Mintlify, Docusaurus, etc.):
{/*---IMPORT samples.Test--*/}
{/*---FUN exampleTest--*/}
{/*---END--*/}Note
MDX tooling (Mintlify, Docusaurus) rejects raw HTML comments, so .mdx directives use the JSX-expression form
{/*---…--*/}. The three-dash opener (<!--- / {/*---) is intentional — standard comments <!-- and {/* are
ignored, so ordinary comments in your docs pass through untouched.
<!---IMPORT samples.Test-->
Pushes "samples.Test." onto the prefix list used by subsequent FUN/FUNS lookups. Multiple IMPORTs are allowed;
when more than one prefix resolves a short name, the first import wins.
Package wildcards (samples.*) are not supported.
<!---FUN exampleTest-->
<!---END-->
Inserts the body of the referenced Kotlin function between the directives, wrapped in a ```kotlin fence.
If the function contains //SampleStart / //SampleEnd comments, only the region between them is emitted; multiple
pairs are concatenated, separated by a blank line. If the function has no markers, the whole body is emitted (without
the outer { }).
Valid targets are fun declarations, classes / objects / interfaces, and top-level or member properties
(KtNamedFunction, KtClassOrObject, KtProperty). For class, object, or property targets, the snippet contents come
from //SampleStart / //SampleEnd markers inside the declaration body — the wrapper name is not emitted, only the
marker-bracketed region. Enum entries, type aliases, local declarations, and .kts scripts are not valid targets.
Don't wrap names in backticks.
<!---FUNS sample_v*-->
<!---END-->
Expands to every function matching the Ant-style glob (*, ?) over the fully-qualified names reachable from the
current IMPORT prefixes. Matches are emitted in deterministic order: first by containing file path, then by source
offset.
When groupSamples.beforeGroup / afterGroup are set and there are two or more matches, the whole group is wrapped by
those strings; each individual match is wrapped by beforeSample / afterSample.
Zero matches: fails the task in strict mode, or warns under ignoreMissing.
Closes FUN or FUNS.
Minimal end-to-end setup (lifted from integration-tests/fixtures/basic/).
Tip
A ready-to-copy consumer project lives at integration-tests/fixtures/basic/.
The sibling fixtures under integration-tests/fixtures/ cover MDX, FUNS globs,
ignoreMissing, and korroCheck — useful references for less-common configurations.
settings.gradle.kts:
rootProject.name = "korro-example"build.gradle.kts:
plugins {
id("io.github.devcrocod.korro") version "0.2.1"
}
repositories {
mavenCentral()
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
}
korro {
docs {
from(fileTree("docs"))
baseDir = layout.projectDirectory.dir("docs")
}
samples {
from(fileTree("samples"))
}
}samples/Example.kt:
package samples
fun example() {
//SampleStart
println("hello")
//SampleEnd
}docs/foo.md (before korro):
# Example
<!---IMPORT samples-->
<!---FUN example-->
<!---END-->After ./gradlew korro:
# Example
<!---IMPORT samples-->
<!---FUN example-->
```kotlin
println("hello")
```
<!---END-->- The analysis backend moved from Dokka 1.x (K1) to the Kotlin Analysis API (K2, standalone mode).
- The DSL is now nested and Property-based (config-cache safe).
docs = …/samples = …becamedocs { from(…); baseDir = … }/samples { from(…); outputs.from(…) }. korroGenerateis cacheable and writes out-of-place tobuild/korro/docs/.korrodepends on it and applies the output onto the source tree; usekorroCheckin CI.- Strict-by-default: unresolved
FUN/FUNSfails the build. Opt back in to the old warn-and-continue behavior withbehavior { ignoreMissing = true }. - Assert rewriting is off by default. Restore with
behavior { rewriteAsserts = true }. FUNSis now implemented as a glob-filter directive.- MDX files (
.mdx) are supported natively via a JSX-expression directive form{/*---FUN ...--*/}. korroCleanis removed;korroTestis deferred.
Full upgrade guide: MIGRATION.md.