Skip to content

Commit bc91d9a

Browse files
committed
fix(measuring): gate SD-2447 alignment heuristic on default stops only
The SD-2447 heuristic forces the last N tabs to bind to the last N end/center/decimal stops. It was added because TOC styles often have ONLY a right-aligned dot-leader stop, and tabStops gets seeded with synthetic 0.5" defaults from origin (seedDefaultsFromZero=true). Greedy then lands on a default 0.5" grid stop instead of the alignment stop — hence the heuristic. But for paragraphs with an EXPLICIT start-aligned stop ahead of the alignment stop (TOC1 style with 'start@740, end@9360, end@10080': template_format and similar Word lease templates), greedy correctly lands on the start stop and the alignment stop downstream — no force needed. The heuristic over-fires and binds tab 0 to the right alignment stop, producing the broken render: leader BEFORE the title with the page number jammed against it. Fix: compute greedy first; only apply the heuristic when greedy would land on a 'source: default' stop. When greedy already lands on an explicit stop, use it. Mirrored in measuring/dom and remeasure. Effect: - template_format TOC: now renders '1. BUSINESS POINTS........1' matching Word and the published baseline. - HVY-25 / SD-2447 fixture / sd-1480 line 1: behavior preserved. - All test suites pass (measuring-dom 332, layout-bridge 1192, layout-engine 644).
1 parent e9eaa04 commit bc91d9a

2 files changed

Lines changed: 24 additions & 10 deletions

File tree

packages/layout-engine/layout-bridge/src/remeasure.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -874,19 +874,23 @@ const applyTabLayoutToLines = (
874874
const absCurrentX = cursorX + effectiveIndent;
875875
let stop: TabStopPx | undefined;
876876
let target: number;
877+
// Mirror of measuring/dom: only force the SD-2447 heuristic when greedy
878+
// would land on a `source:default` stop (synthetic 0.5" grid). Explicit
879+
// start stops should win greedy.
880+
const greedy = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
881+
const greedyOnDefault = greedy.stop?.source === 'default';
877882
const forcedAlignment =
878-
typeof tabOrdinal === 'number' && Number.isFinite(tabOrdinal)
883+
greedyOnDefault && typeof tabOrdinal === 'number' && Number.isFinite(tabOrdinal)
879884
? getAlignmentStopForOrdinal(tabOrdinal, tabRunIdx)
880885
: null;
881886
if (forcedAlignment && forcedAlignment.stop.pos > absCurrentX + TAB_EPSILON) {
882887
stop = forcedAlignment.stop;
883888
target = forcedAlignment.stop.pos;
884889
tabStopCursor = forcedAlignment.index + 1;
885890
} else {
886-
const next = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
887-
stop = next.stop;
888-
target = next.target;
889-
tabStopCursor = next.nextIndex;
891+
stop = greedy.stop;
892+
target = greedy.target;
893+
tabStopCursor = greedy.nextIndex;
890894
}
891895
const clampedTarget = Number.isFinite(maxAbsWidth) ? Math.min(target, maxAbsWidth) : target;
892896
const relativeTarget = clampedTarget - effectiveIndent;

packages/layout-engine/measuring/dom/src/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,16 +1564,26 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P
15641564
// inputs (explicit + synthetic TabRuns) don't produce out-of-order ordinals.
15651565
// Mirrors consumeTabOrdinal() in layout-bridge/src/remeasure.ts.
15661566
sequentialTabIndex = Math.max(sequentialTabIndex, resolvedTabIndex + 1);
1567-
const forcedAlignment = getAlignmentStopForOrdinal(resolvedTabIndex, runIndex);
1567+
// Compute greedy first so we can decide whether the SD-2447 heuristic is
1568+
// actually needed. The heuristic exists because when tabStops are seeded
1569+
// with synthetic 0.5" defaults from origin (TOC styles with only an
1570+
// alignment stop), greedy lands on a default before reaching the
1571+
// alignment stop. When the paragraph has an explicit start-aligned stop
1572+
// ahead of the alignment stop (e.g. TOC1 with `start@740, end@9360`),
1573+
// greedy already finds the correct stop and the heuristic over-fires.
1574+
// Only force the heuristic when greedy would land on a `source:default`
1575+
// stop — which is precisely the SD-2447 condition.
1576+
const greedy = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
1577+
const greedyOnDefault = greedy.stop?.source === 'default';
1578+
const forcedAlignment = greedyOnDefault ? getAlignmentStopForOrdinal(resolvedTabIndex, runIndex) : null;
15681579
if (forcedAlignment && forcedAlignment.stop.pos > absCurrentX + TAB_EPSILON) {
15691580
stop = forcedAlignment.stop;
15701581
target = forcedAlignment.stop.pos;
15711582
tabStopCursor = forcedAlignment.index + 1;
15721583
} else {
1573-
const nextStop = getNextTabStopPx(absCurrentX, tabStops, tabStopCursor);
1574-
target = nextStop.target;
1575-
tabStopCursor = nextStop.nextIndex;
1576-
stop = nextStop.stop;
1584+
target = greedy.target;
1585+
tabStopCursor = greedy.nextIndex;
1586+
stop = greedy.stop;
15771587
}
15781588
const maxAbsWidth = currentLine.maxWidth + effectiveIndent;
15791589
const clampedTarget = Math.min(target, maxAbsWidth);

0 commit comments

Comments
 (0)