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
81 changes: 81 additions & 0 deletions packages/layout-engine/painters/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10920,6 +10920,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',
Expand Down
41 changes: 40 additions & 1 deletion packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7696,11 +7696,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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid fabricating PM positions for newline-split runs

The newline expansion path seeds cursor with textRun.pmStart ?? 0, which means runs that originally had no PM mapping are rewritten with synthetic pmStart/pmEnd values. Those fabricated offsets are later emitted as data-pm-* attributes during rendering, so in documents rendered without real PM coordinates the caret/click mapping can resolve to incorrect positions (often near the document start) instead of using the no-position fallback path. Preserve pmStart/pmEnd as undefined when the source run has no PM metadata.

Useful? React with 👍 / 👎.


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[]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Expand inline-newline runs only once per paragraph

This call now recomputes newline expansion for the full paragraph run list every time a single line is rendered. Since renderLine invokes sliceRunsForLine per measured line, long paragraphs incur repeated full-array scans and allocations, turning rendering into avoidable O(lines × runs) work and causing noticeable paint slowdowns on large documents with tracked changes. Cache or precompute the expanded run array per paragraph/layout pass instead of rebuilding it per line.

Useful? React with 👍 / 👎.

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
Expand Down
Loading