diff --git a/src/lib/editor/ToolbarExpanded.svelte b/src/lib/editor/ToolbarExpanded.svelte index 5739629..5c345ff 100644 --- a/src/lib/editor/ToolbarExpanded.svelte +++ b/src/lib/editor/ToolbarExpanded.svelte @@ -685,9 +685,8 @@ } // --- Indent (Einzug) --- - // In a list, step the list point one level via indentListForward/Backward (the same - // commands the Tab keymap uses — see indent.ts); outside a list, step the paragraph's - // left indent. + // In a list, step the list point one level (indentListForward/Backward — the Tab + // keymap's commands, see indent.ts); outside a list, step the paragraph's left indent. function changeIndent(dir: 1 | -1) { if (!editor) return; const c = editor.chain().focus(); diff --git a/src/lib/editor/image.ts b/src/lib/editor/image.ts index 9055d38..4182068 100644 --- a/src/lib/editor/image.ts +++ b/src/lib/editor/image.ts @@ -4,10 +4,8 @@ import type { Node as PMNode } from '@tiptap/pm/model'; import { dropCursor } from '@tiptap/pm/dropcursor'; // Inline, as-character image (Word's "in line with text"). width/height are -// unscaled document px @96dpi (like tableRow.ts rowHeight); export converts them -// to cm and import back, so size round-trips exactly. Position is fully determined -// by the paragraph + text offset + the paragraph's alignment — no free coordinates. -// The node view adds corner resize handles (aspect-locked, zoom-aware). +// unscaled doc px @96dpi (like tableRow.ts rowHeight); export converts to cm and +// back, so size round-trips exactly. The node view adds aspect-locked resize handles. declare module '@tiptap/core' { interface Commands { diff --git a/src/lib/editor/pageBreaks.ts b/src/lib/editor/pageBreaks.ts index 7c7e2a9..5c6b6e9 100644 --- a/src/lib/editor/pageBreaks.ts +++ b/src/lib/editor/pageBreaks.ts @@ -402,10 +402,9 @@ export const PageBreaks = Extension.create({ } gaps.sort((a, b) => a - b); const advance = gaps.length ? gaps[gaps.length >> 1] : 0; - // Group rects into lines by top proximity, not vertical overlap: decorative - // fonts (e.g. Trattatello) render ink boxes taller than the line advance, so - // an overlap test chains whole paragraphs into one phantom line. Only the top - // is used downstream; same-line runs sit within a fraction of the advance. + // Group rects into lines by top proximity, not overlap: decorative fonts (e.g. + // Trattatello) make ink boxes taller than the advance, so an overlap test would + // chain paragraphs into one phantom line. Tolerance is a fraction of the advance. const tol = advance > 0 ? advance * 0.6 : 3; const lines: { top: number; bottom: number }[] = []; let lineTop = -Infinity; @@ -980,11 +979,9 @@ export const PageBreaks = Extension.create({ isUpdating = false; - // This pass measured against the spacers present at its start. When the result - // changes — e.g. after an orientation switch, where the prior orientation's - // spacers still sat in the DOM and suppressed a heading's top-margin collapse, - // shifting its measured top — re-run so the next pass measures with the new - // spacers and the layout settles. Bounded against a two-layout ping-pong. + // Each pass measures against the spacers present at its start, so a changed + // result needs another pass to re-measure and settle (e.g. after an orientation + // switch). Bounded by MAX_CONVERGE_PASSES against a two-layout ping-pong. if (placementsChanged && convergePasses < MAX_CONVERGE_PASSES) { convergePasses++; schedule(); diff --git a/src/lib/editor/pageField.ts b/src/lib/editor/pageField.ts index c0ebdc3..f7ba797 100644 --- a/src/lib/editor/pageField.ts +++ b/src/lib/editor/pageField.ts @@ -1,9 +1,8 @@ import { Node } from '@tiptap/core'; -// Inline atom fields for header/footer content: current page number and total -// page count. The DOM text is a placeholder — HeaderFooterLayer.svelte patches -// every [data-page-field] span with the real per-page value after rendering. -// Round-trips to ODF / (export/import odt.ts). +// Inline atom fields for header/footer: current page number and total page count. The +// DOM text is a placeholder — HeaderFooterLayer.svelte patches each [data-page-field] +// span per page. Round-trips to ODF /. function pageFieldNode(name: string, kind: 'number' | 'count') { return Node.create({ diff --git a/src/lib/editor/trailingNode.ts b/src/lib/editor/trailingNode.ts index a0b41dd..5f94388 100644 --- a/src/lib/editor/trailingNode.ts +++ b/src/lib/editor/trailingNode.ts @@ -1,12 +1,9 @@ import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from '@tiptap/pm/state'; -// Keep an empty paragraph at the end of the document when the last block is a -// table. A table is an isolating block, so if it sits at the very end there is -// nowhere to click or type below it — the cursor gets trapped inside the table -// and the user can't continue the document. When the doc ends with a table we -// append a paragraph; once it's there the last child is a paragraph, so the -// check no-ops on the next round (no loop). +// Keep an empty paragraph after a trailing table: a table is isolating, so at the very +// end the cursor gets trapped with nowhere to type below. Appending a paragraph fixes +// it and self-terminates (once the last child is a paragraph the check no-ops). const trailingNodeKey = new PluginKey('trailingNode'); // Block types that can't sit at the document end without trapping the cursor. diff --git a/src/lib/export/odt.ts b/src/lib/export/odt.ts index a60d764..8d67085 100644 --- a/src/lib/export/odt.ts +++ b/src/lib/export/odt.ts @@ -74,10 +74,9 @@ const EXT_BY_MIME: Record = { const CELL_LIST_BULLET_STYLE = 'TblListBullet'; const CELL_LIST_NUMBER_STYLE = 'TblListNumber'; -// Heading sizes/margins shown in the editor (editor.css); odf-kit's Heading_20_N -// defaults are larger, so we rewrite them on export. Margins are the editor's em -// values (top 1.5em, bottom 0.5em) resolved against each heading's font size, in cm. -// Exported: import/odt.ts uses these as the defaults to suppress on re-import. +// Heading sizes/margins shown in the editor (editor.css); odf-kit's larger Heading_20_N +// defaults are rewritten to these on export. Margins are the em values (top 1.5em, +// bottom 0.5em) in cm; import/odt.ts uses them as defaults to suppress on re-import. export const HEADING_STYLE_OVERRIDES: { name: string; fontSize: string; marginTop: string; marginBottom: string }[] = [ { name: 'Heading_20_1', fontSize: '20pt', marginTop: '1.058cm', marginBottom: '0.353cm' }, { name: 'Heading_20_2', fontSize: '16pt', marginTop: '0.847cm', marginBottom: '0.282cm' }, @@ -453,11 +452,9 @@ function applyOrderedListFormats(odtBytes: Uint8Array, formats: (OrderedFmt | nu return rezipOdt(files); } -// Whole-list indent: the `indent` attr on a top-level bulletList/orderedList (cm) -// shifts the entire list. odf-kit emits each level in label-alignment mode (the -// paragraph margin is ignored), so the shift must be added to the L# list-style's -// per-level fo:margin-left and text:list-tab-stop-position. One entry per top-level -// list in odf-kit's L# order (0 = no shift). +// Whole-list indent (cm) on a top-level bulletList/orderedList. odf-kit uses +// label-alignment mode (ignores the paragraph margin), so the shift goes onto the L# +// list-style's per-level fo:margin-left + text:list-tab-stop-position. One per list (0=none). function collectListIndents(node: TiptapNode, result: number[]): void { for (const child of node.content ?? []) { if (child.type === 'bulletList' || child.type === 'orderedList') { @@ -494,10 +491,9 @@ function applyListIndents(odtBytes: Uint8Array, indents: number[]): Uint8Array { return rezipOdt(files); } -// odf-kit emits nested lists as bare sharing the top-level L# style, -// so a nested list of a *different* kind/format (e.g. ordered inside bullets) -// loses its type. Give those nested lists their own minted 6-level list style — -// the same pattern applyCellBlocks uses for in-cell lists. +// odf-kit emits nested lists as bare sharing the top-level L# style, so a +// nested list of a different kind/format (e.g. ordered inside bullets) loses its type. +// Mint it its own 6-level list style (same pattern applyCellBlocks uses for cell lists). type ListDef = { ordered: boolean; numFormat: string; numSuffix: string }; function listDefOf(node: TiptapNode): ListDef { @@ -1033,10 +1029,9 @@ function imageFrameXml(img: ImageExport, index: number): string { ); } -// Resolve image sentinels: swap each IMG{i}IMG for its , add the -// binary picture files, and register each in META-INF/manifest.xml. content.xml -// already declares the draw/svg/xlink namespaces (odf-kit), and rezipOdt re-zips -// arbitrary binary entries, so no other plumbing is needed. +// Resolve image sentinels: swap each IMG{i}IMG for its , add the binary +// picture files, and register each in META-INF/manifest.xml. content.xml already +// declares the draw/svg/xlink namespaces and rezipOdt handles binary entries. function applyImages(odtBytes: Uint8Array, images: ImageExport[]): Uint8Array { if (!images.length) return odtBytes; const files = unzipSync(odtBytes); @@ -1106,11 +1101,9 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU // Orientation comes from the Layout panel; odf-kit swaps the A4 dimensions // automatically (29.7×21cm) and writes style:print-orientation accordingly. orientation, - // Margins (cm) come from the Layout panel via App state. They match the - // editor's on-screen padding so exported line wrapping / page flow is identical. - // With a header/footer, ODF's vertical margin is the page-edge→header distance; - // applyHfPostProcess sizes the header box so the body still starts at the - // editor's margin (Word-style mapping). + // Margins (cm) from the Layout panel, matching the editor's padding so line + // wrapping / page flow is identical. With a header/footer the vertical margin is + // the edge→zone distance; applyHfPostProcess keeps the body starting at the margin. marginTop: `${headerPara ? headerDist : margins.top}cm`, marginBottom: `${footerPara ? footerDist : margins.bottom}cm`, marginLeft: `${margins.left}cm`, @@ -1204,11 +1197,9 @@ function hfAlign(para: TiptapNode): AlignValue | null { return ta === 'center' || ta === 'right' || ta === 'justify' ? ta : null; } -// Header/footer post-processing on styles.xml: resolve the LBR/PGC sentinels in the -// emitted runs, rewrite odf-kit's fixed header/footer geometry (0.6/0.5cm) to the -// Word-style mapping (page margin = HF distance, min-height fills up to the body -// margin so the body starts exactly at the editor's margin), and apply the -// paragraph alignment to the named Header/Footer styles. +// 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). function applyHfPostProcess(odtBytes: Uint8Array, margins: PageMargins, headerPara: TiptapNode | null, footerPara: TiptapNode | null, headerDist: number, footerDist: number): Uint8Array { if (!headerPara && !footerPara) return odtBytes; diff --git a/src/lib/export/pdf.ts b/src/lib/export/pdf.ts index dd7c915..bcaece3 100644 --- a/src/lib/export/pdf.ts +++ b/src/lib/export/pdf.ts @@ -1,11 +1,6 @@ -// PDF export that matches the editor exactly AND keeps selectable/searchable text. -// -// Why this shape: letting the browser re-render at print time drifts the text (true -// vector metrics ≠ on-screen hinted metrics), which broke the page-break band masks. -// Instead we RASTER the editor's own on-screen render (where content, big text, tables, -// page-break bands and headers/footers are already correct) and overlay an INVISIBLE -// text layer positioned from the same on-screen measurements — so the PDF looks like a -// screenshot of the editor but its text can still be selected and searched. +// PDF export that matches the editor exactly AND keeps selectable text. The browser's +// print-time re-render drifts text (vector ≠ hinted metrics) and broke the band masks, +// so we raster the on-screen render and overlay an invisible, positioned text layer. import { generateHTML } from '@tiptap/core'; import { PAGE_W_PORTRAIT, PAGE_H_PORTRAIT, type Orientation } from '../storage/pageOrientation'; @@ -190,12 +185,9 @@ export async function exportPdf(opts: PdfOptions): Promise { } } -// --------------------------------------------------------------------------- -// Vector path: native browser print. Crisp, tiny, fonts embedded by the browser -// (incl. system fonts). The browser paginates natively (breaks tables cleanly), -// and headers/footers are emitted as CSS @page margin boxes (basic text + page -// numbers; rich inline formatting isn't expressible there). -// --------------------------------------------------------------------------- +// Vector path: native browser print. Crisp, tiny, fonts embedded by the browser. It +// paginates natively (clean table breaks); headers/footers become CSS @page margin +// boxes (basic text + page numbers; rich inline formatting isn't expressible there). export interface PrintPdfOptions { json: Json; diff --git a/src/lib/import/odt.ts b/src/lib/import/odt.ts index 9b4e1f7..d962e56 100644 --- a/src/lib/import/odt.ts +++ b/src/lib/import/odt.ts @@ -6,10 +6,9 @@ import { PX_PER_CM, cmToPx, type PageMargins } from '../storage/pageMargins'; import type { Orientation } from '../storage/pageOrientation'; import { languageFromOdf, NO_LANGUAGE, type DocumentLanguage } from '../storage/documentLanguage'; -// .odt → TipTap JSON, inverting export/odt.ts. Anything the editor can express -// becomes its native node/mark/attr; values matching the editor's defaults are -// suppressed so a round trip (editor → LibreOffice → editor) doesn't accrete -// explicit attrs. Unsupported content degrades gracefully and is reported. +// .odt → TipTap JSON, inverting export/odt.ts. Editor-expressible content becomes its +// native node/mark/attr; values matching the editor's defaults are suppressed so round +// trips don't accrete explicit attrs. Unsupported content degrades gracefully (reported). type Mark = { type: string; attrs?: Record }; type Node = { diff --git a/src/lib/import/styleResolver.ts b/src/lib/import/styleResolver.ts index 9910362..790ff80 100644 --- a/src/lib/import/styleResolver.ts +++ b/src/lib/import/styleResolver.ts @@ -1,10 +1,9 @@ import type { PageMargins } from '../storage/pageMargins'; import type { Orientation } from '../storage/pageOrientation'; -// Resolves ODF style indirection for the importer: every producer (our export, -// LibreOffice re-saves, Word) spreads formatting across named styles, automatic -// styles, and parent-style-name chains. This module flattens those chains into -// per-style property maps so import/odt.ts can read effective values directly. +// Resolves ODF style indirection for the importer: producers (our export, LibreOffice, +// Word) spread formatting across named/automatic styles and parent-style-name chains. +// This flattens those into per-style property maps so import/odt.ts reads effective values. export const NS = { office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0', @@ -304,10 +303,9 @@ export class StyleResolver { }; } - // Page margins + orientation from the master page's layout. With a header/footer, - // ODF's vertical margin ends at the zone — the editor's body margin is margin + - // zone height + spacing (inverse of the export's Word-style mapping). Page *size* - // is intentionally ignored — the editor is A4-only. + // Page margins + orientation from the master page's layout. With a header/footer the + // body margin is page margin + zone height + spacing (inverse of the export mapping). + // Page size is intentionally ignored — the editor is A4-only. pageGeometry(): { margins: PageMargins; orientation: Orientation } | null { const props = this.pageLayoutEl()?.getElementsByTagNameNS(NS.style, 'page-layout-properties')[0] ?? null; if (!props) return null;