feat: rtl mixed bidi#3320
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
b2b0fed to
45b3ed0
Compare
45b3ed0 to
8ce8602
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ce8602149
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const tr = state.tr.delete(boundary.left.pmStart, boundary.left.pmEnd).scrollIntoView(); | ||
| view.dispatch(tr); |
There was a problem hiding this comment.
Preserve Backspace undo boundaries for mixed bidi
When this shortcut handles Backspace at a mixed-direction boundary, it returns before the normal Backspace keymap runs, but this direct tr.delete(...) skips the dispatchHistoryBoundary(view) call and inputType meta that core/extensions/keymap.js applies for all other Backspace deletes. In that scenario, the deletion can be merged into the preceding typing undo group instead of becoming its own undo step, so undo may remove both the prior edit and this boundary deletion. Add the same history boundary/input metadata here or delegate through the existing Backspace command path after resolving the target range.
Useful? React with 👍 / 👎.
…space chain
The MixedBidiBackspace extension previously bound Backspace via addShortcuts
and dispatched its own transaction. That bypassed the canonical handleBackspace
chain in core/extensions/keymap.js, skipping:
- dispatchHistoryBoundary (so undo grouping diverged from regular Backspace),
- the tr.setMeta('inputType', 'deleteContentBackward') hop (tracked-changes
helpers gate Backspace-specific wrapping on this meta - see
trackChangesHelpers/trackedTransaction.js:447,
trackChangesHelpers/replaceAroundStep.js:162),
- deleteBlockSdtAtTextBlockStart for SDT block boundaries,
- the rest of the specialized run-aware ladder.
Refactor:
- Expose mixedBidiBackspace as a chain command via addCommands(); shape
matches the existing backspaceAcrossRuns / backspaceNextToRun pattern
(factory returning ({state, view, tr, dispatch}) => boolean).
- Insert into the Backspace chain in keymap.js after backspaceAcrossRuns
(specialized cross-run handling) and before deleteSelection (generic
fallback), guarded with `?? false` so the chain works when the extension
is unregistered.
- Split the detection logic into pure resolveMixedBidiBackspaceRange so the
command body stays tiny.
- Tests rewritten to call the chain command shape directly and pin: chain
dry-run mode (dispatch=undefined) returns true without mutating tr,
non-mixed boundaries fall through, both RTL+LTR and LTR+RTL boundaries
handled, posAtDOM is skipped early for pure-LTR/RTL lines.
…ine-direction The shouldAssignPerRunRtlDir / normalizeRtlDateTokenForWordParity helpers and their regexes (RTL_DATE_LIKE_TOKEN_RE, STRONG_RTL_CHAR_RE, LATIN_DIGIT_NEUTRAL_ONLY_RE) lived at the bottom of renderer.ts. After #3307 moved the rtl-paragraph feature folder to inline-direction with an explicit axis scope, the renderer was the wrong home: these helpers are paint-time decisions about how to project w:rPr/w:rtl onto a rendered span's dir attribute, which is exactly what features/inline-direction owns. Extract into features/inline-direction/run-direction.ts: - Combine the two-step decision (set dir=rtl? else set dir=ltr for date-like?) into a single resolveRunDirectionAttribute helper that returns 'rtl' | 'ltr' | null. - Expose normalizeRtlDateTokenForWordParity alongside since it shares RTL_DATE_LIKE_TOKEN_RE. - Inline the decision table as JSDoc, explicitly scoping the heuristic to current SD-3098 fixtures and pointing at the spec sections plus the known follow-up gaps (w:dir, w:bdo, w:lang/@bidi numeric, presentation forms). Renderer collapses the per-span direction logic to one helper call. 22 new unit tests in run-direction.test.ts cover both branches of the rtl-tagged decision table, the date-like ltr fallback for non-tagged runs, and the regex coverage smoke tests.
… paint directive Per §17.3.2.30, w:rPr/w:rtl does two things at the model level: forces the complex-script formatting stack, and acts as a directionality override for weak/neutral characters (NOT a forced visual flip of strong-LTR text - §17.3.2.30 explicitly says behavior on strong-LTR is unspecified). Update the JSDoc on RunBidiContext.rtl to make explicit: - rtl: true is the source signal (w:rPr/w:rtl was set in OOXML) - It is NOT a directive that every consumer must project to dir="rtl" in the rendered DOM - The painter decides the DOM projection per its Word-parity rules (resolveRunDirectionAttribute in features/inline-direction) - Exporters must preserve rtl: true on round-trip regardless of paint decisions, since dropping it would lose source semantics Doc-only change. No code or type signature changes.
Linear: SD-2933
This PR closes the mixed-bidi hardening milestone for RTL editing.
Included:
dirassignment in mixed RTL/LTR content to improve token order stability.mixed-bidi-backspaceextension for visual-left deletion at RTL/LTR boundaries with fail-open behavior.Result:
Notes