feat: render StreamingMarkdownState#575
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
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
StreamingMarkdownStatewith append-only updates, snapshot/linkStateFlows, 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.
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 | ||
| } |
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 |
Owner
|
Thank you very much for the contribution @Stream29 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR implements a
StreamingMarkdownStateand its rendering logic.Fixes #315
Type of change
Please delete options that are not relevant.
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: