docs: clean up comments

This commit is contained in:
Steffen Becker committed 2026-06-21 12:29:25 +02:00
1 parent 64ae976ecf
commit 20eb94d7c0
9 files changed
+49 -79

No files matched your search

+2 -3
View File
@@ -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();
+2 -4
View File
@@ -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<ReturnType> {
+6 -9
View File
@@ -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();
+3 -4
View File
@@ -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 <text:page-number>/<text:page-count> (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 <text:page-number>/<text:page-count>.
function pageFieldNode(name: string, kind: 'number' | 'count') {
return Node.create({
+3 -6
View File
@@ -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.
+18 -27
View File
@@ -74,10 +74,9 @@ const EXT_BY_MIME: Record<string, string> = {
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 <text:list> 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 <text:list> 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 <draw:frame>, 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 <draw:frame>, 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;
+6 -14
View File
@@ -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<void> {
}
}
// ---------------------------------------------------------------------------
// 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;
+3 -4
View File
@@ -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<string, unknown> };
type Node = {
+6 -8
View File
@@ -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;