Skip to main content

livemd_flutter 0.5.0 copy "livemd_flutter: ^0.5.0" to clipboard
livemd_flutter: ^0.5.0 copied to clipboard

A lossless continuous Markdown Live Preview editor for Flutter.

LiveMD Flutter #

A lossless, continuous Markdown Live Preview editor for Flutter. It follows the editing model used by Obsidian rather than a conventional WYSIWYG editor: Markdown remains the only source of truth, rendered syntax stays in the same native editing surface, and the caret reveals source only when it enters that source range.

Quick start (5 minutes) #

Requires Dart ^3.13.1 and Flutter >=3.35.0. Your Flutter installation must satisfy both constraints; check with flutter --version.

1. Install #

From your Flutter app's directory, run:

flutter pub add livemd_flutter:^0.5.0

2. Replace lib/main.dart #

This complete example runs without assets, storage services, or player plugins:

import 'package:flutter/material.dart';
import 'package:livemd_flutter/livemd_flutter.dart';

void main() => runApp(const MaterialApp(home: MarkdownPage()));

class MarkdownPage extends StatefulWidget {
  const MarkdownPage({super.key});

  @override
  State<MarkdownPage> createState() => _MarkdownPageState();
}

class _MarkdownPageState extends State<MarkdownPage> {
  late final LiveMarkdownController _controller;

  @override
  void initState() {
    super.initState();
    _controller = LiveMarkdownController(
      text: '# Hello\n\nEdit **Markdown** here.\n\n- [ ] Try a task',
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Markdown editor')),
      body: LiveMarkdownEditor(
        controller: _controller,
        onChanged: (markdown) {
          // Demo only: replace this with your app's persistence logic.
          debugPrint(markdown);
        },
      ),
    );
  }
}

3. Run #

flutter run

You should see an editable heading, bold text, and a task checkbox. Move the caret into formatted text to reveal its Markdown source. Changes are printed to the debug console, not saved; restarting the app restores the sample.

Already have an app? Use MarkdownPage as a route, or put LiveMarkdownEditor in your own page. Create the controller once in initState, not in build, and dispose it when the page is removed. Inside a Column, wrap the editor in Expanded to give it a bounded editing area.

Connect it to your app #

  • Load and save: pass your stored Markdown as the controller's initial text. Read controller.text for the current source, or persist the markdown received by onChanged. The package does not provide file, database, or cloud storage. Debounce frequent writes in your app if needed.
  • Open links: implement onLinkTap: (destination, label) { ... } for external links and onWikiLinkTap: (target, alias) { ... } for note navigation. The editor reports taps; your app validates destinations and decides where to navigate.
  • Task checkboxes: toggling a task updates the Markdown automatically. Use onTaskToggle only if you also need a task-specific notification.
  • Images and playback: network and asset: images have a default resolver; declare asset files in your app's pubspec.yaml. Use imageResolver for custom image loading, and inject videoBuilder / audioBuilder for actual playback. No audio/video player is bundled.

For parameters and types, see the API reference. The quick-start source matches the example above. The full example app demonstrates links, tables, images, and injected audio/video players. To run either from a checkout of this repository:

cd example
flutter pub get
flutter run -t lib/quick_start.dart # Minimal editor
# Or: flutter run                 # Full feature demo

Why the continuous engine matters #

The editor owns one global TextEditingValue, selection, composing range, and undo history for the entire document. Its main surface is one continuous native editor projected from exact UTF-16 source ranges. A pointer-edited table cell temporarily receives native text focus, but every local value is mapped back into that same global source and history.

That makes these native editing behaviors possible:

  • selection, Shift+Arrow, copy, and IME composition across block boundaries;
  • programmatic global selections that stay synchronized with the visible caret;
  • one undo/redo history for typing, structural commands, and task toggles;
  • stable line boxes and baselines while inline source markers appear;
  • pointer actions that are distinct from arrow-key source traversal.

For example, clicking a rendered link invokes onLinkTap, while moving the caret into that same link with an arrow key reveals [label](destination). Clicking a task checkbox toggles [ ]/[x]; moving left from the task text into the prefix reveals the complete task source.

Supported behavior #

Area Current support
Document editing One global source, selection, composing range, scroll surface, and undo/redo history
Headings ATX levels 1–6, closing hashes, stable content typography and baseline
Inline Nested bold/italic, strikethrough, highlight, multi-backtick code spans
Links Markdown destinations/titles, balanced destination parentheses, Wiki targets and aliases, pointer callbacks
Images Standalone previews (network or asset: destinations) and line-height inline thumbnails via an injectable resolver; pointer clicks report onImageTap, while source reveal keeps a standalone preview below the source line
Video ![alt](destination.mp4) uses an injectable videoBuilder: a 240 px standalone preview or a line-height inline projection, with distinct pointer action and source traversal
Audio ![alt](destination.mp3) uses an injectable audioBuilder: a compact 48 px standalone player or a line-height inline projection, with distinct pointer action and source traversal
Literal states Escapes, word-internal underscores, and incomplete syntax stay lossless and literal
Lists Unordered, ordered, tasks, nesting, Enter continuation/exit, Shift+Enter continuation, Tab/Shift+Tab, marker Backspace
Ordered lists Following same-level items are renumbered after structural Enter
Quotes Rendered quote marker plus Enter/exit/nesting commands
Fenced code The full-width code container stays mounted while fences/source appear; raw and rendered text share one width, scaler, and height budget
Tables One real GFM table layout drives rich inline/media rendering, hit testing, wrapped row heights, and native pointer cell editing; arrow traversal opens a native whole-table source layer in the same footprint; Tab/Shift+Tab/Enter navigate cells, gutter controls add rows/columns, and the context menu mutates both axes
Other complex blocks Pointer-edit Callout titles and bodies inside the rendered shell; keyboard source traversal and thematic breaks preserve the block height budget
Layout No preview/editor remount; rendered Markdown owns its run-metric line heights (heading lines, rows, inline placeholders) with regression tests for Y stability

Without a real wrap, inline media reveal at the same footprint as their source, so following text keeps its X/Y position. Standalone media are the deliberate exception: revealing source adds exactly one normal text line above the retained preview, while the preview itself neither resizes nor collapses. Media source uses a natural-height caret rather than stretching it to the preview. If an embed source cannot fit on one line at the available width, the editor keeps it as literal source instead of constructing a projection that would overflow or reorder its wrapped lines.

The positioned syntax tree is intentionally conservative while text is being edited: unsupported or incomplete constructs remain literal source instead of being guessed and reformatted.

Visual customization #

The APIs below require livemd_flutter 0.5.0 or later. All Markdown styling uses LiveMarkdownThemeData. Register application-wide defaults in ThemeData.extensions, then override only what differs on an editor:

ThemeData(
  extensions: const [
    LiveMarkdownThemeData(
      bodyStyle: TextStyle(fontSize: 15, height: 1.3),
      h1Style: TextStyle(fontSize: 26),
      linkStyle: TextStyle(decoration: TextDecoration.none),
      appearance: LiveMarkdownAppearance(
        highlightColor: Color(0xffd9c9ff),
        calloutBorderRadius: BorderRadius.zero,
      ),
    ),
  ],
)
LiveMarkdownEditor(
  controller: controller,
  theme: const LiveMarkdownThemeData(
    bodyStyle: TextStyle(fontSize: 17),
    appearance: LiveMarkdownAppearance(
      calloutBorderSide: BorderSide.none,
    ),
  ),
)

The local font size preserves the global line height, colors, and other fields. There is only one precedence order:

  1. Host-adaptive Openbox defaults.
  2. The nearest ThemeData extension for LiveMarkdownThemeData.
  3. The editor's theme, merged field by field.

TextStyles merge their properties (inherit: false replaces a style). Appearance merges individual fields. Decorations, borders and layout policies replace the corresponding value. merge overlays partial themes; copyWith replaces non-null fields. To remove local overrides, supply a new partial theme or set theme to null. Remove the global extension too to restore defaults.

Supported theme fields #

Fields What they control
bodyStyle Body font, size, color and line height; also the base for table and Callout text
h1Style–h6Style Heading typography
linkStyle Markdown/Wiki link styles; decoration defaults to underline unless explicitly specified
codeStyle, codeBlockDecoration Code typography and painted fenced-code background
tableBodyStyle, tableHeadStyle, tableBorder Table text and border
tableColumnWidth, tableVerticalAlignment Column width policy and vertical cell alignment; horizontal text alignment comes from Markdown
tableCellDecoration, tableHeaderCellDecoration Body/header cell decoration
tableCellHorizontalPadding, tableHeaderCellHorizontalPadding Symmetric left/right cell padding, in nonnegative logical pixels; vertical spacing remains layout-owned
appearance Source, highlight, quote, task, Callout and active-cell paint; Callout left border and corner radius

The appearance defaults are host outline for document syntax, onSurfaceVariant for table syntax, yellow at alpha 0.38 for highlights, primary at alpha 0.5 for quotes, and primary for checked tasks. Callout accent inherits the effective task color; its background uses the accent at alpha 0.10. Active cells use surfaceContainerHighest at alpha 0.78. Explicit fills retain their alpha.

Callout border defaults to a left edge of width 3 in the accent color, and its radius defaults to 6. Use BorderSide.none or BorderRadius.zero for a borderless or square shell. These settings paint without adding padding or clipping content; choose border widths that fit the existing inset.

Defaults and application-owned layout #

LiveMarkdownThemeData.openbox() is the public default preset. It keeps the bodyMedium-based text with height 1.5, heading sizes 32/28/24/21/18/16, and host-derived colors. It works in light/dark hosts without a fixed page palette. Local DefaultTextStyle is not automatically adopted; pass its style as theme.bodyStyle when the page's local typography should apply.

padding and editorDecoration configure the editing surface, not Markdown semantics. The host owns page background, width, positioning and toolbars. videoBuilder and audioBuilder own player appearance within existing slots. Cursor and selection colors follow the host textSelectionTheme.

Keep the same controller/editor key when changing style. Paint-only changes preserve document state and geometry. Font, line-height and horizontal-padding changes may reflow text. Flutter theme transitions interpolate explicit values; use the same set of fields in light/dark themes for predictable transitions. Column-width policies and vertical-alignment enums switch discretely.

Callout padding/icon, media slot dimensions, table vertical spacing, arbitrary block replacement and menu customization are not styling controls. See the embedded-host example for Openbox, compact desktop and dark borderless hosts using one controller.

Advanced source access #

LiveMarkdownController.value exposes the complete TextEditingValue; the compatibility properties text and selection remain available. The public MarkdownSyntaxParser also exposes lossless UTF-16 SourceRange nodes for applications that need source-aware commands or decorations.

Table structure operations are also available through applyMarkdownEditingCommand: insert/delete the current row or column with the six tableInsert... / tableDelete... enum values. The built-in table hover controls and context menu use the same command path, so each mutation is one document undo transaction.

Using as a dependency #

The library is a pure Dart/Flutter package with no native code, so it runs on every Flutter platform. Add it to your pubspec.yaml:

dependencies:
  livemd_flutter: ^0.5.0

The package requires Dart ^3.13.1 and Flutter >=3.35.0. Alternatively, pin the Git release tag:

dependencies:
  livemd_flutter:
    git:
      url: https://github.com/sd0116baka/livemd-flutter.git
      ref: v0.5.0

Media playback is intentionally app-owned: the editor renders standalone media slots and compact inline projections, and reports pointer actions, but the actual player widget is injected through imageResolver, videoBuilder, and audioBuilder. The example app wires video_player and just_audio; your app can supply any player. Revealing a standalone media source intentionally adds one source line above the still-visible preview, matching the observed Obsidian media-source treatment instead of collapsing the preview.

Current boundary #

Callout titles and bodies support pointer editing inside the rendered shell, while keyboard traversal can reveal the raw block source. A table pointer click uses a rendered cell editor, while arrow-key traversal opens a native whole-table source editor over an invisible copy of the same table geometry, so the following content stays fixed even for wrapped rich cells. Both the active cell and whole-table editor map selection, composing ranges, and undo transactions back to global UTF-16 offsets. A standalone media preview remains visible while its source is active, but its embedded controls are temporarily non-interactive; leave the source range to operate the player again. Obsidian ![[...]] vault embeds/transclusions, math, footnotes, reference links, and full CommonMark delimiter-flanking coverage are also future work.

The repository includes the observed behavior oracle in doc/obsidian_live_preview_behavior.md and a broad sample document in test/fixtures/obsidian_live_preview_matrix.md.

0
likes
160
points
235
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A lossless continuous Markdown Live Preview editor for Flutter.

Repository (GitHub)

License

MIT (license)

Dependencies

flutter, flutter_markdown_plus

More

Packages that depend on livemd_flutter