fix(painter-dom): keep the empty-line caret on the start of RTL lines
Fixes #3955
The bug
Put the caret on an empty paragraph in a right-to-left document and it does not
sit on the start of the line. It sits a space width inside it.
Cause
renderLine paints an empty line with a superdoc-empty-run span that carries
the line's PM positions, and fills it with :
// Preserve PM positions for DOM caret mapping on empty lines.
if (runsForLine.length === 0) {
...
span.innerHTML = ' ';
A non-breaking space has a real advance width, and the caret for an empty line
is drawn at the placeholder's left edge. On an LTR line that edge is the line
start, so nothing shows. On an RTL line the line starts at the right edge, so
the caret ends up one space width inside the line.
Measured
A Hebrew document, Arial 16px, empty paragraph after a line of text:
| x | |
|---|---|
| line box right edge (= line start, RTL) | 697.7 |
.superdoc-empty-run box | 693.3 → 697.7 |
| caret | 693.3 |
4.4px off the start, exactly the placeholder's width and exactly its left edge.
Two checks that it really is the placeholder's advance driving the caret, both
done from the page with CSS only, no code change:
.superdoc-empty-run { letter-spacing: 20px }→ placeholder box widens to
673.3 → 697.7, caret moves to 673.3. It tracks the box.- placeholder given zero advance → caret lands on 697.7, the line start.
The gap is proportional to the font size, so it grows with zoom and with larger
text.
Fix
Fill the placeholder with a zero-width space. It keeps the span's font metrics,
and with them the caret's height (measured: unchanged at 17px), while
contributing no advance, so the caret lands on the line start in both
directions.
Tests
- New:
renderLine empty-line placeholderinrender-line.test.ts— asserts
the zero-width filler and the preservedpmStart/pmEnd, on an LTR and an
RTL line. Both fail onmainand pass with the fix. - Updated: the existing empty-line placeholder assertion in
index.test.ts. packages/layout-engine/painters/domsuite: 61 files, 1547 tests, all pass.
One note on how I ran them: vp would not start on my machine (Windows, Node
22.20) — it fails resolving #module-sync-enabled out of vite-plus-core
before it reads any config — so I ran the painter suite with the workspace
vitest directly, using the aliases from vite.sourceResolve.ts. For the same
reason I could not run pnpm run format:check or pnpm run lint; the change
follows the surrounding style and stays well inside the line width used in the
file.