Skip to content

fix: tab-stop column measurement + right/center tab alignment - #16

Draft
jmcopeland wants to merge 3 commits into
extend-hq:mainfrom
jmcopeland:fix/plain-tab-column-measurement
Draft

fix: tab-stop column measurement + right/center tab alignment#16
jmcopeland wants to merge 3 commits into
extend-hq:mainfrom
jmcopeland:fix/plain-tab-column-measurement

Conversation

@jmcopeland

Copy link
Copy Markdown
Contributor

Summary

Fixes plain tab-stop columns drifting off their stop on wrapped/broken second lines (the second party line of a signature block landing ~58px short of the first).

Root cause: a tab renders as a fixed-width spacer sized tabStop − runningLineWidth, while the text before it lays out at real canvas width. The running width was tracked with the per-character heuristic estimateTextAdvanceWidthPx, which over-estimates long lower-case runs. When the estimate diverges from the browser's actual width, the post-tab column shifts left by the difference — small for short labels, large for long lines, so the second line of a two-line column visibly misaligns.

Fix: track the running line width with the same canvas metrics the text actually renders at (updateMeasuredLineWidthPxForText, a canvas-accurate analog of the existing estimator) for paragraphs that carry explicit tab stops, in both render paths. Columns now land on their stop across every line. Paragraphs without explicit tab stops keep the existing heuristic, so wrap/line-count behavior elsewhere is unchanged.

Refs #15.

Verification

  • Multi-variant signature-block repro (repros/signature-block-variants.docx): the left-tab-only (C) and two-paragraph (D) styles now align both party lines on the stop (measured col-2 at 671–672px on both lines; previously the second line landed at ~614px).
  • New regression test tests/unit/plain-tab-line-break-columns.test.ts stubs canvas metrics and asserts preTabWidth + spacerWidth == tabStop on both lines; confirmed to fail against the old heuristic.
  • All existing fork unit tests pass.

Not in scope

The signature underline row (____⇥____) whose second column is wide enough to overrun the right margin still wraps to a new line (browser break-word) where Word lets the unbreakable run overflow the margin. The tab now positions that column correctly; the wrap is a separate overflow/line-break-model gap tracked in #15.

Test plan

  • pnpm --filter @extend-ai/react-docx-playground dev, import repros/signature-block-variants.docx, confirm variant C/D party columns align on both lines.
  • pnpm vitest run tests/unit/plain-tab-line-break-columns.test.ts

Plain tab-stop columns positioned the run after a tab using a
per-character heuristic for the running line width, while the run itself
lays out at real canvas width. On long lower-case runs the heuristic
over-estimates, so wrapped/broken second lines drifted left of the stop
(e.g. a signature block's second party line landing ~58px short).

Track the running width with the same canvas metrics the text renders at
for paragraphs that carry explicit tab stops, in both render paths, so
tabbed columns land on their stop across every line.

Refs extend-hq#15

Co-Authored-By: Justin Copeland <justin.copeland@legalontech.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

@jmcopeland is attempting to deploy a commit to the Extend Team on Vercel.

A member of the Team first needs to authorize it.

The plain-tab path advanced to every stop and left-aligned the following
text, so a segment after a right (or center) tab stop was never actually
right-aligned. Size the tab spacer so the following text segment ends at
(right) or straddles (center) the stop, measured with the same canvas
metrics the text renders at, when it fits before the stop; otherwise fall
back to the plain left-advance (matching Word, which degrades an overrun
tab to left). Applied to both the read-only and editable render paths.

This unifies the signature-block cases: a short right-column trailer
right-aligns at its stop, while a long wrapping run still flows and returns
to the margin. Regression test in tests/unit/right-tab-alignment.test.ts.

Refs extend-hq#15

Co-Authored-By: Justin Copeland <justin.copeland@legalontech.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jmcopeland jmcopeland changed the title fix: measure tab-stop column runs with canvas metrics fix: tab-stop column measurement + right/center tab alignment Jul 14, 2026
@jmcopeland

Copy link
Copy Markdown
Contributor Author

Updated: this PR now also makes the plain-tab path alignment-aware. In addition to the canvas-accurate column measurement, a segment after a right (or center) tab stop is now right-aligned / centered at the stop when it fits before it (falling back to the plain left-advance otherwise, matching Word's overrun degrade). This unifies the signature-block cases — a short right-column trailer right-aligns at its stop, while a long wrapping run still flows and returns to the margin. New regression test tests/unit/right-tab-alignment.test.ts. Supersedes #14 (the left-right grid), which mishandled the no-line-break wrap case.

Two correctness gaps and a structural cleanup for plain tab columns:

- Wrap-aware running width: the tab tracker only reset at explicit line
  breaks, so a right/center tab after a run that WRAPS to the margin (e.g.
  a signature block whose right column is one continuous run) resolved past
  the stop and the trailer landed mid-line. Simulate the browser's greedy
  word-wrap (canvas metrics, content-box width) so the tab resolves on the
  correct visual line. Matches Word.

- Tracked-changes path: the read-only viewer delegates to renderParagraphRuns
  when tracked changes / comments / special tab layouts are shown (the redline
  browse view's path), which had its own tab tracker that bypassed the above.
  Threaded the content-box width through ParagraphRunRenderOptions and made it
  wrap-aware too.

- Refactor: the tab-stop parsing, wrap-aware advance, and aligned-spacer
  resolution were duplicated per render path (how these bugs slipped through
  one path at a time). Extracted buildTabStopsWithAlignPx / advanceTabLineWidthPx
  / resolveAlignedTabSpacerPx as the single source of truth; both paths now
  call them.

Refs extend-hq#15

Co-Authored-By: Justin Copeland <justin.copeland@legalontech.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@andrewlu0

Copy link
Copy Markdown
Collaborator

hi! is this ready or still in draft?

@jmcopeland

Copy link
Copy Markdown
Contributor Author

hi! is this ready or still in draft?

It's ready

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