fix(reflow): wrapped bullet continuation got bullet-gap spacing instead of wrap spacing

buildBulletItem computed `leading` as the median over ALL paragraph line gaps. In a bullet list
the bullet-to-bullet gap (~14.2pt) is larger than the within-bullet wrap gap (~12.2pt) and dominates
the median, so a newly-wrapped continuation line was placed ~2pt too low (visible extra spacing
between e.g. the Architecture line and its "Design Patterns…" continuation).

Use the WRAP spacing instead: prefer the edited item's own internal line gap (if it already wraps),
then any hanging-continuation gap in the block, then the all-lines median, then font*1.2. Only
affects multi-line (wrapped) bullet items; single-line edits emit no continuation so are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Furqan-14
2026-06-17 21:40:59 +05:30
co-authored by Claude Opus 4.8
parent 6edec0cf22
commit 63369d4259
+21 -6
View File
@@ -108,12 +108,27 @@ export function buildBulletItem(para: any, runLineIndex: number): { subPara: any
const itemLines = lines.slice(start, end);
if (!itemLines.length) return null;
// Paragraph leading (line-spacing) from all baselines — used so a newly-wrapped line gets the
// document's true spacing, not a guess.
const baselines = lines.map((l: any) => l.baseline_y).filter((b: any) => typeof b === 'number');
const deltas: number[] = [];
for (let i = 0; i < baselines.length - 1; i++) deltas.push(Math.abs(baselines[i] - baselines[i + 1]));
const leading = deltas.length ? median(deltas) : (itemLines[0].runs?.[0]?.font_size ?? 12) * 1.2;
// Leading for a NEWLY-WRAPPED line must be the WRAP spacing — the gap between a bullet line and its
// own hanging continuation — NOT the bullet-to-bullet spacing, which is often larger (list items
// carry extra spacing). The paragraph-wide median is dominated by the bullet gaps and over-spaces a
// wrapped continuation (e.g. 14.2pt bullet gap vs 12.2pt wrap). Prefer: (1) the edited item's own
// internal line spacing if it already wraps, (2) any wrap spacing elsewhere in the block (a line
// hanging at the text indent, i.e. not flush-left), (3) the all-lines median, (4) font*1.2.
const hasBl = (i: number) => typeof lines[i].baseline_y === 'number';
const dlt = (i: number) => Math.abs(lines[i].baseline_y - lines[i + 1].baseline_y);
const itemDeltas: number[] = [];
for (let i = start; i + 1 < end; i++) if (hasBl(i) && hasBl(i + 1)) itemDeltas.push(dlt(i));
const wrapDeltas: number[] = [];
const allDeltas: number[] = [];
for (let i = 0; i + 1 < lines.length; i++) {
if (!hasBl(i) || !hasBl(i + 1)) continue;
allDeltas.push(dlt(i));
if (!flushLeft(lines[i + 1])) wrapDeltas.push(dlt(i)); // line i+1 is a hanging continuation
}
const leading = itemDeltas.length ? median(itemDeltas)
: wrapDeltas.length ? median(wrapDeltas)
: allDeltas.length ? median(allDeltas)
: (itemLines[0].runs?.[0]?.font_size ?? 12) * 1.2;
// Strip a leading bullet marker (+ any leading space) from the first line; the text indent is
// where the real text begins (which the wrapped lines already hang to).