feat: a switch decides whether a printout carries the review markup

The Review tab's "Print markup", on by default and stored only when off
(edentext-print-markup). Off, the raster paths drop the bar layer from the
clone and skip the comment pages, the vector path emits neither the markup CSS
nor the list.

printPdf rebuilds its options field by field, so it never saw commentsHeading
either — the printed list was headed in English whatever the UI language.
This commit is contained in:
Steffen Becker committed 2026-09-04 19:30:02 +02:00
1 parent 2aecb1a1b3
commit 29433e1b1d
8 files changed
+54 -8

No files matched your search

+4
View File
@@ -45,6 +45,7 @@
import { loadPageDecor, savePageDecor, EMPTY_PAGE_DECOR, type PageDecor } from './lib/storage/pageDecor';
import { loadLineNumbering, saveLineNumbering, DEFAULT_LINE_NUMBERING, type LineNumbering } from './lib/storage/lineNumbering';
import { loadFoldMarks, saveFoldMarks } from './lib/storage/foldMarks';
import { printMarkup } from './lib/storage/printMarkup.svelte';
import { loadDocumentLanguage, saveDocumentLanguage, odfFromLanguage, type DocumentLanguage } from './lib/storage/documentLanguage';
import { setTableLanguage } from './lib/storage/tableOptions.svelte';
import { spellController } from './lib/spell/controller';
@@ -942,6 +943,7 @@
pageFormat,
numPages,
commentsHeading: t().comments.title,
printMarkup: printMarkup(),
});
} catch (err) {
console.error('[pdf] Export failed:', err);
@@ -968,6 +970,7 @@
pageFormat,
numPages,
commentsHeading: t().comments.title,
printMarkup: printMarkup(),
});
} catch (err) {
console.error('[pdf] Print failed:', err);
@@ -999,6 +1002,7 @@
footerEvenDoc,
differentOddEven,
commentsHeading: t().comments.title,
printMarkup: printMarkup(),
});
} catch (err) {
console.error('[pdf] Print failed:', err);
@@ -7,6 +7,7 @@
import { countText, type TextStats } from '../../../utils/wordCount';
import type { DocumentLanguage } from '../../../storage/documentLanguage';
import { recordChanges, setRecordChanges } from '../../../storage/trackChanges.svelte';
import { printMarkup, setPrintMarkup } from '../../../storage/printMarkup.svelte';
import { revisions } from '../../../editor/extensions/trackChanges';
import { OPEN_THESAURUS_EVENT } from '../../../spell/thesaurus';
import { shortcutHint } from '../../../editor/shortcuts';
@@ -165,6 +166,14 @@
onclick={() => editor?.chain().focus().rejectRevisions(true).run()}
/>
</div>
<RibbonButton
variant="small"
icon="print"
label={t().revisions.printMarkup}
title={t().revisions.printMarkupHint}
active={printMarkup()}
onclick={() => setPrintMarkup(!printMarkup())}
/>
</RibbonGroup>
<div class="ribbon-sep"></div>
+3 -1
View File
@@ -66,7 +66,9 @@ A printed document says what the screen says: a bar in the margin beside every c
commented block, and the comment bodies as a list after the document. The list is not a
nicety — a comment's text is nowhere on the page, so a bar alone could only say that one
exists. A resolved comment prints nowhere, and a document with no markup prints exactly as
before (no list, no CSS, no margin shift). There is no on/off switch; both products have one.
before (no list, no CSS, no margin shift). The Review tab's **Print markup** toggle
(`storage/printMarkup.svelte.ts`) turns the whole thing off, as both products offer;
`printPdf` rebuilds its options field by field, so a new one has to be copied there too.
- **The margin strip is paid for.** The print engine clips to the page area, so a bar at a
negative offset is silently dropped — measured, not assumed. `printPdf` takes
+15 -7
View File
@@ -31,6 +31,8 @@ export interface PdfOptions {
numPages?: number;
/** Heading of the printed comment list; no comments, no extra pages. */
commentsHeading?: string;
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
printMarkup?: boolean;
}
type Run = { str: string; x: number; y: number; h: number }; // doc px, relative to .paper top-left
@@ -42,13 +44,14 @@ function pageDims(format: PageFormat, orientation: Orientation): { pageW: number
// Off-screen, scale-1, theme-neutral copy of the live .paper. Rendered identically to
// the editor (no print re-layout), so the raster + measured text positions agree.
function buildClone(paper: HTMLElement, pageW: number): { holder: HTMLElement; clone: HTMLElement; style: HTMLStyleElement } {
function buildClone(paper: HTMLElement, pageW: number, markup: boolean): { holder: HTMLElement; clone: HTMLElement; style: HTMLStyleElement } {
const clone = paper.cloneNode(true) as HTMLElement;
clone.style.transform = 'none';
clone.style.width = `${pageW}px`; // pin width so capture geometry can't drift on var inheritance
clone.classList.remove('show-formatting-marks', 'hf-editing');
// The margin bar prints at one weight: which one the caret is on is an editing state.
clone.querySelectorAll('.change-bar.active').forEach((el) => el.classList.remove('active'));
if (markup) clone.querySelectorAll('.change-bar.active').forEach((el) => el.classList.remove('active'));
else clone.querySelectorAll('.change-bar-layer').forEach((el) => el.remove());
// html2canvas paints an inline background from one bounding rect, so a wrapped
// highlight covers the bold term preceding it on the first line. Lift bold-and-
@@ -223,7 +226,7 @@ export async function renderPaperToCanvas(opts: PdfOptions, scale = 2): Promise<
const { pageW, pageH } = pageDims(opts.pageFormat ?? 'A4', orientation);
const cycle = pageH + PAGE_GAP;
const { holder, clone, style } = buildClone(paper, pageW);
const { holder, clone, style } = buildClone(paper, pageW, opts.printMarkup !== false);
document.head.appendChild(style);
document.body.appendChild(holder);
const cleanup = () => { holder.remove(); style.remove(); };
@@ -324,7 +327,7 @@ export async function exportPdf(opts: PdfOptions): Promise<void> {
const landscape = (opts.orientation ?? 'portrait') === 'landscape';
try {
const runs = collectRuns(clone);
const comments = printedComments(opts.source);
const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
const extra = comments.length
? await renderCommentPages(opts, comments, pageW, pageH, scale)
: { images: [], runs: [] };
@@ -380,7 +383,7 @@ export async function printRaster(opts: PdfOptions): Promise<void> {
} finally {
cleanup();
}
const comments = printedComments(opts.source);
const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
if (comments.length) imgs = imgs.concat((await renderCommentPages(opts, comments, pageW, pageH, scale)).images);
const title = (opts.fileName ?? deriveFilename(opts.json)).replace(/\.(odt|pdf)$/i, '');
@@ -445,6 +448,8 @@ export interface PrintPdfOptions {
differentOddEven?: boolean;
/** Heading of the printed comment list; no comments, no list. */
commentsHeading?: string;
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
printMarkup?: boolean;
}
// First-row column weights from table JSON (honours colspan); mirrors tableView.
@@ -617,14 +622,17 @@ export function printPdf(opts: PrintPdfOptions): void {
headerEvenDoc: opts.headerEvenDoc ?? null,
footerEvenDoc: opts.footerEvenDoc ?? null,
differentOddEven: opts.differentOddEven ?? false,
commentsHeading: opts.commentsHeading,
printMarkup: opts.printMarkup,
};
const title = (o.fileName ?? deriveFilename(o.json)).replace(/\.(odt|pdf)$/i, '');
// The layers are Svelte DOM and generateHTML knows nothing of them, so this path
// rebuilds the review markup from the marks the document itself carries.
const host = document.createElement('div');
host.innerHTML = buildBodyHtml(o.json);
const comments = printedComments(host);
const review = markReviewBlocks(host);
const markup = o.printMarkup !== false;
const comments = markup ? printedComments(host) : [];
const review = markup && markReviewBlocks(host);
const list = comments.length ? commentListHtml(comments, o.commentsHeading ?? 'Comments') : '';
const iframe = document.createElement('iframe');
+2
View File
@@ -827,6 +827,8 @@ const de: Messages = {
},
revisions: {
record: 'Änderungen verfolgen',
printMarkup: 'Markup drucken',
printMarkupHint: 'Randbalken und Kommentarliste mitdrucken',
recordHint: 'Jede Bearbeitung als Änderung aufzeichnen',
accept: 'Annehmen',
acceptHint: 'Die Änderung an der Einfügemarke annehmen',
+2
View File
@@ -833,6 +833,8 @@ const en = {
},
revisions: {
record: 'Track changes',
printMarkup: 'Print markup',
printMarkupHint: 'Print the margin bar and the comment list',
recordHint: 'Record every edit as a revision',
accept: 'Accept',
acceptHint: 'Accept the change at the cursor',
+1
View File
@@ -19,6 +19,7 @@
- **`pageDecor.ts`** — the page's own decoration (`edentext-page-decor`, the key removed when all three are off): background colour, a border around the text area at `paddingCm` from it, and a text watermark (LibreOffice's Format ▸ Watermark, field for field). All three ride the ODF page layout — the watermark as a `fontwork-plain-text` `<draw:custom-shape>` named `PowerPlusWaterMarkObject` in the **master page's header**, the only place LibreOffice reads one from (probed: hung off the master page directly it is dropped, and an undeclared `xmlns:draw` in styles.xml drops it too). Word keeps the background on `w:document` (plus `w:displayBackgroundShape`), the border in `w:sectPr` **after `w:pgMar`** — the child order is fixed, and ahead of the header references Word's reader loses those — and the watermark as the VML `_x0000_t136` shape, whose definition has to travel or LibreOffice cannot resolve it. `PageDecorLayer.svelte` draws border and watermark per page; the background is a `--color-page-bg` override on `.paper`. ODF lays border + `fo:padding` out *inside* the page margin, shrinking the text area — the editor and Word (`offsetFrom="text"`) keep text at the margin — so the ODT export shrinks every `fo:margin-*` by border + padding and the import grows them back (`decorInsetCm`).
- **`trackChanges.svelte.ts`** — whether the **document** records revisions (`edentext-record-changes`, the key removed when off, as it is by default in both word processors). It travels with the file — ODF `text:track-changes` on the change registry, Word `w:trackRevisions` in settings.xml (both probed against soffice) — so an import adopts the file's flag and a new document starts off; only the open document's state lives under the key. A reactive singleton because the extension reads it on every transaction while the Review tab flips it; the recorded changes themselves live in the document, on the marks.
- **`lineNumbering.ts`** — numbers in the left margin (`edentext-line-numbering`, the key removed when off, as it is by default in both word processors). ODF keeps one `<text:linenumbering-configuration>` in `office:styles`, Word a `<w:lnNumType>` per `w:sectPr` (probed). `LineNumberLayer.svelte` measures the lines: CSS exposes no line boxes, so a Range per block is taken and its distinct client-rect tops are the lines.
- **`printMarkup.svelte.ts`** — whether a printout carries the review markup (`edentext-print-markup`, the key removed at its **on** default, so it is the off state that is stored — the switch both word processors offer). A reactive singleton like `trackChanges.svelte.ts`: the Review tab flips it, all three export paths read it. What it governs lives in `export/reviewPrint.ts`.
- **`foldMarks.ts`** — fold + punch marks in the left margin (`edentext-fold-marks`, the key removed when off; neither word processor prints any by default). The DIN 5008 positions (87/192mm folds, 148.5mm punch) are this module's constants; `FoldMarkLayer.svelte` draws them per page — the folds on **both** edges (both hands find the crease), the punch mark only left, where the pages are filed. They export as named lines (`EdenFoldMark*`) in the master page's header — the one place that repeats on every page, where LibreOffice's own DIN templates keep them — ODF as page-relative `<draw:line>`s, DOCX as VML `<v:line>`s; both importers read the names back into the flag and never into the zone's content.
- **`pageOrientation.ts`** — `'portrait' | 'landscape'`. `applyOrientationVars` sets `--user-page-{width,height}`; landscape swaps the A4 dimensions (matching odf-kit's automatic swap).
+18
View File
@@ -0,0 +1,18 @@
// Whether a printout carries the review markup — the margin bar beside a changed or
// commented block, and the comment list after the document (`export/reviewPrint.ts`).
// A reactive singleton like trackChanges.svelte.ts: the Review tab flips it, the three
// export paths read it. On by default; both word processors offer the same switch.
const KEY = 'edentext-print-markup';
let current = $state(localStorage.getItem(KEY) !== 'false');
export function printMarkup(): boolean {
return current;
}
export function setPrintMarkup(on: boolean): void {
current = on;
if (on) localStorage.removeItem(KEY);
else localStorage.setItem(KEY, 'false');
}