flutter_md 0.3.1
flutter_md: ^0.3.1 copied to clipboard
Fast Markdown parser and canvas renderer for Flutter with cross-widget text selection, streaming parsing and syntax highlighting.
0.3.1 #
Rendering #
- IMPROVED: Highlighted code blocks are cheaper with identical pixels:
MarkdownHighlighter.highlightreturns a flat list with one span per run of text painted alike. On real devices the first display is 20-31% cheaper and a streamed chunk 20-40%. Code reading the nested token spans sees flat ones. - IMPROVED:
MarkdownHighlighterkeeps its latest results (up to 256 KB of code), so a block seen before is not tokenized again. - IMPROVED: Layout is cheaper with identical output: in the headless benchmark, up to 22% off a first frame of a large mixed document and up to 14% on lists. A table that fits no longer measures the longest word of every cell, list items with the same marker share one laid-out painter, and unstyled spans skip re-encoding the parent style.
- IMPROVED: Streaming is 5–8× cheaper per chunk on real devices. An update
keeps the painters of the unchanged blocks at the start of the document, and
records only what follows them. Applies to a theme without
builder,imageResolver,blockFilterorspanFilter. - IMPROVED: Relayout at a new width is 16–24× cheaper: lists and tables keep their text painters across layouts instead of rebuilding them.
- IMPROVED: Rebuilding a parent no longer lays out a document whose text and
theme values did not change, even when the
Markdownor theme is a new object. - FIXED:
MarkdownPainter.updatereleased replaced block painters and pictures only on garbage collection; it now disposes them. A painter from a hostbuilderis still left to the host. - CHANGED:
paragraphFromMarkdownSpansleavesstylenull on an unstyled child span, which inherits the parentTextSpan's style. The result looks the same; code readingchild.styledirectly sees null.
Parser #
- ADDED:
Markdown.fromStringAsyncandMarkdownDecoder.convertAsyncparse a long document in slices of 4 ms by default, pausing until the end of the frame when one is on the way. The result equals that of the sync parse. For 5 MB, the longest gap between frames falls from 129 to 19 ms on an iPad Pro and from 258 to 34 ms on a Galaxy S22 Ultra, for 2-3 times the total time. A single huge block is still parsed in one go. - FIXED: A paragraph with more than 65535 links or images hung the parser in an endless loop. Links are now matched by a cursor, with no size limit.
- FIXED: Hostile input no longer freezes the UI. Parsing is linear for
unclosed emphasis markers, backticks after an escaped backslash, tables that
fail validation, label openers, spaces inside a heading (32 KB took more than
15 seconds), deeply nested
>(8 KB with a fence took more than 20 seconds) and unclosed^{/_{in inline math. - CHANGED: A fenced code block is parsed as nested blocks down to 32 quote levels. Below that the quote body is inline text, so input cannot overflow the stack however deep it nests.
Selection #
- FIXED: "Looking up a deactivated widget's ancestor is unsafe" from
MarkdownSelectionScope's controller listener. Since the desktop-menu fix in 0.3.0 it readsTheme.of(context)on every clear, and a host that clears the selection or re-registers documents from a widget'sdispose(a chat body leaving a virtualized list) notifies while the scope above that body is deactivated. The scope now reads the platform indidChangeDependencies.
Benchmarks #
- ADDED:
docs/benchmarks.md: results per device with hardware, versions and method, for an iPad Pro, a Galaxy S22 Ultra and a MacBook Pro. The on-device suite isbenchmark_compare/tool/device_bench.sh.
0.3.0 #
Upgrading from 0.2.x? See the migration guide. Ordinary
MarkdownWidget/MarkdownSelectionScopeusers need no code changes; hosts that implement the render / selection extension points do. Coming from pub.dev 0.0.8? 0.1.0 and 0.2.0 were never published there — read the 0.0.x → 0.2.x guide first, then this one.
Breaking vs 0.2.0 #
Source-breaking only for hosts that implement the render / selection
extension points, reach into a default block painter's fields, or read the
quote / alert model directly. Ordinary MarkdownWidget /
MarkdownSelectionScope users are unaffected.
- REMOVED:
BlockPainter$Quote.painterandBlockPainter$Alert.bodyPainter. Both painters now stack nested children, so a single bodyTextPainterno longer exists. ReadrenderedText/fragments(fromMultiPainterSelectable) instead. - CHANGED:
BlockPainter$QuoteandBlockPainter$Alertmix inMultiPainterSelectableinstead ofSelectableTextBlock. Subclasses that overrodeselectionPainter/selectionOriginmust move tofragments. Both constructors gained an optionalchildren:(the nested block painters) andblocks:(the matching child models, which keep selection offsets in sync when a child painter exposes no selectable text); a custombuilderthat rebuilds them must pass both for bodies with nested blocks.BlockPainter$Quote.inheritFromgives a nested child the theme the default quote uses. - CHANGED: A quote / alert whose body contains a fenced block now has
empty
spans; its content is in the newMD$Quote.blocks/MD$Alert.blocks. Code that buildsBlockPainter$Quote/Alert(spans: …)in a customMarkdownThemeData.builder, or otherwise walksspans, must handleblocks. Plain quotes / alerts keep the leaf shape (spans, emptyblocks). - CHANGED:
SelectableBlockPaintergainedpositionAndAffinityForLocal,caretRectFor,hitsRenderedTextAtandselectionHighlightAboveCachedContent. Custom painters built on theSelectableTextBlock/MultiPainterSelectablemixins inherit working implementations; painters thatimplements SelectableBlockPainterdirectly must add all four. - CHANGED:
MarkdownSelectionSurfacegainedtextDirection,hitForGlobal,hitsSelectableGlyphs,isLinkAtGlobal,caretRectFor,localBoxesForRange,hasSelectionHandleLeadersandclearSelectionHandleLayersIfLinked. Custom surfaces must implement them. - CHANGED:
MarkdownSelectionController.startAtGlobal/selectWordAtGlobal/selectBlockAtGlobalignore points outside every mounted surface (newrequireContainment, defaulttrue) instead of clamping onto the nearest markdown. PassrequireContainment: falsefor the old clamping. - CHANGED:
ParagraphGestureHandler.hitTestInlineSpanWithPointerEventreturns null for a point outside the painter's box. A tap in the empty space before right-to-left text used to snap onto the line's last glyph and open a link ending it. - CHANGED:
MarkdownThemeData.copyWithreturnsMarkdownThemeDatainstead ofThemeExtension<MarkdownThemeData>(callers gain, overriders must narrow). - CHANGED:
MarkdownRenderObject.updateSelectiontakes an optionalmarkdown:.@meta.internal, butMarkdownWidgetsubclasses that call it must now invoke it beforeupdate(see the registry fix below). - CHANGED: With an
imageResolverset,paragraphFromMarkdownSpansmay emitMarkdownImageSpan(aPlaceholderSpan) children, soTextPainter.plainTextholds U+FFFC where the model has alt text.SelectableTextBlock.renderedTextandSelectableFragment.lengthnow come from the model text. Lay out / paint custom painters withlayoutMarkdownText/paintMarkdownText. - CHANGED: Code spans render in
Menlo(iOS / macOS) /Consolas(Windows) instead of the unresolvable'monospace', with a fallback chain on every platform (see Monospace font resolution). Expect golden diffs there.
Inline images & link icons #
- ADDED:
images can render as pictures, and links can carry an icon in front of their label (a site favicon). Opt-in throughMarkdownThemeData.imageResolver— without one, images keep rendering as their alt text, so nothing is fetched behind the host's back (tracking pixels). - ADDED:
MarkdownImage, the picture interface: it reports the size of its slot in the line (layout), paints itself into it (paint) and callsnotifyListenerswhen it loads, fails or animates. Implement it to control sources, caching and appearance;MarkdownImageResolvermaps aMarkdownImageRequest(kind,url,text— the alt text or link label — andtitle) to one;MarkdownImageConstraintscarries the line's width and text style. - ADDED:
MarkdownProviderImage(any FlutterImageProvider— network, file, asset, memory or a disk-cached third-party provider;.iconfor a text-sized link icon) and the ready-madeMarkdownProviderImageResolver(http(s) anddata:images,/favicon.icolink icons,baseUrifor relative sources, swappableimageProvider/faviconUri). - ADDED:
MarkdownImageSpan,layoutMarkdownTextandpaintMarkdownTextfor custom block painters whose text comes fromparagraphFromMarkdownSpans; a painter that keeps calling plainTextPainter.layoutstill lays out (a text-sized empty slot per picture). - Selection is unchanged: a picture counts as its alt text (a link icon as
nothing), so offsets, highlight and copied text match
markdownBlockRenderedTextwhether or not pictures are shown; a range touching the alt text highlights the whole picture, and a double tap on it selects it. - Pictures are owned by the rendered document: a request seen before reuses the loaded image (streaming rebuilds and repeated images load once), and images that leave the document are disposed. A size change re-lays out the widget; any other change only repaints it.
- A resolver (or
imageProvidercallback) that throws is reported throughFlutterError.reportErrorand the image falls back to its alt text; the rest of the document still renders. Anullresult is remembered for the request while it stays in the document. In-page anchors (#section) never get a link icon, anddata:image MIME types match case-insensitively. - Keep the resolver stable (a
constresolver's.call, a field or a top-level function): a different resolver drops and reloads every picture. - Known limitations: a failed load is not retried while its URL stays in the document; an animated image re-records the document's cached picture on every frame; a link icon that fails to load collapses, shifting its label once. Keyboard character steps walk an image's alt text one code unit at a time, so an endpoint can sit inside it — the whole picture highlights while the copy holds part of the alt text.
Syntax highlighting #
- FIXED: Grammar rules marked
greedy(Prism's semantics) now match across tokens claimed by earlier rules. A comment marker inside a string ("a # b"in Python / Bash,'http://…'in JS) no longer turns the rest of the line into a comment, and Ruby / Elixir#{…}interpolation is highlighted inside strings. Highlighting stays lossless. - FIXED: Tokenizing is linear in the number of tokens: each rule builds its result in one pass instead of splicing per match. A 10,000-line C block went from ~7 s to ~0.3 s; typical blocks are a little faster too. A greedy rule whose matches keep starting inside other tokens falls back to plain matching after a bounded number of tries.
- FIXED:
HighlightThemes.githubDark/githubLightcolor diffs: inserted and deleted lines get GitHub's green / red, hunk headers (coord) the function color. Previously a diff rendered uncolored.
Parser #
- FIXED: List items at uneven indents are kept: in
- a/- b/- c,bwas dropped (the shallowercreplaceda's children instead of joining them).
Quote / alert nested fenced code #
- FIXED: Fenced code (
```lang/~~~) inside a blockquote or GitHub alert is re-parsed as nestedMD$CodeinMD$Quote.blocks/MD$Alert.blocks, instead of being consumed as inline monospace by backtick pairing (``+ monospace +``). The fenced code keeps its indentation, fences inside nested quotes (> > ```…) are nested code too, and a fence may follow the>after up to three spaces or a tab (lines inside the code keep their indentation verbatim). Blank>lines around an alert body add no extra gap. - ADDED: Optional
blocksonMD$Quote/MD$Alert(empty for leaf inline bodies). Render,markdownBlockRenderedTextandMarkdown.textwalk nested children when present (Markdown.textyields the children's span text, not raw markup). Nested children route throughMarkdownThemeData.builderlike top-level blocks, so a host that replaces the code painter (a fence with a copy button, say) gets it inside> …too. Prose nested inside a quote (paragraphs / lists / headings beside a fence) usesquoteStyle; code / table chrome keeps the document theme. Links open on tap in every nested child kind, in either text direction. - FIXED: A quote / alert whose nested table overflows still paints, and a
host-supplied non-selectable nested painter keeps selection offsets in sync
with
markdownBlockRenderedText.
Glyph-tight hit testing #
- CHANGED: Hover I-beam and link hit-testing use rendered line/glyph
boxes, not the full max-width layout of a paragraph. Empty horizontal
gutter beside a short line (quote and alert bodies included) is no longer
I-beam / click-to-open. Glyph boxes are cached per
TextPainterlayout, so hovering stays cheap over long fenced code blocks. - CHANGED: Gesture starts (
hitsSelectableContent) stay surface- bounds. A press inside the document or bubble arms text selection. Drag-extend may still clamp outside glyphs. - CHANGED: Mouse single-click that misses selectable content while a non-collapsed range is active clears the selection, instead of leaving the old range.
- ADDED:
SelectableBlockPainter.hitsRenderedTextAt,MarkdownSelectionSurface.hitsSelectableGlyphs, andMarkdownSelectionController.hitsSelectableGlyphs.
Selection host gates #
- ADDED:
MarkdownSelectionScope.canStartSelectionAt— optional host gate so enclosing UIs can refuse selection starts on chrome (links, code headers) without the scope claiming the pointer. A refusal also un-arms the drag (the recognizers still join the arena while a range is live, so a chrome tap can dismiss it), so a long press / drag on refused chrome neither extends the active selection nor shows the toolbar. - ADDED:
MarkdownSelectionScope.enableTouchGestures— when false, touch / stylus / trackpad selection recognizers stay off; mouse multi-click, handles, toolbar, and keyboard remain. Focus loss also does not clear the range (host viewport may steal focus). - ADDED:
MarkdownSelectionScope.enableTouchConsecutiveTaps— when false (withenableTouchGesturestrue), touch long-press → word → drag-extend still arms, but touch multi-tap / horizontal-drag selection does not. Chat hosts use this so taps stay with the viewport while continuous text entry works. Focus loss also does not clear the range in that mode. - ADDED:
MarkdownSelectionScope.ownsSelectionChrome— optional host gate so only the scope that owns the selection’s document paints handles/toolbar when several scopes share one controller (chat per-body mounts). A non-owning or disabled sibling clears only handle leaders that reference its ownLayerLinks, and removes its own adaptive toolbar when ownership moves away;toolbarWantedis left for the owning scope to restore on settle. - ADDED:
MarkdownSelectionSurface.hasSelectionHandleLeaders,clearSelectionHandleLayersIfLinked, andMarkdownSelectionScopeState.selectionHandleLeadersAttachedfor observing coherent handle-leader attachment across multi-scope hosts. - CHANGED: Flipping
enabledfrom false → true with an existing non-collapsed range restores handles; whentoolbarWantedis set, restores the toolbar. While disabled, the scope stays inert for chrome (no toolbar / handles) but keepstoolbarWantedfor that restore path. Disabling the scope mid-drag aborts the drag.
Dynamic cursor resolution & span bounding boxes #
- ADDED:
MarkdownThemeData.cursorResolverandMarkdownWidget.cursorResolver(MarkdownCursorResolver) — unopinionated hook to dynamically resolve hover mouse cursors per local offset, block index, and block model, falling back to default link/text/defer cursors when returning null. The hover cursor survives rebuilds that pass a new resolver closure and re-resolves when the theme or resolver changes. - ADDED:
MarkdownSelectionSurface.localBoxesForRange(backed by the internalMarkdownPainter.localBoxesForRange) — fast query returning content-local bounding boxes for an arbitrary character range within a block, directly from cached block painters without re-layout.
Registry / multi-body selection #
- FIXED:
removeDocumentdefers while a surface for that id is mounted and flushes ondetachSurface; a laterputDocumentcancels the pending remove. ParentState.disposecan run before child detach — eager remove left hittable surfaces with no registry entry (rangeFor/ ordering broken). - FIXED:
rangeForand endpoint ordering ignore unregistered document ids instead of treating them as index-1(which painted every body from the start of the registry through the other endpoint). - CHANGED:
MarkdownRenderObjectheals withputDocumenton attach / when the controller is wired while already attached, so a mounted selectable surface is never missing from the registry. Prefer explicit app registration for unmounted docs and unique reading-orderordervalues. - FIXED: Recycling a
MarkdownWidgetelement onto a differentdocumentId(a virtualized chat list reusing a slot) overwrote the outgoing document's registry model with the incoming body.MarkdownWidgetnow rewires the selection registry before pushing the new model, andMarkdownRenderObject.updateSelectiontakes the incomingmarkdown:so the heal lands on the right id. - FIXED:
putDocumentwithout an explicitorderappended at_docs.lengthwhich sorted ahead of sparse explicit orders (chat hosts keyorderon the message id), so a healed body jumped to the front of the conversation and reversed extracted text. It now sorts after every registered document. - FIXED: Documents sharing an
ordercould swap places on any re-sort (List.sortis not stable), silently reversing extracted text. Ties now break on registration sequence. - FIXED:
MarkdownSelectionScope.onSelectionChangedfired synchronously from inside build / layout — a streamingputDocumentreconciling the range away (fromMarkdownWidget.updateRenderObject) or a deferredremoveDocumentflushing fromRenderObject.detachthrew "setState() called during build" in any host that rebuilds from the callback. Delivery is now coalesced to the end of the frame in those phases. - FIXED:
selectAllskips documents with no blocks, so an empty streaming message no longer turns it into a no-op. - FIXED: Reconciling a range onto a document with a duplicated block keeps the copy nearest the old position.
- FIXED: A drag update that lands on the current position no longer notifies listeners.
Selection chrome (SelectionArea parity) #
- ADDED: Read-only selection chrome on the controller /
MarkdownSelectionScopepath brought up to FlutterSelectableRegion/SelectionAreaquality without remounting SelectionArea:- Content-gated touch
TapAndHorizontalDragGestureRecognizer+ long-press (consecutive taps, noDoubleTapGestureRecognizerarena delay); mouseTapAndPanGestureRecognizer. - Gesture starts require a hit on mounted selectable markdown (chrome / empty space do not nearest-neighbor clamp); clamp remains for extend across gaps.
- Soft-wrap affinity on handles; directed base/extent edges with reverse handle types; coincident-caret separation; handle proxies when an endpoint surface unmounts (virtualization), on the correct end for reversed selections too.
- Native
SelectionOverlayhandles + magnifier on touch; LeaderLayer follow across scroll / multi-widget hosts. - Keyboard Copy / Select-all / Shift-extend / Esc; adaptive Copy / Select-all toolbar with live re-anchoring.
- Word / block granular multi-tap and long-press; link hand cursor / I-beam on selectable content.
- Content-gated touch
- ADDED:
MarkdownSelectionController.isSelectionReversed(base after extent in reading order),selectionEndpointGlobalRect({base})(global caret rect of a mounted endpoint, null when unmounted, no proxying),selectionEndpointOverlaps(rect, {base})andanySelectionEndpointOverlaps(rect)(whether a mounted endpoint caret intersects a global rect), andtoolbarWanted(the toolbar-restore intent; reset when the registry drops or collapses the range). - CHANGED: Toolbar hides while expanding (body or handle drag) and may re-show on drag end; anchors recompute on selection change, host/ancestor scroll, and mounted-surface layout change. Scroll-driven restores are skipped while a drag is active.
- CHANGED: Toolbar placement — both endpoints in clip use stock
above-preferring anchors; bottom-only endpoint prefers below that caret;
neither endpoint in clip (tall mid-viewport) top-pins so the below-fallback
cannot sink to the host bottom; empty intersection hides the overlay while
toolbarWantedrestores on scroll-back / remount. - CHANGED: Desktop selection toolbar parity with stock Flutter:
- On desktop (
macOS,Linux,Windows), mouse drags, double/triple clicks, keyboard shortcuts (Cmd/Ctrl+A), and programmatic selection updates do not pop up the toolbar. - Right-click on desktop shows the context toolbar at the click coordinates without mutating the active selection (no select-word / caret collapse).
- Desktop context menu preserves its right-click anchor when triggering actions such as "Select all", rather than jumping to selection endpoints.
- Desktop context menu dismisses immediately upon scroll, but survives streamed updates, sibling updates and resizes, with or without a selection.
- On mobile (
Android,iOS), touch gestures and programmatic selection (selectAll,selection = ...) continue to present the adaptive toolbar and handles with action items.
- On desktop (
- FIXED: Selection highlight stays outside the glyph
Picturecache — under glyphs by default (sharp text), with a second pass above opaque chrome (selectionHighlightAboveCachedContent) for code fences, table zebra rows, and inline monospace / highlight backgrounds. - FIXED: Focus loss does not clear selection while a pointer drag is active
(list rebuilds during autoscroll); geometry walks defer off
performLayout. - FIXED:
Escwith no selection is no longer swallowed by the scope, so it reaches ancestors (closing a dialog, say).
Edge autoscroll while selecting #
- ADDED: Edge-zone autoscroll while dragging (body / handle / long-press)
near the padded viewport — host-union hard-stop, direction arming gate,
past-viewport max velocity while the union still allows that direction
(
MarkdownSelectionScope.autoscroll,MarkdownSelectionAutoscrollConfigwithcopyWith/==, defaultedgeZone: 48). Edge bands are capped at half the viewport; speed is time-based, not tied to the pointer event rate; a vertical edge drag only drives a verticalScrollable; pointer cancel and long-press cancel stop it. The scroll surface is resolved once per drag. - ADDED:
MarkdownAutoscrollTarget— the scroll surface autoscroll drives, behind three members (viewport,canScroll,applyScrollDelta). Deltas are screen-space content movement (positive moves content up), never scroll offsets, so reverse axes and inverted anchors are the adapter's problem. - ADDED:
MarkdownScrollableAutoscrollTarget— the built-in sliver adapter (ScrollableState/ScrollPosition), used when no resolver is configured. HandlesAxisDirection.up/.leftsign flipping. - ADDED:
MarkdownSelectionAutoscrollConfig.targetResolver(MarkdownAutoscrollTargetResolver+MarkdownAutoscrollRequest) so a host with its own scroll implementation — an anchored chat viewport, aRenderBoxthat positions children itself, a transform canvas — can be driven without aScrollableanywhere in the tree. PlusMarkdownCallbackAutoscrollTargetandMarkdownAutoscrollViewport(global bounds + band inset) for closure hosts. - ADDED:
MarkdownSelectionAutoscrollConfig.useHostUnionGate(defaulttrue). Turn it off when the markdown bodies are the scrolling content and the host builds only what is visible: such a host's mounted union is barely larger than the viewport, so the gate would veto a drag that should keep paging through history. The target'scanScrolland the delta it reports applying are then the only stops.
Layout #
- FIXED:
MarkdownWidgetreports its intrinsic height and max intrinsic width, soIntrinsicHeight(and similar) no longer collapses it to 0. - FIXED: A fenced code block too narrow for its padding is inert for
selection, so
selectAllat tiny widths no longer crashes, and no longer throwsTextPainter.paint called when text geometry was not yet calculatedwhile painting (at widget widths of a few dozen pixels, less inside a quote or alert).
Right-to-left layout #
- FIXED: With
MarkdownThemeData.textDirectionset toTextDirection.rtl, blocks were still laid out left-to-right: paragraphs and headings sat against the left edge, list bullets, quote bars and the alert accent bar were on the left, and table columns ran left to right. Blocks now mirror: text aligns to the right edge, bullets and bars sit on the right, and the first table column is on the right (unaligned data cells start on the right; explicit:--/--:alignment stays physical). Link taps, hit testing and text selection follow the mirrored positions, under tight and min-width constraints too. Left-to-right layout is unchanged. - FIXED: Selection handles on right-to-left text sit at the logical start
(right edge) and end (left edge) of the selection. Each handle follows the
textDirectionof the document it sits on, not the ambientDirectionality: the two can differ, and one selection can span documents of both directions. Proxy handles for a scrolled-off edge land on the reading start / end of the visible selection in either direction. - FIXED: A table too wide to fit overflows past its end edge in either direction, so the columns read first stay in view (to the left of the widget for right-to-left text).
- FIXED: Link taps inside quotes account for the quote's indent.
- CHANGED: Under tight constraints (for example a fixed-width chat
bubble), right-to-left content aligns to the full width, like
Text. Under loose constraints the widget still sizes to its content. - CHANGED: Fenced code blocks always render left-to-right, even in right-to-left documents.
- ADDED:
ParagraphGestureHandler.hitTestInlineSpanWithPointerEventtakes an optionalorigin, for painters whose text is not painted at the block origin. - FIXED: A dry layout at other constraints, made after the real layout, left hit testing, highlights and link taps at the measured positions while the glyphs stayed put. The real layout is now restored after measuring.
Monospace font resolution #
- FIXED: Inline
`code`and fenced blocks pick a monospace family the host platform can actually resolve. Flutter hands the family straight to the platform font manager, and CoreText (iOS / macOS) and DirectWrite (Windows) do not know the CSS generic'monospace'— code silently rendered in the proportional body face there. Those two now getMenlo/Consolas. Android, Fuchsia, Linux and web keep'monospace', so the primary face there is unchanged. Note that on web it is unchanged but still wrong: CanvasKit / skwasm reach no system fonts, so no family name resolves there and code has always rendered in the proportional default. Bundling a monospace asset and pointingmonospaceFontFamilyat it is the only fix, and that is a host decision. - CHANGED: code spans now carry a
fontFamilyFallbackon every platform: the monospace chain followed by whatevertextStylealready carried. A monospace face covers Latin and little else, so the body face's chain is appended rather than replaced — a host's CJK / emoji coverage keeps working inside`code`and fenced blocks. - ADDED:
MarkdownThemeData.monospaceFontFamily/monospaceFontFamilyFallbackto pin a bundled face. Both default to the platform-resolved chain above and are resolved once, in the constructor. One knob covers both inline`code`and fenced blocks — there was previously no supported way to change the fenced-block face at all:BlockPainter$Codebuilt its style in a static method reading a global, so even overridingtextStyleFormoved inline code only. An empty fallback list means "no monospace fallbacks", not "unset" —textStyle's own chain still applies. - CHANGED:
MarkdownThemeData.mergeThemeresolves the default face fromThemeData.platformrather than the globaldefaultTargetPlatform, so an app that adapts its Material theme to another platform gets that platform's face.ThemeData.platformdefaults todefaultTargetPlatform, so this only moves for an app that deliberately overrode it.
0.2.0 #
Upgrading from 0.0.x? See the migration guide. 0.2.x is almost entirely backward compatible — the only required code change is a new
alertbranch for directMD$Block.map/switchcallers.
- ADDED: Opt-in, dependency-free syntax highlighting for fenced code blocks
(65+ languages, GitHub light/dark themes). Assign a
SyntaxHighlighterto the newMarkdownThemeData.highlighterfield; the default (unset) renders code as plain monospace, so existing usage is unchanged. New public API onpackage:flutter_md/highlight.dart:SyntaxHighlighter,MarkdownHighlighter,CodeHighlightTheme,Grammar,GrammarToken,compileHighlightPattern(SyntaxHighlighter/CodeHighlightThemeare also re-exported from the main entrypoint). Each language is its own library (package:flutter_md/highlight/<lang>.dart, e.g.HighlightDart.grammar) with no central registry, so importing one never references the others and unused grammars tree-shake away — a Dart-only app adds ~0 beyond the engine; all 65 add ~62 KB gzipped.HighlightThemes.githubDark/githubLight(highlight/themes.dart) provide ready themes;allHighlightLanguages(highlight/all.dart) is a convenience registry of every grammar for demos/tooling (it references all languages, so unused ones can no longer tree-shake away). The highlighter only partitions text — never edits it — so selection and copy stay aligned. Grammars are generated bytool/highlight_codegen(adapted from Prism, MIT). - ADDED: Cross-block and cross-widget text selection. A
MarkdownSelectionControlleranchors the selection on the immutable model, so it spans multiple blocks and multipleMarkdownWidgets and survives list disposal (e.g. chat scrolling). New public API:MarkdownSelectionController,MarkdownSelectionScope,MarkdownSelectionGroup,MarkdownPosition,MarkdownSelection,MarkdownDocumentRef,MarkdownSelectedContent(+ document/block),MarkdownSelectionFormatter/MarkdownPlainTextFormatter/MarkdownMarkupFormatter,MarkdownReconciliationPolicy,MarkdownSelectionSurface,markdownBlockRenderedText, andSelectableBlockPainter/SelectableTextBlock. - ADDED:
StreamingMarkdownParser, an incremental parser for streaming sources such as LLM token output. It freezes completed blocks (a block ends at a blank line, outside any open code fence) so only the still-growing tail is re-parsed as tokens arrive — turning theO(N²)cost of re-parsing the whole buffer on every token into roughlyO(tail)(3–14× faster on a full message stream inbenchmark/streaming_benchmark.dart).parser.add(chunk)returns the growingMarkdown, always identical block-for-block toMarkdown.fromString(everythingSoFar), and aStream<String>.toMarkdown()extension wires it into a stream transform. Pass a configuredMarkdownDecoder(e.g.inlineMath: true) to matchMarkdown.fromString. The batchMarkdownDecoderhot path is byte-for-byte unchanged. - ADDED:
MarkdownMarkupFormatter, a built-in "Copy as Markdown" formatter. Pass it togetText()(or setcontroller.formatter) to reconstruct Markdown structure on copy — heading#s, nested list markers with task checkboxes, blockquote/alert>prefixes, fenced code and pipe tables — for blocks the selection covers in full; partially-selected boundary blocks fall back to the plain sliced text so nothing outside the selection is emitted. The default copy behaviour is unchanged (MarkdownPlainTextFormatter). - ADDED:
MarkdownWidgetgains optionaldocumentIdandcontrollerparameters (resolved from the ambient scope). Backward compatible: a widget with nodocumentIdis inert. - ADDED: Lists and tables are now interactively selectable. A new
MultiPainterSelectablemixin (+SelectableFragment) maps pointer positions and highlight boxes across the manyTextPainters of a list's items or a table's cells, so a drag can start or end inside a list item or table cell and the copied text keeps the\n/\tseparators ofmarkdownBlockRenderedText. - ADDED: Keyboard shortcuts and a context toolbar on
MarkdownSelectionScope, mirroringSelectableRegion/SelectableText. When focused:Ctrl/Cmd+Ccopies,Ctrl/Cmd+Aselects all,Shift+arrows extend by character / word / line / document (and vertically by geometry),Escclears. Right-click (desktop) / long-press (mobile) shows an adaptive Copy / Select-all toolbar. The scope is now aStatefulWidgetwith a publicMarkdownSelectionScopeState(copySelection/selectAll/clearSelection/showToolbar/hideToolbar/contextMenuButtonItems/contextMenuAnchors). New customization params:focusNode,enabled,selectionColor,contextMenuBuilder,magnifierConfiguration,selectionControls,onSelectionChanged. New controller ops:selectionColor,globalSelectionRects,moveSelectionEdgeToGlobal, and theextendSelectionBy*family;MarkdownPosition.copyWith. - ADDED: Native selection handles and a magnifier on touch platforms,
driven by Flutter's
SelectionOverlay. Selection endpoints pushLeaderLayers from the render objects so the handles follow the content as it scrolls (and across multipleMarkdownWidgets); dragging a handle adjusts the selection and shows the platform magnifier. Handles/magnifier respect the platform (selectionControls,magnifierConfiguration) and are absent on desktop, matchingSelectableText. New surface geometry:localSelectionRects,setSelectionHandleLayers,repaintSelection, andMarkdownSelectionController.selectionHandleEndpoints/MarkdownHandleEndpoints. - ADDED: Word- and block-granular selection gestures. Double-click/tap
selects the word under the pointer, triple-click/tap selects the whole block,
a single click collapses (clears) the selection, and
Shift-click extends it. Dragging after a double/triple click keeps word/block granularity; a touch long-press grabs the whole word (then extends by word), and a touch double-tap selects the word and pops the toolbar. Word boundaries use the platform word segmentation (TextPainter.getWordBoundary), so double-click keeps intra-word punctuation like apostrophes (can't). New controller ops:selectWordAtGlobal,selectBlockAtGlobal,wordSelectionAt,blockSelectionAt,extendSelectionGranular, andwordRangeIn; new surface geometryMarkdownSelectionSurface.wordBoundaryForGlobal. - ADDED: Mouse cursor feedback — a
MarkdownWidgetshows the click (hand) cursor over actionable links, the text (I-beam) cursor while it participates in a selection controller, and otherwise the default cursor. - CHANGED:
MarkdownWidget's render object now draws the selection highlight outside the cached contentPictureand becomes a repaint boundary when selectable, so selection/drag repaints do not rebuild the glyph cache. The highlight color is now customizable via the controller / scope. The highlight is painted on top of (rather than beneath) the glyphs, so a translucent selection stays visible over opaque backgrounds — code fences,inline code, and==marked==spans. - EXAMPLE: Reworked the demo tabs — a longer, richer chat (tables, code, nested/task lists, alerts, math, token-by-token streaming with a typing indicator, Select-all/Clear) and a Selection tab that spans every block type.
0.1.0 #
- ADDED: GitHub-style alert blocks (
> [!NOTE],> [!TIP],> [!IMPORTANT],> [!WARNING],> [!CAUTION]) via the newMD$Alertblock andMD$AlertType. - ADDED: GitHub task-list items (
- [ ]/- [x]) viaMD$ListItem.checkedandMD$ListItem.isTask, rendered with a checkbox. - ADDED: Table column alignment (
:---,:--:,---:) captured onMD$Table.alignmentsand applied when rendering. - ADDED:
linkStyleonMarkdownThemeDatato customize link text styling (thanks @inamhusain, #22). - ADDED: Per-type alert accent colors via
MarkdownThemeData.alertColorsandalertColorFor. - ADDED: Opt-in
$...$inline LaTeX math conversion to Unicode, disabled by default. Enable withMarkdownDecoder(inlineMath: true)orMarkdown.fromString(text, inlineMath: true). Supports LaTeX commands (\alpha,\rightarrow, ...), superscripts/subscripts (x^2,H_2O,x^{10}), is code-span and code-block safe, and preserves currency ($5). The command table is configurable viamathReplacements(extend the exportedkMarkdownMathCommands). Originally proposed in #21 by @ibragimov05. - FIXED:
\$is now a recognized backslash escape, producing a literal dollar sign (and opting a$...$run out of math conversion). - CHANGED: Thematic breaks now support
***and___(and spaced variants like- - -), and no longer greedily consume text after---. - CHANGED:
~~~fenced code blocks are now recognized in addition to```. - FIXED: Emphasis no longer leaks to the end of the line for stray or
unterminated markers (e.g.
5 * 6 = 30,**bold never closed). - FIXED: Intraword underscores are no longer treated as emphasis
(e.g.
snake_case,object_idare preserved). - FIXED: ATX headings require a space after
#;#hashtagand 7+#are no longer headings, and trailing#sequences are stripped. - FIXED: Emphasis surrounding a link/image is now merged onto the link span.
- FIXED: Link/image targets support
<url>and single-quoted titles. - FIXED:
MarkdownThemeData.copyWithno longer dropsbuilderandonLinkTap. - BREAKING:
MD$Block.map/maybeMapgained analertbranch for the newMD$Alertblock type. - PERFORMANCE: Rewrote the parser hot path — a single-span fast path for
plain text, first-code-unit guards that keep regexes off paragraph lines,
hand-rolled list-line and link-target parsing (removing per-line / per-link
RegExpallocation), lazy link-extraction gated on[, and a range-copy escape rebuild (no more per-character hash-set lookups). Together with math now being opt-in, the default parse path is roughly 45% faster across representative workloads (links −68%, lists −61%, escapes −68%). Output is byte-identical, guarded by a golden snapshot test. - TESTS: Added a golden characterization snapshot, a corner-case regression
suite, span-offset invariants, and unit tests for the node model, theme, and
widget; wired every test file into
test/unit_test.dartso CI runs the full suite (370+ tests, previously only a fraction ran).parser.dart,nodes.dart,markdown.dart,theme.dart, andwidget.dartare now at ~100% line coverage. - ADDED:
benchmark/parser_benchmark.dart(a multi-scenariobenchmark_harnesssuite) andbenchmark/compare.dart(a low-noise before/after comparison tool). - DOCS: Documented alerts, task lists, table alignment, thematic-break variants, and opt-in inline math in the README.
0.0.8 #
- CHANGED: New table render
- FIXED: Invalidate and relayout render object after system fonts changed.
0.0.7 #
- FIXED: Preserved indentation on line breaks within list items [#4].
- FIXED: Inline code no longer processes inner Markdown syntax [#10].
- CHANGED: Improved theme support.
- ADDED: Dark mode support in the example app.
0.0.6 #
- FIXED: Fixed escaping of special characters. [#6]
0.0.5 #
- FIXED: Fixed parsing url such as
[text](https://domain.com/path(with)brackets).
0.0.4 #
- CHANGED: Improved link tap handling.
0.0.3 #
- FIXED: Links inside lists now work correctly.
0.0.2 #
- ADDED: All field in
MarkdownThemeData()are now optional. - ADDED:
MarkdownThemeData{}.headingStyleFormethod to customize heading styles. - FIXED: Remove clipping for canvas. Fixes one line text trim at browsers.
- FIXED: Correctly apply styles to text in blocks.
0.0.1 #
- ADDED: Initial release with basic functionality.