From be96ee626c93b80221ea3ec854b7850896eb8f4d Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 7 Aug 2026 23:31:17 +0200 Subject: [PATCH] docs: apply rules for comments --- src/App.svelte | 26 ++++----- src/lib/components/ContextMenu.svelte | 5 +- src/lib/components/Editor.svelte | 8 ++- src/lib/components/HeaderFooterLayer.svelte | 24 ++++----- src/lib/components/TableBorderPicker.svelte | 2 +- src/lib/components/TablePicker.svelte | 7 ++- src/lib/components/ToolbarExpanded.svelte | 2 +- src/lib/editor/extensions.ts | 2 +- src/lib/editor/extensions/paragraphBox.ts | 2 +- .../editor/extensions/tableCellBackground.ts | 8 ++- src/lib/editor/extensions/tableCellBorders.ts | 24 ++++----- .../editor/extensions/tableColumnResize.ts | 4 +- src/lib/editor/extensions/tableHeaderRow.ts | 9 ++-- src/lib/editor/extensions/tableRowResize.ts | 2 +- src/lib/editor/extensions/tableSplit.ts | 10 ++-- src/lib/editor/extensions/textBox.ts | 14 +++-- src/lib/editor/shortcuts.ts | 7 ++- src/lib/export/docx.ts | 5 +- src/lib/export/odt.ts | 46 +++++++--------- src/lib/export/pdf.ts | 9 ++-- src/lib/import/docx.ts | 13 ++--- src/lib/import/imageFormats.ts | 9 ++-- src/lib/import/odt.ts | 54 ++++++++----------- src/lib/import/styleResolver.ts | 7 ++- src/lib/storage/headerFooter.ts | 14 +++-- src/lib/styles/styleSheet.ts | 3 +- tests/lo-roundtrip.test.ts | 5 +- tests/roundtrip.test.ts | 6 +-- tests/unit/docx-header-floating-image.test.ts | 5 +- tests/unit/table-split.test.ts | 5 +- 30 files changed, 137 insertions(+), 200 deletions(-) diff --git a/src/App.svelte b/src/App.svelte index afac012..2a973b5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -60,11 +60,11 @@ // top toolbars target hfEditor instead of the body editor (activeEditor below). let headerDoc: HfDoc = $state(loadHfDoc('header')); let footerDoc: HfDoc = $state(loadHfDoc('footer')); - // First-page header/footer (Word "Different First Page"), shown on page 1 when on. + // First-page header/footer, shown on page 1 when the flag is on. let headerFirstDoc: HfDoc = $state(loadHfDoc('header', 'first')); let footerFirstDoc: HfDoc = $state(loadHfDoc('footer', 'first')); let differentFirstPage: boolean = $state(loadDifferentFirstPage()); - // Even-page header/footer (Word "Different Odd & Even Pages"), shown on even pages when on. + // Even-page header/footer, shown on even pages when the flag is on. let headerEvenDoc: HfDoc = $state(loadHfDoc('header', 'even')); let footerEvenDoc: HfDoc = $state(loadHfDoc('footer', 'even')); let differentOddEven: boolean = $state(loadDifferentOddEven()); @@ -1112,11 +1112,9 @@ z-index: 190; } - /* Toolbar scroll region: an overlay pinned over the editor (which fills the full - height), so the document scrolls under the floating island and stays visible in - the transparent gaps around it. pointer-events pass through everywhere except - the island pieces themselves. The z-index keeps the whole toolbar — incl. its - dropdowns, which live inside the translated stack — above the editor overlays. */ + /* Toolbar scroll region: an overlay pinned over the full-height editor, so the document + scrolls under the floating island and stays visible in the gaps around it. Pointer + events pass through except on the island; z-index lifts it (and its dropdowns). */ .toolbar-region { position: absolute; top: 0; @@ -1154,10 +1152,9 @@ /* Custom scrollbar strip under the toolbar (a native one auto-hides on macOS). The thumb's width/position track the visible fraction of the stack; only rendered when the toolbar overflows. */ - /* Runs along the inside of the island's bottom edge. Lives inside the island - (header / expanded row) and is counter-translated (inline style) so it stays - put while the stack scrolls; z:1 paints it above the island surface but below - the dropdowns (z ≥ 200 in the same stacking context). */ + /* Runs along the inside of the island's bottom edge, counter-translated (inline style) + so it stays put while the stack scrolls; z:1 paints it above the island surface but + below the dropdowns (z ≥ 200 in the same stacking context). */ .toolbar-scrollbar { position: absolute; left: 0; @@ -1269,10 +1266,9 @@ gap: 2px; } - /* Editable document title: borderless at rest, text auto-sizes to its - content (via the hidden .doc-name-sizer mirror), an animated underline - grows in on focus, and a pencil hint fades in on hover. Drives the - suggested save filename. */ + /* Editable document title, borderless at rest: the text auto-sizes to its content (via + the hidden .doc-name-sizer mirror), an underline grows in on focus, a pencil hint + fades in on hover. Drives the suggested save filename. */ .doc-name { position: relative; display: inline-flex; diff --git a/src/lib/components/ContextMenu.svelte b/src/lib/components/ContextMenu.svelte index f4553f0..7a08bca 100644 --- a/src/lib/components/ContextMenu.svelte +++ b/src/lib/components/ContextMenu.svelte @@ -41,9 +41,8 @@ }); // Keep the panel inside the window; a right-click near an edge otherwise opens a menu - // that runs off-screen. Measured from the layout box (offset*), not - // getBoundingClientRect, so the flip transform below can't feed back into the next - // measurement. + // that runs off-screen. Measured from the layout box (offset*), not getBoundingClientRect, + // so the flip transform below can't feed back into the next measurement. $effect(() => { void [top, left, items]; if (!menuEl) return; diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index ad0d892..b01ec18 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -74,11 +74,9 @@ let headerDistPx = $derived(cmToPx((hfDistances ?? DEFAULT_HF_DISTANCES).header)); let mBottomPx = $derived(cmToPx(pageMargins.bottom)); let mTopPx = $derived(cmToPx(pageMargins.top)); - // Effective top/bottom margins (px) for other pages vs. page 1's own header/footer - // (different first page); pageBreaks reads these to start each page's content below - // its header and end it above its footer. - // "rest" = every page ≥ 2; fold the even variant in (max) so even pages never overlap - // a taller even zone — pageBreaks uses one --pb-content-*-rest for all of them. + // Effective top/bottom margins (px) pageBreaks reads to keep content clear of the + // header/footer: "first" = page 1's own zone, "rest" = every page ≥ 2 with the even + // variant folded in (max), since one --pb-content-*-rest covers all of them. let evenTopReach = $derived(differentOddEven ? hfReachPx(headerEvenDoc ?? null, headerDistPx) : 0); let evenBottomReach = $derived(differentOddEven ? hfReachPx(footerEvenDoc ?? null, footerDistPx) : 0); let effTopRest = $derived(Math.max(mTopPx, hfReachPx(headerDoc ?? null, headerDistPx), evenTopReach)); diff --git a/src/lib/components/HeaderFooterLayer.svelte b/src/lib/components/HeaderFooterLayer.svelte index 5566318..a8fd158 100644 --- a/src/lib/components/HeaderFooterLayer.svelte +++ b/src/lib/components/HeaderFooterLayer.svelte @@ -82,8 +82,8 @@ return { top, left, width, height }; } // The active (edited) zone grows to fit its content, keeping the anchored edge fixed - // (footer bottom at footerDistPx, header top at headerDistPx) so the Word-style - // boundary line sits exactly at the content's edge toward the body. + // (footer bottom at footerDistPx, header top at headerDistPx) so the boundary line + // sits exactly at the content's edge toward the body. function activeZoneBox(zone: HfZone, page: number) { const b = zoneBox(zone, page); // A visible ProseMirror trailing break (a caret line past the real content) is @@ -116,7 +116,7 @@ let headerEvenHtml = $derived(staticHtml(headerEvenDoc)); let footerEvenHtml = $derived(staticHtml(footerEvenDoc)); - // Which variant a page shows (Word precedence): page 1 → first (if on), other even + // Which variant a page shows, in precedence order: page 1 → first (if on), other even // pages → even (if on), everything else → default/odd. Each variant, when on, always // shows its own (possibly empty) zone, never the default. function variantFor(page: number): HfVariant { @@ -338,7 +338,7 @@ opacity: 0.6; } - /* Word-style affordance: a single dashed line at the edge facing the page body + /* Boundary affordance: a single dashed line at the edge facing the page body (footer top, header bottom) instead of a rectangle around the content. */ .hf-active::after { content: ''; @@ -355,7 +355,7 @@ bottom: 0; } - /* Word-style zone label tab, pinned to the zone's top-left corner. */ + /* Zone label tab, pinned to the zone's top-left corner. */ .hf-tag { position: absolute; transform: translateY(calc(-100% - 2px)); @@ -380,10 +380,9 @@ line-height: 1.15; white-space: pre-wrap; } - /* The live editor's editable root is also a `.tiptap`, so the global - `.paper .tiptap` rules (96px padding, 1123px min-height, page-background - gradient) leak in and push the text out of the clipped zone. Reset them — - higher specificity than `.paper .tiptap`, plus !important for the gradient. */ + /* The live editor's editable root is also a `.tiptap`, so the global `.paper .tiptap` + rules (96px padding, 1123px min-height, page gradient) leak in and push the text out + of the clipped zone. Reset them — higher specificity, plus !important for the gradient. */ .hf-layer .hf-zone :global(.tiptap) { padding: 0; min-height: 0; @@ -399,10 +398,9 @@ anchored edge while the real content stays put (set per active zone). */ margin-bottom: var(--hf-tb-offset, 0px); } - /* The live editor's paragraph also matches `.paper .tiptap p` (margin-bottom - 0.212cm), which the static `

` doesn't — with flex-end alignment that gap - shifts the text up on activation. Match the static zero margin (higher - specificity than `.paper .tiptap p`). */ + /* The live editor's paragraph also matches `.paper .tiptap p` (margin-bottom 0.212cm), + which the static `

` doesn't — with flex-end alignment that gap shifts the text up + on activation. Match the static zero margin (higher specificity). */ .hf-layer .hf-zone :global(.tiptap p) { margin: 0; } diff --git a/src/lib/components/TableBorderPicker.svelte b/src/lib/components/TableBorderPicker.svelte index 3763e95..287e998 100644 --- a/src/lib/components/TableBorderPicker.svelte +++ b/src/lib/components/TableBorderPicker.svelte @@ -38,7 +38,7 @@ { id: 'none', on: [] }, ]; - // Word-like button states, re-read per transaction (tick) and pen change: a preset + // Button states, re-read per transaction (tick) and pen change: a preset // is active when its boundaries all render the pen border; 'none' when borderless. const active = $derived( tick >= 0 && editor ? activeBorderPresets(editor.state, { widthPt, color }) : null, diff --git a/src/lib/components/TablePicker.svelte b/src/lib/components/TablePicker.svelte index 051343a..edf9c56 100644 --- a/src/lib/components/TablePicker.svelte +++ b/src/lib/components/TablePicker.svelte @@ -316,10 +316,9 @@ opacity: 0.9; } - /* Bleeds to both panel edges below the grid, like the style gallery's manage row. - The panel is shrink-to-fit, so its width must come from the grid alone: width:0 - keeps this row out of that calculation, and min-width then fills the resolved - width plus the two negative margins. The label truncates instead (title = full). */ + /* Bleeds to both panel edges below the grid. The panel is shrink-to-fit, so its width + must come from the grid alone: width:0 keeps this row out of that calculation, and + min-width then fills the resolved width plus the two negative margins. */ .table-manage { display: flex; align-items: center; diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte index 4c57187..afc53d2 100644 --- a/src/lib/components/ToolbarExpanded.svelte +++ b/src/lib/components/ToolbarExpanded.svelte @@ -147,7 +147,7 @@ const DEFAULT_LINE_HEIGHT = '1'; const LINE_HEIGHTS = ['1', '1.15', '1.5', '2']; - // Word binds only these three; 1.15 has no key. + // Only these three have a key; 1.15 has none. const LH_SHORTCUTS: Record = { '1': 'lineHeight1', '1.5': 'lineHeight15', '2': 'lineHeight2', }; diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index 67e9031..d514dca 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -119,7 +119,7 @@ export const extensions = [ // LibreOffice (the sheet resolves a character style on that portion). ListMarker.configure({ sheet: styleSheet }), // resizable:false keeps TipTap's columnResizing plugin off; TableView, the two - // resize plugins, and ResizableTableRow supply Word-style drag instead. Table + // resize plugins, and ResizableTableRow supply the drag handling instead. Table // (unlike TableKit) doesn't auto-add children, so ResizableTableRow is listed here. Table.configure({ resizable: false, View: TableView }), ResizableTableRow, diff --git a/src/lib/editor/extensions/paragraphBox.ts b/src/lib/editor/extensions/paragraphBox.ts index 16c1d2b..8fb5793 100644 --- a/src/lib/editor/extensions/paragraphBox.ts +++ b/src/lib/editor/extensions/paragraphBox.ts @@ -164,7 +164,7 @@ export const ParagraphBox = Extension.create({ }, }); -// Word-like active states for the paragraph border picker: a preset is active when every +// Active states for the paragraph border picker: a preset is active when every // covered block renders exactly the pen border on every side the preset targets. `none` // is active when every covered block is borderless. null when no block is selected. export function activeParagraphBorderPresets( diff --git a/src/lib/editor/extensions/tableCellBackground.ts b/src/lib/editor/extensions/tableCellBackground.ts index 5a3ec4d..69a725f 100644 --- a/src/lib/editor/extensions/tableCellBackground.ts +++ b/src/lib/editor/extensions/tableCellBackground.ts @@ -1,11 +1,9 @@ import { Extension } from '@tiptap/core'; import { HEADER_SHADE } from './tableHeaderRow'; -// Word/LibreOffice cell shading: a `backgroundColor` attr on table cells, rendered -// as the cell's CSS background and round-tripped to ODF fo:background-color -// (export/odt.ts exportTable, import/odt.ts convertTable). Set via the built-in -// setCellAttribute command (TableToolbar's ColorPicker). The default cell renderHTML -// emits no style, so this global attribute merges in cleanly. +// Cell shading: a `backgroundColor` attr on table cells, rendered as the cell's CSS +// background and round-tripped to ODF fo:background-color. Set via the built-in +// setCellAttribute, so it covers a whole CellSelection or just the cursor's cell. export const TableCellBackground = Extension.create({ name: 'tableCellBackground', diff --git a/src/lib/editor/extensions/tableCellBorders.ts b/src/lib/editor/extensions/tableCellBorders.ts index 05af254..18cba83 100644 --- a/src/lib/editor/extensions/tableCellBorders.ts +++ b/src/lib/editor/extensions/tableCellBorders.ts @@ -3,13 +3,9 @@ import type { CommandProps } from '@tiptap/core'; import type { EditorState } from '@tiptap/pm/state'; import { selectedRect, isInTable } from '@tiptap/pm/tables'; -// Word/LibreOffice table borders: per-side attrs (`borderTop/Right/Bottom/Left`) on -// table cells. `null` = the table default (0.5pt solid black), `'none'` = no border, -// else the canonical `'pt solid #RRGGBB'`. Round-trips to ODF fo:border-* and DOCX -// w:tcBorders (export/odt.ts exportTable, import/odt.ts convertTable, export/import -// docx.ts). setTableBorders applies a Word-style preset to the selected cell region, -// writing BOTH sides of every affected boundary (incl. the facing side of neighbour -// cells outside the region) so collapsed borders never disagree between two cells. +// Per-side border attrs (`borderTop/Right/Bottom/Left`) on table cells: `null` = the +// table default (0.5pt solid black), `'none'` = no border, else the canonical +// `'pt solid #RRGGBB'`. Round-trips to ODF fo:border-* and DOCX w:tcBorders. export const DEFAULT_BORDER_WIDTH_PT = 0.5; export const DEFAULT_BORDER_COLOR = '#000000'; @@ -68,10 +64,9 @@ const SIDE_META: Record = { borderLeft: { data: 'data-border-left', css: 'border-left' }, }; -// Walks every boundary a preset targets in the selected rect, reporting the two -// facing (cellPos, side) pairs of each; a pos is null at the table edge. Boundaries -// running inside a merged cell are skipped. Shared by the setTableBorders command -// and the active-preset reader below. +// Walks every boundary a preset targets in the selected rect, reporting both facing +// (cellPos, side) pairs — writing both is what keeps collapsed borders from disagreeing. +// A pos is null at the table edge; boundaries inside a merged cell are skipped. function forEachBoundary( rect: ReturnType, preset: BorderPreset, @@ -157,10 +152,9 @@ export const BORDER_PRESETS: BorderPreset[] = [ // (a border beats 'none', wider beats narrower; attr null = the table default). type EffectiveBorder = { widthPt: number; color: string } | 'none'; -// Word-like active states for the picker: a preset is active when every boundary it -// targets renders exactly the pen border (width + color) — so with the default thin -// pen a fresh table lights up every preset; picking a thicker pen turns them off. -// 'none' is active when the whole region renders borderless. null outside a table. +// Active states for the picker: a preset is active when every boundary it targets renders +// exactly the pen border (width + color), so the default thin pen lights up a fresh table +// and a thicker pen turns it off. 'none' = borderless region; null outside a table. export function activeBorderPresets( state: EditorState, spec: Exclude, diff --git a/src/lib/editor/extensions/tableColumnResize.ts b/src/lib/editor/extensions/tableColumnResize.ts index a053759..503cf2a 100644 --- a/src/lib/editor/extensions/tableColumnResize.ts +++ b/src/lib/editor/extensions/tableColumnResize.ts @@ -7,7 +7,7 @@ import { TableMap, cellAround, pointsAtCell } from '@tiptap/pm/tables'; import { columnWeightsFromRow, columnPercents } from './tableView'; import { PX_PER_CM } from '../../storage/pageMargins'; -// Word-style column resizing: an inner border trades width between its two columns +// Column resizing: an inner border trades width between its two columns // (table width fixed); an outer edge moves the table's `marginLeft`/`marginRight` (cm) // and the adjacent column. Live drag pokes the DOM; release writes the attrs. @@ -111,7 +111,7 @@ function handleMouseMove(view: EditorView, event: MouseEvent): void { } } -// Outer-edge drag, Word-style: only the adjacent column gives up width, so every +// Outer-edge drag: only the adjacent column gives up width, so every // other gridline stays put. All lengths in unscaled document px; the edge stops at // the text margin (margins never go negative) and at the column's minimum width. export function edgeResize( diff --git a/src/lib/editor/extensions/tableHeaderRow.ts b/src/lib/editor/extensions/tableHeaderRow.ts index 187a4ea..54f1bc2 100644 --- a/src/lib/editor/extensions/tableHeaderRow.ts +++ b/src/lib/editor/extensions/tableHeaderRow.ts @@ -5,12 +5,9 @@ import type { EditorState } from '@tiptap/pm/state'; import { selectedRect, isInTable } from '@tiptap/pm/tables'; import { parseTableLook, type TableRegion } from '../../styles/tableStyles'; -// Word/LibreOffice "header row"/"header column" as a styling preset: the first row (or -// column) gets bold text + a light grey fill, both of which round-trip to ODF/DOCX. The -// fill doubles as the marker, so no and no page repetition. -// A table carrying a *table style* owns these two areas through its Table Style Options -// instead, so there the toggle reads and writes that flag — one state, two surfaces -// (this button and the gallery's checkbox), never disagreeing. +// "Header row"/"header column" as a styling preset: the first row (or column) gets bold +// text + a light grey fill, both round-tripping to ODF/DOCX. The fill doubles as the +// marker, so no and no page repetition. export const HEADER_SHADE = '#F2F2F2'; diff --git a/src/lib/editor/extensions/tableRowResize.ts b/src/lib/editor/extensions/tableRowResize.ts index 975dd56..2f1df63 100644 --- a/src/lib/editor/extensions/tableRowResize.ts +++ b/src/lib/editor/extensions/tableRowResize.ts @@ -7,7 +7,7 @@ import type { EditorView } from '@tiptap/pm/view'; import { cellAround } from '@tiptap/pm/tables'; import { domCellAround, tableColumnResizeKey } from './tableColumnResize'; -// Word-style row-height drag: changes the row above the grid line, min-height +// Row-height drag: changes the row above the grid line, min-height // semantics (stored as `rowHeight` in tableRow.ts). Live preview is a meta-only // Decoration.node (no history); yields to column resize at a corner. diff --git a/src/lib/editor/extensions/tableSplit.ts b/src/lib/editor/extensions/tableSplit.ts index a152686..442806a 100644 --- a/src/lib/editor/extensions/tableSplit.ts +++ b/src/lib/editor/extensions/tableSplit.ts @@ -5,13 +5,9 @@ import type { Node as PMNode } from '@tiptap/pm/model'; import { TableMap, selectedRect, isInTable } from '@tiptap/pm/tables'; import { columnWeightsFromRow } from './tableView'; -// Word/LibreOffice "Split Cells…": divide the current cell (or a clean rectangular -// cell selection) into `cols` × `rows` sub-cells. prosemirror-tables only ships -// splitCell (un-merge), so this is custom. The table is a strict rectangular grid, -// so adding sub-columns/rows means inserting grid lines and letting every other -// cell that crosses a new line grow its span (bridge). -// Implemented by reading the grid via TableMap, expanding it, partitioning the -// cell's region, and rebuilding the table node in one step. +// "Split Cells…": divide the current cell (or a clean rectangular cell selection) into +// `cols` × `rows` sub-cells; prosemirror-tables only ships splitCell (un-merge). Reads +// the grid via TableMap, inserts grid lines (crossing cells bridge), rebuilds the table. declare module '@tiptap/core' { interface Commands { diff --git a/src/lib/editor/extensions/textBox.ts b/src/lib/editor/extensions/textBox.ts index 77926c3..26e9616 100644 --- a/src/lib/editor/extensions/textBox.ts +++ b/src/lib/editor/extensions/textBox.ts @@ -7,10 +7,9 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view'; import type { EditorView } from '@tiptap/pm/view'; import { HANDLES, MIN_SIZE_PX, clamp, parsePx, pageContentHeightPx, type WrapMode } from './image'; -// A text box / basic shape: a block-level frame with editable block content, a fill, -// a stroke, and the image's wrap model (inline = in-flow; left/right/topBottom float). -// Round-trips to ODF / or , and to -// DOCX DrawingML /. +// A text box / basic shape: a block-level frame with editable block content, a fill, a +// stroke, and the image's wrap model (inline = in-flow; left/right/topBottom float). +// Round-trips to ODF // and DOCX . export type ShapeKind = 'textbox' | 'roundRect' | 'ellipse'; @@ -214,10 +213,9 @@ export const TextBox = Node.create({ }, }); -// Node view: like ImageView, an axis-aligned wrapper reserves the rotated bounding -// box and a centered rotor carries fill/stroke/rotation plus the resize/rotate -// handles — but the rotor holds an editable contentDOM and auto-grows with its text, -// so a ResizeObserver re-fits the wrapper whenever the rotor's size changes. +// Node view: like ImageView, an axis-aligned wrapper reserves the rotated bounding box and +// a centered rotor carries fill/stroke/rotation plus the handles — but the rotor holds an +// editable contentDOM and auto-grows, so a ResizeObserver re-fits the wrapper. class TextBoxView { dom: HTMLElement; contentDOM: HTMLElement; diff --git a/src/lib/editor/shortcuts.ts b/src/lib/editor/shortcuts.ts index cad2bfc..4f0fa76 100644 --- a/src/lib/editor/shortcuts.ts +++ b/src/lib/editor/shortcuts.ts @@ -1,9 +1,8 @@ import { withShortcut } from '../i18n/shortcut'; -// Every shortcut this app binds itself, in ProseMirror keymap syntax ('Mod' = -// Ctrl/Cmd). Ids are stable so a later remapping UI only overrides this map. -// TipTap's own defaults (Mod-b/i/u, Mod-Shift-s/h, Mod-z, Mod-Shift-7/8, -// Mod-Shift-l/e/r/j, Tab in tables) are not listed — they are not ours to bind. +// Every shortcut this app binds itself, in ProseMirror keymap syntax ('Mod' = Ctrl/Cmd). +// Ids are stable so a later remapping UI only overrides this map. TipTap's own defaults +// (Mod-b/i/u, Mod-Shift-s/h, Mod-z, Mod-Shift-7/8, …) are not ours to bind and not listed. export const DEFAULT_SHORTCUTS = { // Character formatting — LibreOffice's sub/superscript keys; Word's Ctrl+= pair diff --git a/src/lib/export/docx.ts b/src/lib/export/docx.ts index c20365b..76e7810 100644 --- a/src/lib/export/docx.ts +++ b/src/lib/export/docx.ts @@ -119,9 +119,8 @@ function markerRunProps(node: TiptapNode): Writable } // ---- numbering registry ---------------------------------------------------- -// docx needs all numbering definitions up front (passed to the Document). We build -// the body paragraphs first, allocating one reference per top-level list and filling -// in each nesting level's format the first time we meet a list node at that depth. +// docx needs every numbering definition up front, so the body is built first: one +// reference per top-level list, each nesting level's format filled in when first met. class Numbering { readonly config: { reference: string; levels: ILevelsOptions[] }[] = []; private map = new Map(); diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index 029062b..ada10c6 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -98,9 +98,8 @@ const PBX = '\uE00C'; const TOC_SENT = ''; // Sentinel bracketing a hoisted text box's blocks (marker paragraphs TBX S{i} TBX … -// TBX E{i} TBX): replaceTextBoxes hoists the box's blocks to top level so every -// existing pass serializes them; applyTextBoxes wraps the region back into a -// / or . U+E008. +// TBX E{i} TBX): hoisted to top level, they ride every existing pass; applyTextBoxes +// wraps the region back into a / or custom shape. U+E008. const TBX = ''; // Sentinel bracketing a hoisted multi-column section's blocks, same mechanism as @@ -465,10 +464,9 @@ function textBoxDescriptor(node: TiptapNode): TextBoxExport { }; } -// Swap each top-level textBox for a pair of marker paragraphs bracketing its child -// blocks, hoisted to top level. The hoisted blocks then ride every existing export -// pass unchanged (custom attrs, list styles, inline sentinels, images); applyTextBoxes -// re-wraps the serialized region into the drawing element. Top-level only by schema. +// Swap each top-level textBox for a pair of marker paragraphs bracketing its child blocks, +// hoisted to top level so they ride every existing export pass unchanged (custom attrs, +// list styles, inline sentinels, images); applyTextBoxes re-wraps the serialized region. function replaceTextBoxes(doc: TiptapNode, boxes: TextBoxExport[]): TiptapNode { if (!doc.content?.length) return doc; const content: TiptapNode[] = []; @@ -810,10 +808,9 @@ function applyTableStyleNames(odtBytes: Uint8Array, tables: (TableStyleRef | nul `$1 style:parent-style-name="${odfStyleName(ref.name)}"`, ); }); - // Which conditional areas the table opts into: ODF's own table:use-*-styles pair with - // a table template, so the toggles survive a round trip through our own importer. - // Lookahead, not \b: a hyphen is a word boundary, so \b would also match - // ])[^>]*/g, (tag) => { const ref = tables[nth++]; return ref ? `${tag} ${odfLookAttrs(ref.look)}` : tag; @@ -2070,13 +2067,13 @@ function linkHrefOf(marks: TiptapNode['marks'] = []): string | undefined { return href ? String(href) : undefined; } -// Emit each text node as an odf-kit run; link-marked runs become via addLink. -// forceBold bakes bold onto every run regardless of marks — used for header-row cells, -// whose bold is presentational (CSS) in the editor and so isn't stored as a mark. // The sheet of the export in flight, so the run emitters can resolve character styles // without threading it through every cell/list helper (mirrors docLangTag in docx.ts). let exportSheet: StyleSheet = builtinStyleSheet(); +// Emit each text node as an odf-kit run; link-marked runs become via addLink. +// `force` bakes formatting onto every run regardless of marks — for header/region cells, +// whose formatting is presentational (CSS) in the editor and so isn't stored as marks. function applyRuns(p: ParagraphBuilder | CellBuilder, content: TiptapNode[] = [], force: TextProps = {}) { const forced = Object.keys(force).length ? textFormatting(force) : null; for (const node of content) { @@ -2349,12 +2346,9 @@ function exportTable(node: TiptapNode, doc: OdtDocument, contentWidthCm: number, t.addRow((r: RowBuilder) => { for (const cell of row.content ?? []) { if (cell.type !== 'tableCell' && cell.type !== 'tableHeader') continue; - // Emit the cell's runs (SEG-separated) and record its block descriptor. - // addCell runs synchronously, so the push is in document order — matching - // how applyCellBlocks later walks cells in content.xml. - // colSpan/rowSpan make odf-kit emit number-columns/rows-spanned and the - // placeholders; those never match the - // applyCellBlocks cell regex, so cellBlocks stays one-per-real-cell aligned. + // Emit the cell's runs (SEG-separated) and record its descriptor; addCell is + // synchronous, so the push stays in document order, matching applyCellBlocks' + // walk. Spans make odf-kit emit covered cells, which its cell regex never matches. const opts: CellOptions = { padding: CELL_PADDING }; const colspan = (cell.attrs?.colspan as number) ?? 1; const rowspan = (cell.attrs?.rowspan as number) ?? 1; @@ -2826,11 +2820,9 @@ export type HfExport = { // The full document → .odt pipeline, DOM-free; returns the .odt bytes. export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAULT_MARGINS, orientation: Orientation = 'portrait', hf?: HfExport, language?: { language: string; country: string } | null, pageFormat: PageFormat = 'A4', styles: StyleSheet = builtinStyleSheet()): Promise { - // Collect embedded images and swap them for IMG sentinels before serialization; - // applyImages resolves the sentinels and writes the Pictures/ + manifest entries. - // Text boxes and columns sections are hoisted after replacePageBreaks (so PGB never - // lands on their blocks) and before the inline passes (which then cover the hoisted - // blocks too). + // Images become IMG sentinels before serialization; applyImages resolves them and writes + // the Pictures/ + manifest entries. Text boxes and columns hoist after replacePageBreaks + // (so PGB misses their blocks) and before the inline passes (which then cover them). exportSheet = styles; const images: ImageExport[] = []; const tocs: TocExport[] = []; @@ -2859,7 +2851,7 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU evenHeaderPara = replaceHfImages(evenHeaderPara, hfImages); evenFooterPara = replaceHfImages(evenFooterPara, hfImages); // With the flag on, page 1 is independent: whenever a side has a zone on either - // variant, emit both (an empty one blanks its side, matching the editor and Word). + // variant, emit both — an empty one blanks its side, as the editor shows it. if (differentFirstPage) { const empty = (): TiptapNode => ({ type: 'paragraph', content: [] }); if (headerPara || firstHeaderPara) { headerPara ??= empty(); firstHeaderPara ??= empty(); } @@ -3067,7 +3059,7 @@ function hfParaProps(para: TiptapNode): string[] { // Header/footer post-processing on styles.xml: resolve LBR/PGC sentinels, apply the // paragraph alignment to the Header/Footer styles, and rewrite the geometry to the -// Word-style mapping (page margin = HF distance, min-height fills up to the body margin). +// ODF's own mapping (page margin = HF distance, min-height fills up to the body margin). function applyHfPostProcess(odtBytes: Uint8Array, margins: PageMargins, headerPara: TiptapNode | null, footerPara: TiptapNode | null, headerDist: number, footerDist: number, firstHeaderPara: TiptapNode | null = null, firstFooterPara: TiptapNode | null = null, pageCount = 1, hfImages: ImageExport[] = [], evenHeaderPara: TiptapNode | null = null, evenFooterPara: TiptapNode | null = null): Uint8Array { if (!headerPara && !footerPara && !firstHeaderPara && !firstFooterPara && !evenHeaderPara && !evenFooterPara) return odtBytes; diff --git a/src/lib/export/pdf.ts b/src/lib/export/pdf.ts index 476a731..fbece4c 100644 --- a/src/lib/export/pdf.ts +++ b/src/lib/export/pdf.ts @@ -203,9 +203,8 @@ function materializeListMarkers(root: HTMLElement): void { } // Off-screen raster of the live .paper at scale 1, captured to one tall canvas covering -// every page (page i occupies [i*cycle, i*cycle+pageH]). Shared by the PDF download and -// the raster print path; the caller invokes cleanup() once done reading `clone`. `scale` -// is the oversampling factor @ A4 (2 ≈ 192 dpi, 3 ≈ 288 dpi). +// every page (page i occupies [i*cycle, i*cycle+pageH]); the caller invokes cleanup() once +// done reading `clone`. `scale` = oversampling @ A4 (2 ≈ 192 dpi, 3 ≈ 288 dpi). export async function renderPaperToCanvas(opts: PdfOptions, scale = 2): Promise<{ canvas: HTMLCanvasElement; clone: HTMLElement; pages: number; pageW: number; pageH: number; cycle: number; scale: number; cleanup: () => void; @@ -355,11 +354,11 @@ export interface PrintPdfOptions { pageFormat?: PageFormat; headerDoc: HfDoc; footerDoc: HfDoc; - // First-page overrides (Word "Different First Page"); rendered via @page :first. + // First-page overrides, rendered via @page :first. headerFirstDoc?: HfDoc; footerFirstDoc?: HfDoc; differentFirstPage?: boolean; - // Even-page overrides (Word "Different Odd & Even Pages"); rendered via @page :left + // Even-page overrides, rendered via @page :left // (in an LTR document left pages are the even ones), which :first still overrides. headerEvenDoc?: HfDoc; footerEvenDoc?: HfDoc; diff --git a/src/lib/import/docx.ts b/src/lib/import/docx.ts index bfd96b3..09cf1ee 100644 --- a/src/lib/import/docx.ts +++ b/src/lib/import/docx.ts @@ -246,9 +246,8 @@ function documentLanguage(stylesDoc: Document | null, warnings: Set): Do } // ---- block conversion (paragraphs, lists, tables) -------------------------- -// A Word/LibreOffice TOC is a `TOC` field spanning several paragraphs (begin+instrText -// first, cached entries between, end last), each entry itself a nested PAGEREF field — so -// field depth is tracked (across one convertBlocks call) to match the TOC's own end. +// A TOC is a `TOC` field spanning several paragraphs, each entry a nested PAGEREF field, +// so field depth is tracked (across one convertBlocks call) to match the TOC's own end. type TocFieldState = { fieldDepth: number; tocDepth: number; instr: string[] }; function scanTocField(p: Element, st: TocFieldState): { emit: boolean } { @@ -880,11 +879,9 @@ function convertInline(p: Element, ctx: Ctx, baseRun: RunProps, defaults: BlockD // date/time field (replaced by its live node). const skipResult = () => fieldMode === 'result' && (hfFields || !!fieldDateTime); - // Route a drawing/pict result: an image is inline; a text box is a block node - // riding ctx.pendingBlocks. The one-paragraph header/footer schema holds inline - // (as-character) images but not boxes — those are dropped with a warning. A floating - // drawing (page background, watermark, fold marks) can't be placed in the one-paragraph - // zone and, sized to the page, would overlay the whole document, so it's dropped there. + // Route a drawing/pict result: an image is inline, a text box a block node riding + // ctx.pendingBlocks. The one-paragraph header/footer zone takes as-char images only — + // boxes and floating page-sized drawings (backgrounds, watermarks) are dropped there. const pushDrawn = (n: Node | null, floating: boolean) => { if (!n) return; if (hfFields) { diff --git a/src/lib/import/imageFormats.ts b/src/lib/import/imageFormats.ts index 0555d93..e47d09e 100644 --- a/src/lib/import/imageFormats.ts +++ b/src/lib/import/imageFormats.ts @@ -1,9 +1,6 @@ -// Image-format handling shared by the ODF and DOCX importers. An can only -// render a fixed set of formats; anything else (WMF/EMF/SVM/TIFF/…) is either decoded -// client-side (see convertUnsupportedImages) or skipped with a warning rather than -// emitted as a broken image. The format is resolved from the file extension first, then -// confirmed/recovered from the bytes' magic number so a mislabelled or extensionless -// entry still works. +// Image formats, shared by both importers. An renders only a fixed set, so the +// rest (WMF/EMF/SVM/TIFF/…) is decoded client-side (convertUnsupportedImages) or skipped +// with a warning. Resolved by extension, then by magic number so mislabels still work. import { unzipSync } from 'fflate'; const MIME_BY_EXT: Record = { diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 30ca297..db057ed 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -65,10 +65,9 @@ export interface OdtImportResult { // paragraph's indent lives in the list style rather than its own margin. type BlockKind = 'body' | 'list' | 'cell'; -// `files` is the full unzipped archive so image converters can read Pictures/ -// binaries; imageCache dedupes repeated hrefs into one data-URI. pendingBlocks is -// the side channel for text boxes/shapes found while converting inline content — -// they are block nodes, so convertBlocks flushes them after the anchor paragraph. +// `files` is the full unzipped archive so image converters can read Pictures/ binaries; +// imageCache dedupes repeated hrefs into one data-URI. pendingBlocks is the side channel +// for text boxes found in inline content — convertBlocks flushes them after the anchor. type Ctx = { resolver: StyleResolver; // ODF style name → display name, and the names blocks actually reference: the @@ -119,11 +118,9 @@ function frameRotationDeg(el: Element): number { return ((Math.round((-parseFloat(rot[1]) * 180) / Math.PI) % 360) + 360) % 360; } -// Set rotation + wrap attrs shared by images, text boxes and shapes. A non-as-char -// anchor or an explicit style:wrap makes the element floating (free x/y positions -// collapse to the nearest side — the editor's anchor-float model). An explicit -// as-char anchor is always inline: LibreOffice's named Graphics style carries a -// style:wrap that would otherwise float every inherited as-char frame. +// 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. function applyFrameRotationAndWrap(el: Element, attrs: Record, gp: PropMap): void { const deg = frameRotationDeg(el); if (deg) attrs.rotation = deg; @@ -164,11 +161,9 @@ function convertFrame(frame: Element, ctx: Ctx): Node | null { return { type: 'image', attrs }; } -// A shape's fill color. fo:background-color (LibreOffice's per-shape fill) wins over -// draw:fill when set. A drawn shape's fill defaults to solid when the keyword is -// absent — LibreOffice omits draw:fill and only writes "none" to turn it off, taking -// the color from the (usually inherited) draw:fill-color. defaultSolid is false for -// plain text frames, whose fill defaults to none. +// A shape's fill color; fo:background-color (LibreOffice's per-shape fill) beats draw:fill. +// A drawn shape's fill defaults to solid when the keyword is absent — LibreOffice only +// writes "none" to turn it off. defaultSolid is false for plain text frames (fill none). function shapeFill(gp: PropMap, defaultSolid: boolean): string | null { const bg = gp['fo:background-color']; if (bg !== undefined) return bg === 'transparent' || bg === 'none' ? null : normalizeColor(bg) ?? null; @@ -200,10 +195,9 @@ function shapeStroke(gp: PropMap, defaultSolid: boolean): { color: string | null return { color, widthPt: color ? lengthToPt(gp['svg:stroke-width']) : null }; } -// Fill/stroke attrs from a shape's graphic style, suppressing the editor defaults -// (white fill, 1pt black stroke). Absent/none → explicit null (transparent/no border), -// since omitting the attr would re-apply the default. defaultSolid marks drawn shapes, -// whose fill/stroke default to solid (unlike a plain text frame's). +// Fill/stroke attrs from a shape's graphic style, suppressing the editor defaults (white +// fill, 1pt black stroke). Absent/none → explicit null, since omitting the attr would +// re-apply the default. defaultSolid marks drawn shapes, whose fill/stroke default solid. function shapeStyleAttrs(gp: PropMap, attrs: Record, defaultSolid: boolean): void { const fillColor = shapeFill(gp, defaultSolid); if (fillColor !== '#FFFFFF') attrs.fillColor = fillColor; @@ -363,7 +357,7 @@ export function importOdt(bytes: Uint8Array, convertedImages: ConvertedImages = let blocks = convertBlocks(Array.from(body.children), ctx, 'body'); if (blocks.length === 0) blocks.push({ type: 'paragraph' }); - // Whole-document columns declared on the page layout (Word-style) instead of a + // Whole-document columns declared on the page layout instead of a // text:section: wrap the body's wrappable runs the way a section would be. const pageCols = resolver.pageColumns(); if (pageCols) { @@ -536,9 +530,8 @@ function convertBlocks(elements: Element[], ctx: Ctx, kind: BlockKind, boldByDef const out: Node[] = []; // Emit an anchor block, then any text boxes found inside it (block nodes riding - // ctx.pendingBlocks). In the body they follow the anchor at top level — and an - // empty anchor (our own export's wrapper paragraph) is dropped; elsewhere (cells, - // other boxes) their blocks are unwrapped in place. + // ctx.pendingBlocks). In the body they follow the anchor at top level, and an empty + // anchor (our export's wrapper) is dropped; in cells/boxes they unwrap in place. const pushWithPending = (anchor: Node | null) => { const pending = ctx.pendingBlocks.splice(0); const anchorIsEmpty = anchor?.type === 'paragraph' && !anchor.content?.length; @@ -1301,10 +1294,9 @@ function outlineHeadingEls(listEl: Element): Element[] | null { return out.length ? out : null; } -// `inheritedStyleName`: nested text:list elements usually carry no style-name of -// their own — the outermost list's style governs, with one level def per depth. -// `govMultilevel`: this list sits inside a multilevel (display-levels) chain, so an -// explicit numbering here must never be suppressed to null (null = rejoin the chain). +// `inheritedStyleName`: a nested text:list usually carries no style-name of its own — the +// outermost list's style governs, one level def per depth. `govMultilevel`: inside a +// display-levels chain, so an explicit numbering here is never suppressed (null = rejoin). function convertList(el: Element, ctx: Ctx, inheritedStyleName: string | null, depth: number, govMultilevel = false, baseCycle: OrderedCycle = ROOT_ORDERED_CYCLE): Node | null { const styleName = el.getAttributeNS(NS.text, 'style-name') ?? inheritedStyleName; const levelDef = listLevelDef(ctx.resolver.listStyle(styleName), depth); @@ -1430,13 +1422,9 @@ function listLevelDef(listStyle: Element | null, depth: number): Element | null // ---- tables ------------------------------------------------------------------------ -// ODF border ("0.5pt solid #000000" / "none" / absent) → border attr value: null for -// the editor default (0.5pt black, tolerance for producer unit rounding), 'none' for -// no border, else the canonical 'pt solid #RRGGBB'. Non-solid styles are coerced -// to solid (the editor only renders solid lines). -// treatDefaultAsNull: table cells collapse the 0.5pt-black default to null (= the table's -// own default border). Paragraph borders have no such default, so they pass false to keep -// a declared thin-black rule as an explicit canonical value. +// ODF border ("0.5pt solid #000000" / "none" / absent) → the canonical 'pt solid +// #RRGGBB', 'none', or null; non-solid styles coerce to solid. treatDefaultAsNull folds +// the 0.5pt-black default to null — cells inherit the table's, paragraph borders don't. export function borderAttrFromOdf(raw: string | null | undefined, treatDefaultAsNull = true): string | null { if (!raw || raw === 'none' || raw === 'hidden') return 'none'; let widthPt: number | null = null; diff --git a/src/lib/import/styleResolver.ts b/src/lib/import/styleResolver.ts index 1b5d90b..5c46e83 100644 --- a/src/lib/import/styleResolver.ts +++ b/src/lib/import/styleResolver.ts @@ -462,7 +462,7 @@ export class StyleResolver { // Header/footer of the master page: the content elements, the vertical space the // zone occupies below/above the page margin (height + body-side spacing, for the - // Word-style body-margin reconstruction), and whether per-page variants exist. + // body-margin reconstruction), and whether per-page variants exist. masterPageHF(): { header: Element | null; footer: Element | null; @@ -506,9 +506,8 @@ export class StyleResolver { ?? lengthToCm(props.getAttributeNS(NS.fo, 'min-height')) ?? 0; // style:dynamic-spacing="true" makes the header↔body gap collapsible (the header - // grows into it), so LibreOffice doesn't push the body down by the full spacing — - // reserving it as a fixed body margin is what shoves the text down. Only the fixed - // (non-dynamic) spacing is added to the body margin. + // grows into it), so the body isn't pushed down by it. Only fixed (non-dynamic) + // spacing is added to the body margin. const dynamic = props.getAttributeNS(NS.style, 'dynamic-spacing') === 'true'; const spacing = dynamic ? 0 : (lengthToCm(props.getAttributeNS(NS.fo, spacingAttr)) ?? 0); return height + spacing; diff --git a/src/lib/storage/headerFooter.ts b/src/lib/storage/headerFooter.ts index a70d900..4366227 100644 --- a/src/lib/storage/headerFooter.ts +++ b/src/lib/storage/headerFooter.ts @@ -1,8 +1,6 @@ -// Header/footer content: one single-paragraph TipTap doc per zone (hfExtensions -// schema). The 'default' variant repeats on every page (odd pages when odd/even is -// on); 'first' (Word "Different First Page" / ODF header-first) overrides page 1; -// 'even' (Word "Different Odd & Even Pages" / ODF header-left) overrides even pages. -// Precedence: first (page 1) > even (even pages) > default. null = empty zone. +// Header/footer content: one single-paragraph TipTap doc per zone (hfExtensions schema). +// 'default' repeats on every page (odd pages when odd/even is on), 'first' overrides +// page 1, 'even' overrides even pages — also their precedence. null = empty zone. export type HfZone = 'header' | 'footer'; export type HfVariant = 'default' | 'first' | 'even'; @@ -36,9 +34,9 @@ export function saveDifferentOddEven(on: boolean): void { else localStorage.removeItem(DIFFERENT_ODD_EVEN_KEY); } -// Word's default distance from the page edge to the header/footer text. The body -// margin stays the body margin (Word semantics); export/import convert to ODF's -// margin-to-header model (see export/odt.ts applyHfPostProcess). +// Default distance from the page edge to the header/footer text; the body margin stays +// the body margin. Export/import convert to ODF's margin-to-header model (see +// export/odt.ts applyHfPostProcess). export const HF_DISTANCE_CM = 1.25; // Per-zone distance from the page edge to the header (from top) / footer (from diff --git a/src/lib/styles/styleSheet.ts b/src/lib/styles/styleSheet.ts index 5543e11..9dece68 100644 --- a/src/lib/styles/styleSheet.ts +++ b/src/lib/styles/styleSheet.ts @@ -120,8 +120,7 @@ export function builtinStyleSheet(): StyleSheet { // Inheritance order: every style directly followed by its own children, so the manager's // indent matches the tree. Siblings sort built-ins first (as listed above), then by name. -// `Heading` is an abstract parent (LibreOffice's, holding what all levels share) — never -// assignable, so the gallery hides it (its children stay); the manager passes withAbstract. +// `Heading` is abstract (never assignable), so only withAbstract callers see it. export function styleOrder(sheet: StyleSheet, withAbstract = false, family: StyleFamily = 'paragraph'): Style[] { if (family === 'table') return []; // table styles have no inheritance — listed flat const styles = family === 'character' ? sheet.character : sheet.paragraph; diff --git a/tests/lo-roundtrip.test.ts b/tests/lo-roundtrip.test.ts index c148d24..14579f2 100644 --- a/tests/lo-roundtrip.test.ts +++ b/tests/lo-roundtrip.test.ts @@ -1,7 +1,6 @@ // LibreOffice round-trip leg: editor JSON -> buildOdt -> `soffice --convert-to odt` -// re-save -> importOdt -> compare. Requires LibreOffice (soffice) on PATH; the whole -// suite is skipped when it is absent (so plain `npm test` / CI stay green). -// jsdom (vitest `environment`) supplies the global DOMParser. +// re-save -> importOdt -> compare. Requires soffice on PATH; the whole suite self-skips +// when it is absent (so plain `npm test` / CI stay green). import { describe, it, expect } from 'vitest'; import { writeFileSync, readFileSync, mkdirSync } from 'node:fs'; import { execSync } from 'node:child_process'; diff --git a/tests/roundtrip.test.ts b/tests/roundtrip.test.ts index e22c559..eaf26fb 100644 --- a/tests/roundtrip.test.ts +++ b/tests/roundtrip.test.ts @@ -780,7 +780,7 @@ describe('Leg 2: foreign (LibreOffice/Word-style) .odt → importOdt', () => { check('shape fill/stroke: explicit none → null stroke', none.attrs.strokeColor === null, none.attrs); }); - it('wraps the body in columns when the page layout declares them (Word-style)', () => { + it('wraps the body in columns when the page layout declares them', () => { const stylesXml = ` @@ -828,7 +828,7 @@ describe('Leg 2: foreign (LibreOffice/Word-style) .odt → importOdt', () => { }); describe('Leg 3: header/footer → buildOdt → importOdt', () => { - it('round-trips header/footer content, fields, and the Word-style geometry mapping', async () => { + it('round-trips header/footer content, fields, and the geometry mapping', async () => { const header: N = { type: 'doc', content: [{ type: 'paragraph', attrs: { textAlign: 'right' }, content: [ { type: 'text', text: 'Bericht ', marks: [{ type: 'bold' }, { type: 'textStyle', attrs: { color: '#C00000' } }] }, { type: 'text', text: '2026' }, @@ -851,7 +851,7 @@ describe('Leg 3: header/footer → buildOdt → importOdt', () => { firstDiff(normalize(header), normalize(hfRes.header))); check('hf: footer round-trips', firstDiff(normalize(footer), normalize(hfRes.footer)) === null, firstDiff(normalize(footer), normalize(hfRes.footer))); - // The body margins survive the Word-style header/footer geometry mapping. + // The body margins survive the header/footer geometry mapping. const hm = hfRes.margins!; check('hf: body margins preserved through geometry mapping', !!hm && Math.abs(hm.top - 3) < 0.05 && Math.abs(hm.bottom - 2) < 0.05 && diff --git a/tests/unit/docx-header-floating-image.test.ts b/tests/unit/docx-header-floating-image.test.ts index 44f38ba..b8b1338 100644 --- a/tests/unit/docx-header-floating-image.test.ts +++ b/tests/unit/docx-header-floating-image.test.ts @@ -17,9 +17,8 @@ function docx(files: Record): Uint8Array { } // Header with a floating full-page VML background ("Falzmarken"-style, position:absolute, -// A4-sized) followed by a genuine inline (as-character) logo. Word templates place page -// backgrounds/watermarks in the header this way; our one-paragraph zone can't position -// them, and at full size they overlay the whole document — so they must be dropped. +// A4-sized) followed by a genuine inline (as-character) logo. Templates put backgrounds +// and watermarks there; the one-paragraph zone can't position them, so they're dropped. const HEADER_XML = `