Reader: Fußnoten und Seitenzahlen in Verlags-EPUBs; sw v96
- <a id="x"/> (XHTML) blieb beim Parsen als HTML offen und umschloss den folgenden Text, in jedem weiteren Absatz neu (Suhrkamp/transpect): die inline-Elemente werden vorher geschlossen. - Seitenzahlen der Druckausgabe (epub:type="pagebreak") als kleine graue Marke statt am Text klebend („3Anton Jäger“). - Fußnoten-Popover: Absatz der Notiz auch bei id auf einem inline-Element, ohne eigene Nummer und Seitenzahl, mehrabsätzige Noten (Gedichte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d659fc809
commit
3f2e309c9c
3 changed files with 40 additions and 2 deletions
|
|
@ -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.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; }
|
.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 */
|
/* Highlight marks */
|
||||||
.reader-no-bold * { font-weight: inherit !important; }
|
.reader-no-bold * { font-weight: inherit !important; }
|
||||||
.reader-content img { max-width: 100%; height: auto; display: block; }
|
.reader-content img { max-width: 100%; height: auto; display: block; }
|
||||||
|
|
|
||||||
|
|
@ -1942,7 +1942,16 @@ function _looksLikeFootnoteLink(el, href) {
|
||||||
return !!(el.closest('sup') || el.querySelector('sup'));
|
return !!(el.closest('sup') || el.querySelector('sup'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters are XHTML, but parsed as HTML (forgiving with broken books). HTML
|
||||||
|
// knows no self-closing <a id="x"/>: 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) {
|
function sanitizeEpubHtml(html) {
|
||||||
|
html = html.replace(_SELF_CLOSING_INLINE, '<$1$2></$1>');
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
|
||||||
doc.querySelectorAll('script, iframe, object, embed, style, head, meta, link').forEach(el => el.remove());
|
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)
|
// Remove any non-blob, non-data src that slipped through (broken relative paths)
|
||||||
if (src && !src.startsWith('blob:') && !src.startsWith('data:')) el.removeAttribute('src');
|
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') {
|
if (el.tagName === 'A') {
|
||||||
const href = el.getAttribute('href') || '';
|
const href = el.getAttribute('href') || '';
|
||||||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||||
|
|
@ -6021,11 +6035,31 @@ function showFootnotePopover(link) {
|
||||||
dismissFootnotePopover();
|
dismissFootnotePopover();
|
||||||
const targetEl = document.getElementById(link.dataset.footnoteRef);
|
const targetEl = document.getElementById(link.dataset.footnoteRef);
|
||||||
if (!targetEl) return;
|
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
|
||||||
|
// (<p><span><a id="F400" …>143</a></span> Note…</p>).
|
||||||
|
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;
|
if (!container) return;
|
||||||
|
|
||||||
const clone = container.cloneNode(true);
|
const clone = container.cloneNode(true);
|
||||||
clone.querySelector('sup')?.remove();
|
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');
|
const popover = document.createElement('div');
|
||||||
popover.className = 'footnote-popover';
|
popover.className = 'footnote-popover';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v95';
|
const CACHE = 'diora-v96';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
'/static/js/app.js',
|
'/static/js/app.js',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue