Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// EPUB parser (requires JSZip)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function resolveEpubPath(base, relative) {
|
|
|
|
|
if (!relative) return '';
|
|
|
|
|
if (relative.startsWith('/')) return relative.slice(1);
|
|
|
|
|
const hashIdx = relative.indexOf('#');
|
|
|
|
|
const frag = hashIdx >= 0 ? relative.slice(hashIdx) : '';
|
|
|
|
|
const rel = hashIdx >= 0 ? relative.slice(0, hashIdx) : relative;
|
|
|
|
|
const parts = (base + rel).split('/');
|
|
|
|
|
const resolved = [];
|
|
|
|
|
for (const p of parts) {
|
|
|
|
|
if (p === '..') resolved.pop();
|
|
|
|
|
else if (p !== '.') resolved.push(p);
|
|
|
|
|
}
|
|
|
|
|
return resolved.join('/') + frag;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function parseEpub(arrayBuffer) {
|
|
|
|
|
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
|
|
|
|
|
|
|
|
// 1. Find OPF via container.xml
|
|
|
|
|
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
|
|
|
|
const containerDoc = new DOMParser().parseFromString(containerXml, 'application/xml');
|
|
|
|
|
const rootfileEl = containerDoc.querySelector('rootfile');
|
|
|
|
|
if (!rootfileEl) throw new Error('Ungültige EPUB-Datei (container.xml ohne rootfile)');
|
|
|
|
|
const opfPath = rootfileEl.getAttribute('full-path');
|
|
|
|
|
const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/') + 1) : '';
|
|
|
|
|
|
|
|
|
|
// 2. Parse OPF
|
|
|
|
|
const opfText = await zip.file(opfPath).async('text');
|
|
|
|
|
const opfDoc = new DOMParser().parseFromString(opfText, 'application/xml');
|
|
|
|
|
|
|
|
|
|
const title = opfDoc.querySelector('metadata > title, metadata > *|title')?.textContent?.trim() || 'Unbekannter Titel';
|
|
|
|
|
const author = opfDoc.querySelector('metadata > creator, metadata > *|creator')?.textContent?.trim() || 'Unbekannter Autor';
|
|
|
|
|
|
|
|
|
|
// 3. Build manifest: id → {href, mediaType, properties}
|
|
|
|
|
const manifest = {};
|
|
|
|
|
opfDoc.querySelectorAll('manifest > item').forEach(item => {
|
|
|
|
|
manifest[item.getAttribute('id')] = {
|
|
|
|
|
href: opfDir + item.getAttribute('href'),
|
|
|
|
|
mediaType: item.getAttribute('media-type') || '',
|
|
|
|
|
properties: item.getAttribute('properties') || '',
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. Build image map: abs zip path → blob URL (parallel)
|
|
|
|
|
const imageMap = {};
|
|
|
|
|
await Promise.all(
|
|
|
|
|
Object.values(manifest)
|
|
|
|
|
.filter(({mediaType}) => mediaType.startsWith('image/'))
|
|
|
|
|
.map(async ({href, mediaType}) => {
|
|
|
|
|
try {
|
|
|
|
|
const buf = await zip.file(href).async('arraybuffer');
|
|
|
|
|
imageMap[href] = URL.createObjectURL(new Blob([buf], {type: mediaType}));
|
|
|
|
|
} catch (e) { /* missing asset */ }
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 5. Parse TOC
|
|
|
|
|
const toc = await _parseEpubToc(zip, opfDoc, manifest);
|
|
|
|
|
|
|
|
|
|
// 6. Get spine items; chapters are returned raw for progressive DOM injection
|
|
|
|
|
const spineItems = Array.from(opfDoc.querySelectorAll('spine > itemref'))
|
|
|
|
|
.map(ref => manifest[ref.getAttribute('idref')]?.href)
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
// All chapters unpacked at once: one after the other, every chapter waits for
|
|
|
|
|
// JSZip's own ticks (a book like Das Kapital has 148 of them).
|
|
|
|
|
const texts = await Promise.all(spineItems.map(href => {
|
|
|
|
|
const f = zip.file(href);
|
|
|
|
|
return f ? f.async('text').catch(() => null) : null;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const chapters = [];
|
|
|
|
|
let slice = performance.now();
|
|
|
|
|
for (let i = 0; i < spineItems.length; i++) {
|
|
|
|
|
const href = spineItems[i];
|
|
|
|
|
if (texts[i] == null) continue; // missing
|
|
|
|
|
try {
|
|
|
|
|
let chapterText = texts[i];
|
|
|
|
|
texts[i] = null;
|
|
|
|
|
// Strip style/script blocks early — publisher EPUBs often embed large base64 fonts in CSS.
|
|
|
|
|
// This shrinks the string before regex and DOMParser work on it.
|
|
|
|
|
chapterText = chapterText.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
|
|
|
chapterText = chapterText.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
|
|
|
const chapterDir = href.includes('/') ? href.substring(0, href.lastIndexOf('/') + 1) : '';
|
|
|
|
|
const withBlobs = _injectImageBlobs(chapterText, chapterDir, imageMap);
|
|
|
|
|
const sanitized = sanitizeEpubHtml(withBlobs);
|
|
|
|
|
chapters.push(`<div id="epub-chapter-${i}" data-epub-src="${href}">${sanitized}</div>`);
|
|
|
|
|
// Let the page breathe (spinner, taps) every ~50 ms of work, not after a fixed count.
|
|
|
|
|
if (performance.now() - slice > 50) {
|
|
|
|
|
await new Promise(r => setTimeout(r, 0));
|
|
|
|
|
slice = performance.now();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) { /* skip unreadable */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {title, author, chapters, toc, imageMap};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 12:28:41 +01:00
|
|
|
// A book's length in printed pages, from its file without drawing it: a PDF's pages, an EPUB's
|
|
|
|
|
// spine text by CHARS_PER_BOOK_PAGE (as the reader estimates it). The library's length sorts.
|
|
|
|
|
async function bookPagesOfFile(buf, type) {
|
|
|
|
|
if (type === 'pdf') {
|
PDF-Reader: ein Zeichenweg, Seitenmodus neu, Seitensprung für beide Modi (WIP, ungetestet)
- 58-pdf.js: _pdfDraw – eine Zeichnung je Canvas (die vorige wird
abgebrochen und abgewartet), Pixeldichte, höchstens 16,7 MP; Seiten, die
gehen, geben auch pdf.js' Seitendaten frei; ein gemeinsamer pdf.js-Worker
(_pdfLoad) auch für Uploads, Seitenzahl, Hefte und Cover; Inhalts-
verzeichnis parallel aufgelöst, auch mit Seitenindex als Ziel;
ResizeObserver: Drehen, Breite, Leisten – neu an derselben Seite.
- 66-pdf-paginated.js: Seitenmodus auf _pdfDraw, Zuschnitt in
Bildschirmkoordinaten (gedrehte Seiten) und höchstens 2,5× der ganzen
Seite (eine fast leere Seite wird nicht unendlich groß); nächste und
vorige Seite vorgezeichnet; Tippen zum Blättern blendet die Leisten
nicht mehr ein; Wischen nur deutlich seitwärts; Mausrad; Pinch setzt die
Vorschau zurück; nach dem Blättern wird gespeichert und 'diora-pdf-turn'
gemeldet (E-Ink-Refresh).
- pdfGoToPage für beide Modi: Inhaltsverzeichnis, Seitenzahl im Kopf,
Lesezeichen, Tasten (Seitenmodus jetzt auch Leertaste, Bild auf/ab).
- Beim Scrollen wird die aktuelle Seite mitgeführt (Lesezeichen sagten
immer „Seite 1“).
- Schalter Scrollen | Seiten im PDF-Panel; Zoom-Regler zeichnet erst neu,
wenn er ruht.
- openBook hängt seine Listener über _readerListen an; closeReader nimmt
sie ab (vorher sammelten sie sich bei jedem Öffnen an). Abbrechen und
ein zweites Buch geben das alte Dokument frei; der entschlüsselte
PDF-Puffer wird nicht mehr gehalten.
- Invertieren dreht die Farbtöne zurück (Fotos keine Negative).
- e2e/fixtures/make_test_pdf.py: Test-PDF mit gemischten Formaten,
Outline, Links und einer fast leeren Seite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:52:28 +01:00
|
|
|
const pdf = await _pdfLoad(new Uint8Array(buf)).promise;
|
|
|
|
|
try {
|
|
|
|
|
return pdf.numPages;
|
|
|
|
|
} finally {
|
|
|
|
|
try { pdf.destroy(); } catch (e) {}
|
|
|
|
|
}
|
2026-10-25 12:28:41 +01:00
|
|
|
}
|
|
|
|
|
const zip = await JSZip.loadAsync(buf);
|
|
|
|
|
const containerXml = await zip.file('META-INF/container.xml').async('text');
|
|
|
|
|
const opfPath = new DOMParser().parseFromString(containerXml, 'application/xml').querySelector('rootfile')?.getAttribute('full-path');
|
|
|
|
|
if (!opfPath) return 0;
|
|
|
|
|
const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/') + 1) : '';
|
|
|
|
|
const opfDoc = new DOMParser().parseFromString(await zip.file(opfPath).async('text'), 'application/xml');
|
|
|
|
|
const hrefs = {};
|
|
|
|
|
opfDoc.querySelectorAll('manifest > item').forEach(item => { hrefs[item.getAttribute('id')] = opfDir + item.getAttribute('href'); });
|
|
|
|
|
let chars = 0;
|
|
|
|
|
for (const ref of opfDoc.querySelectorAll('spine > itemref')) {
|
|
|
|
|
const f = zip.file(hrefs[ref.getAttribute('idref')] || '');
|
|
|
|
|
if (!f) continue;
|
|
|
|
|
const html = (await f.async('text').catch(() => ''))
|
|
|
|
|
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '').replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
|
|
|
const body = new DOMParser().parseFromString(html, 'text/html').body;
|
|
|
|
|
chars += body ? body.textContent.length : 0;
|
|
|
|
|
}
|
|
|
|
|
return chars ? Math.max(1, Math.round(chars / CHARS_PER_BOOK_PAGE)) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
async function _parseEpubToc(zip, opfDoc, manifest) {
|
|
|
|
|
// Try EPUB3 nav document
|
|
|
|
|
const navItem = Object.values(manifest).find(m => m.properties.includes('nav'));
|
|
|
|
|
if (navItem) {
|
|
|
|
|
try {
|
|
|
|
|
const navText = await zip.file(navItem.href).async('text');
|
|
|
|
|
const navDoc = new DOMParser().parseFromString(navText, 'application/xhtml+xml');
|
|
|
|
|
const tocNav = navDoc.querySelector('nav[epub\\:type="toc"]') || navDoc.querySelector('nav');
|
|
|
|
|
if (tocNav) {
|
|
|
|
|
const ol = tocNav.querySelector('ol');
|
|
|
|
|
if (ol) return _parseTocOl(ol, navItem.href, 0);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to EPUB2 NCX
|
|
|
|
|
const ncxItem = Object.values(manifest).find(m => m.mediaType === 'application/x-dtbncx+xml');
|
|
|
|
|
if (ncxItem) {
|
|
|
|
|
try {
|
|
|
|
|
const ncxText = await zip.file(ncxItem.href).async('text');
|
|
|
|
|
const ncxDoc = new DOMParser().parseFromString(ncxText, 'application/xml');
|
|
|
|
|
const ncxDir = ncxItem.href.includes('/') ? ncxItem.href.substring(0, ncxItem.href.lastIndexOf('/') + 1) : '';
|
|
|
|
|
return _parseNcxNavMap(ncxDoc.querySelector('navMap'), ncxDir, 0);
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _parseTocOl(ol, navHref, depth) {
|
|
|
|
|
if (!ol || depth > 5) return [];
|
|
|
|
|
const navDir = navHref.includes('/') ? navHref.substring(0, navHref.lastIndexOf('/') + 1) : '';
|
|
|
|
|
const items = [];
|
|
|
|
|
for (const li of Array.from(ol.children)) {
|
|
|
|
|
const a = li.querySelector(':scope > a') || li.querySelector(':scope > span');
|
|
|
|
|
if (a) {
|
|
|
|
|
const rawHref = a.getAttribute('href') || '';
|
|
|
|
|
items.push({
|
|
|
|
|
label: a.textContent.trim(),
|
|
|
|
|
href: rawHref ? resolveEpubPath(navDir, rawHref) : '',
|
|
|
|
|
depth,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const childOl = li.querySelector(':scope > ol');
|
|
|
|
|
if (childOl) items.push(..._parseTocOl(childOl, navHref, depth + 1));
|
|
|
|
|
}
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _parseNcxNavMap(navMap, ncxDir, depth) {
|
|
|
|
|
if (!navMap || depth > 5) return [];
|
|
|
|
|
const items = [];
|
|
|
|
|
for (const navPoint of Array.from(navMap.children)) {
|
|
|
|
|
if (navPoint.tagName !== 'navPoint') continue;
|
|
|
|
|
const label = navPoint.querySelector('navLabel > text')?.textContent?.trim() || '';
|
|
|
|
|
const src = navPoint.querySelector('content')?.getAttribute('src') || '';
|
|
|
|
|
items.push({label, href: src ? resolveEpubPath(ncxDir, src) : '', depth});
|
|
|
|
|
items.push(..._parseNcxNavMap(navPoint, ncxDir, depth + 1));
|
|
|
|
|
}
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _resolveImageBlob(imageMap, absPath) {
|
|
|
|
|
if (imageMap[absPath]) return imageMap[absPath];
|
|
|
|
|
// Fallback: match by decoded filename only
|
|
|
|
|
const name = decodeURIComponent(absPath.split('/').pop()).toLowerCase();
|
|
|
|
|
for (const [k, v] of Object.entries(imageMap)) {
|
|
|
|
|
if (decodeURIComponent(k.split('/').pop()).toLowerCase() === name) return v;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Replace image src/href in raw chapter HTML text with blob URLs before DOMParser sees it.
|
|
|
|
|
// This avoids relying on innerHTML serialisation preserving attributes we set on DOM nodes.
|
|
|
|
|
function _injectImageBlobs(html, chapterDir, imageMap) {
|
|
|
|
|
function subst(src) {
|
|
|
|
|
if (!src || src.startsWith('blob:') || src.startsWith('data:') || src.startsWith('http')) return src;
|
|
|
|
|
return _resolveImageBlob(imageMap, resolveEpubPath(chapterDir, src)) || '';
|
|
|
|
|
}
|
|
|
|
|
// <img src="...">
|
|
|
|
|
html = html.replace(/(<img\b[^>]*?)\bsrc="([^"]*)"/gi,
|
|
|
|
|
(_, pre, src) => { const b = subst(src); return b ? `${pre}src="${b}"` : pre; });
|
|
|
|
|
// SVG <image xlink:href="...">
|
|
|
|
|
html = html.replace(/(<image\b[^>]*?)\bxlink:href="([^"]*)"/gi,
|
|
|
|
|
(_, pre, src) => { const b = subst(src); return b ? `${pre}xlink:href="${b}"` : pre; });
|
|
|
|
|
// SVG <image href="...">
|
|
|
|
|
html = html.replace(/(<image\b[^>]*?)\bhref="([^"]*)"/gi,
|
|
|
|
|
(_, pre, src) => { const b = subst(src); return b ? `${pre}href="${b}"` : pre; });
|
|
|
|
|
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
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
// rather than only the single hardcoded class name. Also role="doc-noteref",
|
|
|
|
|
// more note classes, and a bare note number as the link's text ("3", "[12]", "*").
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
function _looksLikeFootnoteLink(el, href) {
|
|
|
|
|
if (!href.includes('#')) return false;
|
|
|
|
|
const cls = (el.getAttribute('class') || '').toLowerCase();
|
|
|
|
|
if (/\bnote|\bfootnote|\bfn\b/.test(cls)) return true;
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
if (/\b(fnref|noteref|footnote-?ref|endnote-?ref)\b/.test(cls)) return true;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
const epubType = (el.getAttribute('epub:type') || '').toLowerCase();
|
|
|
|
|
if (epubType.includes('noteref')) return true;
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
if ((el.getAttribute('role') || '').toLowerCase() === 'doc-noteref') return true;
|
|
|
|
|
if (el.closest('sup') || el.querySelector('sup')) return true;
|
|
|
|
|
return /^\s*[\[(]?\s*(\d{1,4}|[ivxlc]{1,6}|[a-z]|[*†‡§¶]+)\s*[\])]?\s*$/i.test(el.textContent || '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The number at the start of a note that links back to the text ("3 Endnote…")
|
|
|
|
|
// is a way back, not a reference to another note.
|
|
|
|
|
const _NOTE_BOX_SELECTOR = '.footnote, .endnote, aside, [role="doc-footnote"], [role="doc-endnote"], ' +
|
|
|
|
|
'[epub\\:type~="footnote"], [epub\\:type~="endnote"], [epub\\:type~="rearnote"]';
|
|
|
|
|
function _isNoteBacklink(el) {
|
|
|
|
|
const role = (el.getAttribute('role') || '').toLowerCase();
|
|
|
|
|
const type = (el.getAttribute('epub:type') || '').toLowerCase();
|
|
|
|
|
if (role === 'doc-backlink' || /backlink/.test(type)) return true;
|
|
|
|
|
// Inside a note's box. Not the link itself: a reference can carry the class
|
|
|
|
|
// "footnote" too (<a class="footnote" href="#ftn.fn1">, Suhrkamp/DocBook).
|
|
|
|
|
if (el.parentElement && el.parentElement.closest(_NOTE_BOX_SELECTOR)) return true;
|
|
|
|
|
const block = el.closest('p, li, div, dd');
|
|
|
|
|
if (!block) return false;
|
|
|
|
|
// A note: the block is a link target itself, or holds an empty anchor that is
|
|
|
|
|
// (zeno.org / e-artnow: <p><a id="F258"></a><a href="…#N258">1</a> Note…</p>).
|
|
|
|
|
const anchored = block.id || el.id || Array.prototype.some.call(block.querySelectorAll('[id]'),
|
|
|
|
|
x => x !== el && !(x.textContent || '').trim());
|
|
|
|
|
if (!anchored) return false;
|
|
|
|
|
const text = (block.textContent || '').trim();
|
|
|
|
|
const firstLink = Array.prototype.find.call(block.querySelectorAll('a'), a => (a.textContent || '').trim());
|
|
|
|
|
return firstLink === el && text.startsWith((el.textContent || '').trim());
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
doc.querySelectorAll('*').forEach(el => {
|
|
|
|
|
Array.from(el.attributes).forEach(attr => {
|
|
|
|
|
if (attr.name.startsWith('on') || attr.name === 'title') el.removeAttribute(attr.name);
|
|
|
|
|
});
|
|
|
|
|
const tag = el.tagName.toLowerCase();
|
|
|
|
|
if (tag === 'img') {
|
|
|
|
|
const src = el.getAttribute('src') || '';
|
|
|
|
|
// 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://')) {
|
|
|
|
|
el.setAttribute('target', '_blank');
|
|
|
|
|
el.setAttribute('rel', 'noopener noreferrer');
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
} else if (!(href.includes('#') && _isNoteBacklink(el)) && _looksLikeFootnoteLink(el, href)) {
|
|
|
|
|
// data-footnote-ref (the id) marks the link as a reference; the href
|
|
|
|
|
// as written (data-footnote-href) finds the note in the chapter file it
|
|
|
|
|
// points into (78-footnotes.js) — ids like "fn1" repeat across files.
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
el.dataset.footnoteRef = href.slice(href.indexOf('#') + 1);
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
el.dataset.footnoteHref = href;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
el.removeAttribute('href');
|
|
|
|
|
el.style.cursor = 'pointer';
|
|
|
|
|
} else {
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
// A link within the book (a cross-reference, a note's way back): kept
|
|
|
|
|
// for the apps, which follow it (diora-android native-ui.js).
|
|
|
|
|
if (href && !/^[a-z]+:/i.test(href)) el.dataset.internalHref = href;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
el.removeAttribute('href');
|
|
|
|
|
el.style.cursor = 'default';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return doc.body ? doc.body.innerHTML : doc.documentElement.innerHTML;
|
|
|
|
|
}
|