diff --git a/static/css/app.css b/static/css/app.css index e6d8b7b..a2b225f 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1814,6 +1814,10 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) .footnote-popover.reader-theme-bright { background:#f0f0f0; color:#111; border-color:#bbb; } .footnote-popover-close { position:absolute; top:4px; right:6px; background:none; border:none; color:inherit; opacity:0.5; cursor:pointer; font-size:14px; padding:2px 4px; line-height:1; } +/* Page numbers of the printed edition (epub:type="pagebreak") */ +.reader-content .epub-pagenum { display:inline-block; font-size:0.62em; line-height:1.3; font-weight:400; font-style:normal; letter-spacing:0; text-transform:none; vertical-align:0.35em; padding:0 0.35em; margin-right:0.4em; border:1px solid currentColor; border-radius:3px; opacity:0.45; user-select:none; } +.reader-content .epub-pagenum:empty { display:none; } + /* Highlight marks */ .reader-no-bold * { font-weight: inherit !important; } .reader-content img { max-width: 100%; height: auto; display: block; } diff --git a/static/js/app.js b/static/js/app.js index d12f1a5..b5d9891 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1942,7 +1942,16 @@ function _looksLikeFootnoteLink(el, href) { return !!(el.closest('sup') || el.querySelector('sup')); } +// Chapters are XHTML, but parsed as HTML (forgiving with broken books). HTML +// knows no self-closing : the anchor stays open and swallows the +// text after it — and being a formatting element it is re-opened in every +// following paragraph (Suhrkamp/transpect books put one in each heading and +// note). Close the inline ones; block elements are left alone so the block +// indexes that highlights are anchored to stay the same. +const _SELF_CLOSING_INLINE = /<(a|span|b|i|em|strong|sup|sub|small|u|s|code|font|abbr|cite|q|mark|var)(\s(?:[^<>"']|"[^"]*"|'[^']*')*)?\/>/gi; + function sanitizeEpubHtml(html) { + html = html.replace(_SELF_CLOSING_INLINE, '<$1$2>$1>'); const doc = new DOMParser().parseFromString(html, 'text/html'); doc.querySelectorAll('script, iframe, object, embed, style, head, meta, link').forEach(el => el.remove()); @@ -1957,6 +1966,11 @@ function sanitizeEpubHtml(html) { // Remove any non-blob, non-data src that slipped through (broken relative paths) if (src && !src.startsWith('blob:') && !src.startsWith('data:')) el.removeAttribute('src'); } + // The printed edition's page numbers ("[12]") as small marks, not glued to the text. + const etype = el.getAttribute('epub:type') || ''; + if (/\bpagebreak\b/.test(etype) || el.getAttribute('role') === 'doc-pagebreak') { + el.classList.add('epub-pagenum'); + } if (el.tagName === 'A') { const href = el.getAttribute('href') || ''; if (href.startsWith('http://') || href.startsWith('https://')) { @@ -6021,11 +6035,31 @@ function showFootnotePopover(link) { dismissFootnotePopover(); const targetEl = document.getElementById(link.dataset.footnoteRef); if (!targetEl) return; - const container = targetEl.closest('.footnote') || targetEl.parentElement; + // The note's paragraph, also when the id sits on an inline element in it + // (
143 Note…
). + const inline = /^(A|SPAN|SUP|SUB|B|I|EM|STRONG|SMALL)$/.test(targetEl.tagName); + const container = targetEl.closest('.footnote') || + (inline && targetEl.closest('p, li, dd, blockquote')) || targetEl.parentElement; if (!container) return; const clone = container.cloneNode(true); clone.querySelector('sup')?.remove(); + clone.querySelectorAll('.epub-pagenum').forEach(e => e.remove()); + // The note's own number at its start, a link back to the text ("1 Note…"). + const first = Array.from(clone.querySelectorAll('a')).find(a => a.textContent.trim()); + const num = first ? first.textContent.trim() : ''; + if (/^[\[(]?\d{1,4}[\])]?\.?$/.test(num) && clone.textContent.trim().startsWith(num)) first.remove(); + // Only the number: the note (a poem, a long quote) runs on in the next + // paragraphs, up to the next note's anchor. + if (!clone.textContent.trim()) { + let len = 0; + for (let next = container.nextElementSibling; next && len < 6000; next = next.nextElementSibling) { + if (/^H[1-6]$/.test(next.tagName) || next.querySelector('[id]')) break; + if (!next.textContent.trim()) continue; + clone.appendChild(next.cloneNode(true)); + len += next.textContent.length; + } + } const popover = document.createElement('div'); popover.className = 'footnote-popover'; diff --git a/static/js/sw.js b/static/js/sw.js index dda6cf9..b6114eb 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-v95'; +const CACHE = 'diora-v96'; const SHELL = [ '/static/css/app.css', '/static/js/app.js',