Migration to 1.3

The 1.3 release keeps 1.2.x APIs compiling, but the recommended rendering path has changed. Review the deprecated parser switches and behavior changes before upgrading production chat screens. Removal of the deprecated APIs is planned for 2.0.0.

Requirements

gpt_markdown 1.3 requires Flutter 3.32 or newer and Dart 3.7 or newer. There is no new runtime dependency. Existing 1.2.x code should compile, but output, selection, semantics, and golden images can change.

Leave the legacy parser

plusparse is now the default parser. Delete incremental rather than setting it to true. Do not pass components or inlineComponents, even as empty lists: any of those arguments selects the legacy regex parser. That path does not use the incremental segment cache, span-level reveal, block components, or lazy sliver rendering.

parser.dart
1// 1.3 uses plusparse by default. 2// Remove the deprecated routing switches: 3GptMarkdown( 4 reply, 5 // incremental: true, // delete this 6 // components: [...], // use blockComponents instead 7 // inlineComponents: [...], // use inlinePatterns or inlineDirectives 8) 9 10// These deprecated arguments still compile until 2.0.0, but they select 11// the legacy regex parser and disable segment caching and lazy rendering.

Migrate custom blocks

Replace BlockMd and the old global component list with MarkdownBlockComponent in blockComponents. Use FencedBlockSyntax for fenced containers or implement MarkdownBlockSyntax for another block grammar. The syntax parser stays pure and the builder receives an immutable node, so the parsed document can be cached.

custom_block.dart
1// Before: a regex component selected the legacy parser. 2class CalloutMd extends BlockMd { 3 4 String get expString => r':::callout\\n([\\s\\S]*?)\\n:::'; 5 // ... 6} 7 8GptMarkdown(text, components: [CalloutMd(), ...MarkdownComponent.globalComponents]); 9 10// After: syntax is pure Dart and rendering is a separate builder. 11GptMarkdown( 12 text, 13 blockComponents: [ 14 MarkdownBlockComponent( 15 syntax: const FencedBlockSyntax( 16 type: 'callout', 17 opening: ':::callout', 18 ), 19 builder: (context, node, config) => CalloutBox(body: node.body), 20 ), 21 ], 22)

Migrate custom inline syntax

Replace InlineMd with InlinePattern for text matched in the parsed document. Use InlineDirective when the parser must not inspect a delimited region, such as a server-provided JSON payload. Patterns match the whole document, so prefer lookarounds over ^ and $. Keep pattern and component instances stable across rebuilds.

inline_syntax.dart
1// Before: InlineMd receives only the matched text. 2class ShoutMd extends InlineMd { 3 4 RegExp get exp => RegExp(r'!![A-Za-z]+!!'); 5 // ... 6} 7 8// After: InlinePattern keeps the match and resolved surrounding style. 9GptMarkdown( 10 text, 11 inlinePatterns: [ 12 InlinePattern( 13 pattern: RegExp(r'!![A-Za-z]+!!'), 14 builder: (context, match, style) => TextSpan( 15 text: match[0]!.replaceAll('!!', '').toUpperCase(), 16 style: style.copyWith(fontWeight: FontWeight.bold), 17 ), 18 ), 19 ], 20) 21 22// For a region the parser must not inspect, use InlineDirective instead.

Update inline builders

Prefer span-returning builders for inline content. inlineLinkBuilder receivesLinkBuildDetails, while inlineSourceTagBuilder receivesSourceTagBuildDetails. The deprecated widget builders remain available for compatibility, but WidgetSpan output cannot wrap across lines, is less selectable, and can disappear when nested inside a link label on iOS.

inline_builders.dart
1// New span-returning builders keep inline content wrappable, 2// selectable, and aligned with the surrounding baseline. 3GptMarkdown( 4 reply, 5 inlineLinkBuilder: (details) => details.defaultSpan(), 6 inlineSourceTagBuilder: (details) => details.defaultSpan(), 7 inlineCodeBuilder: (context, code, style, codeStyle) => CodeTextSpan( 8 text: code, 9 style: style, 10 codeStyle: codeStyle, 11 ), 12) 13 14// linkBuilder, sourceTagBuilder, and highlightBuilder still compile, 15// but return WidgetSpan-based output and are deprecated until 2.0.0.

Use SliverGptMarkdown for long documents

Ordinary GptMarkdown remains the compact, eager widget and is the only path with character reveal. For a long answer in its own scrollable viewport, move to SliverGptMarkdown. It eagerly segments the source but delays built-in AST, span, and widget construction until the viewport requests each segment. It composes with other slivers and has no independent scroll controller.

long_document.dart
1// Before: the whole long document is built inside one scroll child. 2SingleChildScrollView( 3 child: GptMarkdown(document), 4) 5 6// After: offscreen segments wait for the viewport. 7SelectionArea( 8 child: CustomScrollView( 9 slivers: [ 10 SliverGptMarkdown( 11 document, 12 config: const GptMarkdownConfig( 13 style: TextStyle(fontSize: 16), 14 ), 15 ), 16 ], 17 ), 18) 19 20// SliverGptMarkdown shows source updates immediately. Character reveal stays 21// on GptMarkdown. Selection covers mounted content, and a very large table, 22// list, paragraph, or fence remains one segment.

Passing deprecated component lists forces a single SliverToBoxAdapter and disables viewport laziness. Keep modern component lists stable, and remember that selection includes only mounted content.

Complete deprecation migration reference

Nothing below is removed in 1.3.0; removal is planned for 2.0.0. This is the complete migration map for the deprecated public surface.

Widget arguments

DeprecatedUse insteadMigration note
incrementalDelete itplusparse is already the default; false selects the legacy parser.
componentsblockComponentsMove inline entries to inlinePatterns or inlineDirectives.
inlineComponentsinlinePatterns / inlineDirectivesUse directives for opaque delimited payloads.

Extension points and registration

DeprecatedUse insteadMigration note
InlineMdInlinePattern / InlineDirectiveChoose InlineDirective when Markdown parsing must be blocked.
BlockMdMarkdownBlockComponent + MarkdownBlockSyntaxFencedBlockSyntax handles :::name containers.
MarkdownComponent.globalComponentsblockComponentsDo not spread the legacy registry.
MarkdownComponent.inlineComponentsinlinePatternsDo not spread the legacy registry.
MdWidgetGptMarkdownDirect use loses segment caching, span reveal, and lazy sliver rendering.

Builders, typedefs, and link widget

DeprecatedUse insteadMigration note
linkBuilder / LinkBuilderinlineLinkBuilder / InlineLinkBuilderReturn an InlineSpan from LinkBuildDetails.
sourceTagBuilder / SourceTagBuilderinlineSourceTagBuilder / InlineSourceTagBuilderUse SourceTagBuildDetails.
highlightBuilder / HighlightBuilderinlineCodeBuilder / InlineCodeBuilderUse inlineCodeStyle when only appearance changes.
LinkButtoninlineLinkBuilder or LinkTextSpanThe package no longer builds LinkButton.
LinkSpanBuilderNo replacementOnly deprecated LinkButton used it.

Built-in regex components: no direct replacement

These were internals of the deprecated regex pipeline. Remove direct references; the modern parser handles each construct itself.

InlineDirectiveMd

Inline directives

IndentMd

Indented content

HTag

Headings

NewLines

Blank-line separation

HrLine

Horizontal rules

CheckBoxMd

Task-list items

RadioButtonMd

Radio options

BlockQuote

Block quotes

UnOrderedList

Unordered lists

OrderedList

Ordered lists

HighlightedText

Inline code

BoldMd

Bold

StrikeMd

Strikethrough

ItalicMd

Italic

LatexMathMultiLine

Display math

LatexMath

Inline math

SourceTag

Citation chips

ATagMd

Links

ImageMd

Images

TableMd

Tables

CodeBlockMd

Fenced code

UnderLineMd

Underline

Not deprecated: MarkdownComponent, MarkdownComponent.generate, MarkdownScope, and AutolinkMd.

Behavior changes to review

Links are text spans

Links now wrap mid-label, remain selectable and copyable, reveal per character, and align to the text baseline. Plain-text snapshots and link goldens change because toPlainText now includes link labels.

RTL and text scaling are corrected

Block alignment follows text direction. Paragraph and widget scaling is resolved once, fixing double-scaled nested lists, quotes, tables, checkboxes, and code blocks.

Malformed syntax stays visible

Malformed links and unclaimed custom matches render as literal source text instead of disappearing. Case-insensitive patterns and top-level alternation are also handled correctly.

Modern blocks can render lazily

GptMarkdown still renders the full document at mount. Use SliverGptMarkdown in a CustomScrollView when viewport-lazy block rendering is required.

Streaming has two independent axes

Character reveal and block entrance animation are separate. blockAnimation can use none, fadeIn, growIn, slideUp, or scaleIn; isStreaming must become false for completed, failed, and historical messages.

Accessibility settles after streaming

While a live reply is rebuilding, semantics intentionally collapse to one node. Structure reopens after a quiet period and reveal completion; selection is stable after the content settles.

Update tests and release checks

Update link goldens and assertions that expect LinkButton or a WidgetSpan. Test both parser paths when maintaining legacy compatibility, but make the modern path the release path. Pump streaming state changes rather than relying only onpumpAndSettle, and assert semantics and selection after the quiet period.

migration_test.dart
1// Link labels are now LinkTextSpan, not LinkButton widgets. 2expect(find.byType(LinkButton), findsNothing); 3 4// RichText subclasses need a widget predicate. 5final richText = find.byWidgetPredicate((widget) => widget is RichText); 6 7// Test the streaming lifecycle explicitly. 8await tester.pumpWidget(GptMarkdown(reply, isStreaming: true)); 9await tester.pump(const Duration(milliseconds: 16)); 10await tester.pumpWidget(GptMarkdown(reply, isStreaming: false)); 11// Then pump until the reveal settles before asserting selection or semantics.

Run package tests

Include docs snippets, example tests, parser tests, streaming tests, and Linux goldens.

Check app lifecycle

Set isStreaming: false on completion, errors, and loaded history messages.

Measure before launch

The published branch benchmarks are macOS profile measurements, not universal FPS or latency guarantees.

Performance evidence is qualified

The branch documentation reports lower median UI and raster work for several cold and streaming fixtures, plus lower first-viewport work with SliverGptMarkdown. Those results compare complete package revisions and were recorded on macOS. Mobile, web, RTL, scroll-through, selection, and very large individual blocks still need representative measurements before they become launch acceptance criteria.