DemoDocsPlaygroundGitHub

An extensible rich text editor framework built on Lexical. Ship faster with production-ready defaults and TypeScript-first APIs.

Documentation

IntroductionInstallation@lyfie/luthor-headless@lyfie/luthor

Resources

DemoFeaturesPlaygroundGitHubluthor @ npmluthor-headless @ npm

Support the Project

Buy me a coffeeStar on GitHub

Built with ❤️ by Lyfie.org

HomeDocsFeaturesDemodev.toMediumGitHubllms.txtllms-full.txt
  1. Home
  2. Docs
  3. Luthor Headless
  4. Edge Case Coverage

Luthor Documentation

Start Here

  • Getting Started
  • Installation
  • Dependencies
  • Capabilities
  • Quickstart: @lyfie/luthor
  • Quickstart: @lyfie/luthor-headless
  • AI Agents and Vibe Coding

@lyfie/luthor (Presets)

  • @lyfie/luthor Overview
  • @lyfie/luthor Architecture
  • Feature Flags
  • Props Reference
  • Presets Catalog
  • Extensive Editor
  • Legacy Rich Editor
  • Accessibility
  • Markdown Editor
  • HTML Editor
  • Papyra Editor
  • Commands Reference

@lyfie/luthor-headless (Runtime)

  • @lyfie/luthor-headless Overview
  • @lyfie/luthor-headless Architecture
  • Extensions and API
  • Metadata Comment System
  • URL and Content Safety
  • Edge Case Coverage
  • Features
  • Typography and Text
  • Structure and Lists
  • Media and Embeds
  • Code and Devtools
  • Interaction and Productivity
  • Customization and Theming
  • Extensions Reference
  • Nodes and Bridges Reference

Integrations

  • React Integration
  • Next.js Integration
  • Astro Integration
  • Remix Integration
  • Vite Integration

Reference Indexes

  • Search Guide
  • Exports Map
  • Preset Selector

Contributing

  • Contributor Guide

Package: headlessType: referenceSurface: tooling

Edge Case Coverage

Luthor is used in documents its authors never see: Japanese notes, Arabic articles, pastes out of Word, files with 10,000 blocks. This page records what is tested, what the tests found, and — as honestly — what cannot be tested in the jsdom environment the suites run in.

What this page answers

  • Which hostile or unusual inputs are covered by tests?
  • What did those tests establish about real behavior?
  • Which risks are known but unverified, and why?

Covered

AreaWhat is asserted
IME compositionBlock-anchor auto-stamping stands down while isComposing() and resumes after. Driven through Lexical's $setCompositionKey, so removing the guard fails the test.
Bidi / RTLArabic and Hebrew round-trip verbatim through markdown and HTML, including mixed LTR/RTL paragraphs, RTL headings and lists, and idempotency on a second pass.
UnicodeZWJ family emoji, skin-tone modifiers, regional-indicator flags, stacked combining marks, Devanagari clusters, and astral-plane codepoints survive both bridges byte-exactly, including across formatting boundaries.
Real-world pasteWord, Google Docs, Sheets, VS Code, Apple Notes, and a plain article selection keep their structure and formatting through the sanitizer.
Scale10k-block markdown, a 100k-character paragraph, 100-deep nested lists, a 50-column table, and 5k-block HTML all convert well inside budget; a growth-ratio test guards against an accidental quadratic.
History100-edit undo/redo chains round-trip exactly; undoing past the start keeps the earliest state instead of emptying the document; a wholesale replacement is one undo; auto-stamped anchors fold into the triggering edit rather than becoming separately undoable.
SSRAll five presets import and renderToString in a DOM-free Node environment.
StrictModeDouble-mount and ten mount/unmount cycles leak no MutationObserver, interval, or document/window listener.
Concurrent input200 edits queued in one tick keep every character and their order; a bulk insertion between keystrokes does not drop them; a programmatic replacement wins cleanly without splicing pending edits into the adopted document.

Measured numbers

Recorded so a future regression reads as a multiple rather than noise (local run, jsdom):

OperationTime
10,000-block markdown parse + serialize~390 ms
5,000-block HTML sanitize + convert~600 ms
100-edit undo + redo chain~2.5 s (dominated by per-commit flushes)

Known gaps

These are documented rather than silently skipped.

The DOM watchdog's observer path

registerEditorDomWatchdog reports only after a settle window (MutationObserver → setTimeout). Lexical's own mutation handling restores an externally written DOM back to the model within roughly 2 ms, so by the time the window closes there is no divergence left to observe — verified directly: divergence is present synchronously after an external write and null 2 ms later.

The detection function (detectEditorDomDivergence) is covered. The isComposing() early-return inside the watchdog's settle callback is reviewed but not asserted; exercising it needs a real browser.

Direction is not stored in the model

Lexical 0.40 does not persist a per-block direction. It emits dir="auto" and lets the browser's bidi algorithm resolve each block, which handles mixed runs better than a stored guess would.

The consequence for hosts: bridge JSON carries direction: null. A host rendering that JSON through its own renderer must emit dir="auto" itself, or RTL content will lay out left-to-right. This is asserted in the suite so the behavior is visible if a future Lexical changes it.

blob: URL lifecycle under StrictMode

ImageComponent revokes a blob: src in its unmount cleanup, with nothing distinguishing a StrictMode teardown from a real one — so the throwaway first mount can revoke a URL the surviving mount still needs. The extensive preset's default uploadHandler returns URL.createObjectURL(file), so the shipped default is what hits this.

Attempts to reproduce it in jsdom were unsuccessful: decorator nodes inserted programmatically did not mount an <img> in the test environment, so any assertion would have passed vacuously. The risk is recorded from code inspection, not from a reproduction. Production hosts should return a persistent URL from their upload handler.

Not covered here

Caret movement, selection geometry, and composition keystroke handling depend on real layout and native IME events. jsdom has neither. These need a browser-driven suite (Playwright or similar) to cover properly.

Previous: URL and Content Safety
Next: Features

On this page

  • What this page answers
  • Covered
  • Measured numbers
  • Known gaps
  • The DOM watchdog's observer path
  • Direction is not stored in the model
  • `blob:` URL lifecycle under StrictMode
  • Not covered here