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 = `