Skip to content

feat: render StreamingMarkdownState#575

Merged
mikepenz merged 10 commits into
mikepenz:developfrom
Stream29:develop
Jun 7, 2026
Merged

feat: render StreamingMarkdownState#575
mikepenz merged 10 commits into
mikepenz:developfrom
Stream29:develop

Conversation

@Stream29

@Stream29 Stream29 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Description

This PR implements a StreamingMarkdownState and its rendering logic.

Fixes #315

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no API changes)
  • Build configuration change
  • Other (please describe):

How Has This Been Tested?

I built a tiny streaming markdown rendering benchmark with ~1000 char document.
The document was splited in Flow<String> and every chunk rerender completeded in 1ms.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Copilot AI review requested due to automatic review settings June 6, 2026 02:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces a streaming-friendly markdown parsing/rendering API to support append-only content updates (e.g., live/streamed markdown), and wires it into Compose + Material2/3 surfaces.

Changes:

  • Added StreamingMarkdownState with append-only updates, snapshot/link StateFlows, and Compose helpers to collect streaming input.
  • Integrated streaming rendering paths into Markdown, LazyMarkdownSuccess, and Material 2/3 wrappers.
  • Updated link-definition lookup to accept CharSequence, added streaming state tests, and bumped markdown dependency + added coroutines deps.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/StreamingMarkdownState.kt New streaming state API, append implementation, and Compose helpers.
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/Markdown.kt New Markdown(StreamingMarkdownState, …) overload and streaming success renderer.
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/LazyMarkdown.kt Added streaming variant of LazyMarkdownSuccess.
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/compose/MarkdownExtension.kt Refactored renderer entrypoint to accept CharSequence via internal function.
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/utils/Extensions.kt Link definition lookup now takes CharSequence.
multiplatform-markdown-renderer/src/commonMain/kotlin/com/mikepenz/markdown/model/MarkdownState.kt Deprecated Flow<String>.asMarkdownState() in favor of streaming API.
multiplatform-markdown-renderer/src/commonTest/kotlin/com/mikepenz/markdown/model/StreamingMarkdownStateTest.kt Added tests covering streaming append behavior and flows.
multiplatform-markdown-renderer/build.gradle.kts Added coroutines core (API) + coroutines test deps.
gradle/libs.versions.toml Bumped markdown version and added coroutines test dependency.
multiplatform-markdown-renderer/api/* API surface updates for new streaming types/overloads.
multiplatform-markdown-renderer-m2/src/commonMain/kotlin/com/mikepenz/markdown/m2/Markdown.kt Added Material 2 wrapper overload for streaming state.
multiplatform-markdown-renderer-m3/src/commonMain/kotlin/com/mikepenz/markdown/m3/Markdown.kt Added Material 3 wrapper overload for streaming state.
multiplatform-markdown-renderer-m2/api/, multiplatform-markdown-renderer-m3/api/ API surface updates for new Material 2/3 overloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings June 6, 2026 02:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.

Comment on lines 58 to 66
val model = remember(node, content, typography) {
// It's safe to pass `CharSequence` and its `toString` here.
// Reason: It's guaranteed that even the source `StringBuilder` changes, The render result is not dirty.
// So it's fine to remember it.
MarkdownComponentModel(
content = content,
content = content.toString(),
node = node,
typography = typography,
)
Comment on lines +406 to +423
@Composable
fun StreamingMarkdownSuccess(
streamingMarkdownState: StreamingMarkdownState,
snapshot: StreamingMarkdownState.Snapshot,
components: MarkdownComponents,
modifier: Modifier = Modifier,
) {
val content = streamingMarkdownState.content

Column(modifier) {
snapshot.stableAst.forEach { node ->
MarkdownElementInternal(node, components, content)
}
snapshot.unstableAstTail.forEach { node ->
MarkdownElementInternal(node, components, content)
}
}
}
Comment on lines +71 to +77
items(
items = nodes,
key = { node -> node.startOffset },
contentType = { node -> node.type }
) { node ->
MarkdownElementInternal(node, components, content)
}
/**
* The accumulated markdown content.
*/
val content: CharSequence
referenceLinkHandler = input.referenceLinkHandler,
)

override val content: StringBuilder = StringBuilder()
Comment on lines +123 to +127
if (lookupLinks) {
val links = lookupLinks(stableChildren, unstableTail)
links.onEach { (key, value) -> referenceLinkHandler.store(key, value) }
linkStateFlow.value = links
}
Copilot AI review requested due to automatic review settings June 6, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Comment on lines +415 to +422
Column(modifier) {
snapshot.stableAst.forEach { node ->
MarkdownElementInternal(node, components, content)
}
snapshot.unstableAstTail.forEach { node ->
MarkdownElementInternal(node, components, content)
}
}
Comment on lines +197 to +237
SelectionContainer {
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Text(
text = "Flow chunks: $emittedChunks / $totalChunks | chars: ${streamingMarkdownState.content.length}",
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = "Append cost: ${appendMicros / 1000.0} ms total",
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = "Snapshot: stable=${snapshot.stableAst.size}, tail=${snapshot.unstableAstTail.size}, links=${links.size}",
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = "Render probe: calls=${renderProbe.componentCalls}, uniqueNodes=${renderProbe.uniqueNodes}",
color = MaterialTheme.colorScheme.onBackground,
)

Markdown(
streamingMarkdownState = streamingMarkdownState,
components = components,
annotator = rememberCheckAnnotator(markdownAnnotatorConfig(showImageAltTooltip = true)),
inlineContent = rememberCheckInlineContent(),
imageTransformer = Coil3ImageTransformerImpl,
extendedSpans = markdownExtendedSpans {
val animator = rememberSquigglyUnderlineAnimator()
remember {
ExtendedSpans(
RoundedCornerSpanPainter(),
SquigglyUnderlineSpanPainter(animator = animator)
)
}
},
modifier = Modifier.fillMaxSize(),
)
Comment on lines +71 to +77
items(
items = nodes,
key = { node -> node.startOffset },
contentType = { node -> node.type }
) { node ->
MarkdownElementInternal(node, components, content)
}
Comment on lines +59 to +61
// It's safe to pass `CharSequence` and its `toString` here.
// Reason: It's guaranteed that even the source `StringBuilder` changes, The render result is not dirty.
// So it's fine to remember it.
Comment on lines +128 to +133
val nextSnapshot = StreamingMarkdownState.Snapshot(
stableAst = stableChildren.toList(),
unstableAstTail = unstableTail.toList(),
)
snapshotStateFlow.value = nextSnapshot
snapshotStateFlow.value
@mikepenz mikepenz merged commit 3842037 into mikepenz:develop Jun 7, 2026
5 checks passed
@mikepenz

mikepenz commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Thank you very much for the contribution @Stream29

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

incremental parsing and render markdown with streams

3 participants