fix: the index's leader fills its own gap, at LibreOffice's indents

Every row carried 200 dots — clipped on screen, but the clipboard, the
PDF and any measurement got all of them. Cut each to what its gap holds,
and step the levels by 0.5cm, which is what LibreOffice's Contents styles
use. The parity runner now waits for the page numbers to settle too.
This commit is contained in:
Steffen Becker committed 2026-08-10 09:32:49 +00:00
1 parent 8690129c83
commit c26aac1bee
3 files changed
+28 -6

No files matched your search

+19 -1
View File
@@ -14,8 +14,10 @@ export type TocEntry = { text: string; level: number; page: number };
// ("Inhalt", "Sommaire", …), or none at all where the file put its heading in a
// separate paragraph (`''`); a freshly inserted one takes this.
const TITLE = 'Table of Contents';
// Enough leader dots to cross the widest gap a page can offer; the row clips the rest.
// Enough leader dots to cross the widest gap a page can offer; fillLeaders cuts each
// row's back to what its own gap holds, measuring one dot with this sample.
const LEADER_DOTS = 200;
const LEADER_PROBE = '..........';
declare module '@tiptap/core' {
interface Commands<ReturnType> {
@@ -235,6 +237,22 @@ class TocView {
}
this.dom.appendChild(row);
});
this.fillLeaders();
}
// As many leader dots as the gap holds. The row clips the rest on screen, but nothing
// else does: 200 of them reach the PDF, the clipboard and every measurement as text.
private fillLeaders(): void {
const leaders = Array.from(this.dom.querySelectorAll<HTMLElement>('.toc-leader'));
if (!leaders.length) return;
leaders[0].textContent = LEADER_PROBE;
const range = document.createRange();
range.selectNodeContents(leaders[0]);
const dot = range.getBoundingClientRect().width / LEADER_PROBE.length;
if (!(dot > 0)) return;
for (const el of leaders) {
el.textContent = '.'.repeat(Math.max(0, Math.floor(el.getBoundingClientRect().width / dot)));
}
}
// Scroll the heading into view and drop the cursor into it.
+5 -4
View File
@@ -219,10 +219,11 @@
.paper .tiptap .toc-entry:hover .toc-text {
text-decoration: underline;
}
.paper .tiptap .toc-level-2 { padding-left: 0.6cm; }
.paper .tiptap .toc-level-3 { padding-left: 1.2cm; }
.paper .tiptap .toc-level-4 { padding-left: 1.8cm; }
.paper .tiptap .toc-level-5 { padding-left: 2.4cm; }
/* One level = 0.5cm, the indent LibreOffice's own Contents 1…5 styles carry. */
.paper .tiptap .toc-level-2 { padding-left: 0.5cm; }
.paper .tiptap .toc-level-3 { padding-left: 1cm; }
.paper .tiptap .toc-level-4 { padding-left: 1.5cm; }
.paper .tiptap .toc-level-5 { padding-left: 2cm; }
.paper .tiptap .toc-text {
white-space: nowrap;
overflow: hidden;
+4 -1
View File
@@ -98,8 +98,11 @@ async function settle(page) {
// The spacers' heights too, not just their count: they go on settling for a while
// after the last one is placed, and the page a line lands on moves with them.
const spacers = Array.from(document.querySelectorAll('[data-page-break-spacer]'));
// The index's page numbers settle after the spacers do — a stale one is a line of
// text that still changes.
const toc = Array.from(document.querySelectorAll('.toc-page')).map((t) => t.textContent).join(',');
const key = el.style.minHeight + '|' + el.children.length + '|' + spacers.length + '|'
+ spacers.reduce((sum, s) => sum + s.offsetHeight, 0);
+ spacers.reduce((sum, s) => sum + s.offsetHeight, 0) + '|' + toc;
const w = window;
if (w.__parityKey !== key) { w.__parityKey = key; w.__paritySince = performance.now(); return false; }
return performance.now() - (w.__paritySince ?? 0) > 3000;