diff --git a/src/layout.test.ts b/src/layout.test.ts index 3b5d01bb..8d4d770a 100644 --- a/src/layout.test.ts +++ b/src/layout.test.ts @@ -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) + }) }) diff --git a/src/line-break.ts b/src/line-break.ts index 57fa1131..6a4c8d1f 100644 --- a/src/line-break.ts +++ b/src/line-break.ts @@ -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) @@ -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)