From c26aac1beef4649d3ceaecd5c65432ac35ad12a0 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Mon, 10 Aug 2026 09:29:23 +0000 Subject: [PATCH] fix: the index's leader fills its own gap, at LibreOffice's indents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/editor/extensions/tableOfContents.ts | 20 +++++++++++++++++++- src/styles/editor.css | 9 +++++---- tests/render-parity/run.mjs | 5 ++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/lib/editor/extensions/tableOfContents.ts b/src/lib/editor/extensions/tableOfContents.ts index 1d7b047..879e9eb 100644 --- a/src/lib/editor/extensions/tableOfContents.ts +++ b/src/lib/editor/extensions/tableOfContents.ts @@ -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 { @@ -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('.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. diff --git a/src/styles/editor.css b/src/styles/editor.css index 738b9a0..bf967f7 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -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; diff --git a/tests/render-parity/run.mjs b/tests/render-parity/run.mjs index 107529b..5738f09 100644 --- a/tests/render-parity/run.mjs +++ b/tests/render-parity/run.mjs @@ -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;