feat: Enhance spine href resolution and normalization in chapter handling

This commit is contained in:
JB
2025-10-13 17:33:45 -07:00
parent f7541388e6
commit bccfd9f5c5
+68 -7
View File
@@ -251,6 +251,55 @@
} }
}; };
let spineHrefList = [];
const spineHrefLookup = new Map();
const refreshSpineLookup = (items) => {
spineHrefList = [];
spineHrefLookup.clear();
if (!Array.isArray(items)) {
return;
}
items.forEach((entry) => {
const href = typeof entry === 'string' ? entry : entry?.href;
const normalized = removeLeadingSlash(href || '');
if (!normalized) {
return;
}
spineHrefList.push(normalized);
const key = normalized.toLowerCase();
if (!spineHrefLookup.has(key)) {
spineHrefLookup.set(key, normalized);
}
});
};
const resolveSpineHref = (value) => {
if (!value) {
return '';
}
const normalized = removeLeadingSlash(value);
if (!normalized) {
return '';
}
const direct = spineHrefLookup.get(normalized.toLowerCase());
if (direct) {
return direct;
}
const segments = normalized.split('/');
for (let index = 1; index < segments.length; index += 1) {
const candidate = segments.slice(index).join('/');
if (!candidate) {
continue;
}
const match = spineHrefLookup.get(candidate.toLowerCase());
if (match) {
return match;
}
}
return normalized;
};
const formatClock = (value) => { const formatClock = (value) => {
if (!Number.isFinite(value) || value < 0) { if (!Number.isFinite(value) || value < 0) {
return '--:--'; return '--:--';
@@ -456,6 +505,7 @@
href: cleanedHref, href: cleanedHref,
normalizedHref: normalized, normalizedHref: normalized,
title: chapter.title || `Chapter ${idx + 1}`, title: chapter.title || `Chapter ${idx + 1}`,
spineHref: chapter.spineHref ? removeLeadingSlash(chapter.spineHref) : null,
}; };
}); });
chapters = dedupeChapters(chapters); chapters = dedupeChapters(chapters);
@@ -812,22 +862,26 @@
await book.ready; await book.ready;
const fragment = typeof options.fragment === 'string' ? options.fragment : ''; const fragment = typeof options.fragment === 'string' ? options.fragment : '';
const baseHref = target.href.split('#')[0]; const baseHref = target.href.split('#')[0];
const displayHref = fragment ? `${baseHref}#${fragment}` : target.href; const displayResolved = resolveSpineHref(target.spineHref || target.href);
const canonicalCandidate = typeof book.canonical === 'function' ? book.canonical(displayHref) : displayHref; const fallbackDisplay = removeLeadingSlash(target.href);
const displayBase = displayResolved || fallbackDisplay;
const displayHref = fragment ? `${displayBase}#${fragment}` : displayBase;
const canonicalSource = fragment ? displayBase : displayHref;
const canonicalCandidate = typeof book.canonical === 'function' ? book.canonical(canonicalSource) : canonicalSource;
const isBlobCanonical = typeof canonicalCandidate === 'string' && canonicalCandidate.startsWith('blob:'); const isBlobCanonical = typeof canonicalCandidate === 'string' && canonicalCandidate.startsWith('blob:');
const canonicalHref = isBlobCanonical ? null : removeLeadingSlash(canonicalCandidate); const canonicalHref = isBlobCanonical ? null : removeLeadingSlash(canonicalCandidate);
const targetHref = removeLeadingSlash(canonicalHref || displayHref);
const syncHref = removeLeadingSlash((canonicalHref || baseHref).split('#')[0]); const syncHref = removeLeadingSlash((canonicalHref || baseHref).split('#')[0]);
console.info('[reader] Displaying chapter', { console.info('[reader] Displaying chapter', {
requestedIndex: normalizedIndex, requestedIndex: normalizedIndex,
href: target.href, href: target.href,
spineHref: target.spineHref,
displayBase,
displayHref, displayHref,
canonicalHref, canonicalHref,
targetHref,
syncHref, syncHref,
}); });
suppressRelocate = true; suppressRelocate = true;
await rendition.display(targetHref); await rendition.display(displayHref);
suppressRelocate = false; suppressRelocate = false;
currentChapterIndex = normalizedIndex; currentChapterIndex = normalizedIndex;
updateControls(); updateControls();
@@ -1022,8 +1076,9 @@
await book.ready; await book.ready;
console.info('[reader] Book ready resolved'); console.info('[reader] Book ready resolved');
try { try {
const spineEntries = (book.spine?.items || []).map((item) => item?.href || '(unknown)'); const spineItems = Array.isArray(book.spine?.items) ? book.spine.items : [];
console.info('[reader] Spine hrefs', spineEntries); refreshSpineLookup(spineItems);
console.info('[reader] Spine hrefs', spineHrefList);
} catch (spineError) { } catch (spineError) {
console.warn('[reader] Unable to log spine hrefs', spineError); console.warn('[reader] Unable to log spine hrefs', spineError);
} }
@@ -1062,11 +1117,17 @@
...chapter, ...chapter,
href: effectiveHref, href: effectiveHref,
normalizedHref: effectiveNormalized, normalizedHref: effectiveNormalized,
spineHref: resolveSpineHref(effectiveHref),
}; };
}); });
chapters = dedupeChapters(chapters); chapters = dedupeChapters(chapters);
} }
chapters = chapters.map((chapter) => ({
...chapter,
spineHref: resolveSpineHref(chapter.spineHref || chapter.href),
}));
currentChapterIndex = Math.min(currentChapterIndex, Math.max(chapters.length - 1, 0)); currentChapterIndex = Math.min(currentChapterIndex, Math.max(chapters.length - 1, 0));
initializeChapters(); initializeChapters();
updateControls(); updateControls();