Reader: Fußnoten-Popover erkennt jetzt mehr EPUB-Konventionen (SW v31)
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 14s

Die Erkennung von Fußnoten-Links war auf class="footnote" + gleiche-Datei-
Anker (href startsWith '#') hartkodiert. Andere Editionen markieren Fußnoten
anders — z.B. die e-artnow-Ausgabe von Marx' "Das Kapital" (zeno.org-Konvertierung)
nutzt gar keine Klasse und verlinkt mit einem <sup> als Kind (statt Vorfahre)
des <a> auf eine ANDERE Kapiteldatei ("../Text/Karl0045.html#F537") — dadurch
griff weder die Klassen- noch die Hash-Prüfung, das Popover blieb tot.

_looksLikeFootnoteLink() erkennt jetzt zusätzlich epub:type="noteref" (EPUB3-
Standard), jede Klasse mit "note"/"footnote"/"fn" im Namen, und als Fallback
jeden Link mit Fragment-Anker, der irgendwo mit einem <sup> verschachtelt ist
(in beide Richtungen) — der in EPUBs praktisch universelle Marker für
Fußnotenziffern. Die Fragment-Extraktion nimmt jetzt alles nach dem letzten
'#' statt nur bei href.startsWith('#'), da das Buch als eine zusammenhängende
DOM gerendert wird und getElementById dateiübergreifend funktioniert.

Verifiziert per Playwright: Marx-Edition zeigt jetzt beim Hover 1326 erkannte
Fußnoten-Links mit korrektem Popover-Inhalt; Žižeks "Weniger als nichts"
(vorher funktionierend, class="footnote"-basiert) weiterhin unverändert
funktionsfähig — keine Regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-08-04 18:55:48 +02:00
parent 42bc090a28
commit 76068f6ed8
2 changed files with 23 additions and 3 deletions

View file

@ -2725,6 +2725,22 @@ function _injectImageBlobs(html, chapterDir, imageMap) {
return html;
}
// EPUBs mark footnote/endnote reference links very inconsistently across
// editions/tools — some use class="footnote" (same-file '#frag' href), some
// use the EPUB3 epub:type="noteref" attribute, and some (e.g. older
// Calibre/zeno.org conversions) use neither and just wrap a plain link to
// another chapter file's fragment in a <sup> — in either direction
// (<sup><a>…</a></sup> or <a><sup>…</sup></a>). Recognize all of these
// rather than only the single hardcoded class name.
function _looksLikeFootnoteLink(el, href) {
if (!href.includes('#')) return false;
const cls = (el.getAttribute('class') || '').toLowerCase();
if (/\bnote|\bfootnote|\bfn\b/.test(cls)) return true;
const epubType = (el.getAttribute('epub:type') || '').toLowerCase();
if (epubType.includes('noteref')) return true;
return !!(el.closest('sup') || el.querySelector('sup'));
}
function sanitizeEpubHtml(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
@ -2745,8 +2761,12 @@ function sanitizeEpubHtml(html) {
if (href.startsWith('http://') || href.startsWith('https://')) {
el.setAttribute('target', '_blank');
el.setAttribute('rel', 'noopener noreferrer');
} else if (el.classList.contains('footnote') && href.startsWith('#')) {
el.dataset.footnoteRef = href.slice(1);
} else if (_looksLikeFootnoteLink(el, href)) {
// The whole book is one concatenated DOM, so a fragment id resolves
// via getElementById regardless of which chapter file it originally
// pointed into — strip any "../Text/other.html" path prefix and keep
// just the id after the last '#'.
el.dataset.footnoteRef = href.slice(href.indexOf('#') + 1);
el.removeAttribute('href');
el.style.cursor = 'pointer';
} else {

View file

@ -2,7 +2,7 @@
* diora service worker caches the app shell for offline use.
*/
const CACHE = 'diora-v30';
const CACHE = 'diora-v31';
const PODCAST_CACHE = 'diora-podcast-v1';
const SHELL = [
'/static/css/app.css',