
Markdown renderer with predictable AST, safe link/image defaults, extensible style model, admonitions, footnotes, syntax-highlighted code blocks, streaming-friendly debounced parsing, and pluggable image loading.
Compose Multiplatform Markdown renderer. Targets Android, iOS, Desktop (JVM), and wasmJs.
0.9.0
orca-core) and Compose renderer (orca-compose)orca-core
org.jetbrains:markdown (intellij-markdown, GFM flavour)orca-compose
OrcaDocument
OrcaStyle)sample-app
// Kotlin Multiplatform (commonMain)
implementation("ru.wertik:orca-core:0.9.0")
implementation("ru.wertik:orca-compose:0.9.0")Gradle resolves platform-specific artifacts automatically (orca-core-jvm, orca-compose-android, etc.).
import ru.wertik.orca.core.OrcaMarkdownParser
import ru.wertik.orca.core.OrcaParser
val parser: OrcaParser = OrcaMarkdownParser()
val document = parser.parse(markdown)
OrcaMarkdownParserusesorg.jetbrains:markdownand is available incommonMain(Android, iOS, Desktop, wasmJs).
val parser = OrcaMarkdownParser()
val document = parser.parseCached(
key = "message-42",
input = markdown,
)Use a stable key per message/item to avoid repeated AST rebuilds across recompositions and list reuse.
val parser = OrcaMarkdownParser(maxTreeDepth = 32)
val result = parser.parseWithDiagnostics(markdown)
val document = result.document
val warnings = result.diagnostics.warnings
val errors = result.diagnostics.errorsimport ru.wertik.orca.compose.Orca
import ru.wertik.orca.compose.OrcaRootLayout
import ru.wertik.orca.core.OrcaMarkdownParser
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
parseCacheKey = "message-42",
rootLayout = OrcaRootLayout.COLUMN, // use when parent already controls scrolling
securityPolicy = OrcaSecurityPolicies.Default,
onLinkClick = { url ->
// open via your app policy
},
onParseDiagnostics = { diagnostics ->
// observe warnings/errors if needed
},
)import ru.wertik.orca.compose.Orca
Orca(
document = document,
)For token-by-token streaming (e.g. LLM responses), Orca debounces parse operations to avoid redundant work:
Orca(
markdown = streamingMarkdown, // updated on every token
parser = OrcaMarkdownParser(),
parseCacheKey = "message-42",
streamingDebounceMs = 80, // default; set 0 to disable
)During fast updates, only the latest markdown value is parsed after the debounce window. The first render is always synchronous (no empty frame), so items in a scrollable list appear at their correct size immediately.
fun interface OrcaParser {
fun parse(input: String): OrcaDocument
fun parseWithDiagnostics(input: String): OrcaParseResult
fun parseCached(key: Any, input: String): OrcaDocument
fun parseCachedWithDiagnostics(key: Any, input: String): OrcaParseResult
}OrcaMarkdownParser options:
OrcaMarkdownParser(
maxTreeDepth = 64,
cacheSize = 64,
enableSuperscript = true, // set false to disable ^text^ parsing
enableSubscript = true, // set false to disable ~text~ parsing
onDepthLimitExceeded = { depth ->
// observe depth truncation if needed
},
)Diagnostics model:
data class OrcaParseResult(
val document: OrcaDocument,
val diagnostics: OrcaParseDiagnostics,
)---)> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION])Term + : Definition)^text^)~text~)^[...]
\n):smile:, :rocket:, :fire:, etc.)*[ABBR]: Full Title)GFMFlavourDescriptor from org.jetbrains:markdown
https://example.com)[^label] and inline ^[...])--- ... ---)+++ ... +++)LazyColumn root for long documentsOrcaRootLayout.LAZY_COLUMN or OrcaRootLayout.COLUMN
Dispatchers.Default)[n]) to jump to definition↩) to return to source blockblockOverride parameterimageContent composable to replace built-in Coil loader<b>, <i>, <s>, <u>, <code>, <a>, <sup>, <sub>, <mark>, <br>, <p>, <h1>-<h6>, <li>, <hr>, <blockquote>, <pre>
&, <, >, ", , etc.)Use OrcaStyle as a single configuration object:
typographyinlinelayoutquotecodetablethematicBreakimageinlineImageadmonitiondefinitionList// Automatically picks light or dark style based on system theme
val style = OrcaDefaults.adaptiveStyle() // @Composableimport androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import ru.wertik.orca.compose.Orca
import ru.wertik.orca.compose.OrcaCodeBlockStyle
import ru.wertik.orca.compose.OrcaStyle
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
style = OrcaStyle(
code = OrcaCodeBlockStyle(
background = Color(0xFFF8F9FB),
borderColor = Color(0xFFD0D7DE),
borderWidth = 1.dp,
),
),
)http, https, mailto
http, https
OrcaSecurityPolicy.Custom policy example:
val policy = OrcaSecurityPolicies.byAllowedSchemes(
linkSchemes = setOf("https", "myapp"),
imageSchemes = setOf("https"),
allowRelativeLinks = true,
allowRelativeImages = true,
)
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
securityPolicy = policy,
)Always keep your own URL-opening policy in onLinkClick.
| Platform | orca-core | orca-compose | Parser |
|---|---|---|---|
| Android | commonMain + jvmMain | full | OrcaMarkdownParser |
| Desktop (JVM) | commonMain + jvmMain | full | OrcaMarkdownParser |
| iOS | commonMain | full | OrcaMarkdownParser |
| wasmJs (Web) | commonMain | full | OrcaMarkdownParser |
Override how specific block types are rendered:
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
blockOverride = mapOf(
OrcaBlock.CodeBlock::class to { block ->
val code = block as OrcaBlock.CodeBlock
MyCustomCodeBlock(code = code.code, language = code.language)
},
),
)Replace the built-in Coil image loader with your own:
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
imageContent = { url, contentDescription ->
// Use Glide, Kamel, or any custom loader
GlideImage(model = url, contentDescription = contentDescription)
},
)Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
style = OrcaStyle(
admonition = OrcaAdmonitionStyle(
collapsible = true,
collapsedByDefault = false,
),
),
)./gradlew --no-daemon --build-cache :orca-core:jvmTest :orca-compose:testDebugUnitTest :sample-app:assembleDebugFor release-like check:
./gradlew --no-daemon --build-cache :sample-app:assembleRelease :sample-app:bundleRelease0.8.0
-alpha, -beta, -rc
MIT. See LICENSE.
Compose Multiplatform Markdown renderer. Targets Android, iOS, Desktop (JVM), and wasmJs.
0.9.0
orca-core) and Compose renderer (orca-compose)orca-core
org.jetbrains:markdown (intellij-markdown, GFM flavour)orca-compose
OrcaDocument
OrcaStyle)sample-app
// Kotlin Multiplatform (commonMain)
implementation("ru.wertik:orca-core:0.9.0")
implementation("ru.wertik:orca-compose:0.9.0")Gradle resolves platform-specific artifacts automatically (orca-core-jvm, orca-compose-android, etc.).
import ru.wertik.orca.core.OrcaMarkdownParser
import ru.wertik.orca.core.OrcaParser
val parser: OrcaParser = OrcaMarkdownParser()
val document = parser.parse(markdown)
OrcaMarkdownParserusesorg.jetbrains:markdownand is available incommonMain(Android, iOS, Desktop, wasmJs).
val parser = OrcaMarkdownParser()
val document = parser.parseCached(
key = "message-42",
input = markdown,
)Use a stable key per message/item to avoid repeated AST rebuilds across recompositions and list reuse.
val parser = OrcaMarkdownParser(maxTreeDepth = 32)
val result = parser.parseWithDiagnostics(markdown)
val document = result.document
val warnings = result.diagnostics.warnings
val errors = result.diagnostics.errorsimport ru.wertik.orca.compose.Orca
import ru.wertik.orca.compose.OrcaRootLayout
import ru.wertik.orca.core.OrcaMarkdownParser
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
parseCacheKey = "message-42",
rootLayout = OrcaRootLayout.COLUMN, // use when parent already controls scrolling
securityPolicy = OrcaSecurityPolicies.Default,
onLinkClick = { url ->
// open via your app policy
},
onParseDiagnostics = { diagnostics ->
// observe warnings/errors if needed
},
)import ru.wertik.orca.compose.Orca
Orca(
document = document,
)For token-by-token streaming (e.g. LLM responses), Orca debounces parse operations to avoid redundant work:
Orca(
markdown = streamingMarkdown, // updated on every token
parser = OrcaMarkdownParser(),
parseCacheKey = "message-42",
streamingDebounceMs = 80, // default; set 0 to disable
)During fast updates, only the latest markdown value is parsed after the debounce window. The first render is always synchronous (no empty frame), so items in a scrollable list appear at their correct size immediately.
fun interface OrcaParser {
fun parse(input: String): OrcaDocument
fun parseWithDiagnostics(input: String): OrcaParseResult
fun parseCached(key: Any, input: String): OrcaDocument
fun parseCachedWithDiagnostics(key: Any, input: String): OrcaParseResult
}OrcaMarkdownParser options:
OrcaMarkdownParser(
maxTreeDepth = 64,
cacheSize = 64,
enableSuperscript = true, // set false to disable ^text^ parsing
enableSubscript = true, // set false to disable ~text~ parsing
onDepthLimitExceeded = { depth ->
// observe depth truncation if needed
},
)Diagnostics model:
data class OrcaParseResult(
val document: OrcaDocument,
val diagnostics: OrcaParseDiagnostics,
)---)> [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION])Term + : Definition)^text^)~text~)^[...]
\n):smile:, :rocket:, :fire:, etc.)*[ABBR]: Full Title)GFMFlavourDescriptor from org.jetbrains:markdown
https://example.com)[^label] and inline ^[...])--- ... ---)+++ ... +++)LazyColumn root for long documentsOrcaRootLayout.LAZY_COLUMN or OrcaRootLayout.COLUMN
Dispatchers.Default)[n]) to jump to definition↩) to return to source blockblockOverride parameterimageContent composable to replace built-in Coil loader<b>, <i>, <s>, <u>, <code>, <a>, <sup>, <sub>, <mark>, <br>, <p>, <h1>-<h6>, <li>, <hr>, <blockquote>, <pre>
&, <, >, ", , etc.)Use OrcaStyle as a single configuration object:
typographyinlinelayoutquotecodetablethematicBreakimageinlineImageadmonitiondefinitionList// Automatically picks light or dark style based on system theme
val style = OrcaDefaults.adaptiveStyle() // @Composableimport androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import ru.wertik.orca.compose.Orca
import ru.wertik.orca.compose.OrcaCodeBlockStyle
import ru.wertik.orca.compose.OrcaStyle
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
style = OrcaStyle(
code = OrcaCodeBlockStyle(
background = Color(0xFFF8F9FB),
borderColor = Color(0xFFD0D7DE),
borderWidth = 1.dp,
),
),
)http, https, mailto
http, https
OrcaSecurityPolicy.Custom policy example:
val policy = OrcaSecurityPolicies.byAllowedSchemes(
linkSchemes = setOf("https", "myapp"),
imageSchemes = setOf("https"),
allowRelativeLinks = true,
allowRelativeImages = true,
)
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
securityPolicy = policy,
)Always keep your own URL-opening policy in onLinkClick.
| Platform | orca-core | orca-compose | Parser |
|---|---|---|---|
| Android | commonMain + jvmMain | full | OrcaMarkdownParser |
| Desktop (JVM) | commonMain + jvmMain | full | OrcaMarkdownParser |
| iOS | commonMain | full | OrcaMarkdownParser |
| wasmJs (Web) | commonMain | full | OrcaMarkdownParser |
Override how specific block types are rendered:
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
blockOverride = mapOf(
OrcaBlock.CodeBlock::class to { block ->
val code = block as OrcaBlock.CodeBlock
MyCustomCodeBlock(code = code.code, language = code.language)
},
),
)Replace the built-in Coil image loader with your own:
Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
imageContent = { url, contentDescription ->
// Use Glide, Kamel, or any custom loader
GlideImage(model = url, contentDescription = contentDescription)
},
)Orca(
markdown = markdown,
parser = OrcaMarkdownParser(),
style = OrcaStyle(
admonition = OrcaAdmonitionStyle(
collapsible = true,
collapsedByDefault = false,
),
),
)./gradlew --no-daemon --build-cache :orca-core:jvmTest :orca-compose:testDebugUnitTest :sample-app:assembleDebugFor release-like check:
./gradlew --no-daemon --build-cache :sample-app:assembleRelease :sample-app:bundleRelease0.8.0
-alpha, -beta, -rc
MIT. See LICENSE.