lexical_core 1.3.0 copy "lexical_core: ^1.3.0" to clipboard
lexical_core: ^1.3.0 copied to clipboard

Pure-Dart editor state model that is wire-compatible with Meta's Lexical editor: node map, sibling-pointer tree, immutable double-buffered state, node registry and JSON import/export. No Flutter dependency.

lexical_core

lexical_core #

Pure-Dart editor state model, wire-compatible with Lexical 0.48.x. No Flutter dependency — it runs under dart test and on the server just as well as in an app.

This is the engine, not the editor. Rendering, input and selection live in lexical_flutter; node types beyond the built-ins live in their own packages.

Install #

dependencies:
  lexical_core: ^1.0.0

Use #

import 'package:lexical_core/lexical_core.dart';

final editor = LexicalEditor();

editor.update(() {
  final paragraph = $createParagraphNode()
    ..append($createTextNode('Hallo ')) 
    ..append($createTextNode('Welt')..toggleFormat(TextFormat.bold));
  $getRoot().append(paragraph);
}, discrete: true);

print(editor.toJsonString());

// Reading needs a read context: nodes resolve their current version through
// the active state, which is what makes stale references harmless.
final text = editor.read(() => $getRoot().getTextContent());

Opening a document authored on the web:

final state = editor.parseEditorStateFromString(jsonFromServer);
editor.setEditorState(state);

What it gives you #

  • Immutable, double-buffered state. Committed states are frozen and share every untouched node with their predecessor, so an edit costs O(changed nodes) and an undo stack is a list of state references.
  • Sibling-pointer tree. Elements track first/last/size; nodes track prev/next/parent — all as keys resolved through the node map. Inserting a node touches four objects regardless of how many children there are.
  • A node registry in place of upstream's static polymorphism, which Dart cannot express. Registration is per editor and closed at construction.
  • Wire-exact JSON. Integers stay integers, omit-vs-explicit-null is matched per field, and derived paragraph fields are re-derived rather than copied.
  • A bounded, non-recursive importer with a configurable policy for unknown node types.

Things worth knowing #

Node accessors need a context. node.getFirstChild() resolves the node's current version through the active editor state, so it only works inside editor.update(), editor.read() or editorState.read(). This is upstream's design and it is what makes a captured node reference safe to keep.

clone() must be overridden by every subclass and return its own type. The framework supplies the key, so implementations never mention it. A subclass that inherits clone() silently downgrades its own type on the next edit; that is caught by an assertion in debug builds.

Never override == or hashCode on a node. Identity is the key. Two versions of one node must stay distinguishable.

Keys are ephemeral. They are not serialized, importing the same document twice produces different keys, and any test asserting a key value is broken by construction.

editorState flushes pending updates — a deliberate divergence from upstream, which returns the pre-update state until a microtask runs. Pass discrete: true when you want the commit to happen inside the update call.

Adding a node type #

class CalloutNode extends ElementNode {
  CalloutNode([this._tone = 'info']);
  String _tone;

  @override
  String get type => 'callout';

  @override
  CalloutNode clone() => CalloutNode(_tone);

  @override
  void afterCloneFrom(covariant CalloutNode prev) {
    super.afterCloneFrom(prev);   // required: the clone loses its pointers otherwise
    _tone = prev._tone;
  }

  @override
  Map<String, Object?> exportJson() => {...super.exportJson(), 'tone': _tone};

  @override
  void updateFromJson(Map<String, Object?> json) {
    super.updateFromJson(json);
    _tone = json['tone'] is String ? json['tone']! as String : 'info';
  }
}

final editor = LexicalEditor(
  nodes: [NodeSpec<CalloutNode>(type: 'callout', create: CalloutNode.new)],
);

Write the model and its round-trip test before any renderer. Rendering bugs are visible and local; serialization bugs corrupt user documents silently and are found weeks later in production data.

Compatibility #

See the repository README for the exact compatibility contract, including why paragraph.textFormat changes on a round-trip and why that is correct.

Licence #

MIT. Derived from Lexical, © Meta Platforms, Inc., also MIT. See NOTICE in the repository root.

0
likes
0
points
787
downloads

Publisher

verified publisherahmadre.com

Weekly Downloads

Pure-Dart editor state model that is wire-compatible with Meta's Lexical editor: node map, sibling-pointer tree, immutable double-buffered state, node registry and JSON import/export. No Flutter dependency.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

characters, meta

More

Packages that depend on lexical_core