ReadViewSDK/Sources/RDReaderView/EPUBCore/Resources/epub-bridge.js
shenlei d7f28c3f10 fix: 修复右对齐短文本显示不完整及多项功能改进
右对齐文本显示修复(如"巫鸿"只显示"鸿"的问题):
- RDEPUBChapterTailNormalizer: 短尾页合并时检查 avoidPageBreakInside 语义,
  避免将带有此标记的短文本(如 right-info 署名)错误合并回上一页
- RDEPUBTextContentView: 续段归一化时跳过右对齐/居中对齐的段落,
  防止错误修改 firstLineHeadIndent 导致首字不可见
- RDEPUBCSSCompatibilityLayer: 为含 text-align:right/center 的 CSS 块
  自动注入 text-indent:0 !important,确保右对齐/居中文本无首行缩进
- RDEPUBTextRendererSupport: 排版属性归一化时将右对齐/居中段落的
  firstLineHeadIndent 重置为 0

图片查看器及脚注点击功能:
- 新增 RDEPUBImageViewController 和 RDEPUBImageViewerCoordinator,
  支持从 WebView 和 TextPage 两种模式查看图片
- epub-bridge.js: 检测图片和脚注图片的点击事件,脚注图片显示弹窗
- RDEPUBJavaScriptBridge: 新增 imageDidTap/footnoteDidTap 桥接消息
- RDEPUBWebView: 新增图片和脚注点击的 delegate 方法
- RDEPUBAttachmentNormalizer: 改进脚注检测,优先使用 alt 文本判断
- RDEPUBPaginationModels: 新增 .footnote 附件类型
- RDEPUBPageLayoutSnapshot: 运行时动态解析附件类型,脚注优先级高于图片

位置解析改进:
- RDEPUBReaderController+LocationResolution: 利用 rangeInfo 提升页码定位精度

其他:
- RDEPUBTextBookCache: schema 版本升级至 13
- RDEPUBReaderTheme: 主题更新
- Pod 项目文件更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-25 15:39:58 +08:00

551 lines
19 KiB
JavaScript

(function() {
if (window.RDReaderBridge) { return; }
window.addEventListener('error', function(event) {
try {
var message = event && (event.message || (event.error && event.error.message)) || 'Unknown JavaScript error';
window.webkit.messageHandlers.{{JAVASCRIPT_ERROR_MESSAGE}}.postMessage(message);
} catch (error) {
}
});
function serializeRange(range) {
if (window.rangy && typeof window.rangy.serializeRange === 'function') {
return window.rangy.serializeRange(range, range && range.startContainer && range.startContainer.ownerDocument);
}
return null;
}
function rangeFromInfo(rangeInfo) {
if (window.rangy && typeof window.rangy.deserializeRange === 'function') {
return window.rangy.deserializeRange(rangeInfo, document);
}
return null;
}
var visiblePageIndex = 0;
var configuredPageCount = 1;
var configuredPageStride = 1;
var relayoutTimer = null;
var fixedLayoutDocument = false;
function isFixedLayoutDocument() {
if (fixedLayoutDocument) { return true; }
fixedLayoutDocument = !!document.getElementById('ss-fixed-root') || !!document.querySelector('.ss-fixed-page');
return fixedLayoutDocument;
}
function ensurePaginationStructure() {
if (isFixedLayoutDocument()) {
return null;
}
var viewport = document.getElementById('ss-reader-viewport');
var content = document.getElementById('ss-reader-content');
if (viewport && content) {
return content;
}
viewport = document.createElement('div');
viewport.id = 'ss-reader-viewport';
content = document.createElement('div');
content.id = 'ss-reader-content';
while (document.body.firstChild) {
content.appendChild(document.body.firstChild);
}
viewport.appendChild(content);
document.body.appendChild(viewport);
return content;
}
function paginationContent() {
if (isFixedLayoutDocument()) {
return null;
}
return ensurePaginationStructure();
}
function effectivePageStride() {
return Math.max(1, configuredPageStride || 1);
}
function measuredPageCount() {
if (isFixedLayoutDocument()) {
return 1;
}
var content = paginationContent();
var scrollingElement = document.scrollingElement || document.documentElement || document.body;
var scrollWidth = Math.max(
content ? content.scrollWidth : 0,
content ? content.offsetWidth : 0,
scrollingElement ? scrollingElement.scrollWidth : 0,
document.documentElement ? document.documentElement.scrollWidth : 0,
document.body ? document.body.scrollWidth : 0,
effectivePageStride()
);
return Math.max(1, Math.ceil(scrollWidth / effectivePageStride()));
}
function totalPageCount() {
return Math.max(configuredPageCount, measuredPageCount());
}
function currentPageOffset() {
return Math.round(visiblePageIndex * effectivePageStride());
}
function reportProgression(fragment) {
var pageCount = totalPageCount();
var progression = pageCount <= 1 ? 0 : visiblePageIndex / Math.max(pageCount - 1, 1);
var lastProgression = progression;
window.webkit.messageHandlers.{{PROGRESSION_CHANGED_MESSAGE}}.postMessage({
progression: progression,
lastProgression: lastProgression,
fragment: fragment || null
});
}
function setVisiblePageIndex(pageIndex) {
if (isFixedLayoutDocument()) {
visiblePageIndex = 0;
return;
}
var pageCount = totalPageCount();
var maxPageIndex = Math.max(0, pageCount - 1);
var safePageIndex = Math.max(0, Math.min(maxPageIndex, Math.round(pageIndex || 0)));
visiblePageIndex = safePageIndex;
var content = paginationContent();
if (content) {
content.style.transform = 'translate3d(' + (-currentPageOffset()) + 'px, 0, 0)';
}
}
function scheduleRelayout() {
if (isFixedLayoutDocument()) { return; }
clearTimeout(relayoutTimer);
relayoutTimer = setTimeout(function() {
setVisiblePageIndex(visiblePageIndex);
reportProgression(null);
}, 60);
}
function pageIndexForElement(target) {
if (isFixedLayoutDocument()) { return 0; }
if (!target) { return 0; }
var rect = target.getBoundingClientRect();
var absoluteLeft = Math.max(0, rect.left + currentPageOffset());
return Math.max(0, Math.floor(absoluteLeft / effectivePageStride()));
}
function fragmentTarget(fragment) {
if (!fragment) { return null; }
var contexts = documentContexts(document, 0, 0);
for (var index = 0; index < contexts.length; index += 1) {
var context = contexts[index];
var target = context.doc.getElementById(fragment)
|| context.doc.querySelector('[name="' + fragment.replace(/"/g, '\\"') + '"]');
if (target) {
return {
context: context,
target: target
};
}
}
return null;
}
var storedHighlights = [];
var storedSearchPresentation = null;
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 clearHighlights() {
storedHighlights = [];
}
function setHighlights(items) {
storedHighlights = Array.isArray(items) ? items.slice() : [];
}
function setSearchPresentation(payload) {
storedSearchPresentation = payload || null;
}
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;
}
return NodeFilter.FILTER_ACCEPT;
}
});
var nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
return nodes;
}
function documentContexts(rootDoc, baseOffsetX, baseOffsetY) {
var items = [];
if (!rootDoc) { return items; }
var offsetX = baseOffsetX || 0;
var offsetY = baseOffsetY || 0;
items.push({
doc: rootDoc,
href: normalizeHref(rootDoc.location && rootDoc.location.href),
offsetX: offsetX,
offsetY: offsetY
});
var frames = rootDoc.querySelectorAll ? rootDoc.querySelectorAll('iframe') : [];
frames.forEach(function(frame) {
try {
if (!frame.contentDocument) { return; }
var frameRect = frame.getBoundingClientRect();
documentContexts(
frame.contentDocument,
offsetX + frameRect.left,
offsetY + frameRect.top
).forEach(function(item) {
items.push(item);
});
} catch (error) {
}
});
return items;
}
var hookedDocuments = new WeakSet();
var hookedFrames = new WeakSet();
function selectionForDocument(doc) {
if (!doc) { return null; }
try {
var view = doc.defaultView || window;
return view.getSelection ? view.getSelection() : null;
} catch (error) {
return null;
}
}
function currentSelectionContext() {
var contexts = documentContexts(document, 0, 0);
for (var index = 0; index < contexts.length; index += 1) {
var context = contexts[index];
var selection = selectionForDocument(context.doc);
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
continue;
}
var text = selection.toString ? selection.toString() : '';
if (!text || !text.trim()) {
continue;
}
return {
context: context,
selection: selection
};
}
return null;
}
function rectPayloadForRange(range, offsetX, offsetY) {
if (!range || range.collapsed) { return []; }
var rects = [];
Array.prototype.forEach.call(range.getClientRects(), function(rect) {
if (!rect || rect.width < 0.5 || rect.height < 0.5) { return; }
rects.push({
x: rect.left + (offsetX || 0),
y: rect.top + (offsetY || 0),
width: rect.width,
height: rect.height
});
});
return rects;
}
function collectHighlightDecorations() {
return storedHighlights.map(function(item, index) {
if (!item || !item.rangeInfo) { return null; }
var range = rangeFromInfo(item.rangeInfo);
var rects = rectPayloadForRange(range, 0, 0);
if (!rects.length) { return null; }
return {
key: item.id || ('highlight-' + index),
kind: item.style === 'underline' ? 'underline' : 'highlight',
color: item.color || '#F8E16C',
rangeInfo: item.rangeInfo,
rects: rects
};
}).filter(Boolean);
}
function collectSearchDecorations() {
if (!storedSearchPresentation || !storedSearchPresentation.keyword) {
return [];
}
var resources = new Map();
if (Array.isArray(storedSearchPresentation.resources)) {
storedSearchPresentation.resources.forEach(function(resource) {
resources.set(normalizeHref(resource.href), resource);
});
}
var keyword = String(storedSearchPresentation.keyword);
var lowerKeyword = keyword.toLowerCase();
var decorations = [];
documentContexts(document, 0, 0).forEach(function(context) {
var resourceState = resources.get(context.href);
if (!resourceState || resourceState.matchCount <= 0) {
return;
}
var activeIndex = typeof resourceState.activeLocalMatchIndex === 'number' ? resourceState.activeLocalMatchIndex : null;
var currentLocalIndex = 0;
var activeRect = null;
var contextStartIndex = decorations.length;
textNodes(context.doc).forEach(function(node) {
var source = node.nodeValue || '';
var lowerSource = source.toLowerCase();
var cursor = 0;
while (cursor < source.length) {
var foundIndex = lowerSource.indexOf(lowerKeyword, cursor);
if (foundIndex < 0) {
break;
}
var range = context.doc.createRange();
range.setStart(node, foundIndex);
range.setEnd(node, foundIndex + keyword.length);
var rects = rectPayloadForRange(range, context.offsetX, context.offsetY);
if (rects.length) {
var isActive = currentLocalIndex === activeIndex;
decorations.push({
key: context.href + ':' + currentLocalIndex,
kind: isActive ? 'activeSearch' : 'search',
rects: rects
});
if (isActive) {
activeRect = rects[0];
}
}
currentLocalIndex += 1;
cursor = foundIndex + keyword.length;
}
});
if (activeRect && !isFixedLayoutDocument()) {
var previousPageIndex = visiblePageIndex;
var activePageIndex = Math.max(0, Math.floor(Math.max(0, activeRect.x + currentPageOffset()) / effectivePageStride()));
setVisiblePageIndex(activePageIndex);
var deltaX = (activePageIndex - previousPageIndex) * effectivePageStride();
if (deltaX !== 0) {
for (var index = contextStartIndex; index < decorations.length; index += 1) {
decorations[index].rects = decorations[index].rects.map(function(rect) {
return {
x: rect.x - deltaX,
y: rect.y,
width: rect.width,
height: rect.height
};
});
}
}
}
});
return decorations;
}
function resolveDecorations() {
return {
highlights: collectHighlightDecorations(),
search: collectSearchDecorations()
};
}
function selectionPayload() {
var payloadContext = currentSelectionContext();
if (!payloadContext) {
return null;
}
var range = payloadContext.selection.getRangeAt(0);
var text = payloadContext.selection.toString();
if (!text || !text.trim()) { return null; }
var pageCount = totalPageCount();
var progression = pageCount <= 1 ? 0 : visiblePageIndex / Math.max(pageCount - 1, 1);
var lastProgression = progression;
return {
href: payloadContext.context.href || null,
text: text,
rangeInfo: serializeRange(range),
progression: progression,
lastProgression: lastProgression
};
}
var selectionTimer = null;
function notifySelectionChange() {
clearTimeout(selectionTimer);
selectionTimer = setTimeout(function() {
var payload = selectionPayload();
window.webkit.messageHandlers.{{SELECTION_CHANGED_MESSAGE}}.postMessage(payload);
}, 120);
}
function handleDocumentClick(event) {
// Check for image tap. Footnote images always show the note popup; regular
// images inside links are left to the link handler below.
var img = event.target;
while (img && img.tagName !== 'IMG') {
img = img.parentElement;
}
if (img && img.tagName === 'IMG') {
var src = img.getAttribute('src');
var classes = (img.getAttribute('class') || '').toLowerCase();
var alt = (img.getAttribute('alt') || '').trim();
var isFootnote = classes.indexOf('qqreader-footnote') >= 0 || alt.length > 0;
if (isFootnote) {
event.preventDefault();
event.stopPropagation();
var footnoteRect = img.getBoundingClientRect();
window.webkit.messageHandlers.{{FOOTNOTE_DID_TAP_MESSAGE}}.postMessage({
alt: alt,
rect: { x: footnoteRect.x, y: footnoteRect.y, width: footnoteRect.width, height: footnoteRect.height }
});
return;
}
// If a regular image is inside a link, let the link handler deal with it.
var linkParent = img.closest ? img.closest('a[href]') : null;
if (!linkParent) {
if (src) {
event.preventDefault();
event.stopPropagation();
var rect = img.getBoundingClientRect();
window.webkit.messageHandlers.{{IMAGE_DID_TAP_MESSAGE}}.postMessage({
src: src,
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
});
return;
}
}
}
var anchor = event.target.closest ? event.target.closest('a[href]') : null;
if (!anchor) { return; }
var href = anchor.getAttribute('href');
if (!href) { return; }
if (/^(https?:|mailto:|tel:)/i.test(href)) {
event.preventDefault();
window.webkit.messageHandlers.{{EXTERNAL_LINK_MESSAGE}}.postMessage({ url: href });
return;
}
event.preventDefault();
window.webkit.messageHandlers.{{INTERNAL_LINK_MESSAGE}}.postMessage({ href: href });
}
function installDocumentHooks(doc) {
if (!doc || hookedDocuments.has(doc)) { return; }
hookedDocuments.add(doc);
doc.addEventListener('selectionchange', notifySelectionChange);
doc.addEventListener('click', handleDocumentClick, true);
}
function installFrameHooks(rootDoc) {
if (!rootDoc || !rootDoc.querySelectorAll) { return; }
Array.prototype.forEach.call(rootDoc.querySelectorAll('iframe'), function(frame) {
if (hookedFrames.has(frame)) { return; }
hookedFrames.add(frame);
var hookFrameDocument = function() {
try {
if (!frame.contentDocument) { return; }
installDocumentHooks(frame.contentDocument);
installFrameHooks(frame.contentDocument);
} catch (error) {
}
};
frame.addEventListener('load', function() {
hookFrameDocument();
scheduleRelayout();
}, { passive: true });
hookFrameDocument();
});
}
installDocumentHooks(document);
installFrameHooks(document);
window.addEventListener('resize', function() {
if (isFixedLayoutDocument()) { return; }
setVisiblePageIndex(visiblePageIndex);
reportProgression(null);
}, { passive: true });
window.addEventListener('load', scheduleRelayout, { passive: true });
if (!isFixedLayoutDocument() && document.fonts && document.fonts.ready) {
document.fonts.ready.then(scheduleRelayout).catch(function() {});
}
if (!isFixedLayoutDocument()) {
Array.prototype.forEach.call(document.querySelectorAll('img, iframe, video'), function(node) {
node.addEventListener('load', scheduleRelayout, { passive: true });
node.addEventListener('error', scheduleRelayout, { passive: true });
});
}
window.RDReaderBridge = {
applyPagination: function(styleText) {
if (isFixedLayoutDocument()) { return; }
if (window.RDInjectedCSS) {
window.RDInjectedCSS.setStyle('ss-reader-pagination', styleText, { includeFrames: false });
} else {
var style = document.getElementById('ss-reader-pagination');
if (!style) {
style = document.createElement('style');
style.id = 'ss-reader-pagination';
document.head.appendChild(style);
}
style.textContent = styleText;
}
ensurePaginationStructure();
installFrameHooks(document);
},
setPageMetrics: function(pageCount, pageStride) {
if (isFixedLayoutDocument()) { return; }
configuredPageCount = Math.max(1, Math.round(pageCount || 1));
configuredPageStride = Math.max(1, Number(pageStride) || configuredPageStride || 1);
setVisiblePageIndex(visiblePageIndex);
scheduleRelayout();
},
scrollToPage: function(pageIndex) {
if (isFixedLayoutDocument()) { return; }
setVisiblePageIndex(pageIndex);
reportProgression(null);
},
scrollToLocation: function(location, fallbackPageIndex, targetRangeInfo) {
if (targetRangeInfo) {
var highlightRange = rangeFromInfo(targetRangeInfo);
var highlightRects = rectPayloadForRange(highlightRange, 0, 0);
if (highlightRects.length) {
var highlightRect = highlightRects[0];
var highlightAbsoluteLeft = Math.max(0, highlightRect.x + currentPageOffset());
setVisiblePageIndex(Math.max(0, Math.floor(highlightAbsoluteLeft / effectivePageStride())));
reportProgression(location && location.fragment ? location.fragment : null);
return;
}
}
if (location && location.fragment) {
var resolvedTarget = fragmentTarget(location.fragment);
if (resolvedTarget) {
if (isFixedLayoutDocument()) {
reportProgression(location.fragment);
return;
}
var rect = resolvedTarget.target.getBoundingClientRect();
var absoluteLeft = Math.max(0, rect.left + resolvedTarget.context.offsetX + currentPageOffset());
setVisiblePageIndex(Math.max(0, Math.floor(absoluteLeft / effectivePageStride())));
reportProgression(location.fragment);
return;
}
}
var pageCount = totalPageCount();
var progression = location && typeof location.progression === 'number' ? location.progression : 0;
var derivedPageIndex = progression > 0 && pageCount > 1
? Math.round(progression * Math.max(pageCount - 1, 0))
: (fallbackPageIndex || 0);
this.scrollToPage(derivedPageIndex);
},
setHighlights: setHighlights,
clearHighlights: clearHighlights,
setSearchPresentation: setSearchPresentation,
resolveDecorations: resolveDecorations,
reportProgression: reportProgression,
selectionPayload: selectionPayload
};
})();