Skip to content

feat(rust): @lexical/mark's model, with JS-recorded scripts/mark and serialization/mark vectors - #158

Open
tmm wants to merge 1 commit into
rust-extension-apifrom
rust-mark
Open

tmm wants to merge 1 commit into
rust-extension-apifrom
rust-mark

Conversation

@tmm

@tmm tmm commented Nov 14, 2026

Copy link
Copy Markdown
Member

Description

The Rust port had no @lexical/mark, so it couldn't read or write the
comment marks Swift and JS anchor in a document.

This adds it behind a new mark feature (needs utils, as upstream):

  • MarkNode with its generated JSON in both forms, its insertion hooks
    (canInsertTextBefore/After, canBeEmpty, isInline,
    excludeFromCopy, canRemoveText, extractWithChild) and its id methods
    (hasID, getIDs, setIDs, addID, deleteID, which removes the mark
    with its last id);
  • $createMarkNode, $isMarkNode, $wrapSelectionInMarkNode,
    $unwrapMarkNode and $getMarkIDs;
  • the MarkStateExtension.ts functions that work without the extension, for
    the default flavor: $addMarkID, $removeMarkID, $getNodeMarkIDs,
    $getMarkIDsAtSelection and $migrateMarkNodesToMarkState.

packages/lexical-conformance gains a mark: true script option, actions
for those functions and methods, a scripts/mark vector (30 cases: wrapping
across text nodes, partial text, several blocks, backward selections,
overlapping marks, line breaks, decorators and inline elements in the range,
typing and deleting at mark edges, unwrapping, ids, MarkState) and a
serialization/mark vector (4 cases). Swift replays both and matched JS on
every case, so no Swift source changes; LexicalMark joins the
ConformanceTests dependencies.

Not ported, each listed in PARITY.md: MarkExtension, MarkStateExtension
and the flavor forms (the extension API has just landed; they come next),
registerMarkIDsListener, registerMarkState's inserted-text rule, and the
DOM parts (createDOM, rendering, the HTML import rule), which come with
html.

Test plan

Before

$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 657 | 657 | 0 | 0 | 0 | |

(No scripts/mark or serialization/mark row.)

After

$ cargo run -q --locked --release -p lexical-conformance
| `scripts/mark` | 30 | 30 | 0 | 0 | 0 | pass |
| `serialization/mark` | 4 | 4 | 0 | 0 | 0 | pass |
| **Total** | 691 | 691 | 0 | 0 | 0 | |

Mutation checks (each reverted): deleteID doing nothing fails 2 cases; a
line break wrapped like text fails 1; ids sorted by code point instead of
UTF-16 fails 1; canBeEmpty true fails 2; excludeFromCopy inverted fails

  1. canInsertTextBefore true fails nothing: headless $insertText puts
    text typed at an inline parent's edge outside it whatever the hook says,
    as PARITY.md notes for LinkNode.

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, cargo +1.88 check -p lexical-editor --all-features, --no-default-features alone and
with mark and mark,extension, and the Rune app's clippy all pass.
UPDATE_VECTORS=1 pnpm exec vitest run src/scripts.test.ts src/serialization.test.ts: 27 passed, and no vector changed (md5). Swift:
swift build --build-tests and swift test --skip-build --filter ConformanceTests, 34 tests in 13 suites passed.

…serialization/mark vectors

## Description

The Rust port had no `@lexical/mark`, so it couldn't read or write the
comment marks Swift and JS anchor in a document.

This adds it behind a new `mark` feature (needs `utils`, as upstream):

- `MarkNode` with its generated JSON in both forms, its insertion hooks
  (`canInsertTextBefore`/`After`, `canBeEmpty`, `isInline`,
  `excludeFromCopy`, `canRemoveText`, `extractWithChild`) and its id methods
  (`hasID`, `getIDs`, `setIDs`, `addID`, `deleteID`, which removes the mark
  with its last id);
- `$createMarkNode`, `$isMarkNode`, `$wrapSelectionInMarkNode`,
  `$unwrapMarkNode` and `$getMarkIDs`;
- the `MarkStateExtension.ts` functions that work without the extension, for
  the default flavor: `$addMarkID`, `$removeMarkID`, `$getNodeMarkIDs`,
  `$getMarkIDsAtSelection` and `$migrateMarkNodesToMarkState`.

`packages/lexical-conformance` gains a `mark: true` script option, actions
for those functions and methods, a `scripts/mark` vector (30 cases: wrapping
across text nodes, partial text, several blocks, backward selections,
overlapping marks, line breaks, decorators and inline elements in the range,
typing and deleting at mark edges, unwrapping, ids, MarkState) and a
`serialization/mark` vector (4 cases). Swift replays both and matched JS on
every case, so no Swift source changes; `LexicalMark` joins the
`ConformanceTests` dependencies.

Not ported, each listed in PARITY.md: `MarkExtension`, `MarkStateExtension`
and the flavor forms (the extension API has just landed; they come next),
`registerMarkIDsListener`, `registerMarkState`'s inserted-text rule, and the
DOM parts (`createDOM`, rendering, the HTML import rule), which come with
`html`.

## Test plan

### Before

```
$ cargo run -q --locked --release -p lexical-conformance
| **Total** | 657 | 657 | 0 | 0 | 0 | |
```

(No `scripts/mark` or `serialization/mark` row.)

### After

```
$ cargo run -q --locked --release -p lexical-conformance
| `scripts/mark` | 30 | 30 | 0 | 0 | 0 | pass |
| `serialization/mark` | 4 | 4 | 0 | 0 | 0 | pass |
| **Total** | 691 | 691 | 0 | 0 | 0 | |
```

Mutation checks (each reverted): `deleteID` doing nothing fails 2 cases; a
line break wrapped like text fails 1; ids sorted by code point instead of
UTF-16 fails 1; `canBeEmpty` true fails 2; `excludeFromCopy` inverted fails
1. `canInsertTextBefore` true fails nothing: headless `$insertText` puts
text typed at an inline parent's edge outside it whatever the hook says,
as PARITY.md notes for `LinkNode`.

`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`, `cargo +1.88
check -p lexical-editor --all-features`, `--no-default-features` alone and
with `mark` and `mark,extension`, and the Rune app's clippy all pass.
`UPDATE_VECTORS=1 pnpm exec vitest run src/scripts.test.ts
src/serialization.test.ts`: 27 passed, and no vector changed (md5). Swift:
`swift build --build-tests` and `swift test --skip-build --filter
ConformanceTests`, 34 tests in 13 suites passed.

Amp-Thread-ID: https://ampcode.com/threads/T-16d45229-bfa8-417a-a091-d3e3888d830d

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants