Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,4 +624,37 @@ describe('layout invariants', () => {
}
}
})

test('trailing collapsible space hangs without breaking across count and walk paths', () => {
// Regression test for issue #11: countPreparedLinesSimple (used by layout())
// and walkPreparedLinesSimple (used by layoutWithLines()) returned different
// lineCount when a trailing collapsible space overflowed the line edge.
//
// CSS white-space: normal behavior: trailing collapsible whitespace hangs
// past the line edge without triggering a line break.
//
// We craft internal data directly because the fake measurement system's
// proportions (space=0.33x, char>=0.4x) can't produce the divergence
// condition: word + space > maxWidth but word + nextSegment <= maxWidth.
const prepared = {
widths: [40, 5, 1],
lineEndFitAdvances: [40, 0, 1],
lineEndPaintAdvances: [40, 0, 1],
kinds: ['text' as const, 'space' as const, 'text' as const],
simpleLineWalkFastPath: true,
breakableWidths: [null, null, null],
breakablePrefixWidths: [null, null, null],
discretionaryHyphenWidth: 5,
tabStopAdvance: 0,
chunks: [{ startSegmentIndex: 0, endSegmentIndex: 3, consumedEndSegmentIndex: 3 }],
}

// 40 + 5 = 45 > 42: space overflows
// 40 + 1 = 41 <= 42: next segment fits if space just hangs
const maxWidth = 42
const counted = countPreparedLines(prepared, maxWidth)
const walked = walkPreparedLines(prepared, maxWidth)
expect(counted).toBe(1)
expect(walked).toBe(counted)
})
})
13 changes: 13 additions & 0 deletions src/line-break.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,13 @@ function walkPreparedLinesSimple(

const newW = lineW + w
if (newW > maxWidth + lineFitEpsilon) {
// CSS behavior: trailing collapsible space hangs past the line edge
// without triggering a line break — matches countPreparedLinesSimple
if (isSimpleCollapsibleSpace(kind)) {
i++
continue
}

if (canBreakAfter(kind)) {
appendWholeSegment(i, w)
emitCurrentLine(i + 1, 0, lineW - w)
Expand Down Expand Up @@ -1029,6 +1036,12 @@ function layoutNextLineRangeSimple(

const newW = lineW + w
if (newW > maxWidth + lineFitEpsilon) {
// CSS behavior: trailing collapsible space hangs past the line edge
// without triggering a line break — matches countPreparedLinesSimple
if (isSimpleCollapsibleSpace(kind)) {
continue
}

if (canBreakAfter(kind)) {
appendWholeSegment(i, w)
return finishLine(i + 1, 0, lineW - w)
Expand Down