diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index 3a1e8f57bf..cf5682d04c 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -11292,6 +11292,87 @@ describe('applyRunDataAttributes', () => { }).not.toThrow(); }); + it('renders all lines when measure indices come from inline-newline expansion', () => { + const inlineNewlineBlock: FlowBlock = { + kind: 'paragraph', + id: 'inline-newline-slice', + runs: [{ text: 'first\nsecond\nthird', fontFamily: 'Arial', fontSize: 16, pmStart: 0, pmEnd: 18 }], + }; + + // Measurer expands inline '\n' into: text, break, text, break, text. + const inlineNewlineMeasure: ParagraphMeasure = { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: 5, + width: 40, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + { + fromRun: 2, + fromChar: 0, + toRun: 2, + toChar: 6, + width: 50, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + { + fromRun: 4, + fromChar: 0, + toRun: 4, + toChar: 5, + width: 40, + ascent: 12, + descent: 4, + lineHeight: 20, + }, + ], + totalHeight: 60, + }; + + const inlineNewlineLayout: Layout = { + pageSize: { w: 400, h: 500 }, + pages: [ + { + number: 1, + fragments: [ + { + kind: 'para', + blockId: 'inline-newline-slice', + fromLine: 0, + toLine: 3, + x: 20, + y: 20, + width: 300, + }, + ], + }, + ], + }; + + const painter = createTestPainter({ + blocks: [inlineNewlineBlock], + measures: [inlineNewlineMeasure], + }); + + expect(() => { + painter.paint(inlineNewlineLayout, mount); + }).not.toThrow(); + + const fragment = mount.querySelector('.superdoc-fragment') as HTMLElement; + expect(fragment).toBeTruthy(); + expect(fragment.textContent).toContain('first'); + expect(fragment.textContent).toContain('second'); + expect(fragment.textContent).toContain('third'); + }); + it('preserves PM positions for lineBreak runs', () => { const lineBreakBlock: FlowBlock = { kind: 'paragraph', diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 350110f057..a1ddfa47cb 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -8004,11 +8004,50 @@ const stripListIndent = (attrs?: ParagraphAttrs): ParagraphAttrs | undefined => * // Returns runs or run slices that fall within the specified character range * ``` */ +const expandRunsForInlineNewlines = (runs: Run[]): Run[] => { + const expanded: Run[] = []; + + for (const run of runs) { + if ((run as TextRun).text && typeof (run as TextRun).text === 'string' && (run as TextRun).text.includes('\n')) { + const textRun = run as TextRun; + const segments = textRun.text.split('\n'); + let cursor = textRun.pmStart ?? 0; + + segments.forEach((segment, idx) => { + expanded.push({ + ...textRun, + text: segment, + pmStart: cursor, + pmEnd: cursor + segment.length, + }); + cursor += segment.length; + + if (idx !== segments.length - 1) { + expanded.push({ + kind: 'break', + breakType: 'line', + pmStart: cursor, + pmEnd: cursor + 1, + sdt: textRun.sdt, + }); + cursor += 1; + } + }); + continue; + } + + expanded.push(run); + } + + return expanded; +}; + export const sliceRunsForLine = (block: ParagraphBlock, line: Line): Run[] => { + const runs = expandRunsForInlineNewlines(block.runs as Run[]); const result: Run[] = []; for (let runIndex = line.fromRun; runIndex <= line.toRun; runIndex += 1) { - const run = block.runs[runIndex]; + const run = runs[runIndex]; if (!run) continue; // FIXED: ImageRun handling - images are atomic units, no slicing needed