diff --git a/src/App.svelte b/src/App.svelte
index 723d1e9..fcc96b0 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -942,7 +942,7 @@
orientation: pageOrientation,
pageFormat,
numPages,
- commentsHeading: t().comments.title,
+ commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
printMarkup: printMarkup(),
});
} catch (err) {
@@ -969,7 +969,7 @@
orientation: pageOrientation,
pageFormat,
numPages,
- commentsHeading: t().comments.title,
+ commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
printMarkup: printMarkup(),
});
} catch (err) {
@@ -1001,7 +1001,7 @@
headerEvenDoc,
footerEvenDoc,
differentOddEven,
- commentsHeading: t().comments.title,
+ commentLabels: { heading: t().comments.title, onPage: t().comments.onPage },
printMarkup: printMarkup(),
});
} catch (err) {
diff --git a/src/lib/export/CLAUDE.md b/src/lib/export/CLAUDE.md
index c41cee9..9014f76 100644
--- a/src/lib/export/CLAUDE.md
+++ b/src/lib/export/CLAUDE.md
@@ -80,6 +80,12 @@ before (no list, no CSS, no margin shift). The Review tab's **Print markup** tog
`generateHTML`, where no layout exists yet: `markReviewBlocks` marks whole top-level
blocks (CSS has no line box to hang a bar on, and only a top-level block's left edge is
the text column's), so a long paragraph with a small change is marked whole.
+- **An entry names its page**, which with the quoted text is what makes it findable in a
+ printout nobody can click — the format LibreOffice prints its own end-of-document
+ comments in. Only the raster paths can say it (`withAnchorPages`): they print the
+ editor's own pagination, while the vector path re-paginates in the browser and its entry
+ simply has no page. The page comes off one `cycle`, so mixed section paper is out — the
+ same limitation `Editor.svelte`'s own current-page readout has.
- **The list is a page, not a flow, in the raster paths.** The live `.paper` is an already
paginated layout nothing can flow into, so `renderCommentPages` lays the list out and
rasters it on its own; a page starts at an entry boundary, since a raster sliced mid-line
diff --git a/src/lib/export/pdf.ts b/src/lib/export/pdf.ts
index fd347ce..fe47675 100644
--- a/src/lib/export/pdf.ts
+++ b/src/lib/export/pdf.ts
@@ -12,7 +12,9 @@ import { columnPercents } from '../editor/extensions/tableView';
import { effectiveOrderedDef, formatOrdinal } from '../utils/orderedListTypes';
import { defaultBulletChar } from '../utils/bulletListTypes';
import { deriveFilename } from './odt';
-import { BAR_STRIP_CM, commentListHtml, markReviewBlocks, printedComments, reviewPrintCss, type PrintedComment } from './reviewPrint';
+import { BAR_STRIP_CM, commentListHtml, markReviewBlocks, printedComments, reviewPrintCss, type CommentLabels, type PrintedComment } from './reviewPrint';
+
+const DEFAULT_COMMENT_LABELS: CommentLabels = { heading: 'Comments', onPage: (n) => `Page ${n}` };
import globalCss from '../../styles/global.css?inline';
import editorCss from '../../styles/editor.css?inline';
@@ -29,8 +31,8 @@ export interface PdfOptions {
orientation?: Orientation;
pageFormat?: PageFormat;
numPages?: number;
- /** Heading of the printed comment list; no comments, no extra pages. */
- commentsHeading?: string;
+ /** Labels of the printed comment list; this module has no i18n of its own. */
+ commentLabels?: CommentLabels;
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
printMarkup?: boolean;
}
@@ -269,6 +271,19 @@ function cropPageDataUrl(
return tmp.toDataURL(mime, quality);
}
+// The page each comment sits on. Only the raster paths can say: they print the editor's
+// own pagination, while the vector path re-paginates in the browser.
+function withAnchorPages(list: PrintedComment[], source: HTMLElement, cycle: number): PrintedComment[] {
+ const base = source.getBoundingClientRect();
+ const scale = source.offsetWidth ? base.width / source.offsetWidth : 1;
+ return list.map((c) => {
+ const el = source.querySelector(`[data-comment="${CSS.escape(c.id)}"]`);
+ if (!el) return c;
+ const top = (el.getBoundingClientRect().top - base.top) / scale;
+ return { ...c, page: Math.max(1, Math.floor(top / cycle) + 1) };
+ });
+}
+
// The comment list as whole extra pages. The live .paper is an already paginated
// layout the list cannot flow into, so it is laid out and rastered on its own; a page
// starts at an entry boundary, because a raster sliced mid-line cuts the text in half.
@@ -285,7 +300,7 @@ async function renderCommentPages(
holder.setAttribute('data-pdf-export', '');
holder.style.cssText = `position:fixed; left:-100000px; top:0; width:${contentW}px; background:#fff; color:#000;`;
holder.innerHTML = ``
- + commentListHtml(list, opts.commentsHeading ?? 'Comments');
+ + commentListHtml(list, opts.commentLabels ?? DEFAULT_COMMENT_LABELS);
document.body.appendChild(holder);
try {
await document.fonts.ready;
@@ -327,7 +342,8 @@ export async function exportPdf(opts: PdfOptions): Promise {
const landscape = (opts.orientation ?? 'portrait') === 'landscape';
try {
const runs = collectRuns(clone);
- const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
+ const comments = opts.printMarkup === false
+ ? [] : withAnchorPages(printedComments(opts.source), opts.source, cycle);
const extra = comments.length
? await renderCommentPages(opts, comments, pageW, pageH, scale)
: { images: [], runs: [] };
@@ -383,7 +399,8 @@ export async function printRaster(opts: PdfOptions): Promise {
} finally {
cleanup();
}
- const comments = opts.printMarkup === false ? [] : printedComments(opts.source);
+ const comments = opts.printMarkup === false
+ ? [] : withAnchorPages(printedComments(opts.source), opts.source, cycle);
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, '');
@@ -446,8 +463,8 @@ export interface PrintPdfOptions {
headerEvenDoc?: HfDoc;
footerEvenDoc?: HfDoc;
differentOddEven?: boolean;
- /** Heading of the printed comment list; no comments, no list. */
- commentsHeading?: string;
+ /** Labels of the printed comment list; this module has no i18n of its own. */
+ commentLabels?: CommentLabels;
/** Whether the review markup prints at all (`storage/printMarkup`); default on. */
printMarkup?: boolean;
}
@@ -622,7 +639,7 @@ export function printPdf(opts: PrintPdfOptions): void {
headerEvenDoc: opts.headerEvenDoc ?? null,
footerEvenDoc: opts.footerEvenDoc ?? null,
differentOddEven: opts.differentOddEven ?? false,
- commentsHeading: opts.commentsHeading,
+ commentLabels: opts.commentLabels,
printMarkup: opts.printMarkup,
};
const title = (o.fileName ?? deriveFilename(o.json)).replace(/\.(odt|pdf)$/i, '');
@@ -633,7 +650,7 @@ export function printPdf(opts: PrintPdfOptions): void {
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 list = comments.length ? commentListHtml(comments, o.commentLabels ?? DEFAULT_COMMENT_LABELS) : '';
const iframe = document.createElement('iframe');
iframe.setAttribute('aria-hidden', 'true');
diff --git a/src/lib/export/reviewPrint.ts b/src/lib/export/reviewPrint.ts
index 08be410..b71370a 100644
--- a/src/lib/export/reviewPrint.ts
+++ b/src/lib/export/reviewPrint.ts
@@ -11,7 +11,14 @@ export const BAR_STRIP_CM = 0.32;
const COMMENT_COLOR = '#c88c00';
-export type PrintedComment = { id: string; n: number; author: string; date: string; text: string; quote: string };
+export type PrintedComment = {
+ id: string; n: number; author: string; date: string; text: string; quote: string;
+ /** The page the anchor sits on, where the caller knows it (the raster paths do). */
+ page?: number;
+};
+
+/** The labels the list is printed with; this module has no i18n of its own. */
+export type CommentLabels = { heading: string; onPage: (n: number) => string };
const OPEN_COMMENT = '[data-comment]:not([data-comment-resolved="true"])';
const MARKED = `${OPEN_COMMENT}, [data-insertion], [data-deletion]`;
@@ -81,12 +88,17 @@ function when(iso: string): string {
}
/** The comment bodies, after the document: what the margin bar cannot carry. */
-export function commentListHtml(list: PrintedComment[], heading: string): string {
- const rows = list.map((c) => ``
- + `${esc(c.author)}${c.date ? ` · ${esc(when(c.date))}` : ''}`
- + (c.quote.trim() ? `${esc(c.quote)}` : '')
- + `${esc(c.text)}`).join('');
- return ``;
+export function commentListHtml(list: PrintedComment[], labels: CommentLabels): string {
+ const rows = list.map((c) => {
+ // Page first, as LibreOffice prints its own end-of-document comments: with the
+ // quote it is what makes an entry findable in a printout nobody can click.
+ const meta = [c.page ? labels.onPage(c.page) : '', c.author, c.date ? when(c.date) : '']
+ .filter(Boolean).map(esc).join(' · ');
+ return `${meta}`
+ + (c.quote.trim() ? `${esc(c.quote)}` : '')
+ + `${esc(c.text)}`;
+ }).join('');
+ return ``;
}
/** The rules both print paths draw the bar and the list with. */
diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts
index 3020a60..4d5833a 100644
--- a/src/lib/i18n/locales/de.ts
+++ b/src/lib/i18n/locales/de.ts
@@ -81,6 +81,7 @@ const de: Messages = {
},
comments: {
title: 'Kommentare',
+ onPage: (n: number) => `Seite ${n}`,
empty: 'Noch keine Kommentare. Text markieren und einen hinzufügen.',
edit: 'Bearbeiten',
resolve: 'Erledigt',
diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts
index 6388636..689edb6 100644
--- a/src/lib/i18n/locales/en.ts
+++ b/src/lib/i18n/locales/en.ts
@@ -84,6 +84,7 @@ const en = {
},
comments: {
title: 'Comments',
+ onPage: (n: number) => `Page ${n}`,
empty: 'No comments yet. Select some text and add one.',
edit: 'Edit',
resolve: 'Resolve',
${esc(heading)}
${rows}