
Blazing-fast AST-based Markdown engine with incremental parsing, token-by-token LLM streaming, full CommonMark coverage, theming, LaTeX math, linting, plugin directives and image loading.
A Blazing-Fast, Cross-Platform Markdown Engine for Compose Multiplatform
One library. One codebase. Pixel-perfect Markdown on Android, iOS, Desktop & Web.
| Feature | Description | |
|---|---|---|
| 🚀 | Blazing Fast | AST-based recursive descent parser with incremental parsing — only re-parses what changed |
| 🌍 | True Cross-Platform | One codebase renders identically on Android, iOS, Desktop (JVM), Web (Wasm/JS) |
| 📐 | 100% Coverage | 372 Markdown features, 652/652 CommonMark Spec tests passing, plus GFM & 20+ extensions |
| 🤖 | LLM-Ready Streaming | First-class token-by-token rendering with 5fps throttling — zero flicker during AI generation |
| 🎨 | Fully Themeable | 30+ configurable properties, built-in GitHub light/dark themes, auto system detection |
| 📊 | LaTeX Math | Inline $...$ and block $$...$$ formulas via integrated LaTeX rendering engine |
| 🔍 | Built-in Linting | 13+ diagnostic rules including WCAG accessibility checks — catch issues at parse time |
| 🖼️ | Image Loading | Coil3 + Ktor3 out-of-the-box, with size specification and custom renderer support |
| 📄 | Pagination | Progressive rendering for ultra-long documents (500+ blocks) with auto load-more |
Real-time token-by-token output with incremental parsing — no flicker, no re-render.
Built-in linting with WCAG accessibility checks — heading jumps, broken footnotes, empty links, and more.
Full HTML block/inline support, GFM tables, admonitions, math, code highlighting, and 20+ extensions.
Add to your gradle/libs.versions.toml:
[versions]
markdown = "0.0.1-alpha.4"
[libraries]
markdown-parser = { module = "io.github.zly2006:markdown-parser", version.ref = "markdown" }
markdown-runtime = { module = "io.github.zly2006:markdown-runtime", version.ref = "markdown" }
markdown-renderer = { module = "io.github.zly2006:markdown-renderer", version.ref = "markdown" }Then in your module's build.gradle.kts:
dependencies {
implementation(libs.markdown.parser)
implementation(libs.markdown.runtime)
implementation(libs.markdown.renderer)
}💡
markdown-rendererbundles Coil3 + Ktor3 for image loading as transitive dependencies.
import com.hrm.markdown.renderer.Markdown
import com.hrm.markdown.renderer.MarkdownTheme
import com.hrm.codehigh.theme.OneDarkProTheme
@Composable
fun MyScreen() {
Markdown(
markdown = """
# Hello World
This is a paragraph with **bold** and *italic* text.
- Item 1
- Item 2
```kotlin
fun hello() = println("Hello")
```
""".trimIndent(),
modifier = Modifier.fillMaxSize(),
theme = MarkdownTheme.auto(), // Follows system light/dark mode
codeTheme = OneDarkProTheme, // Optional: pass a codehigh theme directly
)
}That's it — 3 lines to render beautiful Markdown across all platforms.
This project ships a plugin-friendly architecture based on:
{% tag ... %}) parsed by markdown-parser
video) to native Compose blocksBasic usage:
Markdown(
markdown = """
Custom syntax:
!VIDEO[Demo](https://cdn.example.com/a.mp4){poster=https://cdn.example.com/a.jpg}
""".trimIndent(),
directivePlugins = listOf(VideoDirectivePlugin),
)Plugin skeleton:
object VideoDirectivePlugin : MarkdownDirectivePlugin {
override val id: String = "video"
override val inputTransformers = listOf(VideoSyntaxTransformer())
override val blockDirectiveRenderers = mapOf(
"video" to { scope ->
VideoPlayer(
url = scope.args.getValue("url"),
poster = scope.args["poster"],
title = scope.args["title"],
)
}
)
}
class VideoSyntaxTransformer : MarkdownInputTransformer {
override val id: String = "video-syntax"
override fun transform(input: String): MarkdownTransformResult {
val normalized = input.replace(
Regex("""!VIDEO\[(.*?)\]\((.*?)\)\{poster=(.*?)\}""")
) { match ->
val title = match.groupValues[1]
val url = match.groupValues[2]
val poster = match.groupValues[3]
"""{% video title="$title" url="$url" poster="$poster" %}"""
}
return MarkdownTransformResult(markdown = normalized)
}
}HTML export uses the same directive pipeline:
val html = MarkdownHtml.render(
markdown = markdown,
directivePlugins = listOf(VideoDirectivePlugin),
)Purpose-built for AI/LLM scenarios. Enable isStreaming for flicker-free incremental rendering:
var text by remember { mutableStateOf("") }
var isStreaming by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
llmTokenFlow.collect { token ->
text += token
}
isStreaming = false
}
Markdown(
markdown = text,
isStreaming = isStreaming, // Enables incremental parsing + 5fps throttled rendering
)What happens under the hood:
// Built-in themes
Markdown(markdown = text, theme = MarkdownTheme.light()) // GitHub Light
Markdown(markdown = text, theme = MarkdownTheme.dark()) // GitHub Dark
Markdown(markdown = text, theme = MarkdownTheme.auto()) // Auto-detect
// Full customization (30+ properties)
Markdown(
markdown = text,
theme = MarkdownTheme(
headingStyles = listOf(
TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold),
// h2 ~ h6 ...
),
bodyStyle = TextStyle(fontSize = 16.sp),
codeBlockBackground = Color(0xFFF5F5F5),
// ...and much more
),
onLinkClick = { url -> /* handle click */ },
)Code highlighting and code block/inline code theming are provided by the codehigh library. markdown-renderer no longer bundles its own regex-based highlighter.
import com.hrm.codehigh.theme.DraculaProTheme
import com.hrm.codehigh.theme.LocalCodeTheme
import com.hrm.codehigh.theme.OneDarkProTheme
// Option 1: Pass codehigh theme per Markdown call
Markdown(
markdown = text,
theme = MarkdownTheme.auto(),
codeTheme = OneDarkProTheme,
)
// Option 2: Provide a default codehigh theme globally via CompositionLocal
CompositionLocalProvider(
LocalCodeTheme provides DraculaProTheme
) {
Markdown(
markdown = text,
theme = MarkdownTheme.auto(),
)
}theme controls general Markdown UI (headings, body, tables, quotes, math, etc.)codeTheme controls code highlighting and styling for code blocks and inline codecodeTheme is not provided, the default from codehigh is used| Feature | Details |
|---|---|
| Headings | ATX (# ~ ######), Setext (===/---), custom IDs ({#id}), auto-generated anchors |
| Paragraphs | Multi-line merging, blank line separation, lazy continuation |
| Code Blocks | Fenced (```/~~~) with language highlight (20+ languages), indented, line numbers, line highlighting |
| Block Quotes | Nested, lazy continuation, inner block elements |
| Lists | Unordered/ordered/task lists, nested, tight/loose distinction |
| Tables (GFM) | Column alignment, inline formatting in cells, escaped pipes |
| Thematic Breaks |
---, ***, ___
|
| HTML Blocks | All 7 CommonMark types |
| Link Reference Definitions | Full support with title variants |
| Feature | Details |
|---|---|
| Emphasis | Bold, italic, bold-italic, nested, CJK-aware delimiter rules |
| Strikethrough |
~~text~~, ~text~
|
| Inline Code | Single/multi backtick, space stripping |
| Links | Inline, reference (full/collapsed/shortcut), autolinks, GFM bare URLs, attribute blocks |
| Images | Inline, reference, =WxH size specification, attribute blocks, auto Figure conversion |
| Inline HTML | Tags, comments, CDATA, processing instructions |
| Escapes & Entities | 32 escapable characters, named/numeric HTML entities |
| Line Breaks | Hard (spaces/backslash), soft |
| Feature | Syntax |
|---|---|
| Math (LaTeX) |
$...$ inline, $$...$$ block, \tag{}, \ref{}
|
| Footnotes |
[^label] references, multi-line definitions, block content |
| Admonitions |
> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION]
|
| Highlight | ==text== |
| Super/Subscript |
^text^, ~text~, <sup>, <sub>
|
| Insert Text | ++text++ |
| Emoji |
:smile: shortcodes (200+), ASCII emoticons (40+), custom mappings |
| Definition Lists | Term + : definition format |
| Front Matter | YAML (---) and TOML (+++) |
| TOC |
[TOC] with depth, exclude, ordering options |
| Custom Containers |
:::type with nesting, CSS classes, IDs |
| Diagram Blocks | Mermaid, PlantUML, Graphviz, and more |
| Multi-Column Layout |
:::columns with percentage/pixel widths |
| Tab Blocks |
=== "Tab Title" MkDocs Material style |
| Directives |
{% tag args %}...{% endtag %} with positional/keyword args |
| Spoiler Text |
>!hidden text!< Discord/Reddit style |
| Wiki Links |
[[page]], [[page|display text]]
|
| Ruby Text |
{漢字|かんじ} pronunciation annotations |
| Bibliography |
[@key] citations with [^bibliography] definitions |
| Block Attributes |
{.class #id key=value} kramdown/Pandoc style |
| Page Breaks |
***pagebreak*** for print/PDF export |
| Styled Text |
[text]{.red style="color:red"} inline CSS |
Enable once, catch problems everywhere:
val parser = MarkdownParser(enableLinting = true)
val document = parser.parse(markdown)
document.diagnostics.forEach { diagnostic ->
println("Line ${diagnostic.line}: [${diagnostic.severity}] ${diagnostic.message}")
}13+ diagnostic rules:
| Rule | Severity | Description |
|---|---|---|
| Heading level skip | h1 → h3 without h2 | |
| Duplicate heading ID | Multiple headings generate the same anchor | |
| Invalid footnote ref | ❌ ERROR | Reference to undefined footnote |
| Unused footnote | Footnote defined but never referenced | |
| Empty link target |
[text]() with no URL |
|
| Missing alt text | Images without description | |
| Empty link text | Links invisible to screen readers | |
| Non-descriptive link | "click here", "read more" links | |
| Missing code language | ℹ️ INFO | Fenced code without language tag |
| Table missing header | Screen readers need <th>
|
|
| Long alt text | Alt text > 125 characters |
Follows WCAG 2.1 AA standards for accessibility compliance.
┌─────────────────────────────────────────────────────────────┐
│ Your Compose App │
├─────────────────────────────────────────────────────────────┤
│ markdown-renderer │ markdown-preview │
│ AST → Compose UI │ Interactive demo & showcase │
│ Block/Inline renderers │ Categorized feature browser │
│ Theme system │ │
├───────────────────────────┤ │
│ markdown-parser │
│ Markdown → AST │
│ Streaming / Incremental / Flavour system │
│ Linting / Diagnostics / Post-processors │
└─────────────────────────────────────────────────────────────┘
| Module | Description |
|---|---|
:markdown-parser |
Core parsing engine — Markdown string → AST. Streaming, incremental, multi-flavour. |
:markdown-renderer |
Rendering engine — AST → Compose UI. Theming, image loading, code highlighting. |
:markdown-preview |
Interactive showcase — categorized demo of all supported features. |
:composeApp |
Cross-platform demo app (Android/iOS/Desktop/Web). |
:androidApp |
Android-specific demo app. |
| Spec | Status |
|---|---|
| CommonMark 0.31.2 | 652/652 (100%) ✅ |
| GFM 0.29 | Tables, task lists, strikethrough, autolinks ✅ |
| Markdown Extra | Footnotes, definition lists, abbreviations, fenced code ✅ |
Configure which syntax features to enable:
// Strict CommonMark — no extensions
val doc = MarkdownParser(CommonMarkFlavour).parse(input)
// GFM — CommonMark + tables, strikethrough, autolinks
val doc = MarkdownParser(GFMFlavour).parse(input)
// Extended (default) — everything enabled
val doc = MarkdownParser().parse(input)
// One-shot HTML rendering
val html = HtmlRenderer.renderMarkdown(input, flavour = CommonMarkFlavour)Built-in Coil3 handles images automatically. Need custom rendering? Easy:
Markdown(
markdown = markdownText,
imageContent = { data, modifier ->
// data.url, data.altText, data.width, data.height available
AsyncImage(
model = data.url,
contentDescription = data.altText,
modifier = modifier,
)
},
)Supports size specification in Markdown:

# Android
./gradlew :composeApp:assembleDebug
# Desktop (JVM)
./gradlew :composeApp:run
# Web (Wasm)
./gradlew :composeApp:wasmJsBrowserDevelopmentRun
# Web (JS)
./gradlew :composeApp:jsBrowserDevelopmentRun
# iOS — open iosApp/ in Xcode./gradlew :markdown-parser:jvmTest # Parser tests
./gradlew :markdown-renderer:jvmTest # Renderer tests
./gradlew jvmTest # All tests| # | Category | Coverage |
|---|---|---|
| 1 | Headings | 17/17 (100%) |
| 2 | Paragraphs | 5/5 (100%) |
| 3 | Code Blocks | 17/17 (100%) |
| 4 | Block Quotes | 8/8 (100%) |
| 5 | Lists | 20/20 (100%) |
| 6 | Thematic Breaks | 6/6 (100%) |
| 7 | Tables (GFM) | 11/11 (100%) |
| 8 | HTML Blocks | 10/10 (100%) |
| 9 | Link References | 12/12 (100%) |
| 10 | Block Extensions | 85/85 (100%) |
| 11 | Emphasis | 13/13 (100%) |
| 12 | Strikethrough | 4/4 (100%) |
| 13 | Inline Code | 8/8 (100%) |
| 14 | Links | 27/27 (100%) |
| 15 | Images | 17/17 (100%) |
| 16 | Inline HTML | 8/8 (100%) |
| 17 | Escapes & Entities | 10/10 (100%) |
| 18 | Line Breaks | 5/5 (100%) |
| 19 | Inline Extensions | 50/50 (100%) |
| 20 | Streaming Engine | 27/27 (100%) |
| 21 | Character & Encoding | 10/10 (100%) |
| 22 | HTML Generator | 12/12 (100%) |
| 23 | Linting / WCAG | 19/19 (100%) |
| 24 | Directives | 8/8 (100%) |
| Total | 372/372 (100%) |
📖 Full details: PARSER_COVERAGE_ANALYSIS.md
MIT License · Copyright (c) 2026 huarangmeng
This project is licensed under the MIT License — see the LICENSE file for details.
A Blazing-Fast, Cross-Platform Markdown Engine for Compose Multiplatform
One library. One codebase. Pixel-perfect Markdown on Android, iOS, Desktop & Web.
| Feature | Description | |
|---|---|---|
| 🚀 | Blazing Fast | AST-based recursive descent parser with incremental parsing — only re-parses what changed |
| 🌍 | True Cross-Platform | One codebase renders identically on Android, iOS, Desktop (JVM), Web (Wasm/JS) |
| 📐 | 100% Coverage | 372 Markdown features, 652/652 CommonMark Spec tests passing, plus GFM & 20+ extensions |
| 🤖 | LLM-Ready Streaming | First-class token-by-token rendering with 5fps throttling — zero flicker during AI generation |
| 🎨 | Fully Themeable | 30+ configurable properties, built-in GitHub light/dark themes, auto system detection |
| 📊 | LaTeX Math | Inline $...$ and block $$...$$ formulas via integrated LaTeX rendering engine |
| 🔍 | Built-in Linting | 13+ diagnostic rules including WCAG accessibility checks — catch issues at parse time |
| 🖼️ | Image Loading | Coil3 + Ktor3 out-of-the-box, with size specification and custom renderer support |
| 📄 | Pagination | Progressive rendering for ultra-long documents (500+ blocks) with auto load-more |
Real-time token-by-token output with incremental parsing — no flicker, no re-render.
Built-in linting with WCAG accessibility checks — heading jumps, broken footnotes, empty links, and more.
Full HTML block/inline support, GFM tables, admonitions, math, code highlighting, and 20+ extensions.
Add to your gradle/libs.versions.toml:
[versions]
markdown = "0.0.1-alpha.4"
[libraries]
markdown-parser = { module = "io.github.zly2006:markdown-parser", version.ref = "markdown" }
markdown-runtime = { module = "io.github.zly2006:markdown-runtime", version.ref = "markdown" }
markdown-renderer = { module = "io.github.zly2006:markdown-renderer", version.ref = "markdown" }Then in your module's build.gradle.kts:
dependencies {
implementation(libs.markdown.parser)
implementation(libs.markdown.runtime)
implementation(libs.markdown.renderer)
}💡
markdown-rendererbundles Coil3 + Ktor3 for image loading as transitive dependencies.
import com.hrm.markdown.renderer.Markdown
import com.hrm.markdown.renderer.MarkdownTheme
import com.hrm.codehigh.theme.OneDarkProTheme
@Composable
fun MyScreen() {
Markdown(
markdown = """
# Hello World
This is a paragraph with **bold** and *italic* text.
- Item 1
- Item 2
```kotlin
fun hello() = println("Hello")
```
""".trimIndent(),
modifier = Modifier.fillMaxSize(),
theme = MarkdownTheme.auto(), // Follows system light/dark mode
codeTheme = OneDarkProTheme, // Optional: pass a codehigh theme directly
)
}That's it — 3 lines to render beautiful Markdown across all platforms.
This project ships a plugin-friendly architecture based on:
{% tag ... %}) parsed by markdown-parser
video) to native Compose blocksBasic usage:
Markdown(
markdown = """
Custom syntax:
!VIDEO[Demo](https://cdn.example.com/a.mp4){poster=https://cdn.example.com/a.jpg}
""".trimIndent(),
directivePlugins = listOf(VideoDirectivePlugin),
)Plugin skeleton:
object VideoDirectivePlugin : MarkdownDirectivePlugin {
override val id: String = "video"
override val inputTransformers = listOf(VideoSyntaxTransformer())
override val blockDirectiveRenderers = mapOf(
"video" to { scope ->
VideoPlayer(
url = scope.args.getValue("url"),
poster = scope.args["poster"],
title = scope.args["title"],
)
}
)
}
class VideoSyntaxTransformer : MarkdownInputTransformer {
override val id: String = "video-syntax"
override fun transform(input: String): MarkdownTransformResult {
val normalized = input.replace(
Regex("""!VIDEO\[(.*?)\]\((.*?)\)\{poster=(.*?)\}""")
) { match ->
val title = match.groupValues[1]
val url = match.groupValues[2]
val poster = match.groupValues[3]
"""{% video title="$title" url="$url" poster="$poster" %}"""
}
return MarkdownTransformResult(markdown = normalized)
}
}HTML export uses the same directive pipeline:
val html = MarkdownHtml.render(
markdown = markdown,
directivePlugins = listOf(VideoDirectivePlugin),
)Purpose-built for AI/LLM scenarios. Enable isStreaming for flicker-free incremental rendering:
var text by remember { mutableStateOf("") }
var isStreaming by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
llmTokenFlow.collect { token ->
text += token
}
isStreaming = false
}
Markdown(
markdown = text,
isStreaming = isStreaming, // Enables incremental parsing + 5fps throttled rendering
)What happens under the hood:
// Built-in themes
Markdown(markdown = text, theme = MarkdownTheme.light()) // GitHub Light
Markdown(markdown = text, theme = MarkdownTheme.dark()) // GitHub Dark
Markdown(markdown = text, theme = MarkdownTheme.auto()) // Auto-detect
// Full customization (30+ properties)
Markdown(
markdown = text,
theme = MarkdownTheme(
headingStyles = listOf(
TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold),
// h2 ~ h6 ...
),
bodyStyle = TextStyle(fontSize = 16.sp),
codeBlockBackground = Color(0xFFF5F5F5),
// ...and much more
),
onLinkClick = { url -> /* handle click */ },
)Code highlighting and code block/inline code theming are provided by the codehigh library. markdown-renderer no longer bundles its own regex-based highlighter.
import com.hrm.codehigh.theme.DraculaProTheme
import com.hrm.codehigh.theme.LocalCodeTheme
import com.hrm.codehigh.theme.OneDarkProTheme
// Option 1: Pass codehigh theme per Markdown call
Markdown(
markdown = text,
theme = MarkdownTheme.auto(),
codeTheme = OneDarkProTheme,
)
// Option 2: Provide a default codehigh theme globally via CompositionLocal
CompositionLocalProvider(
LocalCodeTheme provides DraculaProTheme
) {
Markdown(
markdown = text,
theme = MarkdownTheme.auto(),
)
}theme controls general Markdown UI (headings, body, tables, quotes, math, etc.)codeTheme controls code highlighting and styling for code blocks and inline codecodeTheme is not provided, the default from codehigh is used| Feature | Details |
|---|---|
| Headings | ATX (# ~ ######), Setext (===/---), custom IDs ({#id}), auto-generated anchors |
| Paragraphs | Multi-line merging, blank line separation, lazy continuation |
| Code Blocks | Fenced (```/~~~) with language highlight (20+ languages), indented, line numbers, line highlighting |
| Block Quotes | Nested, lazy continuation, inner block elements |
| Lists | Unordered/ordered/task lists, nested, tight/loose distinction |
| Tables (GFM) | Column alignment, inline formatting in cells, escaped pipes |
| Thematic Breaks |
---, ***, ___
|
| HTML Blocks | All 7 CommonMark types |
| Link Reference Definitions | Full support with title variants |
| Feature | Details |
|---|---|
| Emphasis | Bold, italic, bold-italic, nested, CJK-aware delimiter rules |
| Strikethrough |
~~text~~, ~text~
|
| Inline Code | Single/multi backtick, space stripping |
| Links | Inline, reference (full/collapsed/shortcut), autolinks, GFM bare URLs, attribute blocks |
| Images | Inline, reference, =WxH size specification, attribute blocks, auto Figure conversion |
| Inline HTML | Tags, comments, CDATA, processing instructions |
| Escapes & Entities | 32 escapable characters, named/numeric HTML entities |
| Line Breaks | Hard (spaces/backslash), soft |
| Feature | Syntax |
|---|---|
| Math (LaTeX) |
$...$ inline, $$...$$ block, \tag{}, \ref{}
|
| Footnotes |
[^label] references, multi-line definitions, block content |
| Admonitions |
> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION]
|
| Highlight | ==text== |
| Super/Subscript |
^text^, ~text~, <sup>, <sub>
|
| Insert Text | ++text++ |
| Emoji |
:smile: shortcodes (200+), ASCII emoticons (40+), custom mappings |
| Definition Lists | Term + : definition format |
| Front Matter | YAML (---) and TOML (+++) |
| TOC |
[TOC] with depth, exclude, ordering options |
| Custom Containers |
:::type with nesting, CSS classes, IDs |
| Diagram Blocks | Mermaid, PlantUML, Graphviz, and more |
| Multi-Column Layout |
:::columns with percentage/pixel widths |
| Tab Blocks |
=== "Tab Title" MkDocs Material style |
| Directives |
{% tag args %}...{% endtag %} with positional/keyword args |
| Spoiler Text |
>!hidden text!< Discord/Reddit style |
| Wiki Links |
[[page]], [[page|display text]]
|
| Ruby Text |
{漢字|かんじ} pronunciation annotations |
| Bibliography |
[@key] citations with [^bibliography] definitions |
| Block Attributes |
{.class #id key=value} kramdown/Pandoc style |
| Page Breaks |
***pagebreak*** for print/PDF export |
| Styled Text |
[text]{.red style="color:red"} inline CSS |
Enable once, catch problems everywhere:
val parser = MarkdownParser(enableLinting = true)
val document = parser.parse(markdown)
document.diagnostics.forEach { diagnostic ->
println("Line ${diagnostic.line}: [${diagnostic.severity}] ${diagnostic.message}")
}13+ diagnostic rules:
| Rule | Severity | Description |
|---|---|---|
| Heading level skip | h1 → h3 without h2 | |
| Duplicate heading ID | Multiple headings generate the same anchor | |
| Invalid footnote ref | ❌ ERROR | Reference to undefined footnote |
| Unused footnote | Footnote defined but never referenced | |
| Empty link target |
[text]() with no URL |
|
| Missing alt text | Images without description | |
| Empty link text | Links invisible to screen readers | |
| Non-descriptive link | "click here", "read more" links | |
| Missing code language | ℹ️ INFO | Fenced code without language tag |
| Table missing header | Screen readers need <th>
|
|
| Long alt text | Alt text > 125 characters |
Follows WCAG 2.1 AA standards for accessibility compliance.
┌─────────────────────────────────────────────────────────────┐
│ Your Compose App │
├─────────────────────────────────────────────────────────────┤
│ markdown-renderer │ markdown-preview │
│ AST → Compose UI │ Interactive demo & showcase │
│ Block/Inline renderers │ Categorized feature browser │
│ Theme system │ │
├───────────────────────────┤ │
│ markdown-parser │
│ Markdown → AST │
│ Streaming / Incremental / Flavour system │
│ Linting / Diagnostics / Post-processors │
└─────────────────────────────────────────────────────────────┘
| Module | Description |
|---|---|
:markdown-parser |
Core parsing engine — Markdown string → AST. Streaming, incremental, multi-flavour. |
:markdown-renderer |
Rendering engine — AST → Compose UI. Theming, image loading, code highlighting. |
:markdown-preview |
Interactive showcase — categorized demo of all supported features. |
:composeApp |
Cross-platform demo app (Android/iOS/Desktop/Web). |
:androidApp |
Android-specific demo app. |
| Spec | Status |
|---|---|
| CommonMark 0.31.2 | 652/652 (100%) ✅ |
| GFM 0.29 | Tables, task lists, strikethrough, autolinks ✅ |
| Markdown Extra | Footnotes, definition lists, abbreviations, fenced code ✅ |
Configure which syntax features to enable:
// Strict CommonMark — no extensions
val doc = MarkdownParser(CommonMarkFlavour).parse(input)
// GFM — CommonMark + tables, strikethrough, autolinks
val doc = MarkdownParser(GFMFlavour).parse(input)
// Extended (default) — everything enabled
val doc = MarkdownParser().parse(input)
// One-shot HTML rendering
val html = HtmlRenderer.renderMarkdown(input, flavour = CommonMarkFlavour)Built-in Coil3 handles images automatically. Need custom rendering? Easy:
Markdown(
markdown = markdownText,
imageContent = { data, modifier ->
// data.url, data.altText, data.width, data.height available
AsyncImage(
model = data.url,
contentDescription = data.altText,
modifier = modifier,
)
},
)Supports size specification in Markdown:

# Android
./gradlew :composeApp:assembleDebug
# Desktop (JVM)
./gradlew :composeApp:run
# Web (Wasm)
./gradlew :composeApp:wasmJsBrowserDevelopmentRun
# Web (JS)
./gradlew :composeApp:jsBrowserDevelopmentRun
# iOS — open iosApp/ in Xcode./gradlew :markdown-parser:jvmTest # Parser tests
./gradlew :markdown-renderer:jvmTest # Renderer tests
./gradlew jvmTest # All tests| # | Category | Coverage |
|---|---|---|
| 1 | Headings | 17/17 (100%) |
| 2 | Paragraphs | 5/5 (100%) |
| 3 | Code Blocks | 17/17 (100%) |
| 4 | Block Quotes | 8/8 (100%) |
| 5 | Lists | 20/20 (100%) |
| 6 | Thematic Breaks | 6/6 (100%) |
| 7 | Tables (GFM) | 11/11 (100%) |
| 8 | HTML Blocks | 10/10 (100%) |
| 9 | Link References | 12/12 (100%) |
| 10 | Block Extensions | 85/85 (100%) |
| 11 | Emphasis | 13/13 (100%) |
| 12 | Strikethrough | 4/4 (100%) |
| 13 | Inline Code | 8/8 (100%) |
| 14 | Links | 27/27 (100%) |
| 15 | Images | 17/17 (100%) |
| 16 | Inline HTML | 8/8 (100%) |
| 17 | Escapes & Entities | 10/10 (100%) |
| 18 | Line Breaks | 5/5 (100%) |
| 19 | Inline Extensions | 50/50 (100%) |
| 20 | Streaming Engine | 27/27 (100%) |
| 21 | Character & Encoding | 10/10 (100%) |
| 22 | HTML Generator | 12/12 (100%) |
| 23 | Linting / WCAG | 19/19 (100%) |
| 24 | Directives | 8/8 (100%) |
| Total | 372/372 (100%) |
📖 Full details: PARSER_COVERAGE_ANALYSIS.md
MIT License · Copyright (c) 2026 huarangmeng
This project is licensed under the MIT License — see the LICENSE file for details.