From bfc74cb91b4b227ff318a3b34ccd43378e8f7f64 Mon Sep 17 00:00:00 2001 From: Amp Date: Sat, 14 Nov 2026 17:30:21 +0000 Subject: [PATCH] feat(rust): the core nodes' DOM hooks and @lexical/html's HTML export and import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description The Rust port's `html` feature had a DOM (`lexical_editor::dom`) but no node rendered into it, so `html/export` and `html/import` had no suite. This adds, behind the same feature: - `createDOM`, `exportDOM` and the static `importDOM` as `LexicalNode` hooks, with `ElementNode.exportDOM` (indent and direction) and `TextNode`'s behavior as their defaults, and the types they exchange (`DOMExportOutput`, `DOMConversionOutput`, `DOMConversionMap`, ...) in `lexical_editor::lexical_dom`, ported from `LexicalNode.ts`, `LexicalEditor.ts` and the DOM helpers of `LexicalUtils.ts`. - `createEditor`'s `theme` (always) and `html` config (export overrides and extra import conversions), `editor._config`, the conversion cache, and `DEFAULT_EDITOR_DOM_CONFIG`. - The DOM code of the core nodes: `TextNode` (`createDOM` with the theme's class names and the style, `exportDOM`, and the `#text`, `b`, `span` and format-element conversions), `TabNode`, `LineBreakNode`, `ParagraphNode` (with its `extractWithChild`), and `ArtificialNode__DO_NOT_USE`, which the core now registers as JS does. - `LexicalNode.isSelected`. - `lexical_editor::html`, ported from `packages/lexical-html/src/index.ts`: `generate_html_from_nodes`, `generate_dom_from_nodes`, `generate_dom_from_root`, `append_node_to_html` (with `$sliceSelectedTextNodeContent` for a selection) and `generate_nodes_from_dom`. - Conformance suites for `html/export` and `html/import`. `$inlineStylesFromStyleSheets` and the `DOMImportExtension` and `DOMRenderExtension` pipelines are not ported (rust/PARITY.md); no vector needs them. The node packages' DOM parts come next. ## Test plan ### Before ``` $ cargo run -q -p lexical-conformance --release -- html/ | `html/entities` | 2125 | 2125 | 0 | 0 | 0 | pass | | `html/export` | – | – | – | – | – | not implemented | | `html/import` | – | – | – | – | – | not implemented | | `html/parse` | 37 | 37 | 0 | 0 | 0 | pass | | **Total** | 2162 | 2162 | 0 | 0 | 0 | | ``` ### After ``` $ cargo run -q -p lexical-conformance --release -- html/ | `html/entities` | 2125 | 2125 | 0 | 0 | 0 | pass | | `html/export` | 42 | 42 | 0 | 0 | 0 | pass | | `html/import` | 68 | 68 | 0 | 0 | 0 | pass | | `html/parse` | 37 | 37 | 0 | 0 | 0 | pass | | **Total** | 2272 | 2272 | 0 | 0 | 0 | | $ cargo run -q -p lexical-conformance --release | **Total** | 3643 | 3643 | 0 | 0 | 0 | | ``` `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings`, `cargo test --workspace --all-features --locked`, `cargo doc` with `-D warnings`, and `cargo check -p lexical-editor --no-default-features --features ` for each feature are clean. Amp-Thread-ID: https://ampcode.com/threads/T-506c8fa1-4309-40f1-9810-732b311eaeab --- rust/DESIGN.md | 13 +- rust/PARITY.md | 18 +- rust/conformance/src/scripts.rs | 5 +- rust/conformance/src/suites/entities.rs | 1 + rust/conformance/src/suites/html.rs | 67 +++ rust/conformance/src/suites/mod.rs | 10 + rust/conformance/src/suites/property.rs | 1 + rust/conformance/src/suites/serialization.rs | 1 + .../src/code/flat_structure_utils.rs | 1 + rust/lexical-editor/src/editor.rs | 63 ++- rust/lexical-editor/src/html.rs | 447 +++++++++++++++ rust/lexical-editor/src/lexical_dom.rs | 519 ++++++++++++++++++ rust/lexical-editor/src/lib.rs | 10 +- rust/lexical-editor/src/node.rs | 91 +++ rust/lexical-editor/src/nodes/artificial.rs | 69 +++ rust/lexical-editor/src/nodes/element.rs | 31 ++ rust/lexical-editor/src/nodes/line_break.rs | 39 ++ rust/lexical-editor/src/nodes/mod.rs | 1 + rust/lexical-editor/src/nodes/paragraph.rs | 106 ++++ rust/lexical-editor/src/nodes/tab.rs | 19 + rust/lexical-editor/src/nodes/text.rs | 507 +++++++++++++++++ rust/lexical-editor/src/selection_deletion.rs | 2 +- 22 files changed, 2006 insertions(+), 15 deletions(-) create mode 100644 rust/lexical-editor/src/html.rs create mode 100644 rust/lexical-editor/src/lexical_dom.rs create mode 100644 rust/lexical-editor/src/nodes/artificial.rs diff --git a/rust/DESIGN.md b/rust/DESIGN.md index d34ffb52ac6..51778504c6d 100644 --- a/rust/DESIGN.md +++ b/rust/DESIGN.md @@ -165,6 +165,17 @@ model of jsdom's"), since the vectors record what jsdom does: Unlike jsdom it keeps most values it can't parse, collapsing their whitespace, as Swift's does; it rejects a shadow of nothing but identifiers, as jsdom does. +- **The nodes' DOM hooks are `LexicalNode` methods** behind the feature: + `create_dom(this, config, editor)`, `export_dom(this, editor)` and + `import_dom()` (the class's static `importDOM`, read from a default + value when the class registers). Like the other hooks they get the + node's value and its handle, so an export can run on a value that isn't + in the editor state (the text a selection cuts short). The base classes' + behavior is the hooks' defaults (`TextNode` renders its text, + `ElementNode` adds the indent and direction), as for + `get_text_content`. The theme is the JSON of `EditorThemeClasses` + (`CreateEditorArgs.theme`), and a `DOMConversionMap` is a list of + name and importer pairs in the order JS's object keys would be. - **`DOMParser` is a WHATWG tokenizer and tree builder**, checked against jsdom's by the `html/parse` and `html/entities` vectors. As in Swift it leaves out templates, framesets, the `