Skip to content

Latest commit

 

History

History
1305 lines (1036 loc) · 40.5 KB

File metadata and controls

1305 lines (1036 loc) · 40.5 KB

ComposeMarkdownMultiplatform API Reference

English | 简体中文

Detailed reference for the core composables and configuration types in the markdown-multiplatform module.

  • For an overview, installation and feature tour, see README.md.

Table of Contents


Core Composables

MarkdownView

The main entry point for rendering Markdown content. Parses and renders a Markdown string.

Signature (from MarkdownView.kt):

@Composable
fun MarkdownView(
    text: String,
    modifier: Modifier = Modifier,
    markdownRenderConfig: MarkdownRenderConfig =
        remember { MarkdownRenderConfig.Builder().build() },
    actionHandler: ActionHandler? = null,
    renderDependencies: Map<String, Any> = emptyMap(),
    showNotSupported: Boolean = false,
    isStreaming: Boolean = false,
)

Parameters

  • text: The Markdown text to render.
  • modifier: Standard Compose Modifier for sizing, padding, etc.
  • markdownRenderConfig: Rendering configuration. Remember custom instances at the call site.
  • actionHandler: Optional handler for interactions.
  • renderDependencies: Dependencies exposed to custom renderers and string builders.
  • showNotSupported: Whether unsupported elements display fallback text.
MarkdownView(
    text = "# Hello\n\nThis is **Markdown**.",
    modifier = Modifier.fillMaxSize(),
)

For background parsing, use the overload with a required dispatcher:

MarkdownView(
    text = markdown,
    parseDispatcher = Dispatchers.Default,
    onLoading = { CircularProgressIndicator() },
    onError = { error -> Text(error.message.orEmpty()) },
)

Changing text, the parser, or parseDispatcher cancels the previous parse and starts a new one.

Set isStreaming = true while text grows only by appending. Every successful update reuses all top-level nodes except the previous final block, reparses from that block's source-line start, and rebases the new tail's SourceSpan line/input indices. Earlier edits or missing source spans fall back to a full parse. Set isStreaming = false when the stream completes to force one final full parse. MarkdownText supports the same behavior.

The parser is configured through MarkdownRenderConfig.Builder.streamingMarkdownParserFactory. Each component receives its own stateful parser instance. A custom StreamingMarkdownParser fully controls caching, incremental parsing, source positions, AST merging, fallback, and final parsing; its parsing method receives only the complete content and isStreaming.

The factory is null by default. Without one, isStreaming = true falls back to the normal parser and performs a full parse for every update. Configure ::DefaultStreamingMarkdownParser explicitly to enable the built-in incremental workflow. The default streaming implementation creates its own parser from the render configuration and forces at least block source spans.

For each changed input, a custom streaming parser must return a new root Document instance. Returning the same root object can make Compose treat the parse result as unchanged. Preserve the identity of completed, unchanged top-level child blocks so keyed block renderers can skip recomposing the stable prefix; only the replaced tail should receive new node instances.

val config = MarkdownRenderConfig.Builder()
    .streamingMarkdownParserFactory(::DefaultStreamingMarkdownParser)
    .build()
MarkdownView(
    text = streamedMarkdown,
    parseDispatcher = Dispatchers.Default,
    isStreaming = streamInProgress,
)

LazyMarkdownColumn

Parses the complete Markdown string up front, then renders each top-level block as a lazy item.

@Composable
fun LazyMarkdownColumn(
    text: String,
    modifier: Modifier = Modifier,
    markdownRenderConfig: MarkdownRenderConfig =
        remember { MarkdownRenderConfig.Builder().build() },
    actionHandler: ActionHandler? = null,
    renderDependencies: Map<String, Any> = emptyMap(),
    showNotSupported: Boolean = false,
    lazyListState: LazyListState = rememberLazyListState(),
)
LazyMarkdownColumn(
    text = longMarkdownContent,
    modifier = Modifier.fillMaxSize(),
)

LazyMarkdownView

Incrementally reads and parses a line-oriented source. Parsed top-level nodes far from the viewport are recycled in bounded batches and reloaded by source line range when the user scrolls back.

@Composable
fun LazyMarkdownView(
    source: MarkdownLineSource,
    modifier: Modifier = Modifier,
    markdownRenderConfig: MarkdownRenderConfig =
        remember { MarkdownRenderConfig.Builder().build() },
    actionHandler: ActionHandler? = null,
    renderDependencies: Map<String, Any> = emptyMap(),
    showNotSupported: Boolean = false,
    chunkLoaderConfig: MarkdownChunkLoaderConfig = MarkdownChunkLoaderConfig(),
    nestedPrefetchItemCount: Int = 3,
    lazyListState: LazyListState = rememberLazyListState(),
    onLoadingChanged: (Boolean) -> Unit = {},
    onStateChanged: (LazyMarkdownViewState) -> Unit = {},
    onError: (Throwable) -> Unit = {},
)

An overload accepting text: String is also available and internally uses StringMarkdownLineSource, making it easy to compare the same Markdown text with the Android API.

fun interface MarkdownLineSource {
    suspend fun readLines(startLine: Int, lineCount: Int): List<String>
}

The source uses zero-based line indices, must remain immutable, and must support rereading old ranges. Returning fewer lines than requested signals end-of-source. Use StringMarkdownLineSource for content already held in memory, or implement the interface over a file, asset, database, or range API for true lazy I/O.

MarkdownChunkLoaderConfig controls read batch sizes, minNodesAhead / minNodesBehind watermarks, node and source-line cache limits, minimum recycle batch, and source/parser dispatchers. nestedPrefetchItemCount separately controls Compose lazy-item precomposition. Eviction never removes the visible anchor or its safety margin. Stable source-span keys keep the same first visible item when nodes before it are inserted or removed. If no safe recycle batch is available, the cache may temporarily exceed its target instead of moving the viewport. maxCachedSourceLines is also a hard limit for one unconfirmed trailing block or source context.

val source = remember(markdown) { StringMarkdownLineSource(markdown) }

LazyMarkdownView(
    source = source,
    modifier = Modifier.fillMaxSize(),
    chunkLoaderConfig = MarkdownChunkLoaderConfig(
        initialLineCount = 1000,
        incrementalLineCount = 500,
        minNodesAhead = 100,
        minNodesBehind = 30,
        maxCachedNodes = 500,
        maxCachedSourceLines = 10_000,
    ),
)

onLoadingChanged reports only initial loading while no content is available. Background parsing is silent by default. onStateChanged optionally reports InitialLoading, LoadingBefore, LoadingAfter, and Idle; AST recycling never reports loading.

Chunk parses do not share reference-definition state. Use inline links, or choose LazyMarkdownColumn when full-document semantics are required. Custom parsers must emit unique, source-spanned top-level nodes.

LazyMarkdownView creates a dedicated parser from MarkdownRenderConfig and forces at least IncludeSourceSpans.BLOCKS, even if regular rendering is configured with NONE.


MarkdownContent

Renders a single parsed AST node and dispatches to the appropriate block renderer.

Signature (from MarkdownContent.kt):

@Composable
fun MarkdownContent(
    node: Node,
    modifier: Modifier = Modifier,
)

Typically used internally or within custom IBlockRenderer implementations when you need to recursively render child nodes.


MarkdownChildren

A utility composable for iterating and rendering all children of a parent node with proper spacing.

Signature (from MarkdownContent.kt):

@Composable
fun MarkdownChildren(
    parent: Node,
    modifier: Modifier = Modifier,
    children: List<Node>? = null,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    spacerHeight: Dp = currentTheme().spacerTheme.spacerHeight,
    showSpacer: Boolean = currentTheme().spacerTheme.showSpacer,
    childModifierFactory: (child: Node) -> Modifier = {
        Modifier.wrapContentHeight().fillMaxWidth()
    },
    onBeforeChild: (@Composable (child: Node, parent: Node) -> Unit)? = null,
    onAfterChild: (@Composable (child: Node, parent: Node) -> Unit)? = null,
    onBeforeAll: (@Composable (parent: Node) -> Unit)? = null,
    onAfterAll: (@Composable (parent: Node) -> Unit)? = null,
)

Parameters

  • parent: The Node whose children to render.
  • children: Override list of children to render (defaults to parent's children).
  • spacerHeight: Vertical spacing between children. Defaults to theme.spacerTheme.spacerHeight.
  • showSpacer: Whether to insert spacers. Defaults to theme.spacerTheme.showSpacer.
  • onBeforeChild / onAfterChild: Optional composable callbacks before/after each child.
  • onBeforeAll / onAfterAll: Optional composable callbacks before/after all children.

Use Case

When implementing a custom IBlockRenderer (e.g., a custom container block) and you need to render nested content with standard spacing rules.


MarkdownInlineText

Renders the inline children of a block node as styled text using AnnotatedString.

Signature (from MarkdownInlineText.kt):

@Composable
fun MarkdownInlineText(
    parent: Node,
    modifier: Modifier = Modifier,
    textAlign: TextAlign = TextAlign.Start,
    textStyle: TextStyle? = null,
)

Parameters

  • parent: The block node whose inline children to render as text.
  • textAlign: Text alignment.
  • textStyle: Override text style (defaults to theme style).

Both this component and MarkdownText use rememberMarkdownAnnotatedStringResult(...). The helper reads the active registry from ProvideMarkdownLocals; it does not accept a registry directly. MarkdownText provides the cached text-mode registry as an override, while regular inline rendering uses the base registry from MarkdownRenderConfig.


MarkdownText

Text-based rendering that renders the entire Markdown document through a single RichText composable, enabling cross-paragraph text selection. Unlike MarkdownView which renders each block as a separate composable in a Column, MarkdownText merges text blocks into a single AnnotatedString and embeds non-text blocks (code blocks, block quotes, lists, etc.) as inline content.

Signature (from MarkdownText.kt):

@Composable
fun MarkdownText(
    text: String,
    modifier: Modifier = Modifier,
    markdownRenderConfig: MarkdownRenderConfig =
        remember { MarkdownRenderConfig.Builder().build() },
    actionHandler: ActionHandler? = null,
    renderDependencies: Map<String, Any> = emptyMap(),
    showNotSupported: Boolean = false,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    textAlign: TextAlign? = null,
    maxLines: Int = Int.MAX_VALUE,
    minLines: Int = 1,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    isStreaming: Boolean = false,
    onTextLayout: (TextLayoutResult) -> Unit = {},
)

Parameters

  • text: The Markdown string to parse and render.
  • markdownRenderConfig: The configuration for parsing and rendering.
  • actionHandler: Optional handler for link clicks and other actions.
  • showNotSupported: Whether to show text for unsupported elements.
  • overflow: How visual overflow is handled (TextOverflow.Clip, Ellipsis, etc.).
  • softWrap: Whether to break text at soft line breaks.
  • textAlign: Text alignment.
  • maxLines / minLines: Line count constraints for the rendered text.
  • letterSpacing: Spacing between characters.
  • textDecoration: Text decorations (underline, strikethrough).
  • onTextLayout: Callback invoked with TextLayoutResult after text layout.

Example

SelectionContainer {
    MarkdownText(
        text = markdownContent,
        markdownRenderConfig = config,
        modifier = Modifier.padding(16.dp),
        maxLines = 10,
        overflow = TextOverflow.Ellipsis,
    )
}

The async overload adds a required parseDispatcher plus onLoading and onError:

MarkdownText(
    text = markdownContent,
    parseDispatcher = Dispatchers.Default,
    onLoading = { CircularProgressIndicator() },
    onError = { error -> Text(error.message.orEmpty()) },
)

Configuration

MarkdownRenderConfig

MarkdownRenderConfig holds everything needed for parsing and rendering Markdown:

  • A MarkdownTheme describing typography, colors and component styles.
  • A MarkdownParser (powered by commonmark-kotlin).
  • A RenderRegistry mapping node types to renderers.
  • An IncludeSourceSpans policy. It defaults to BLOCKS and can be changed with the builder.

Instances are created via MarkdownRenderConfig.Builder: When creating one inside a Composable, wrap the complete builder expression in remember so parser and renderer instances remain stable across recompositions. The regular markdownParser is created lazily on first access from the stored extensions and IncludeSourceSpans policy. Dedicated streaming and lazy parsers are created separately when needed.

        fun includeSourceSpans(includeSourceSpans: IncludeSourceSpans): Builder
        fun streamingMarkdownParserFactory(
            factory: ((MarkdownRenderConfig) -> StreamingMarkdownParser)?,
        ): Builder
val config = MarkdownRenderConfig.Builder()
    // configure theme, plugins, renderers...
    .build()

MarkdownRenderConfig.Builder

Builder for customizing parsing, theming and rendering behavior using a fluent API.

Key methods (from MarkdownRenderConfig.kt):

class MarkdownRenderConfig {
    class Builder {
        fun markdownTheme(markdownTheme: MarkdownTheme): Builder
        fun addPlugin(plugin: IMarkdownRenderPlugin): Builder
        fun <T : Node> addInlineNodeStringBuilder(
            nodeClass: KClass<T>,
            builder: IInlineNodeStringBuilder<T>,
        ): Builder
        fun <T : Node> addBlockRenderer(
            nodeClass: KClass<T>,
            renderer: IBlockRenderer<T>,
        ): Builder
        fun addExtension(extension: Extension): Builder
        fun markdownTextRenderer(renderer: MarkdownTextRenderer): Builder
        fun markdownContentRenderer(renderer: MarkdownContentRenderer): Builder
        fun build(): MarkdownRenderConfig
    }
}

markdownTheme(markdownTheme: MarkdownTheme)

Sets the visual theme. If not set, uses default MarkdownTheme().

addPlugin(plugin: IMarkdownRenderPlugin)

Registers a rendering plugin. Plugins can provide custom parser extensions, block renderers and inline string builders.

addInlineNodeStringBuilder / addBlockRenderer

Low-level hooks for customizing rendering of specific node types:

  • addInlineNodeStringBuilder(nodeClass, builder): Defines how an inline node type is converted to styled text spans.
  • addBlockRenderer(nodeClass, renderer): Defines how a block node type is rendered as Compose UI.

addExtension(extension: Extension)

Register a commonmark parser extension directly (e.g., TablesExtension.create()). Extensions registered via plugins are also collected automatically.

markdownTextRenderer / markdownContentRenderer

Advanced overrides:

  • markdownTextRenderer: Override how text nodes are rendered.
  • markdownContentRenderer: Override how content nodes are rendered.

MarkdownTheme

MarkdownTheme is the core theme model controlling how Markdown content appears in Compose.

Data Structure

@Stable
data class MarkdownTheme(
    val breakLineHeight: Dp = 1.dp,
    val breakLineColor: Color = Color(0xFFE0E0E0),
    val textStyle: TextStyle = TextStyle(
        fontSize = 18.sp,
        fontFamily = FontFamily.Default,
        color = Color.Black,
        lineHeight = 20.sp,
    ),
    val strongEmphasis: SpanStyle = SpanStyle(fontWeight = FontWeight.Bold),
    val emphasis: SpanStyle = SpanStyle(fontStyle = FontStyle.Italic),
    val code: TextStyle = TextStyle(
        fontFamily = FontFamily.Monospace,
        fontSize = 14.sp,
        color = Color(0xFF37474F),
        background = Color(0xFFF5F5F5),
    ),
    val strikethrough: SpanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough),
    val subscript: SpanStyle = SpanStyle(baselineShift = Subscript),
    val link: TextLinkStyles = TextLinkStyles(...),
    val headStyle: Map<Int, TextStyle> = mapOf(
        HEAD1 to TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold, lineHeight = 36.sp),
        HEAD2 to TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold, lineHeight = 32.sp),
        HEAD3 to TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 28.sp),
        HEAD4 to TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, lineHeight = 24.sp),
        HEAD5 to TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold, lineHeight = 22.sp),
        HEAD6 to TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, lineHeight = 20.sp),
    ),
    val listTheme: ListTheme = ListTheme(),
    val blockQuoteTheme: BlockQuoteTheme = BlockQuoteTheme(),
    val spacerTheme: SpacerTheme = SpacerTheme(),
    val codeBlockTheme: CodeBlockTheme = CodeBlockTheme(),
) {
    companion object {
        const val HEAD1 = 1
        const val HEAD2 = 2
        // ... through HEAD6
    }
}

Heading Levels

Headings are configured via the headStyle map. Keys are integers 1-6, exposed as constants:

MarkdownTheme.HEAD1 // H1
MarkdownTheme.HEAD2 // H2
// ... through HEAD6

BlockQuoteTheme

@Immutable
data class BlockQuoteTheme(
    val borderColor: Color = Color.LightGray,
    val borderWidth: Dp = 5.dp,
    val backgroundColor: Color = Color(0xFFF5F5F5),
    val shape: Shape = RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp),
    val padding: PaddingValues = PaddingValues(horizontal = 12.dp),
    val textStyle: TextStyle? = TextStyle(fontStyle = FontStyle.Italic),
)

SpacerTheme

@Immutable
data class SpacerTheme(
    val showSpacer: Boolean = true,
    val spacerHeight: Dp = 12.dp,
)

ListTheme

@Immutable
data class ListTheme(
    val markerSpacerWidth: Dp = 4.dp,
    val showSpacerInTightList: Boolean = true,
    val tightListSpacerHeight: Dp = 8.dp,
    val markerTextStyle: TextStyle? = TextStyle(
        lineHeight = 24.sp,
        fontSize = 17.sp,
        textAlign = TextAlign.End,
    ),
)

CodeBlockTheme

@Immutable
data class CodeBlockTheme(
    val backgroundColor: Color = Color.White,
    val shape: Shape = RoundedCornerShape(size = 16.dp),
    val borderColor: Color = Color.LightGray,
    val borderWidth: Dp = 0.5.dp,
    val contentTheme: CodeContentTheme = CodeContentTheme(),
    val codeTitleTextStyle: TextStyle = TextStyle(fontSize = 12.sp, color = Color.Gray),
    val codeCopyTextStyle: TextStyle = TextStyle(fontSize = 12.sp, color = Color.Blue),
    val blockModifier: Modifier = Modifier.padding(vertical = 12.dp),
    val headerModifier: Modifier = Modifier.padding(horizontal = 17.dp),
    val showHeader: Boolean = true,
    val showCopyButton: Boolean = true,
)

CodeContentTheme

@Immutable
data class CodeContentTheme(
    val showLineNumber: Boolean = true,
    val softWrap: Boolean = true,
    val maxLines: Int = Int.MAX_VALUE,
    val minLines: Int = 1,
    val contentPadding: PaddingValues = PaddingValues(4.dp),
    val lineNumberPadding: PaddingValues = PaddingValues(
        start = 4.dp, top = 4.dp, bottom = 4.dp, end = 16.dp,
    ),
    val overflow: TextOverflow = TextOverflow.Clip,
    val codeTextStyle: TextStyle = TextStyle(fontSize = 14.sp, lineHeight = 18.sp),
    val lineNumberTextStyle: TextStyle = TextStyle(
        fontSize = 14.sp, lineHeight = 18.sp, color = Color.Gray, textAlign = TextAlign.End,
    ),
    val modifier: Modifier = Modifier.padding(start = 17.dp, end = 17.dp, top = 17.dp),
    val height: Dp? = null,
    val disableSelection: Boolean = false,
)

Example

val theme = MarkdownTheme(
    textStyle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp),
    headStyle = mapOf(
        MarkdownTheme.HEAD1 to TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold),
    ),
    codeBlockTheme = CodeBlockTheme(
        backgroundColor = Color(0xFF1E1E1E),
        contentTheme = CodeContentTheme(
            codeTextStyle = TextStyle(color = Color.White, fontSize = 14.sp),
        ),
    ),
)

val config = MarkdownRenderConfig.Builder()
    .markdownTheme(theme)
    .build()

Plugins & Extension Points

IMarkdownRenderPlugin

The entry point for adding functionality to the Markdown engine.

interface IMarkdownRenderPlugin {
    fun parserExtensions(): List<Extension> = emptyList()
    fun blockRenderers(): Map<KClass<out Node>, IBlockRenderer<*>> = emptyMap()
    fun inlineNodeStringBuilders(): Map<KClass<out Node>, IInlineNodeStringBuilder<*>> = emptyMap()
}

A plugin can:

  • Provide custom parser extensions via parserExtensions() (e.g., TablesExtension.create() for GFM tables).
  • Register block renderers and inline string builders.

Plugins are added via MarkdownRenderConfig.Builder.addPlugin().

You can also extend AbstractMarkdownRenderPlugin for convenience:

abstract class AbstractMarkdownRenderPlugin : IMarkdownRenderPlugin {
    override fun parserExtensions(): List<Extension> = emptyList()
    override fun blockRenderers(): Map<KClass<out Node>, IBlockRenderer<*>> = emptyMap()
    override fun inlineNodeStringBuilders(): Map<KClass<out Node>, IInlineNodeStringBuilder<*>> = emptyMap()
}

IBlockRenderer

Renders a specific block node as Compose UI.

interface IBlockRenderer<T : Node> {
    @Composable
    fun Invoke(
        node: T,
        modifier: Modifier,
    )
}

Parameters

  • node: The commonmark AST node to render.
  • modifier: Modifier from the parent layout. Implementations should apply it to preserve layout consistency.

Implementation Tips

  • Use MarkdownChildren to render nested children with standard spacing.
  • Access the current theme via currentTheme().
  • Access the action handler via currentActionHandler().

IInlineNodeStringBuilder

Converts an inline node into styled text spans within an AnnotatedString.

interface IInlineNodeStringBuilder<T : Node> {
    fun AnnotatedString.Builder.buildInlineNodeString(
        node: T,
        inlineContentMap: MarkdownInlineViewMap,
        markdownTheme: MarkdownTheme,
        actionHandler: ActionHandlerState?,
        indentLevel: Int,
        isShowNotSupported: Boolean,
        renderRegistry: RenderRegistry,
        nodeStringBuilderContext: NodeStringBuilderContext,
    )
}

Parameters

  • node: The inline AST node.
  • inlineContentMap: Collection associating annotation IDs with MarkdownInlineView values.
  • markdownTheme: Current theme for reading styles.
  • actionHandler: Optional interaction handler for links, etc.
  • renderRegistry: Used for recursively building child node strings.
  • nodeStringBuilderContext: Context providing text measurement, density, clipboard, etc.

Registering inline content

Use the public helper to register the map entry and append its matching embedded or standalone annotation in one operation:

fun AnnotatedString.Builder.appendMarkdownInlineContent(
        id: String,
        inlineContent: RichTextInlineContent,
    inlineContentMap: MarkdownInlineViewMap,
        alternateText: String = "\uFFFD",
        overwrite: Boolean = false,
): String
  • The default overwrite = false preserves an existing entry and assigns the new content a deterministic _1, _2, and so on suffix. The helper returns the actual ID.
  • Pass the node class name as the base ID. The helper's occurrence suffix makes each annotation unique, so adding content, URLs, hashes, or source positions to the base ID is unnecessary. Class-only IDs are shorter, avoid exposing content, and centralize ID generation.
  • overwrite = true replaces the entry under the requested ID. Every earlier or later annotation with that ID then resolves to the replacement. Existing and replacement content must both be embedded or both be standalone; cross-type overwrite is rejected. Use overwrite only for stateless, semantically interchangeable occurrences.
  • When using native appendInlineContent(...) or appendStandaloneInlineTextContent(...), manage IDs manually. Reassigning an ID in the map replaces the content for every annotation with that ID.
appendMarkdownInlineContent(
    id = node::class.simpleName ?: "Node",
        inlineContent = buildStatusInlineContent(node),
        inlineContentMap = inlineContentMap,
        alternateText = "[${node.status}]",
)

Helper class:

open class CompositeChildNodeStringBuilder : IInlineNodeStringBuilder<Node> {
    open fun getSpanStyle(node: Node, markdownTheme: MarkdownTheme): SpanStyle? = null
    open fun getParagraphStyle(node: Node, markdownTheme: MarkdownTheme): ParagraphStyle? = null
}

MarkdownInlineView

Represents inline composable content that can be embedded within text.

sealed interface MarkdownInlineView {
    data class MarkdownRichTextInlineContent(
        val inlineContent: RichTextInlineContent,
    ) : MarkdownInlineView
}

RichTextInlineContent has two variants:

  • EmbeddedRichTextInlineContent -- Small inline elements (icons, badges) that sit within the text flow.
  • StandaloneInlineContent -- Full-width block elements (cards, media) rendered as separate sections.

PlaceholderTextUnitConverter

Adaptive inline content converts measured pixel dimensions to sp through the process-wide PlaceholderTextUnitConverter proxy:

object PlaceholderTextUnitConverter {
    var delegate: (density: Density, px: Int) -> TextUnit
    fun convert(density: Density, px: Int): TextUnit
    fun reset()
}

The default delegate uses Compose's native Density.toSp conversion on every platform. Compose 1.10 is the minimum supported Android baseline, so no compatibility switch is required.

For a custom conversion, replace the delegate directly:

PlaceholderTextUnitConverter.delegate = { density, px ->
    with(density) { px.toSp() }
}

Call PlaceholderTextUnitConverter.reset() to restore the native default. The delegate is global to the process, so do not change it during composition.


RenderRegistry & Core Renderers

RenderRegistry is built during MarkdownRenderConfig.Builder.build() and determines how each node type is rendered.

data class RenderRegistry(
    private val blockRenderers: Map<KClass<out Node>, IBlockRenderer<*>>,
    private val inlineNodeStringBuilders: Map<KClass<out Node>, IInlineNodeStringBuilder<*>>,
    val markdownContentRenderer: MarkdownContentRenderer? = null,
    val markdownTextRenderer: MarkdownTextRenderer? = null,
) {
    fun getBlockRenderer(nodeClass: KClass<out Node>): IBlockRenderer<*>?
    fun getInlineNodeStringBuilder(nodeClass: KClass<out Node>): IInlineNodeStringBuilder<*>?
}

You typically interact with it indirectly via Builder.addBlockRenderer(...) and Builder.addInlineNodeStringBuilder(...).

Custom renderer interfaces:

fun interface MarkdownContentRenderer {
    @Composable
    operator fun invoke(node: Node, modifier: Modifier)
}

fun interface MarkdownTextRenderer {
    @Composable
    operator fun invoke(
        parent: Node,
        modifier: Modifier,
        textAlign: TextAlign,
        textStyle: TextStyle?,
    )
}

CodeBlockRenderer (code blocks)

File: core/renders/CodeBlockRenderer.kt

This module contains the composable renderer for fenced and indented code blocks:

  • CodeAnnotator – fun interface for transforming raw code text into a styled AnnotatedString (e.g. syntax highlighting, diff coloring).
  • CodeBlockRendererIBlockRenderer combining a header (language label + copy button) and code content (line numbers, optional scroll).
fun interface CodeAnnotator {
    fun annotate(code: String, language: String, node: Node): AnnotatedString
}

class CodeBlockRenderer(
    private val codeAnnotator: CodeAnnotator? = null,
) : IBlockRenderer<Node>

CodeBlockRenderer reads layout and style from MarkdownTheme.codeBlockTheme:

  • backgroundColor, borderWidth, borderColor, shape, blockModifier, headerModifier
  • showHeader, showCopyButton, codeTitleTextStyle, codeCopyTextStyle
  • contentTheme (code font, line numbers, padding, height, softWrap, etc.)

Syntax highlighting with BasicSyntaxHighlighter

The library ships a ready-made CodeAnnotator that applies regex-based token coloring for 20+ languages. Pass a BasicSyntaxHighlighter instance to enable it:

val config = MarkdownRenderConfig.Builder()
    .addBlockRenderer(
        FencedCodeBlock::class,
        CodeBlockRenderer(codeAnnotator = BasicSyntaxHighlighter()),
    )
    .addBlockRenderer(
        IndentedCodeBlock::class,
        CodeBlockRenderer(codeAnnotator = BasicSyntaxHighlighter()),
    )
    .build()

Customize colors via CodeColors:

val highlighter = BasicSyntaxHighlighter(
    colors = CodeColors(
        keyword    = Color(0xFF569CD6),
        string     = Color(0xFFCE9178),
        comment    = Color(0xFF6A9955),
        number     = Color(0xFFB5CEA8),
        annotation = Color(0xFFDCDC00),
        type       = Color(0xFF4EC9B0),
    )
)

Supported languages (fenced code fence info string, case-insensitive): kotlin, java, javascript/js, typescript/ts, python, swift, rust, go, dart, c, cpp/c++, cs/csharp/c#, ruby, php, sql, bash/sh/shell/zsh, css, html, xml, yaml/yml, toml, json.

Implement CodeAnnotator directly to integrate any third-party highlighter:

val config = MarkdownRenderConfig.Builder()
    .addBlockRenderer(
        FencedCodeBlock::class,
        CodeBlockRenderer(
            codeAnnotator = CodeAnnotator { code, language, _ ->
                myHighlighter.highlight(code, language)
            },
        ),
    )
    .build()

Plugin Modules

TableMarkdownPlugin

Provides GFM table rendering support. Uses commonmark-kotlin-ext-gfm-tables for parsing.

Module: markdown-multiplatform-table

class TableMarkdownPlugin(
    private val tableTheme: TableTheme = TableTheme(),
) : IMarkdownRenderPlugin

TableTheme:

data class TableTheme(
    val borderColor: Color = Color.Gray,
    val borderThickness: Dp = 1.dp,
    val titleBackgroundColor: Color = Color.LightGray,
    val tableHeaderBackgroundColor: Color = Color.White,
    val tableCellBackgroundColor: Color = Color.White,
    val cellTextStyle: TextStyle? = null,
    val headerTextStyle: TextStyle? = TextStyle(fontWeight = FontWeight.Bold),
    val copyTextStyle: TextStyle = TextStyle(fontSize = 12.sp, color = Color.Black),
    val shape: Shape = RoundedCornerShape(8.dp),
    val cellPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
)

Custom renderers:

fun interface TableWidgetRenderer {
    @Composable
    operator fun invoke(node: Node, modifier: Modifier)
}

class TableRenderer(
    private val tableTheme: TableTheme = TableTheme(),
    tableTitleRenderer: TableWidgetRenderer? = null,
    tableCellRenderer: TableWidgetRenderer? = null,
) : IBlockRenderer<TableBlock>

Example:

// Default
val config = MarkdownRenderConfig.Builder()
    .addPlugin(TableMarkdownPlugin())
    .build()

// Custom theme
val config = MarkdownRenderConfig.Builder()
    .addPlugin(TableMarkdownPlugin(
        tableTheme = TableTheme(
            borderColor = Color.Blue,
            borderThickness = 2.dp,
        )
    ))
    .build()

ImageMarkdownPlugin

Provides Markdown image rendering support.

Module: markdown-multiplatform-image

class ImageMarkdownPlugin(
    private val imageTheme: ImageTheme = ImageTheme(),
    private val loadingView: ImageWidgetRenderer = LoadingImageWidgetRenderer(),
    errorView: ImageWidgetRenderer? = null,
) : IMarkdownRenderPlugin

ImageTheme:

@Immutable
data class ImageTheme(
    val alignment: Alignment = Alignment.Center,
    val contentScale: ContentScale = ContentScale.Inside,
    val shape: Shape = RoundedCornerShape(8.dp),
    val modifier: Modifier = Modifier,
    val errorPlaceholderColor: Color = Color(0xFFE0E0E0),
)

Custom loading/error views:

fun interface ImageWidgetRenderer {
    @Composable
    operator fun invoke(
        url: String,
        contentDescription: String?,
        node: Node,
        modifier: Modifier,
    )
}

Example:

val config = MarkdownRenderConfig.Builder()
    .addPlugin(ImageMarkdownPlugin(
        imageTheme = ImageTheme(
            contentScale = ContentScale.Crop,
            shape = RoundedCornerShape(16.dp),
        )
    ))
    .build()

HtmlMarkdownPlugin

Provides HTML inline tag support within Markdown content.

Module: markdown-multiplatform-html

class HtmlMarkdownPlugin(
    customTagHandlers: List<HtmlInlineTagHandler> = emptyList(),
) : IMarkdownRenderPlugin

Default supported tags: <b>, <strong>, <i>, <em>, <u>, <ins>, <del>, <s>, <strike>, <a>, <span> (with inline CSS style support).

Custom tag handler interface:

interface HtmlInlineTagHandler {
    val tagNames: Set<String>

    fun onOpenTag(
        tagName: String,
        rawTag: String,
        builder: AnnotatedString.Builder,
        context: HtmlInlineTagContext,
    )

    fun onCloseTag(
        tagName: String,
        builder: AnnotatedString.Builder,
        context: HtmlInlineTagContext,
    ) {
        builder.pop()
    }
}

HtmlInlineTagContext:

data class HtmlInlineTagContext(
    val node: Node,
    val inlineContentMap: MarkdownInlineViewMap,
    val markdownTheme: MarkdownTheme,
    val actionHandler: ActionHandlerState?,
    val indentLevel: Int,
    val isShowNotSupported: Boolean,
    val renderRegistry: RenderRegistry,
    val nodeStringBuilderContext: NodeStringBuilderContext,
)

Example:

// Default
val config = MarkdownRenderConfig.Builder()
    .addPlugin(HtmlMarkdownPlugin())
    .build()

// Custom <mark> tag handler
class MarkTagHandler : HtmlInlineTagHandler {
    override val tagNames = setOf("mark")

    override fun onOpenTag(
        tagName: String,
        rawTag: String,
        builder: AnnotatedString.Builder,
        context: HtmlInlineTagContext,
    ) {
        builder.pushStyle(SpanStyle(background = Color.Yellow))
    }
}

val config = MarkdownRenderConfig.Builder()
    .addPlugin(HtmlMarkdownPlugin(customTagHandlers = listOf(MarkTagHandler())))
    .build()

Other APIs

ActionHandler

Interface for handling user interactions within rendered Markdown content.

interface ActionHandler {
    fun handleUrlClick(url: String, node: Node) {}
    fun handleCopyClick(node: Node) {}
    fun handleImageClick(imageUrl: String, node: Node) {}
    fun handleCustomEvent(event: CustomEvent, node: Node) {}
}

interface CustomEvent

Example:

val handler = object : ActionHandler {
    override fun handleUrlClick(url: String, node: Node) {
        // Open URL in browser
    }
    override fun handleCopyClick(node: Node) {
        // Copy code block content
    }
}

MarkdownView(
    text = markdownContent,
    actionHandler = handler,
)

NodeStringBuilderContext

Provides context for inline node string builders, including text measurement, styles, and system capabilities.

data class NodeStringBuilderContext(
    val parser: MarkdownParser,
    val layoutContext: TextLayoutContext,
    val designContext: TextStyleContext,
    val systemContext: SystemContext,
    val renderDependencies: Map<String, Any>,
)

data class TextLayoutContext(
    val density: Density,
    val textMeasurer: TextMeasurer,
    val textAlign: TextAlign,
    val sizeConstraints: TextSizeConstraints,
)

data class TextStyleContext(
    val contentColor: Color,
    val textSelectionColors: TextSelectionColors,
    val textStyle: TextStyle,
    val fontFamilyResolver: FontFamily.Resolver,
    val layoutDirection: LayoutDirection,
)

data class SystemContext(
    val clipboard: Clipboard,
    val uriHandler: UriHandler,
    val hapticFeedback: HapticFeedback,
    val softwareKeyboardController: SoftwareKeyboardController?,
    val focusManager: FocusManager,
    val coroutineScope: CoroutineScope,
)

Composition Local Accessors

Convenience functions for accessing current rendering context within custom renderers:

@Composable @ReadOnlyComposable fun currentTheme(): MarkdownTheme
@Composable @ReadOnlyComposable fun currentParser(): MarkdownParser
@Composable @ReadOnlyComposable fun currentRenderRegistry(): RenderRegistry
@Composable @ReadOnlyComposable fun currentActionHandler(): ActionHandlerState
@Composable @ReadOnlyComposable fun currentRenderDependencies(): Map<String, Any>
@Composable @ReadOnlyComposable fun isShowNotSupported(): Boolean

All top-level rendering components accept a renderDependencies map. Composable renderers read it with currentRenderDependencies(), and non-Composable node string builders read the same map from nodeStringBuilderContext.renderDependencies.

actionHandler is provided internally as a stable ActionHandlerState updated with rememberUpdatedState. Node string builders and interaction listeners keep that State and read its value only when an event occurs, so changing the external handler does not rebuild annotated text. renderDependencies and the unsupported-content flag remain direct values because they affect the rendered result and must invalidate rendering when changed.

Custom nodes used by text-mode block rendering can implement NodeContentHashProvider. Override contentHash() with only fields that affect rendering; stateless nodes can use the class-name-based default implementation.


Common Usage Patterns

Small to Medium Markdown Content

  • Use MarkdownView with default configuration.
MarkdownView(text = markdownContent)

Large Scrollable Documents

  • Use LazyMarkdownColumn when the whole Markdown string can be parsed up front.
LazyMarkdownColumn(
    text = longContent,
    markdownRenderConfig = config,
    modifier = Modifier.fillMaxSize(),
)
  • Use LazyMarkdownView when source I/O, parsing, and AST memory must also be incremental.
LazyMarkdownView(
    source = markdownLineSource,
    markdownRenderConfig = config,
    modifier = Modifier.fillMaxSize(),
)

Full-Featured Rendering

  • Enable multiple plugins for tables, images, and HTML support.
val config = MarkdownRenderConfig.Builder()
    .addPlugin(TableMarkdownPlugin())
    .addPlugin(ImageMarkdownPlugin())
    .addPlugin(HtmlMarkdownPlugin())
    .build()

MarkdownView(
    text = richContent,
    markdownRenderConfig = config,
)

Advanced Customization

  • Create a shared MarkdownRenderConfig:
    • Set markdownTheme to match your design system.
    • Add plugins for extended syntax.
    • Register custom block renderers or inline builders as needed.
    • Use addExtension() for custom commonmark parser extensions.