Skip to main content

livemd_flutter 0.3.0 copy "livemd_flutter: ^0.3.0" to clipboard
livemd_flutter: ^0.3.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.

Why the continuous engine matters #

The editor owns one TextEditingValue, FocusNode, input connection, selection, composing range, and undo history for the entire document. Rendered content is projected from exact UTF-16 source ranges instead of switching between per-block preview widgets and temporary text fields.

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, focus, scroll surface, undo/redo
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 Rendered inline pictures (network or asset: bundle destinations via an injectable resolver) inside a reserved row height with a broken-image fallback; pointer clicks report onImageTap while arrow traversal reveals source
Video ![alt](destination.mp4) renders a video slot (an injectable videoBuilder supplies the player) with the same reserved row, onVideoTap pointer action, and source reveal
Audio ![alt](destination.mp3) renders an audio slot (an injectable audioBuilder supplies the player) with the same reserved row, onAudioTap pointer action, and source reveal
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
Complex blocks Rendered fenced code, Callout shell, GFM table, and thematic break until the caret enters their source
Layout No preview/editor remount; rendered Markdown owns its run-metric line heights (heading lines, rows, inline placeholders) with regression tests for Y stability

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.

Usage #

late final LiveMarkdownController controller;

@override
void initState() {
  super.initState();
  controller = LiveMarkdownController(
    text: '# Hello\n\nMove the caret into **Markdown**.',
  );
}

@override
Widget build(BuildContext context) {
  return LiveMarkdownEditor(
    controller: controller,
    onChanged: (markdown) => save(markdown),
    onLinkTap: (destination, label) {
      openLink(destination);
    },
    onWikiLinkTap: (target, alias) {
      openNote(target);
    },
    onTaskToggle: (sourceRange, checked) {
      logTaskChange(sourceRange, checked);
    },
  );
}

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

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.

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:
    git:
      url: https://github.com/sd0116baka/livemd-flutter.git
      ref: main

Once published to pub.dev, the same dependency becomes:

dependencies:
  livemd_flutter: ^0.3.0

Media playback is intentionally app-owned: the editor renders image, video, and audio embeds into reserved rows 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.

Current boundary #

Complex blocks keep a rendered shell and preserve source offsets, but table cell editing and editing Callout text inside its rendered shell are not yet a 1:1 Obsidian implementation. Entering those source ranges currently reveals the complete block source. Embeds, 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
0
points
235
downloads

Publisher

unverified uploader

Weekly Downloads

A lossless continuous Markdown Live Preview editor for Flutter.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, flutter_markdown_plus

More

Packages that depend on livemd_flutter