diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 9d0db66..6125574 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -106,10 +106,9 @@ type Run = { type?: string; attrs?: { height?: number; wrap?: string }; marks?: { type?: string; attrs?: { fontSize?: string; fontFamily?: string } }[] }; const para = doc.content?.[0] as { content?: Run[]; attrs?: { spaceBefore?: number; spaceAfter?: number; fontSize?: string; fontFamily?: string } } | undefined; const inline = para?.content ?? []; - // The zone's biggest text run sizes its lines: LibreOffice grows the band to hold them - // when fo:min-height is smaller (probed, a 3-line header), and a zone set throughout in - // 10pt reserves 10pt — the body default only stands in for a run that declares none. - // A line is its font's own natural height, which is what the band renders at. + // A line is its font's own natural height: the zone's biggest run sizes its lines and + // LibreOffice grows the band to hold them where fo:min-height is smaller (probed). + // The body default only stands in for a run that declares no size of its own. const line = (size?: string, family?: string) => size ? (parseFloat(size) * 96) / 72 * singleLineHeight(family) : HF_LINE_PX; // The paragraph mark is the strut every run that declares no size of its own takes. @@ -148,9 +147,8 @@ let effBottomRest = $derived(Math.max(mBottomPx, hfReachPx(footerDoc ?? null, footerDistPx, true), evenBottomReach)); let effBottomFirst = $derived(Math.max(mBottomPx, hfReachPx((differentFirstPage ? footerFirstDoc : footerDoc) ?? null, footerDistPx, true))); // Per-section reaches for pageBreaks: "topFirst|topRest|bottomFirst|bottomRest" in px, - // one group per section, comma-separated. Section 1 repeats the four vars below. - // A section with page margins of its own (w:pgMar, its own ODF page layout) measures - // against those instead of the document's; `marginsFirst` is its first page's. + // one group per section, comma-separated. Section 1 repeats the four vars below; a + // section with page margins of its own measures against those (`marginsFirst` = page 1). let sectionReach = $derived([ [effTopFirst, effTopRest, effBottomFirst, effBottomRest], ...extraHfSections.map((s) => { diff --git a/src/lib/components/HeaderFooterLayer.svelte b/src/lib/components/HeaderFooterLayer.svelte index 2cb693e..eefc14a 100644 --- a/src/lib/components/HeaderFooterLayer.svelte +++ b/src/lib/components/HeaderFooterLayer.svelte @@ -199,8 +199,6 @@ // below re-run on (the map's identity changes with each pagination pass). type ZoneParams = [number, number, string, unknown]; - // Lay out the zone's tabs: static HTML no ProseMirror plugin reaches. The advances are - // layout px, so only a content change invalidates them — not the zoom transform. // The chapter a page runs under: the last heading at or above the field's outline // level that has started by then (ODF text:chapter, Word STYLEREF). function chapterOn(page: number, level: number): string { @@ -212,6 +210,8 @@ return text; } + // The zone's tabs: static HTML no ProseMirror plugin reaches. The advances are layout + // px, so only a content change invalidates them — not the zoom transform. function layOutTabs(node: HTMLElement, _params: ZoneParams) { const apply = () => layOutZoneTabs(node); apply(); diff --git a/src/lib/editor/extensions/bookmark.ts b/src/lib/editor/extensions/bookmark.ts index 1887095..8858812 100644 --- a/src/lib/editor/extensions/bookmark.ts +++ b/src/lib/editor/extensions/bookmark.ts @@ -1,10 +1,9 @@ import { Mark, mergeAttributes } from '@tiptap/core'; import type { Node as PMNode, Mark as PMMark } from '@tiptap/pm/model'; -// A bookmark: a named range of text, the target a cross-reference or an internal link -// points at. A mark, not a point node — both formats store a range, and a REF field -// needs that range to recompute its text. Round-trips to ODF text:bookmark-start/-end -// and DOCX w:bookmarkStart/w:bookmarkEnd. +// A bookmark: a named range of text, the target a cross-reference or internal link points +// at. A mark, not a point node — both formats store a range (ODF text:bookmark-start/-end, +// DOCX w:bookmarkStart/-End) and a REF field needs it to recompute its text. export type BookmarkRef = { name: string; from: number; to: number; text: string }; @@ -26,8 +25,7 @@ export function bookmarkNameOf(marks: readonly PMMark[]): string | null { } // Every bookmark range in document order. Adjacent text nodes sharing a name merge into -// one range; a name that reappears later (a paste, or across a paragraph boundary) gets -// its own entry and every lookup below takes the first. +// one range; a name that reappears later gets its own entry, and lookups take the first. // ponytail: no cross-paragraph merge — the DOCX exporter can't write one either. export function bookmarks(doc: PMNode): BookmarkRef[] { const out: BookmarkRef[] = []; diff --git a/src/lib/editor/extensions/pageBreak.ts b/src/lib/editor/extensions/pageBreak.ts index a2305ec..6df5851 100644 --- a/src/lib/editor/extensions/pageBreak.ts +++ b/src/lib/editor/extensions/pageBreak.ts @@ -1,14 +1,9 @@ import { Extension } from '@tiptap/core'; import { DEFAULT_SHORTCUTS } from '../shortcuts'; -// The text-flow attrs of a paragraph/heading. breakBefore: 'page' round-trips to ODF -// fo:break-before and DOCX w:pageBreakBefore; widowControl: false turns off the -// widow-orphan minimum (w:widowControl, fo:widows/fo:orphans); keepNext keeps the block -// on the page its successor starts on (w:keepNext, fo:keep-with-next) — headings do -// that anyway in both, so the attr only marks the other blocks; keepLines holds all of -// a block's lines on one page (w:keepLines, fo:keep-together). null = default; -// sectionBreak marks the first block of a new section, which carries its own -// header/footer set (storage/headerFooter.ts `HfSet`). +// The text-flow attrs of a paragraph/heading, null = default: breakBefore 'page', widow +// control, keepNext (a heading has it anyway, so the attr marks the other blocks), +// keepLines, and sectionBreak — which opens a section (storage/headerFooter.ts). declare module '@tiptap/core' { interface Commands { diff --git a/src/lib/editor/extensions/pageBreaks.ts b/src/lib/editor/extensions/pageBreaks.ts index f00afd9..b3ceb80 100644 --- a/src/lib/editor/extensions/pageBreaks.ts +++ b/src/lib/editor/extensions/pageBreaks.ts @@ -258,11 +258,9 @@ export const PageBreaks = Extension.create({ let isUpdating = false; let rafId: number | null = null; let lastPlacementsKey = ''; - // The layout before the current one. A block below a float's overhang moves further - // than the spacer the model accounts for, so two layouts can each imply the other — - // seeing the older one come back means a ping-pong, and the current one is kept. - // null, not '': that is the real key of a spacer-free layout, and a reset - // sharing it would read "all spacers removed" as a ping-pong and skip the dispatch. + // The layout before the current one: two layouts can each imply the other (a block + // below a float's overhang moves further than the model's spacer), so seeing the older + // key return means a ping-pong. null, not '' — that is a spacer-free layout's own key. let prevPlacementsKey: string | null = null; // Each pass measures against the spacers left by the previous pass, so a changed // result needs one more pass to re-measure and settle (see calculate). Bounded so @@ -535,11 +533,9 @@ export const PageBreaks = Extension.create({ return lines; } - // `minLines` is the widow-orphan minimum, or 1 for a leaf taller than one page - // slot — there the rule is unsatisfiable and splitting beats overflowing. - // What a keep-with-next block has to fit below itself, in unscaled doc px: as - // many lines of the successor as widow-orphan control would keep together - // anyway. An empty block falls back to its whole height. + // What a keep-with-next block has to fit below itself, in unscaled doc px: as many + // lines of the successor as widow-orphan control would keep together anyway. An + // empty block falls back to its whole height. function firstLinesHeight(el: HTMLElement, scale: number, want: number): number { const lines = getLineRects(el); if (!lines.length) return el.offsetHeight; @@ -875,10 +871,9 @@ export const PageBreaks = Extension.create({ } } const cs = getComputedStyle(child); - // The block's own space above, and how much of it the last pass took off - // at a page top (the decoration zeroes both padding and margin). - // A cell's first block never carries it (LibreOffice adds none there), so - // the missing padding is the document, not a decoration to add back. + // The block's own space above, and how much of it the last pass took off at + // a page top. A cell's first block never carries any (LibreOffice adds none + // there), so missing padding is the document, not a decoration to add back. const spaceAbove = parseFloat(cs.getPropertyValue('--space-before')) || 0; const dropped = spaceAbove > 0.5 && !inTableCell && parseFloat(cs.paddingTop) < 0.5 && parseFloat(cs.marginTop) < 0.5 @@ -1193,6 +1188,8 @@ export const PageBreaks = Extension.create({ let extraShift = 0; const guarded = leaf.naturalHeight <= CONTENT_HEIGHT && leaf.el.getAttribute('data-widow-control') !== 'false'; + // Taller than a page slot: the widow-orphan rule is unsatisfiable there, + // and splitting beats overflowing. const minLines = guarded ? MIN_KEPT_LINES : 1; while (boundaryNatural < leaf.naturalHeight) { const split = findLineSplit(leaf.el, boundaryNatural, scale, minLines); diff --git a/src/lib/editor/extensions/tabStops.ts b/src/lib/editor/extensions/tabStops.ts index 5135487..ccfc585 100644 --- a/src/lib/editor/extensions/tabStops.ts +++ b/src/lib/editor/extensions/tabStops.ts @@ -121,10 +121,9 @@ export function nextStopCm(x: number, stops: number[], interval: number): number return Math.floor(x / interval + 1e-9) * interval + interval; } -// A run of tabs wider than the line continues on the next one, as LibreOffice lays it -// out; Chromium instead hangs the leftover tabs and starts the next line at the margin. -// Walked from the pen BEFORE the run — the one thing a break of ours can't move — so the -// answer is the same whether or not the break is already in place. +// A run of tabs wider than the line continues on the next one, as LibreOffice lays it out +// (Chromium hangs the leftover tabs instead). Walked from the pen BEFORE the run — the one +// thing a break of ours can't move — so the answer holds with the break already in place. function runBreaks(view: EditorView, blockEl: HTMLElement, tabs: number[], stops: TabStop[], scale: number, originX: number): number[] { // Only a run of two or more can outgrow a line, and reading the block's geometry // forces a reflow — so the ordinary single tab costs nothing here. @@ -189,10 +188,8 @@ function measure(view: EditorView): TabLayout { const blockEnd = pos + node.nodeSize - 1; // Where the pen stands (cm from the text margin) after the tab before this one. - // Carrying it along the line is what makes one pass self-consistent: read from the - // DOM instead and every tab but the line's first measures the advance the *previous* - // pass gave the one before it, so the layout takes another pass to settle — and on a - // long document, where pagination keeps moving lines, it may never get a stable one. + // Carrying it along the line is what makes one pass self-consistent: read from the DOM + // instead and each tab measures the advance the previous pass gave the one before it. let pen: number | null = null; let lineTop = NaN; for (let t = 0; t < tabs.length; t++) { @@ -228,10 +225,9 @@ function measure(view: EditorView): TabLayout { return { widths: out, breaks }; } -// Natural width of a doc range: the extent of each line it covers, added up — so a -// segment pushed onto the next line still reads its own width. Not the sum of the rects: -// a range crossing an inline element yields one for the element's box and one for the -// text inside it, and adding those counts the text twice. +// Natural width of a doc range: the extent of each line it covers, added up — so a segment +// pushed onto the next line still reads its own width. Not the sum of the rects: one comes +// from an inline element's box and one from the text in it, counting that text twice. function rangeWidth(view: EditorView, from: number, to: number): number { // Biased outwards, or the range swallows the tab's own span at either end. const a = view.domAtPos(from, 1); @@ -316,10 +312,9 @@ export function layOutZoneTabs(zone: HTMLElement): void { const scale = para.offsetWidth ? rect.width / para.offsetWidth : 1; const cs = getComputedStyle(para); const padLeft = parseFloat(cs.paddingLeft || '0'); - // The line's own width, not clientWidth: that is rounded up to whole px, and half a - // pixel of it is enough to wrap the run a right-aligned stop puts at the very end. - // One px of slack: a run ending exactly on the boundary wraps, and a stop a pixel - // short of it is a pixel nobody sees. + // The line's own width, not clientWidth: that is rounded up to whole px, and half a pixel + // is enough to wrap the run a right-aligned stop puts at the end. One px of slack, since + // a run ending exactly on the boundary wraps and a pixel short of it shows on nothing. const lineCm = (rect.width / (scale || 1) - padLeft - parseFloat(cs.paddingRight || '0') - 1) / PX_PER_CM; const stops = clampStops(parseTabStops(para.getAttribute('data-tab-stops')), lineCm); if (!scale) { para.style.whiteSpace = wrapping; return; } diff --git a/src/lib/export/docx.ts b/src/lib/export/docx.ts index 88818e6..d486095 100644 --- a/src/lib/export/docx.ts +++ b/src/lib/export/docx.ts @@ -998,10 +998,9 @@ function blocksToDocx(content: TiptapNode[], num: Numbering, contentWidthCm: num textBoxes.push(textBoxDocxDescriptor(node)); out.push(new Paragraph({ children: [new TextRun({ text: `${TBX}${i}${TBX}` })] })); } else if (node.type === 'tableOfContents') { - // A real, recognized TOC field (hyperlinked, over the index's own heading levels); - // Word/LibreOffice populate + link it on field update (features.updateFields does - // this on open). Title is a plain bold paragraph so it isn't itself listed, and is - // omitted where the index has none; our importer regenerates the node. + // A real TOC field over the index's own heading levels, populated and linked on + // field update (features.updateFields). The title is a plain bold paragraph so it + // isn't itself listed, and is omitted where the index has none. const rawTitle = node.attrs?.title; const tocTitle = typeof rawTitle === 'string' ? rawTitle : 'Table of Contents'; const depth = Number(node.attrs?.maxLevel); @@ -1248,10 +1247,9 @@ export async function buildDocx( const headerDist = Math.min(hf?.headerDistanceCm ?? HF_DISTANCE_CM, margins.top); const footerDist = Math.min(hf?.footerDistanceCm ?? HF_DISTANCE_CM, margins.bottom); - // Page geometry rides every sectPr (Word requires it per section); the size is - // document-wide, the margins are the section's own where it has them. Fresh - // Header/Footer instances per section so each sectPr — including the body-final - // one our importer reads — carries its own references (Word's "Link to Previous"). + // Page geometry rides every sectPr (Word requires it per section): the size is + // document-wide, the margins the section's own where it has them. Fresh Header/Footer + // instances per section, or a later one inherits ("Link to Previous"). const pagePropsFor = (i: number) => { const m = setAt(i).margins ?? margins; return { diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 8a00299..df1b0c9 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -526,10 +526,9 @@ type CrossRefExport = { name: string; format: 'text' | 'page' }; const bookmarkNameOf = (node: TiptapNode): string => String(node.marks?.find((m) => m.type === 'bookmark')?.attrs?.name ?? ''); -// Bracket each bookmark's text with the BMS/BME sentinels and replace every `crossRef` -// node with an XRF-sentinel run. Splicing into the run text (rather than into a run -// emitter) is what makes both ride every odf-kit path — cells and custom paragraphs -// included. applyBookmarks resolves them after serialization. +// Bracket each bookmark's text with the BMS/BME sentinels and every `crossRef` node with +// an XRF-sentinel run; applyBookmarks resolves them after serialization. Spliced into the +// run text, not a run emitter, so both ride every odf-kit path (cells included). function replaceBookmarks(node: TiptapNode, refs: CrossRefExport[]): TiptapNode { if (!node.content?.length) return node; const content: TiptapNode[] = []; @@ -925,10 +924,9 @@ function expandCellPadding(odtBytes: Uint8Array): Uint8Array { return rezipOdt(files); } -// A dragged table edge → the table style's fo:margin-left/-right + style:width (with -// odf-kit's table:align="margins" the table fills exactly what's left), and keep-rows -// → style:may-break-between-rows. odf-kit names table styles Table1, Table2, … in -// document order, matching the descriptor list. +// A dragged table edge → the style's fo:margin-left/-right + style:width (with odf-kit's +// table:align="margins" the table fills what's left); keep-rows → may-break-between-rows. +// odf-kit names table styles Table1, Table2, … in document order, as the descriptors are. function applyTableProps(odtBytes: Uint8Array, margins: (TableProps | null)[], contentWidthCm: number): Uint8Array { if (margins.every(m => m === null)) return odtBytes; @@ -3229,11 +3227,8 @@ function contentsHeadingStyle(): string { ); } -// The full : a source (title + per-level entry templates that -// carry a right tab stop, page number, and link markers so LibreOffice rebuilds it as a -// linking TOC) plus a cached index-body (title + one entry paragraph per heading). -// A level's entry style: the file's own named one where the index came with it, else -// the automatic Contents_20_N this export mints. +// A level's entry style: the file's own named one where the index came with it, else the +// automatic Contents_20_N this export mints. function tocLevelStyle(toc: TocExport, level: number): string { const own = toc.levelStyles?.[level - 1]; return own ? odfStyleName(own) : `Contents_20_${level}`; @@ -3274,12 +3269,9 @@ function tocXml(toc: TocExport, index: number): string { return `${source}${body}`; } -// Resolve TOC sentinels: rewrite each marker TOC{i}TOC to its -// and mint the Contents_20_* paragraph styles it references. // BMS/BME/XRF sentinels → , and // . All three are legal anywhere in paragraph content, so a plain -// replace works wherever odf-kit put the run — inside a included. Bookmark -// names are sanitized on insert, so nothing here needs XML escaping. +// replace works wherever odf-kit put the run — inside a included. function applyBookmarks(odtBytes: Uint8Array, refs: CrossRefExport[]): Uint8Array { const files = unzipSync(odtBytes); const contentBytes = files['content.xml']; @@ -3623,8 +3615,6 @@ function hfAlign(para: TiptapNode): AlignValue | null { return ta === 'center' || ta === 'right' || ta === 'justify' ? ta : null; } -// fo:* paragraph-properties for a header/footer paragraph: alignment plus the paragraph -// background ("colored field") and per-side borders ("rule line"). Empty ⇒ no override. // for a zone paragraph: odf-kit builds the body's, but a header/footer // style is written by hand here. function tabStopsXml(attrs: TiptapNode['attrs']): string { @@ -3640,7 +3630,8 @@ function tabStopsXml(attrs: TiptapNode['attrs']): string { return `${inner}`; } -// The whole of a zone paragraph, or '' when it needs none. +// The whole of a zone paragraph, or '' when it needs none: +// alignment plus the paragraph background ("colored field") and per-side borders. function hfParaPropsXml(para: TiptapNode): string { const props: string[] = []; const align = hfAlign(para); @@ -3771,11 +3762,9 @@ function ensureDrawNamespaces(styles: string): string { return missing.length ? styles.replace(/ -// XML (first = page 1, left = even pages). Runs and page fields become -// referencing minted automatic text styles (pushed via `mint`); hardBreak → line-break. -// suffix null = the section's own /; 'first'/'left' are the -// per-page variants ODF hangs beside it. +// A variant zone paragraph → XML (suffix null = the +// section's own zone). Runs and page fields become s referencing minted +// automatic text styles (pushed via `mint`); hardBreak → line-break. function hfVariantZoneXml(kind: 'header' | 'footer', suffix: 'first' | 'left' | null, para: TiptapNode, pageCount: number, mint: (styleXml: string) => void, prefix = ''): string { let styleSeq = 0; // Distinct minted-style prefix per variant so first + even styles never collide. diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index 8bfaaeb..fc98840 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -967,9 +967,9 @@ function snapPt(v: number): number { } // ---- inline conversion (runs, marks, fields, images) ----------------------- -// Word wraps a citation, a bibliography entry or any content control in a w:sdt whose -// content is ordinary inline content — walk through the wrapper, or the runs inside it -// (a citation's whole visible text) never reach the paragraph. + +// Word wraps a citation or any content control in a w:sdt holding ordinary inline +// content — walk through the wrapper, or those runs never reach the paragraph. function inlineChildren(el: Element): Element[] { return Array.from(el.children).flatMap((c) => { if (c.namespaceURI !== W || c.localName !== 'sdt') return [c]; @@ -1662,8 +1662,9 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null { // Table-level border defaults; per-cell w:tcBorders override per side. Edge cells // fall back to the outer sides, interior cells to insideH/insideV. - // Word's w:tblStyle: only the name comes back — the look rides on the cell attrs, and - // the editor re-derives the regions from its own registry (refreshTableStyles). + + // From w:tblStyle only the name comes back — the look rides on the cell attrs, and the + // editor re-derives the regions from its own registry (refreshTableStyles). const styleEl = fc(fc(tbl, 'tblPr'), 'tblStyle'); const styleId = styleEl ? wVal(styleEl) : null; const pad = docxCellPadding(tbl, styleId, ctx); diff --git a/src/lib/import/docxStyles.ts b/src/lib/import/docxStyles.ts index 5d9eab9..aecfd2a 100644 --- a/src/lib/import/docxStyles.ts +++ b/src/lib/import/docxStyles.ts @@ -52,8 +52,7 @@ export function wVal(el: Element): string | null { // → stops in cm from the left text margin, w:pos's origin (which the editor's // attr shares). 'clear' removes an inherited stop and 'bar' is a rule, not a stop. -// w:leader → the fill character. "heavy" is a bold underscore, which we can only -// render as the plain one. +// w:leader → the fill character; "heavy" is a bold underscore we can only draw plain. const DOCX_LEADER: Record = { dot: '.', hyphen: '-', underscore: '_', heavy: '_', middleDot: '·', }; diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 1906552..f1e786c 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -146,10 +146,7 @@ function frameRotationDeg(el: Element): number { return ((Math.round((-parseFloat(rot[1]) * 180) / Math.PI) % 360) + 360) % 360; } -// Rotation + wrap attrs, shared by images, text boxes and shapes. A non-as-char anchor or -// an explicit style:wrap floats the element (free x/y collapse to the nearest side). An -// explicit as-char anchor stays inline: LibreOffice's Graphics style carries a style:wrap. -// Where an as-char frame sits against the line. `baseline`/`char` measure from the text +// Where an as-char frame sits against the line: `baseline`/`char` measure from the text // baseline, `text`/`line` from the character area, and `from-top` puts the frame's top // svg:y below the baseline whatever the relation says (all probed against LibreOffice). function applyInlineVAlign(el: Element, attrs: Record, gp: PropMap): void { @@ -2009,11 +2006,9 @@ function convertTable(el: Element, ctx: Ctx): Node | null { const colspan = parseInt(cellEl.getAttributeNS(NS.table, 'number-columns-spanned') ?? '1', 10) || 1; const rowspan = parseInt(cellEl.getAttributeNS(NS.table, 'number-rows-spanned') ?? '1', 10) || 1; const cellStyleName = cellEl.getAttributeNS(NS.table, 'style-name'); - // ODF has no table-level cell margin — it sits on each cell's style, so the first - // cell's stands for the table and any cell that disagrees keeps its own. - // fo:padding defaults to 0 in ODF, so a side nobody declares really is zero — - // leaving it open would hand the cell Word's implicit 0.19cm instead (probed: - // LibreOffice sets an unstyled cell's text flush against the column edge). + // ODF has no table-level cell margin: it sits on each cell's style, and fo:padding + // defaults to 0, so a side nobody declares really is zero — left open, the cell + // would take Word's implicit 0.19cm instead (probed). const padSides = ctx.resolver.cellPadding(cellStyleName).map((v) => v ?? 0); if (cellPad === undefined) cellPad = cellPaddingAttr(padSides); const ownPad = cellPaddingAttr(padSides, cellPad ?? DEFAULT_CELL_PADDING); diff --git a/src/lib/import/styleResolver.ts b/src/lib/import/styleResolver.ts index c01015f..432a88b 100644 --- a/src/lib/import/styleResolver.ts +++ b/src/lib/import/styleResolver.ts @@ -81,10 +81,9 @@ export function layerTextProps(base: PropMap, over: PropMap): PropMap { return Object.assign(out, over); } -// LibreOffice holds a paragraph's two vertical margins in one item, so a style that -// declares just one of them does not inherit the other — the missing one falls back to -// the default paragraph style's value (probed: heading margins, and the space after a -// paragraph that only sets fo:margin-top). +// LibreOffice holds a paragraph's two vertical margins in one item, so a style declaring +// just one does not inherit the other: the missing one falls back to the default +// paragraph style's value (probed on heading margins). const VERTICAL_MARGINS = ['fo:margin-top', 'fo:margin-bottom'] as const; export function layerParaProps(base: PropMap, over: PropMap, poolDefaults: PropMap): PropMap { diff --git a/tests/render-parity/run.mjs b/tests/render-parity/run.mjs index 02ccee7..98dcc81 100644 --- a/tests/render-parity/run.mjs +++ b/tests/render-parity/run.mjs @@ -239,10 +239,9 @@ function compare(ref, ed) { return issues; } -// Whitespace-insensitive: spell decorations split text nodes mid-word and the two -// engines needn't agree on word boundaries — only on what sits on a line. A tab or -// index leader collapses to one token too: how many dots fill the gap is the fill's -// own business, and both engines end it at the same stop. +// Whitespace-insensitive: spell decorations split text nodes mid-word and the engines +// needn't agree on word boundaries, only on what sits on a line. A tab or index leader +// collapses to one token too — both engines end the fill at the same stop. const norm = (s) => s.replace(/[\s\u00a0\u00ad]+/g, '').replace(/([.\u00b7_-])\1{2,}/g, '\u2026'); // ------------------------------------------------------------------- driver