Skip to content

fix(layout-engine): render underlined tabs flush with text underline (SD-3330)#3611

Merged
harbournick merged 7 commits into
mainfrom
tadeu/sd-3347-feature-render-word-style-underlines
Jun 3, 2026
Merged

fix(layout-engine): render underlined tabs flush with text underline (SD-3330)#3611
harbournick merged 7 commits into
mainfrom
tadeu/sd-3347-feature-render-word-style-underlines

Conversation

@tupizz
Copy link
Copy Markdown
Contributor

@tupizz tupizz commented Jun 2, 2026

What

Underlined tab characters now render their underline flush with the text underline, producing one continuous line as Word does. Fixes SD-3330 and delivers the core of SD-3347 (Word-style underlined fill-in / signature lines).

Linear: SD-3347 (parent feature), SD-3330 (child bug). Related: SD-1289, SD-1499, SD-3103.

The bug

In a DOCX where a run carries <w:u w:val="single"/> and contains <w:tab/>, Word draws a single continuous underline under the text and the tab advance. SuperDoc drew the tab's underline ~8px below the text underline, so the line looked broken where text met tabs.

Root cause: the tab underline is a border-bottom on the tab box. The box was the full line height and vertical-align: bottom, so the border sat at the bottom of the line box (including leading) — not at the text baseline where text-decoration underlines render. ECMA-376 §17.3.2.40 specifies the underline appears "directly below the character height (less all spacing above and below the characters on the line)", i.e. at the baseline, not the line-box bottom.

The fix

painters/dom/src/runs/tab-run.ts: for underlined tabs, pin the box top to the line-box top and end it at a baseline offset derived from the resolved line metrics (ascent/descent/lineHeight), so the border-bottom lands flush with the adjacent text underline. No DOM measurement is added (honors the painter's SD-2957 no-measure invariant).

The change is gated to underlined tabs only — non-underlined tabs keep their previous geometry, so tab-stop positioning and nearby line layout are unchanged.

Validation (Layout Engine mode)

Measured the vertical step between the tab underline and the text underline on underlined tab stops (1).docx: +8px → −1px (visually continuous).

Fixture Result
underlined tab stops (1) continuous flush line (SD-3330 repro) ✅
tab-underlines clean blank signature fill-in lines ✅
sd-1289-tabs-with-lines By:/Name:/Title: lines flush with labels (were floating low) ✅
tab-stops-test-signer-area tab-stop positioning unchanged ✅
Asset Purchase Agreement, NVCA SPA ×2 (57K–128K chars) load without crash, tabs render normally ✅
  • New regression test runs/tab-run.test.ts (inline + positioned paths): RED on main, GREEN with the fix.
  • Full @superdoc/painter-dom suite: 1183 passed. tsc -b typecheck clean.

Notes / follow-ups

  • SD-3330 part 2 ("can't underline tabbed spaces in the editor") does not reproduce on current main: toggleUnderline, the toolbar, mark application, and isActive('underline') all handle tab nodes correctly. It appears to have been fixed since the reported v1.38.0; only the rendering remained.
  • None of the supplied fixtures use right/center/decimal tab stops, so the positioned-tab path is covered by the unit test rather than a real fixture. Worth a spot-check if such a doc surfaces.

Underlined tab characters render their underline as a border-bottom on the
tab box. The box was the full line height and bottom-aligned, so the border
landed ~descent+half-leading below the text-decoration underline of adjacent
text, making a continuous underline look broken where text meets tabs.

Anchor the underlined tab box to the line-box top and end it at the baseline
offset derived from the resolved line metrics (ascent/descent/lineHeight), so
the border-bottom sits flush with the text underline. Gated to underlined tabs
only; non-underlined tabs keep their previous geometry to avoid any tab-stop
or line-layout regression.

Covers SD-3347 signature/fill-in line rendering. No DOM measurement is added
(SD-2957). Adds a regression test for both the inline and positioned paths.
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 2, 2026

SD-3330

SD-3347

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Refactor the rendering of underlined tabs to use the same text-decoration mechanism as adjacent text, ensuring consistent baseline alignment and weight. This change addresses issues where the underline appeared misaligned due to the previous border-bottom approach. The tests have been updated to reflect these changes, ensuring that both underlined and plain tabs render correctly without unexpected borders. This aligns with the goal of maintaining visual fidelity across text and tab elements (SD-3330).
@tupizz tupizz self-assigned this Jun 3, 2026
@tupizz tupizz marked this pull request as ready for review June 3, 2026 00:01
@tupizz tupizz requested a review from a team as a code owner June 3, 2026 00:01
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 3 files

Linked issue analysis

Linked issue: SD-3347: Feature: Render Word-style underlines

Status Acceptance criteria Notes
Underlined tab characters render their underline flush with adjacent text underline (one continuous line matching Word). PR changes tab rendering so underlined inline tabs use text-decoration on a baseline-aligned box and positioned tabs draw border-bottom at a computed baseline offset. This addresses the SD-3330 vertical offset and produces a continuous line.
Fill-in and signature lines that appear as visible blank underlined lines in Word render as visible blank lines in SuperDoc (not ordinary text underlines). PR gates behavior to underlined tabs and fills inline tab boxes with transparent whitespace so the underline is drawn via text-decoration, preserving blank-line appearance rather than collapsing to text underline.
Behavior validated against targeted fixtures that reproduce tab-based and underline-based line patterns. Author ran the change against the supplied fixtures listed in the PR and reports expected results for the key fixtures reproducing the issue.
Preserve alignment, spacing, and tab-stop positioning (no layout regressions around tabs). The change is scoped to underlined tabs only; positioned-tab path uses a border at computed offset so layout and positioning remain unchanged for non-underlined tabs. The author explicitly measured tab-stop positioning and noted no change.
No regression to ordinary text underline rendering (text underlines remain correct and underline weight matches tabs). text-run.ts now sets an explicit font-scaled textDecorationThickness and tab underline thickness reuses the same scaling, ensuring uniform weight across text and tab underlines; non-underlined tabs remain invisible.
Regression test coverage added for the underlined-tab behavior. A focused test file covering inline and positioned underlined and plain tabs was added and author reports RED on main → GREEN with fix.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layout-engine/painters/dom/src/runs/tab-run.ts Outdated
tupizz added 3 commits June 2, 2026 21:11
…ht (SD-3330)

The text-decoration approach for inline tab underlines required filling the tab
with transparent whitespace for the browser to underline. That filler is
selectable content, so selecting a line produced a broken/clipped selection
highlight across the tab region, and it complicated the editor underline flow.

Revert inline tabs to a border-bottom at the computed baseline offset (no filler,
no selectable content, no selection artifacts). Keep the matched weight: the
border width and text-decoration-thickness both use the shared font-scaled
underlineThicknessPx, so text and tab underlines render at the same integer-px
weight. Trade-off: the border position is a formula approximation of the text
underline baseline (within ~1px) rather than the browser's exact placement, but
it has no interaction side effects.
Applying underline to an already-rendered tab in the editor did not show until
an unrelated edit forced a rebuild. Root cause: deriveBlockVersion (the paint
cache key the DomPainter compares to decide whether to reuse a fragment) encoded
a tab run as just text + "tab", omitting its marks. Toggling underline produced
an identical version, so the painter reused the cached, non-underlined fragment.

Include the tab's underline (style + color) in its version, matching how text
runs already encode their underline. Now a tab mark change invalidates the paint
cache and the underline appears immediately. Adds a regression test asserting the
version changes when a tab gains/recolors an underline.

Pre-existing; reproduced on main. Independent of the tab underline rendering fix.
…-3330)

A line containing only tabs was measured at the 12px default and rendered ~4px
shorter than a text or empty line in the same paragraph, so tab fill-in lines
sat at a different height than typed text.

Two parts:
- Adapter (paragraph converter): a bare tab carries no font of its own, so give
  it the paragraph's resolved default font (mirroring the empty-paragraph run).
- Measuring: when a paragraph has no sized text run, fall back to any run that
  carries a font size/family (e.g. a tab) instead of the 12px default, so the
  tab's font drives the line height.

Add fontFamily/fontSize to the TabRun type (the tab legitimately carries them for
line height and underline weight) and drop the matching casts. Tab widths are
unaffected. Adds a measuring regression test asserting a tab-only line matches a
text line of the same font. Pre-existing; independent of the underline work.
@harbournick harbournick self-assigned this Jun 3, 2026
Copy link
Copy Markdown
Collaborator

@harbournick harbournick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@harbournick harbournick merged commit c18caea into main Jun 3, 2026
69 checks passed
@harbournick harbournick deleted the tadeu/sd-3347-feature-render-word-style-underlines branch June 3, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants