animated_streaming_markdown 0.3.7 selection, incomplete-link, and custom-widget demo

Contributors Forks Stargazers Issues License Pub Version


animated_streaming_markdown logo

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: AnimatedStreamingMarkdownSelectionArea coordinates 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 Fade preset and also includes Gravity.
  • 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.html or copy files manually.
  • KaTeX-style LaTeX rendering: inline $...$ / \(...\) and display $$...$$ / \[...\] math render through the bundled pure-Dart/Flutter renderer derived from flutter_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
  1. About The Project
  2. Getting Started
  3. Usage
  4. Documentation
  5. Roadmap
  6. Contributing
  7. License
  8. Contact
  9. Acknowledgments

About The Project

animated_streaming_markdown provides 2 main layers:

  • Parser: MarkdownStreamParser for typed replace/append requests
  • Renderer: AnimatedStreamingMarkdown for 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.

(back to top)

Built With

  • Flutter
  • Dart
  • Tree-sitter

(back to top)

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

  1. Add dependency:
    dependencies:
     animated_streaming_markdown: ^0.3.7
    
  2. Install packages:
    flutter pub get
    

(back to top)

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(...)
  • AnimatedMarkdownSelectionController
  • AnimatedMarkdownSelectionValue
    • sourceText
    • selection
    • hasSelection
    • selectedMarkdown
  • 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

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

(back to top)

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.

(back to top)

Contributing

Contributions are welcome.

  1. Fork the project
  2. Create your branch (git checkout -b feature/your-feature)
  3. Commit your changes (git commit -m "Add your feature")
  4. Push branch (git push origin feature/your-feature)
  5. Open a Pull Request

See CONTRIBUTING.md for local setup, repository layout, and quality gates.

(back to top)

Support

If this package helps you, consider buying me a coffee:

Buy Me a Coffee

(back to top)

License

Distributed under the Apache-2.0 License. See LICENSE for details.

(back to top)

Contact

(back to top)

Acknowledgments

(back to top)

Libraries

animated_streaming_markdown
Streaming Markdown parsing, rendering, animation, selection, and copy.
markdown_core
Parser, document, source selection, and copy contracts without Flutter types.