animated_streaming_markdown 0.4.0
animated_streaming_markdown: ^0.4.0 copied to clipboard
Flutter markdown streaming package for Android/iOS/macOS/Linux/Windows/web, with Tree-sitter WASM and KaTeX math support.
animated_streaming_markdown
Streaming Markdown parser + renderer for Flutter, optimized for incremental append flows.
Native targets and Flutter web are supported, including zero-config Tree-sitter WASM assets for published builds.
Explore the docs »
View Demo
·
Report Bug
·
Request Feature
Latest Update #
- Document pipeline (0.4.0): replace parsing, static rendering and animation through public contracts, or extend the defaults with syntax rules and renderer definitions. See the migration guide and runnable custom pipeline example. Existing constructors retain their compatibility path.
- 0.3.7 source-backed controller: directional selection now lives in an optional
AnimatedMarkdownSelectionController, so pointer, keyboard, copy, and programmatic changes share one source of truth. - TextField-like selection auto-scroll: edge dragging advances every frame, keeps the moving endpoint revealed, supports vertical viewports and horizontal tables, and stops without momentum on release.
- Lazy sliver selection:
AnimatedStreamingMarkdownSelectionAreacoordinates selection across mounted sliver children without disabling lazy rendering. - Browser-like flat highlight: selection paints as one continuous layer per line, supports touch long-press and handles, and includes non-text content such as images and LaTeX in its real laid-out bounds.
- Semantic streaming links and selectable custom widgets: incomplete links expose label, destination, completion state, and source offsets without leaking raw syntax; custom blocks can opt into atomic, text, or fragment-level selection.
- Stable streaming state: incremental appends preserve settled render state and source-backed selection; the chat example keeps each assistant renderer alive while messages are recycled by the list.
- Rich clipboard on every supported target: Web, Android, iOS, macOS, Windows, and Linux receive HTML plus plain text, with a safe plain-text fallback when the host clipboard rejects rich data.
- Animation without layout jolts: settled word tokens compact into lighter static spans while preserving token geometry; the example defaults to the original
Fadepreset and also includesGravity. - Flutter web is first-class: published builds include the generated Tree-sitter WASM parser asset, so app developers do not need to edit
web/index.htmlor copy files manually. - KaTeX-style LaTeX rendering: inline
$...$/\(...\)and display$$...$$/\[...\]math render through the bundled pure-Dart/Flutter renderer derived fromflutter_math_fork; no separate math dependency is required. - Real chatbot example: the example app can connect to local Ollama plus ChatGPT/OpenAI, Claude, Gemini, and Grok-compatible cloud APIs.
Table of Contents
About The Project #
animated_streaming_markdown provides 2 main layers:
- Parser:
MarkdownStreamParserfor typedreplace/appendrequests - Renderer:
AnimatedStreamingMarkdownfor block rendering, token reveal animations, inline images, links, selection, and KaTeX-compatible LaTeX math - Tables: stable shared-width Markdown tables with left-aligned viewport framing and row-by-row reveal during streaming
It is designed for chat-like or streaming text interfaces where markdown arrives progressively and needs stable UI updates.
Built With #
Getting Started #
Prerequisites #
- Flutter
>=3.10.0 - Dart SDK
>=3.0.0 <4.0.0 - Native toolchain for your target platform (Android/iOS/macOS/Linux/Windows)
- No extra setup is required for Flutter web consumers; the package ships the generated WASM parser asset and falls back safely when needed.
The package keeps a Flutter 3.10.0 compatibility path and uses newer
nonlinear text scaling APIs when the running SDK provides them. Current stable
Flutter releases are supported as well.
Installation #
- Add dependency:
dependencies: animated_streaming_markdown: ^0.3.7 - Install packages:
flutter pub get
Usage #
1) Start parser worker and stream markdown #
final parser = MarkdownStreamParser();
await parser.start();
final setResult = await parser.replace('# Hello');
final appendResult = await parser.append('\n\nStreaming **markdown** chunk...');
2) Render blocks with AnimatedStreamingMarkdown #
AnimatedStreamingMarkdown(
blocks: appendResult.blocks,
tokenStaggerDelay: const Duration(milliseconds: 180),
tokenAnimationDuration: const Duration(milliseconds: 240),
enableSelection: true,
);
The built-in animation is a Fade reveal. Supply tokenAnimationBuilder only
when opting into a custom effect such as Gravity or Rotate in.
3) Control selection and selectable slivers #
Box mode creates its selection area internally. Supply a controller only when the app needs to observe or change the source range:
final selectionController = AnimatedMarkdownSelectionController();
AnimatedStreamingMarkdown(
blocks: appendResult.blocks,
enableSelection: true,
selectionController: selectionController,
selectionScrollPadding: const EdgeInsets.all(20),
);
// selectionController.selection = const TextSelection(...);
// selectionController.selectAll();
Sliver mode needs one wrapper around the CustomScrollView. The wrapper and
renderer must share the controller, and one wrapper manages one Markdown
renderer:
AnimatedStreamingMarkdownSelectionArea(
controller: selectionController,
scrollPadding: const EdgeInsets.all(20),
child: CustomScrollView(
slivers: [
AnimatedStreamingMarkdown(
blocks: appendResult.blocks,
asSliver: true,
enableSelection: true,
selectionController: selectionController,
),
],
),
);
Dispose an app-owned controller with the surrounding State.
4) Keep a streaming renderer stable #
Keep one MarkdownStreamParser alive for each active document and call
append(chunk) only for new chunks. Preserve the identity of the widget that
owns a message renderer (for example with a stable message key in a
ListView); do not create a new parser or changing renderer key from build.
This lets settled token animation, selection, and controller state survive
later messages and list recycling. Use replace(markdown) when an update is a
complete snapshot rather than an append.
5) Render LaTeX math with KaTeX-compatible syntax #
LaTeX is supported in both inline and display forms:
AnimatedStreamingMarkdown.fromMarkdown(
markdown: r'''
Inline math: $x^2 + y^2 = z^2$
Display math:
$$
\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$
''',
);
Use latexBuilder to wrap or replace the default bundled math widget:
AnimatedStreamingMarkdown(
blocks: appendResult.blocks,
latexBuilder: (context, latex) {
return latex.defaultWidget;
},
);
Custom image and LaTeX widgets retain the renderer's semantic selection proxy.
When blockBuilder replaces a complete block with a non-text object, declare
the object's plain-text meaning with AnimatedMarkdownSelectable:
blockBuilder: (context, block) {
return AnimatedMarkdownSelectable(
plainText: block.block.content,
child: MyCustomMarkdownObject(block: block.block),
);
},
The default constructor selects the custom object atomically. For a custom text
renderer, including Flutter's SelectableText, use the character-level
constructor:
return AnimatedMarkdownSelectable.text(
plainText: block.block.content,
child: SelectableText(block.block.content),
);
For a composite object, use AnimatedMarkdownSelectable.fragments and wrap
each visible text region with AnimatedMarkdownSelectionFragment, supplying
its local plainTextStart. Buttons and other siblings remain interactive while
the declared fragments join the surrounding Markdown selection. Plain/raw/rich
copy always comes from the controller's original Markdown source.
Incomplete streaming links are also exposed semantically through
MarkdownBlock.inlineLinks. By default, [Hel paints nothing,
[Hello](https://hello paints a tappable https://hello, and the completed
[Hello](https://hello) paints the linked label. Override only the temporary
projection with incompleteLinkTextBuilder when an application prefers the
label or wants to suppress the construct until completion.
Only a direct inline link at the active streamed tail is provisional; code,
autolinks, images, and escaped opening brackets are not reclassified.
6) Important APIs #
MarkdownStreamParser.start()MarkdownStreamParser.replace(markdown)MarkdownStreamParser.append(chunk)MarkdownStreamParser.parse(operation, text)MarkdownStreamParser.dispose()MarkdownSyncParser.parseMarkdown(markdown)warmUpStreamingMarkdownParser(includeWorker: true)AnimatedStreamingMarkdown(...)AnimatedStreamingMarkdown.fromMarkdown(...)AnimatedStreamingMarkdownSelectionArea(...)AnimatedMarkdownSelectionControllerAnimatedMarkdownSelectionValuesourceTextselectionhasSelectionselectedMarkdown
- Renderer options:
blocks,asSliver,enableSelection,selectionStrategy,selectionController,selectionScrollPadding,tokenStaggerDelay,tokenAnimationDuration,tokenAnimationBuilder,tokenCompaction,showCodeBlockCopyButton,blockBuilder,imageBuilder,latexBuilder,incompleteLinkTextBuilder,AnimatedMarkdownSelectable
selectionStrategy accepts plain, raw, or rich. Rich copy keeps the
selected source range as the authority and supplies HTML and plain text to the
clipboard. See Selection Copy for the
platform details and fallback behavior.
For a complete integration sample, check example/lib/src/demos/markdown_cases_demo.dart.
For the full chatbot sample with Ollama, ChatGPT/OpenAI, Claude, Gemini, and Grok providers, check example/lib/main.dart.
Documentation #
- Documentation site
- Live web demo and 0.3.7 interaction recording
- Package page
- Generated API reference
- Example app
- Migration guide: 0.2.x to 0.3.x
- 0.4.0 document pipeline: replace Parser, Renderer, and Animator
- Pipeline Lab: lazy code parsers, custom rendering, and tokenization
The document pipeline guide describes the current repository APIs. The published
API reference follows the latest pub.flutter-io.cn release and may not contain these
0.4.0 additions until publication. Existing blocks, fromMarkdown, and asSliver
examples remain supported; opting into fromDocument selects the new pipeline.
The documentation site is built with Docusaurus from docs/ and
deployed to GitHub Pages by Deploy Documentation.
Run the docs site locally:
cd website
npm ci
npm run start
Build the static site:
cd website
npm run build
Migration notes for 0.3.0 #
0.3.0 keeps the 0.2.x API available, but the preferred names now describe
the package behavior more directly:
| 0.2.x name | 0.3.x preferred name |
|---|---|
StreamingMarkdownParseWorker |
MarkdownStreamParser |
request(op: 'set', ...) |
replace(markdown) |
request(op: 'append', ...) |
append(chunk) |
StreamingMarkdownParseResult.renderNodes |
MarkdownParseResult.blocks |
StreamingMarkdownRenderView |
AnimatedStreamingMarkdown |
nodes |
blocks |
sliver |
asSliver |
tokenArrivalDelay |
tokenStaggerDelay |
tokenFadeInDuration |
tokenAnimationDuration |
tokenFadeInRelativeToDelay |
tokenAnimationDurationFactor |
allowUnclosedInlineDelimiters |
allowIncompleteInlineSyntax |
enableTextSelection |
enableSelection |
customBlockBuilder |
blockBuilder |
markdownTheme |
theme |
Roadmap #
- Done: Incremental parser worker (
replace/append) - Done: Streaming renderer for markdown block nodes
- Done: Per-token custom animation builder API
- Done: Example with multiple animation presets
- Done: Docusaurus documentation site for
samnn.dev - Done: Convenience constructors and sync parser helpers
- Done: Opt-in code block copy button
- Done: KaTeX-compatible LaTeX math rendering
- Done: Render-backed selection with stable ranges, table traversal, and edge auto-scroll
- Next: Performance optimization across parser, rendering, token compaction, and benchmarks
- Next: Feature development guided by real application requirements and user requests
See the open issues for proposed features and known issues.
Contributing #
Contributions are welcome.
- Fork the project
- Create your branch (
git checkout -b feature/your-feature) - Commit your changes (
git commit -m "Add your feature") - Push branch (
git push origin feature/your-feature) - Open a Pull Request
See CONTRIBUTING.md for local setup, repository layout, and quality gates.
Support #
If this package helps you, consider buying me a coffee:
License #
Distributed under the Apache-2.0 License. See LICENSE for details.
Contact #
- Documentation: https://samnn.dev
- API reference: https://pub.flutter-io.cn/documentation/animated_streaming_markdown/latest/
- Repository: https://github.com/samnn152/streaming-markdown
- Issues: https://github.com/samnn152/streaming-markdown/issues
