Specs / Reference

Lexical Parity

Lexical Feature Parity — Catalog and Outcomes

Status: spec only; implementation pending. This doc enumerates the Lexical features we explicitly commit to implementing, maps each to a weaver primitive, and defines the gradeable outcome that says “parity is shipped.” The PRD non-goal (§5) “100% feature surface of Lexical day one” stands — this catalog is the load-bearing subset we do commit to, with each gap from full Lexical called out explicitly rather than silently omitted.

The reference is Lexical’s documentation and the lexical-playground feature set, surveyed as of 2026-05.

How to read this catalog

Each row maps a Lexical primitive (node, plugin, command, hook) to one of three states:

StatusMeaning
In v1Shipped at parity; the row names the weaver equivalent.
🔁 In v1 via pluginNot in @weaver/core, but the v1 first-party plugin set covers it.
v2 / out of scopeDeliberately deferred; the row says why.

A reviewer should be able to read each row and confirm by pointing at the corresponding spec section or a follow-up issue.

1. Node / block kinds

Lexical’s node is weaver’s block (LoroTreeNode + LoroMap of typed attrs + optional LoroText; see block-model.md §2).

Lexical nodeweaver kindStatusNotes
RootNode(implicit; the LoroTree root)Not a user-visible block; structural.
TabNodeinline (literal tab) inside LoroText, plus a tab-indent plugin (@weaver/plugins-tab)🔁Lexical ships TabNode + LexicalTabIndentationPlugin. We model the character in inline text; structural indent for lists uses block.indent/block.outdent directly.
ParagraphNodeparagraphDefault block; markdown shortcut to other kinds.
TextNode (text leaves with format)LoroText + marksInline text is LoroText; format is CRDT mark/unmark. Lexical’s “format bitmask” is encoded as overlapping marks.
LineBreakNode (soft break)inline or <br> analog inside LoroTextSoft break inside a text-bearing block.
ElementNode (custom container)plugin-registered block kind with children: true🔁Plugins extend the kind catalog.
DecoratorNode (React-rendered atomic node)block kind with hasInline: false, plugin-supplied React adapterE.g., image, embed, mention, divider.
HeadingNodeheading (level 1–3 UI; 1–6 schema)
QuoteNodequoteSingle-level only in v1.
ListNode (ul / ol)bullet-list-item, numbered-list-item (nesting via tree children)Lexical models the list container as one node; weaver models each item as a block whose children are its nested items. Functionally equivalent.
ListItemNodebullet-list-item, numbered-list-item, to-do
CodeNodecode block kindtree-sitter highlighting.
CodeHighlightNodeinline span emitted by tree-sitter highlighterNot a CRDT op; render-time decoration.
LinkNodelink markweaver models link as a mark over LoroText, not a separate element.
AutoLinkNodelink mark + auto-linker plugin🔁Plugin detects URL-shaped runs and applies link.
OverflowNode (Lexical’s character-limit affordance)Deferred. Lexical ships @lexical/overflow + LexicalCharacterLimitPlugin; weaver plugins can implement equivalent behavior over LoroText length.
HorizontalRuleNode (already row above, but Lexical also exports @lexical/react/LexicalHorizontalRuleNode)divider block kindSame block as HorizontalRuleNode; named separately for cross-ref.
PageBreakNode (playground)divider with pageBreak: true attr or a future page-break plugin block kind🔁First-party Lexical playground node; deferred to a v1 plugin if requested.
Embed-shaped playground nodes — FigmaNode, TweetNode, YouTubeNode, ExcalidrawNodeembed block kind with allowlisted providers🔁These are Lexical playground demos showing how to register provider-specific embeds; weaver covers them through one polymorphic embed kind in v1.
AutocompleteNode (playground)inline “agent-pending” preview rendered via agent-pending mark🔁Lexical’s autocomplete is a custom node; weaver’s equivalent is the agent-suggestion overlay (see ai-agent.md).
MarkNode (annotations)comment-anchor markweaver uses an internal mark to anchor comment threads; the comment payload itself lives in a sibling LoroDoc container.
TableNode / TableRowNode / TableCellNodetable / table-row / table-cell block kindsBlock-table, not Database (see ADR 0002). Fixed columns.
HorizontalRuleNodedivider
ImageNode (playground-only in Lexical)image block kindOPFS cache + R2.
Custom EmbedBlockNode (e.g. YouTube, Twitter in playground)embed block kindAllowlisted providers; iframe sandbox.
HashtagNodeinline plugin-registered span + mention analog🔁Lexical ships this as a published package (@lexical/hashtag + LexicalHashtagPlugin), not playground-only. v1 first-party set if demand exists; not core.
KeywordNode (playground)Niche; not in v1.
EmojiNode (playground replacement)🔁Trivial plugin; OS emoji is the default.
CollapsibleContainerNode (toggle in lexical-playground/src/plugins/CollapsiblePlugin/)toggle block kindLexical’s collapsible lives in the playground source, not a first-party @lexical/* package.
LayoutContainerNode / LayoutItemNode (multi-column in playground)Multi-column layout not in v1; could be a plugin in v2.
PollNode / StickyNode / EquationNode (playground curiosities)Demo-only in Lexical too; not committed for v1.

2. Marks / inline formatting

Lexical encodes formatting as a bitmask on TextNode. weaver encodes it as overlapping CRDT marks on LoroText (see block-model.md §3 “Marks shipped in v1”).

Lexical formatweaver markStatus
boldbold
italicitalic
underlineunderline
strikethroughstrike
code (inline)code (inline; cannot overlap link)
subscript🔁 — plugin-supplied mark; not core.
superscript🔁 — plugin-supplied mark; not core.
highlighthighlight (color enum)
Custom marks (e.g. comments)comment-anchor (internal)

3. Commands & editor operations

Lexical’s command bus is editor.dispatchCommand(COMMAND, payload). weaver’s command bus is the Effect-TS surface in @weaver/core (see architecture.md §4).

The command symbols below (text.*, block.*, selection.*, clipboard.*) name the v1 command-bus surface. Their full enumeration belongs in a forthcoming command-bus.md spec; block.* structural commands are already defined in block-model.md §3. Until command-bus.md lands, treat each row as a contract the future spec must keep.

Lexical command (or capability)weaver equivalentStatus
Text formatting (FORMAT_TEXT_COMMAND)text.toggleMark(blockId, range, markKind, attrs?)
Element formatting (FORMAT_ELEMENT_COMMAND — align)block.setAttr(blockId, "align", value) for align-aware kinds
Insert paragraph / line breakblock.split / soft-break op
Insert node at selection (INSERT_…_COMMAND family)block.insert(parentId, index, kind, attrs)
INDENT_CONTENT_COMMAND / OUTDENT_CONTENT_COMMANDblock.indent(blockId) / block.outdent(blockId)
INSERT_TAB_COMMANDtext.insertTab(blockId, offset) (inserts a tab character; structural indent goes through block.indent)
Remove text / nodesblock.delete, text.delete
Undo / redo (UNDO_COMMAND, REDO_COMMAND)Loro UndoManager peer-scoped by origin (see ADR 0001)
CLEAR_HISTORY_COMMANDeditor.clearHistory() — drops the UndoManager stack without touching the doc
CLEAR_EDITOR_COMMANDeditor.clear() — replaces the LoroDoc content with the empty-doc template
Selection ops (SELECT_ALL_COMMAND etc.)weaver selection.* commands operating on Cursor anchors
SELECTION_CHANGE_COMMAND (notification)useSelection() hook subscribes to selection changes; no imperative bus event needed
Keyboard (KEY_*_COMMAND family — arrows, enter, backspace, etc.)core keymap in @weaver/dom; plugins register key handlers via the plugin contract
Focus / blur (FOCUS_COMMAND, BLUR_COMMAND)editor.focus() / editor.blur() on the surface
Drag & drop (DRAGSTART_COMMAND / DRAGOVER_COMMAND / DRAGEND_COMMAND / DROP_COMMAND)drag handle UI dispatches block.move(blockId, newParentId, newIndex)
Clipboard (COPY_COMMAND / CUT_COMMAND / PASTE_COMMAND)clipboard.* surface with HTML / Markdown / weaver+loro binary serialization
CAN_UNDO_COMMAND / CAN_REDO_COMMAND introspectionuseUndoState() hook
Read-only modeeditor.setEditable(false) toggle

4. Plugins (Lexical’s first-party @lexical/react set)

Lexical ships ~30 packages under @lexical/*. weaver bundles equivalent behavior either into @weaver/core / @weaver/react or into the v1 first-party plugin set.

Lexical pluginweaver locationStatus
LexicalComposer (root provider)<WeaverEditor> React component
RichTextPlugin / PlainTextPlugincore; the editor is rich-text-only in v1
HistoryPlugincore; backed by Loro UndoManager
AutoFocusPluginoption on <WeaverEditor autoFocus>
OnChangePlugincore hook useOnDocChange (subscribes to LoroDoc diffs, debounced)
MarkdownShortcutPluginplugin in v1 first-party set (@weaver/plugins-markdown)🔁
ListPlugin / CheckListPlugincore; list kinds are built-in
LinkPlugin / ClickableLinkPlugin / AutoLinkPluginplugin (@weaver/plugins-link)🔁
CodeHighlightPlugincore via tree-sitter for code blocks
TablePlugincore; table kind is built-in
EmojiPickerPluginplugin (@weaver/plugins-emoji)🔁
TypeaheadMenuPlugin (the underpinning Lexical actually ships; “MentionsPlugin” in the wild is a playground example built on top)core; mention is a built-in inline-mode block kind; typeahead menu UI lives in @weaver/react
HashtagPluginplugin if needed🔁
DraggableBlockPlugin (block handle drag)core UI (@weaver/react’s drag handle)
FloatingTextFormatToolbarPlugin (Lexical playground)core UI (floating toolbar in @weaver/react)
HorizontalRulePlugin (@lexical/react)core; divider is a built-in block kind
TabIndentationPlugin (@lexical/react)core keymap for Tab / Shift-Tab → block.indent / block.outdent (structural) and text.insertTab (inline)
ClearEditorPlugincore; editor.clear() command
CharacterLimitPlugin (@lexical/react, paired with @lexical/overflow)
AutoEmbedPlugin (Lexical playground)plugin (@weaver/plugins-embed) — paste-URL → embed block🔁
@lexical/headless (no-DOM editor for SSR / server-side)⏳ — server-side LoroDoc operates without a DOM today; a typed wrapper for “headless” use lands when there is a customer for it.
SpeechToTextPlugin
CollaborationPlugin from @lexical/yjs (shared-history pattern is documented atop this, not a separate first-party plugin)core; CRDT collab is native, not a plugin (see ADR 0001)
CommentPlugin (playground)core, anchored by comment-anchor mark; sibling LoroDoc container holds thread payloads
TableOfContentsPluginderived from SQLite mirror outline (see wasm-strategy.md §2.2)
@lexical/markdown (exposes transformers + MarkdownShortcutPlugin)plugin (@weaver/plugins-markdown) covers transformers, import, and export🔁
HTML import / exportplugin (@weaver/plugins-html)🔁
LayoutPlugin (multi-column)
PollPlugin / StickyPlugin / EquationPlugin (playground demos)

5. Hooks & React surface

Lexical hookweaver equivalentStatus
useLexicalComposerContext()useWeaverEditor() returning the EditorContext
useLexicalCommand()useCommand() registering a typed handler against the command bus
Selection hooks ($getSelection, $createRangeSelection, etc.)useSelection() hook returning typed Cursor ranges; mutation via selection.* commands
Node lookup ($getNodeByKey)useBlock(id) / findBlock(id)
useEditable()option on <WeaverEditor editable={...}> + useEditable() reader

6. Serialization & import / export

Lexical capabilityweaverStatus
editor.toJSON()doc.exportSnapshot() (Loro snapshot, binary) and doc.toJSON() (debug-friendly tree of Block<K>)
HTML import / export@weaver/plugins-html🔁
Markdown import / export@weaver/plugins-markdown🔁
Custom serializer plugin APIplugin-registered serializer; visits the block tree

7. Architectural differences — not parity items

These are differences from Lexical we deliberately preserve, not gaps to close:

  • CRDT as the source of truth (D1, ADR 0001). Lexical holds an EditorState tree; collab is via @lexical/yjs syncing two states. weaver has one state.
  • No React-managed editing surface (architecture.md §1). Lexical’s core (lexical) is framework-agnostic; its common binding is @lexical/react which puts the editing surface under React. weaver’s surface is imperative DOM patched from Loro diffs.
  • Block-as-unit (ADR 0002). Lexical mixes block / inline / mark in one node type system. weaver makes the block a first-class primitive with separate inline/mark surfaces.
  • AI agents as peers, not API calls (ai-agent.md). Lexical has no first-class agent model.
  • Effect-TS plugin contract. Lexical plugin authoring is a set of editor registrations (editor.registerCommand, editor.registerNodeTransform, editor.registerUpdateListener) — the React layer is one binding atop those primitives. weaver plugins are Effect Layers with typed error channels and exhaustive Match.tag pattern matching; the contract is structurally different, not just “React vs not-React.”

Closing these would be re-becoming Lexical. They are not in the parity rubric.

Outcome rubric

The Lexical-parity catalog is delivered when an independent grader, seeing only the implemented @weaver/core + @weaver/react + the v1 first-party plugin set, can mark each criterion below as binary pass/fail.

Completeness

  • Every row marked ✅ In v1 has a corresponding implementation that is reachable from a public export of @weaver/core or @weaver/react.
  • Every row marked 🔁 In v1 via plugin has a corresponding implementation in a published @weaver/plugins-* package.
  • Every row marked links to an open issue or RFC URL in the row’s “Notes” column explaining the deferral; nothing in this column is implemented in v1; the grader can click each URL and reach a non-404 page.
  • No ⏳ row’s named primitive is reachable from the public exports of @weaver/core / @weaver/react / the published @weaver/plugins-* set. (Grader test: grep the public exports; no match.)
  • The total count of ✅ + 🔁 rows in §1 (Node / block kinds) is ≥ 22.
  • The total count of ✅ + 🔁 rows in §2 (Marks) is ≥ 7.
  • The total count of ✅ + 🔁 rows in §3 (Commands) is ≥ 18.
  • The total count of ✅ + 🔁 rows in §4 (Plugins) is ≥ 20.
  • The total count of ✅ + 🔁 rows in §5 (Hooks) is ≥ 5.
  • The total count of ✅ + 🔁 rows in §6 (Serialization) is ≥ 4.

Fidelity

  • For each ✅ block kind, applying the equivalent Lexical demo content (HTML or Markdown) via @weaver/plugins-html / @weaver/plugins-markdown produces a document whose serialized block tree (kind, depth, mark set per text run, link href, image src) equals Lexical’s post-import tree under the documented normalizer in @weaver/plugins-html/normalizer.ts (tolerance: whitespace runs collapsed; element IDs ignored).
  • For each ✅ command, dispatching the documented payload produces a post-command serialized block tree equal to Lexical’s post-command tree, under the same normalizer, on the same input fixture.
  • Undo / redo, after a sequence of N commands, returns Loro.toJSON(doc) to a value that deep-equals the snapshot taken before the sequence began (excluding container internal IDs).

Traceability

  • Each row in this catalog links to one of: the relevant code file, the relevant ADR, or an open issue (markdown URL).
  • A lint script (scripts/lint-parity-refs.ts) exists and exits non-zero when a row’s referenced file/URL does not resolve.
  • The CI workflow .github/workflows/ci.yml invokes that lint script on any PR touching specs/lexical-parity.md (verifiable by an evaluator: induce a broken reference in a test PR; CI fails).

Output quality

  • The catalog is a single file (specs/lexical-parity.md) with §1–§7 in the documented order.
  • Status icons (✅ 🔁 ⏳) appear exactly as listed in the legend; no ad-hoc statuses.
  • Architectural differences (§7) are listed once and not interleaved with parity rows.

Reproducibility

  • The “render side-by-side” fidelity check is reproducible by a fresh contributor with the repo and a Lexical-playground clone; the steps are documented in this file’s §“How to read this catalog” or in a sibling test-plan file.

See also

  • prd.md — D1 (LoroDoc as single source of truth) and §5 non-goal “100% feature surface of Lexical day one” are the bedrock for this catalog.
  • architecture.md — where the command bus, plugin contract, and reactivity model are defined.
  • benchmarks.md — the perf bar this parity must clear.
  • playground.md — the demo surface that exercises the parity items.
  • comparison.md — the narrative comparison (this file is the operational catalog).
  • Lexical docs and lexical-playground — source of truth for what Lexical ships.