
Transforms Markdown into composable functions with inline embedding of code, customizable preprocessing and rendering hooks, and optional directory aggregation support for streamlined function access.
ComposeMark is a Kotlin Multiplatform + KSP toolchain for turning Markdown documents (and inline
Markdown snippets) into Jetpack Compose @Composable functions. It lets you blend Markdown with
embedded Compose blocks, intercept every stage of the rendering pipeline, and ship optional runtime
helpers for common scenarios.
<Composable> blocks support optional attributes, lift their own import lines into the
generated file, and run through the same preprocessing/render pipelines as Markdown blocks.Map<String, @Composable (Modifier) -> Unit>
entries with deterministic snake_case keys, duplicate-key detection, and explicit path tracking.composeMarkPlugin { ... }) for installing interceptors across Markdown, composable, and
block-list stages.io.github.arashiyama11:composemark-plugin) that provides front
matter decoding, inline embed helpers, and page scaffold utilities on top of the core runtime.core: Kotlin Multiplatform runtime, pipelines, annotations, and the ComposeMark base class.processor: KSP processor and Gradle plugin (io.github.arashiyama11.composemark) written in
Kotlin/JVM.plugin: Optional Compose runtime plugins (front matter, inline embed, page scaffold) published
separately.sample: Android demo app that consumes the local artifacts..github/consumer-test: end-to-end consumer project that exercises JVM and browser targets.plugins {
id("com.google.devtools.ksp")
id("io.github.arashiyama11.composemark") version "0.0.0-alpha08" // optional but recommended
}
dependencies {
implementation("io.github.arashiyama11:composemark-core:0.0.0-alpha08")
ksp("io.github.arashiyama11:composemark-processor:0.0.0-alpha08")
}
composeMark {
rootPath = project.projectDir.absolutePath // defaults to projectDir
watch("docs/**/*.md", "docs/**/*.mdx") // optional: wires into every ksp* task input
}The Gradle plugin automatically:
composemark.root.path into the KSP extension (defaulting to projectDir)watch(...) patterns (resolved relative to rootPath) as incremental inputs to all
ksp* tasks.plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.google.devtools.ksp")
id("io.github.arashiyama11.composemark") version "0.0.0-alpha08"
}
kotlin {
sourceSets.named("commonMain") {
kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
}
}
dependencies {
kspCommonMainMetadata("io.github.arashiyama11:composemark-processor:0.0.0-alpha08")
}
composeMark {
rootPath = project.layout.projectDirectory.dir("docs").asFile.path
watch("**/*.md", "**/*.mdx")
ensureCommonKspBeforeKotlinCompile() // wires kspCommonMainKotlinMetadata before compile tasks
}
// Recommended to keep Kotlin compile tasks waiting for metadata generation:
tasks.matching { it.name.startsWith("compileKotlin") }
.configureEach { it.dependsOn("kspCommonMainKotlinMetadata") }composemark.root.path must resolve to the directory where your Markdown lives whenever you rely on
directory aggregation. All file reads are relative to this path.
Refer to .github/consumer-test for an end-to-end KMP configuration.
Annotate an interface with @GenerateMarkdownContents, describe each piece of Markdown via the path
or inline annotations, and delegate to the generated implementation:
@GenerateMarkdownContents(MyComposeMark::class)
interface Docs {
@Composable
@GenerateMarkdownFromPath("docs/intro.md")
fun Intro(modifier: Modifier = Modifier)
@Composable
@GenerateMarkdownFromSource(
"""
# Inline example
<Composable>
import androidx.compose.material3.Text
Text("Hello from Compose block!")
</Composable>
""".trimIndent()
)
fun Inline(modifier: Modifier = Modifier)
companion object : Docs by DocsImpl
}Key behaviours:
InterfaceNameImpl; supply implName = "MyDocs" on
@GenerateMarkdownContents to override it.<Composable> sections may declare their own import (and optional import ... as Alias)
statements; these are hoisted to the top of the generated file.Modifier are registered in the generated contents map.Add a property annotated with @GenerateMarkdownFromDirectory to aggregate a folder:
@GenerateMarkdownContents(MyComposeMark::class)
interface DirDocs {
@GenerateMarkdownFromDirectory(
dir = "docs",
includes = ["**/*.md", "**/*.mdx"],
excludes = ["drafts/**"]
)
val contents: Map<String, @Composable (Modifier) -> Unit>
companion object : DirDocs by DirDocsImpl
}Runtime characteristics:
snake_case (getting-started.md →
getting_started). Duplicate keys fail the KSP round with an error.includes and do not match excludes are processed. At least one match is
required (otherwise the processor reports an error).composemark.root.path; the processor fails fast if the directory is
missing.Conflicts between directory-derived function names and user-defined abstract functions are skipped to avoid double generation.
ComposeMark wraps a MarkdownRenderer (defaults backed by multiplatform-markdown-renderer) and
exposes hooks for every stage:
markdownBlockPreProcessorPipeline, composableBlockPreProcessorPipeline, and
blockListPreProcessorPipeline
renderMarkdownBlockPipeline, renderComposableBlockPipeline, and renderBlocksPipeline
Each pipeline carries a mutable PreProcessorMetadata map which is snapshot into a
CompositionLocal (RenderContext.metadata) so renderers and plugins can share structured data.
Utility types:
Block.markdown(...) and Block.composable(...) let you mix Markdown and Compose blocks
explicitly and render them via ComposeMark.RenderBlocks(...).composeMarkPlugin { ... } registers interceptors with PipelinePriority and order hints;
plugins can opt-in to every stage without subclassing.Install these from ComposeMark.setup():
[cm-inline:slot] placeholders into inline slots and exposes
helpers to build InlineTextContent.These plugins are multiplatform and depend only on the core runtime.
composeMark extension with rootPath and watch(...).ksp.composemark.root.path extra property and forwards it into the KSP extension.watch(...) as inputs to every ksp* task with
PathSensitivity.RELATIVE.Project.ensureCommonKspBeforeKotlinCompile() helper to ensure metadata KSP tasks run
before Kotlin compilation (useful for KMP builds).Apache-2.0
ComposeMark is a Kotlin Multiplatform + KSP toolchain for turning Markdown documents (and inline
Markdown snippets) into Jetpack Compose @Composable functions. It lets you blend Markdown with
embedded Compose blocks, intercept every stage of the rendering pipeline, and ship optional runtime
helpers for common scenarios.
<Composable> blocks support optional attributes, lift their own import lines into the
generated file, and run through the same preprocessing/render pipelines as Markdown blocks.Map<String, @Composable (Modifier) -> Unit>
entries with deterministic snake_case keys, duplicate-key detection, and explicit path tracking.composeMarkPlugin { ... }) for installing interceptors across Markdown, composable, and
block-list stages.io.github.arashiyama11:composemark-plugin) that provides front
matter decoding, inline embed helpers, and page scaffold utilities on top of the core runtime.core: Kotlin Multiplatform runtime, pipelines, annotations, and the ComposeMark base class.processor: KSP processor and Gradle plugin (io.github.arashiyama11.composemark) written in
Kotlin/JVM.plugin: Optional Compose runtime plugins (front matter, inline embed, page scaffold) published
separately.sample: Android demo app that consumes the local artifacts..github/consumer-test: end-to-end consumer project that exercises JVM and browser targets.plugins {
id("com.google.devtools.ksp")
id("io.github.arashiyama11.composemark") version "0.0.0-alpha08" // optional but recommended
}
dependencies {
implementation("io.github.arashiyama11:composemark-core:0.0.0-alpha08")
ksp("io.github.arashiyama11:composemark-processor:0.0.0-alpha08")
}
composeMark {
rootPath = project.projectDir.absolutePath // defaults to projectDir
watch("docs/**/*.md", "docs/**/*.mdx") // optional: wires into every ksp* task input
}The Gradle plugin automatically:
composemark.root.path into the KSP extension (defaulting to projectDir)watch(...) patterns (resolved relative to rootPath) as incremental inputs to all
ksp* tasks.plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.google.devtools.ksp")
id("io.github.arashiyama11.composemark") version "0.0.0-alpha08"
}
kotlin {
sourceSets.named("commonMain") {
kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
}
}
dependencies {
kspCommonMainMetadata("io.github.arashiyama11:composemark-processor:0.0.0-alpha08")
}
composeMark {
rootPath = project.layout.projectDirectory.dir("docs").asFile.path
watch("**/*.md", "**/*.mdx")
ensureCommonKspBeforeKotlinCompile() // wires kspCommonMainKotlinMetadata before compile tasks
}
// Recommended to keep Kotlin compile tasks waiting for metadata generation:
tasks.matching { it.name.startsWith("compileKotlin") }
.configureEach { it.dependsOn("kspCommonMainKotlinMetadata") }composemark.root.path must resolve to the directory where your Markdown lives whenever you rely on
directory aggregation. All file reads are relative to this path.
Refer to .github/consumer-test for an end-to-end KMP configuration.
Annotate an interface with @GenerateMarkdownContents, describe each piece of Markdown via the path
or inline annotations, and delegate to the generated implementation:
@GenerateMarkdownContents(MyComposeMark::class)
interface Docs {
@Composable
@GenerateMarkdownFromPath("docs/intro.md")
fun Intro(modifier: Modifier = Modifier)
@Composable
@GenerateMarkdownFromSource(
"""
# Inline example
<Composable>
import androidx.compose.material3.Text
Text("Hello from Compose block!")
</Composable>
""".trimIndent()
)
fun Inline(modifier: Modifier = Modifier)
companion object : Docs by DocsImpl
}Key behaviours:
InterfaceNameImpl; supply implName = "MyDocs" on
@GenerateMarkdownContents to override it.<Composable> sections may declare their own import (and optional import ... as Alias)
statements; these are hoisted to the top of the generated file.Modifier are registered in the generated contents map.Add a property annotated with @GenerateMarkdownFromDirectory to aggregate a folder:
@GenerateMarkdownContents(MyComposeMark::class)
interface DirDocs {
@GenerateMarkdownFromDirectory(
dir = "docs",
includes = ["**/*.md", "**/*.mdx"],
excludes = ["drafts/**"]
)
val contents: Map<String, @Composable (Modifier) -> Unit>
companion object : DirDocs by DirDocsImpl
}Runtime characteristics:
snake_case (getting-started.md →
getting_started). Duplicate keys fail the KSP round with an error.includes and do not match excludes are processed. At least one match is
required (otherwise the processor reports an error).composemark.root.path; the processor fails fast if the directory is
missing.Conflicts between directory-derived function names and user-defined abstract functions are skipped to avoid double generation.
ComposeMark wraps a MarkdownRenderer (defaults backed by multiplatform-markdown-renderer) and
exposes hooks for every stage:
markdownBlockPreProcessorPipeline, composableBlockPreProcessorPipeline, and
blockListPreProcessorPipeline
renderMarkdownBlockPipeline, renderComposableBlockPipeline, and renderBlocksPipeline
Each pipeline carries a mutable PreProcessorMetadata map which is snapshot into a
CompositionLocal (RenderContext.metadata) so renderers and plugins can share structured data.
Utility types:
Block.markdown(...) and Block.composable(...) let you mix Markdown and Compose blocks
explicitly and render them via ComposeMark.RenderBlocks(...).composeMarkPlugin { ... } registers interceptors with PipelinePriority and order hints;
plugins can opt-in to every stage without subclassing.Install these from ComposeMark.setup():
[cm-inline:slot] placeholders into inline slots and exposes
helpers to build InlineTextContent.These plugins are multiplatform and depend only on the core runtime.
composeMark extension with rootPath and watch(...).ksp.composemark.root.path extra property and forwards it into the KSP extension.watch(...) as inputs to every ksp* task with
PathSensitivity.RELATIVE.Project.ensureCommonKspBeforeKotlinCompile() helper to ensure metadata KSP tasks run
before Kotlin compilation (useful for KMP builds).Apache-2.0