The legacy regex parser remains supported. Existing components,
inlineComponents, and incremental: false retain their routing, though all
three are deprecated in 1.3.0 and removed in 2.0.0. There is no
required migration for widget/legacy-component integrations and no new runtime
dependency. Direct AST consumers using exhaustive switches over sealed MdNode
must add a case for the new MdCustomBlock variant.
MdDocument trees separately from styled spans.Theme changes invalidate presentation, not parsing. A quote or heading builds its styled content once, and reveal counting uses that exact content. Nested quotes no longer render duplicate subtrees for counting. Tables own their scroll controllers in widget state, retaining scroll state during appends and disposing controllers on unmount.
PlusparseRenderer.renderDocument(context, document, config) is available when
an integration already has an AST. render(context, source, config) remains
available and does parsing plus rendering. For custom blocks, pass a registry
to Plusparse.parse and matching blockComponents to the render config.
Use blockComponents for new block syntax and inlinePatterns or
inlineDirectives for inline syntax. See custom components.
The block registry dispatches by first character and opening prefix rather than
trying every extension at every character. Syntax produces a pure-Dart payload;
its Flutter builder owns presentation. No central renderer switch edits are
needed for new custom block types.
Component lists and parsed payloads must be treated as immutable. Keep lists and component instances stable across normal builds; replace entries when syntax or builder behavior changes. A replacement invalidates the relevant caches. Existing legacy callback-cache semantics are unchanged.
For a long answer in its own scrollable viewport, use the sliver path below.
Putting ordinary GptMarkdown inside SingleChildScrollView still builds and
lays out the entire answer at mount. The sliver path postpones offscreen work
until the reader scrolls toward it. It is an explicit integration choice because
it changes scrolling, selection extent, and character-reveal behavior.
SelectionArea(
child: CustomScrollView(
slivers: [
SliverGptMarkdown(
document,
config: GptMarkdownConfig(
style: TextStyle(fontSize: 16),
),
),
],
),
)
SliverGptMarkdown segments the document eagerly, but creates spans and widgets
only when the viewport requests a segment, including its cache extent. Custom
syntax matchers also run during segmentation to identify opaque block boundaries. It has
no independent scroll controller and composes with other slivers. Source updates
appear immediately; character reveal remains on GptMarkdown. Offscreen
segments can be disposed and rebuilt, and selection covers mounted content.
Legacy component lists use a single SliverToBoxAdapter to preserve their
whole-document semantics; that fallback is not viewport-lazy.
A giant individual paragraph, list, table, or code fence remains one segment. Neither API virtualizes rows inside a table or lines inside a fence. Blank-line segmentation retains its existing loose-list limitations. New custom syntaxes must consume their complete block, including internal blank lines.
These optional policies trade content-based column sizing and live syntax coloring for lower update cost, without changing existing defaults:
GptMarkdown(
source,
styleSheet: const GptMarkdownStyleSheet(
table: TableStyle(columnWidth: FixedColumnWidth(160)),
codeBlock: CodeBlockStyle(highlightWhileStreaming: false),
),
)
Fixed widths skip intrinsic column measurement. The default still sizes columns
to content using its existing measurement behavior. This is necessary for custom
cells containing LayoutBuilder, which cannot answer intrinsic-size queries. By
default the built-in table scrolls horizontally and has unbounded horizontal
constraints, so use fixed widths rather than flex widths there. With
TableStyle(overflow: TableOverflow.wrap) the table is laid out against the
available width instead, and flex widths work.
Deferred highlighting displays all current code as plain monospace text while
the fence is open and highlights when its closing fence arrives. An input that
never closes remains plain. Custom codeBuilder implementations control their
own highlighting; this policy only affects the default code widget.
See native profile results for representative cold mounts and the distinction between eager rendering and first-screen work.
flutter test --no-pub tool/benchmarks/render_architecture_benchmark_test.dart
flutter test --no-pub test/regression/render_work_reuse_test.dart
The benchmark uses APIs supported before this refactor so the same file can be run against an isolated original revision. It measures nested-quote construction and actual animation ticks while source is unchanged. Deterministic tests pin builder counts, segment reuse, theme invalidation, and viewport-lazy rendering.
Debug widget-test timings are diagnostics, not production frame budgets. Use profile builds on representative target devices with recorded source/chunk sequences to measure build and raster durations separately. Include cold mounts, appends, reveal backlogs, long unbroken blocks, tables, math, selection, and RTL. Source normalization, prefix comparisons and metadata reconciliation still do work proportional to document size on updates; do not claim constant-time streaming or a universal speedup.
On macOS with Flutter 3.44.2 / Dart 3.12.2, the same benchmark file was run
against original revision 5f92914 and this refactor in separate processes.
These are one-run debug host averages, not device frame-time guarantees:
| Workload | Original | Refactor |
|---|---|---|
| Depth-10 quote, source to spans | 2,573 µs | 231 µs |
| Inline builder calls for one element in that quote | 1,024 | 1 |
| Reveal backlog, average tick/pump | 3,828 µs | 3,162 µs |
| Redundant inline builds during 40 backlog ticks | 827 | 0 |
The quote workload deliberately exercises pathological nesting. Its roughly 11x timing improvement is not a claim about ordinary chat replies. The backlog timing includes Flutter layout/paint and the test harness, so eliminating all redundant source work does not eliminate the rest of a frame’s cost.