
High-performance Markdown parser and renderer with AST-based incremental parsing, streaming (LLM) rendering, built-in image loading, LaTeX math, customizable themes, pagination, and broad CommonMark/GFM extensions support.
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 = "1.2.1"
[libraries]
markdown-parser = { module = "io.github.huarangmeng:markdown-parser", version.ref = "markdown" }
markdown-runtime = { module = "io.github.huarangmeng:markdown-runtime", version.ref = "markdown" }
markdown-renderer = { module = "io.github.huarangmeng: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, anddiagram-renderfor Mermaid / PlantUML / DOT blocks, 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.directive.args.getValue("url"),
poster = scope.directive.args["poster"],
title = scope.directive.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)
}
}DirectiveBlockRenderScope and DirectiveInlineRenderScope are snapshot-based.
Use scope.directive as the single structured entry point for directive data.
For HTML export, HtmlDirectiveFallback and HtmlInlineDirectiveFallback also receive
snapshot objects instead of parser AST nodes.
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-preview │
│ Interactive demo & showcase │
├─────────────────────────────────────────────────────────────┤
│ markdown-renderer │
│ AST → Compose UI / HTML │
│ Block/Inline renderers, theme, images │
├─────────────────────────────────────────────────────────────┤
│ markdown-runtime │
│ Directive plugins / input transforms / runtime pipeline │
├─────────────────────────────────────────────────────────────┤
│ 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-runtime |
Directive runtime layer — plugin registry, input transforms, directive pipeline, source maps. |
: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
markdown-renderer integrates the following external libraries for math, code highlighting, and diagrams:
| Capability | Maven modules used by this project | Repository |
|---|---|---|
| LaTeX math |
io.github.huarangmeng:latex-base, io.github.huarangmeng:latex-parser, io.github.huarangmeng:latex-renderer
|
huarangmeng/latex |
| Code highlighting |
io.github.huarangmeng:codehighlight-parser, io.github.huarangmeng:codehighlight-render
|
huarangmeng/codehigh |
| Diagram blocks |
io.github.huarangmeng:diagram-core, io.github.huarangmeng:diagram-layout, io.github.huarangmeng:diagram-parser, io.github.huarangmeng:diagram-render
|
huarangmeng/diagram |
If you only need a subset of the rendering stack, you can depend on the corresponding upstream library directly.
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 = "1.2.1"
[libraries]
markdown-parser = { module = "io.github.huarangmeng:markdown-parser", version.ref = "markdown" }
markdown-runtime = { module = "io.github.huarangmeng:markdown-runtime", version.ref = "markdown" }
markdown-renderer = { module = "io.github.huarangmeng: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, anddiagram-renderfor Mermaid / PlantUML / DOT blocks, 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.directive.args.getValue("url"),
poster = scope.directive.args["poster"],
title = scope.directive.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)
}
}DirectiveBlockRenderScope and DirectiveInlineRenderScope are snapshot-based.
Use scope.directive as the single structured entry point for directive data.
For HTML export, HtmlDirectiveFallback and HtmlInlineDirectiveFallback also receive
snapshot objects instead of parser AST nodes.
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-preview │
│ Interactive demo & showcase │
├─────────────────────────────────────────────────────────────┤
│ markdown-renderer │
│ AST → Compose UI / HTML │
│ Block/Inline renderers, theme, images │
├─────────────────────────────────────────────────────────────┤
│ markdown-runtime │
│ Directive plugins / input transforms / runtime pipeline │
├─────────────────────────────────────────────────────────────┤
│ 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-runtime |
Directive runtime layer — plugin registry, input transforms, directive pipeline, source maps. |
: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
markdown-renderer integrates the following external libraries for math, code highlighting, and diagrams:
| Capability | Maven modules used by this project | Repository |
|---|---|---|
| LaTeX math |
io.github.huarangmeng:latex-base, io.github.huarangmeng:latex-parser, io.github.huarangmeng:latex-renderer
|
huarangmeng/latex |
| Code highlighting |
io.github.huarangmeng:codehighlight-parser, io.github.huarangmeng:codehighlight-render
|
huarangmeng/codehigh |
| Diagram blocks |
io.github.huarangmeng:diagram-core, io.github.huarangmeng:diagram-layout, io.github.huarangmeng:diagram-parser, io.github.huarangmeng:diagram-render
|
huarangmeng/diagram |
If you only need a subset of the rendering stack, you can depend on the corresponding upstream library directly.
MIT License · Copyright (c) 2026 huarangmeng
This project is licensed under the MIT License — see the LICENSE file for details.