feat(epub): align wxread css and overlay pagination behavior

This commit is contained in:
shen
2026-05-25 22:15:24 +08:00
parent c0aac56083
commit 23182e8b5a
31 changed files with 2585 additions and 1691 deletions
@@ -81,163 +81,22 @@ enum RDEPUBJavaScriptBridge {
let payload = jsonString(from: searchPayload(presentation), fallback: "null")
return """
(function() {
var payload = \(payload);
function normalizeHref(rawHref) {
if (!rawHref) { return ''; }
var value = String(rawHref);
value = value.replace(/^.*?:\\/\\/[^/]+\\//, '');
value = value.split('#')[0];
value = value.replace(/^\\/+/, '');
try {
value = decodeURIComponent(value);
} catch (error) {
}
return value;
}
function ensureStyle(doc) {
if (!doc || !doc.head || doc.getElementById('ss-reader-search-style')) { return; }
var style = doc.createElement('style');
style.id = 'ss-reader-search-style';
style.textContent = '.ss-reader-search-mark{background:rgba(248,225,108,0.55);color:inherit;border-radius:2px;padding:0;}.ss-reader-search-mark-active{background:rgba(255,159,67,0.75);color:inherit;border-radius:2px;padding:0;}';
doc.head.appendChild(style);
}
function clearMarks(doc) {
if (!doc || !doc.body) { return; }
var marks = doc.querySelectorAll('span.ss-reader-search-mark, span.ss-reader-search-mark-active');
marks.forEach(function(mark) {
var textNode = doc.createTextNode(mark.textContent || '');
if (mark.parentNode) {
mark.parentNode.replaceChild(textNode, mark);
}
});
doc.body.normalize();
}
function searchableDocuments(rootDoc) {
var items = [];
if (!rootDoc) { return items; }
items.push({ doc: rootDoc, href: normalizeHref(rootDoc.location && rootDoc.location.href) });
var frames = rootDoc.querySelectorAll ? rootDoc.querySelectorAll('iframe') : [];
frames.forEach(function(frame) {
try {
if (frame.contentDocument) {
items.push({
doc: frame.contentDocument,
href: normalizeHref(frame.contentWindow && frame.contentWindow.location && frame.contentWindow.location.href)
});
}
} catch (error) {
}
});
return items;
}
function textNodes(doc) {
if (!doc || !doc.body) { return []; }
var walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
if (!node || !node.nodeValue || !node.nodeValue.trim()) {
return NodeFilter.FILTER_REJECT;
}
var parent = node.parentNode;
if (!parent || !parent.nodeName) {
return NodeFilter.FILTER_REJECT;
}
var name = parent.nodeName.toLowerCase();
if (['script', 'style', 'noscript'].indexOf(name) >= 0) {
return NodeFilter.FILTER_REJECT;
}
if (parent.classList && (parent.classList.contains('ss-reader-search-mark') || parent.classList.contains('ss-reader-search-mark-active'))) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
var nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
return nodes;
}
function applyToDocument(doc, resourceState, keyword) {
clearMarks(doc);
ensureStyle(doc);
if (!doc || !doc.body || !resourceState || !keyword || resourceState.matchCount <= 0) {
return;
}
var lowerKeyword = String(keyword).toLowerCase();
var activeIndex = typeof resourceState.activeLocalMatchIndex === 'number' ? resourceState.activeLocalMatchIndex : null;
var currentLocalIndex = 0;
var activeElement = null;
textNodes(doc).forEach(function(node) {
var source = node.nodeValue || '';
var lowerSource = source.toLowerCase();
var cursor = 0;
var replaced = false;
var fragment = doc.createDocumentFragment();
while (cursor < source.length) {
var foundIndex = lowerSource.indexOf(lowerKeyword, cursor);
if (foundIndex < 0) {
break;
}
replaced = true;
if (foundIndex > cursor) {
fragment.appendChild(doc.createTextNode(source.slice(cursor, foundIndex)));
}
var mark = doc.createElement('span');
mark.className = currentLocalIndex === activeIndex ? 'ss-reader-search-mark-active' : 'ss-reader-search-mark';
mark.textContent = source.slice(foundIndex, foundIndex + keyword.length);
fragment.appendChild(mark);
if (currentLocalIndex === activeIndex) {
activeElement = mark;
}
currentLocalIndex += 1;
cursor = foundIndex + keyword.length;
}
if (!replaced) {
return;
}
if (cursor < source.length) {
fragment.appendChild(doc.createTextNode(source.slice(cursor)));
}
if (node.parentNode) {
node.parentNode.replaceChild(fragment, node);
}
});
if (activeElement && activeElement.scrollIntoView) {
activeElement.scrollIntoView({ block: 'center', inline: 'center', behavior: 'auto' });
}
}
var docs = searchableDocuments(document);
var states = new Map();
if (payload && Array.isArray(payload.resources)) {
payload.resources.forEach(function(resource) {
states.set(normalizeHref(resource.href), resource);
});
}
docs.forEach(function(item) {
applyToDocument(item.doc, states.get(item.href), payload && payload.keyword ? payload.keyword : '');
});
if (!window.RDReaderBridge) { return false; }
window.RDReaderBridge.setSearchPresentation(\(payload));
return true;
})();
"""
}
static func resolveDecorationsScript() -> String {
"""
(function() {
if (!window.RDReaderBridge) { return { highlights: [], search: [] }; }
return window.RDReaderBridge.resolveDecorations();
})();
"""
}
/// JS
private static func escapedJavaScriptTemplateLiteral(_ string: String) -> String {
string