diff --git a/static/js/app.js b/static/js/app.js index 97c1309..1a1b1e1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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 — in either direction +// ( or ). 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 { diff --git a/static/js/sw.js b/static/js/sw.js index 4bcf391..41ab2c4 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -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',