feat(epub): align wxread css and overlay pagination behavior
This commit is contained in:
@@ -11,6 +11,14 @@ enum RDEPUBAsset: String {
|
||||
case bridgeScript = "epub-bridge"
|
||||
/// 固定版式 HTML 模板(epub-fixed-layout.html),用于渲染 pre-paginated 类型的 EPUB
|
||||
case fixedLayoutTemplate = "epub-fixed-layout"
|
||||
/// WXRead 对齐的 default.css
|
||||
case wxReadDefaultCSS = "wxread-default"
|
||||
/// WXRead 对齐的 replace.css
|
||||
case wxReadReplaceCSS = "wxread-replace"
|
||||
/// WXRead 对齐的 dark.css
|
||||
case wxReadDarkCSS = "wxread-dark"
|
||||
/// WXRead 对齐的 replaceForLatinLanguageBook.css
|
||||
case wxReadLatinReplaceCSS = "wxread-replace-latin"
|
||||
|
||||
/// 对应的文件扩展名
|
||||
var fileExtension: String {
|
||||
@@ -19,6 +27,8 @@ enum RDEPUBAsset: String {
|
||||
return "js"
|
||||
case .fixedLayoutTemplate:
|
||||
return "html"
|
||||
case .wxReadDefaultCSS, .wxReadReplaceCSS, .wxReadDarkCSS, .wxReadLatinReplaceCSS:
|
||||
return "css"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,4 +75,4 @@ enum RDEPUBAssetRepository {
|
||||
}
|
||||
|
||||
/// 用于定位当前模块 Bundle 的标记类
|
||||
private final class RDEPUBAssetBundleToken {}
|
||||
private final class RDEPUBAssetBundleToken {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,7 +61,17 @@ extension RDEPUBWebView {
|
||||
|
||||
/// 生成固定版式加载签名(用于去重相同请求)
|
||||
func fixedLayoutLoadSignature(publicationKey: String, request: RDEPUBFixedRenderRequest) -> String {
|
||||
[
|
||||
let searchSignature = [
|
||||
request.searchPresentation?.keyword ?? "",
|
||||
request.searchPresentation?.resources.map {
|
||||
[
|
||||
$0.href,
|
||||
String($0.matchCount),
|
||||
String($0.activeLocalMatchIndex ?? -1)
|
||||
].joined(separator: "|")
|
||||
}.joined(separator: ",") ?? ""
|
||||
].joined(separator: "#")
|
||||
return [
|
||||
publicationKey,
|
||||
"fixed",
|
||||
request.spread.resources.map(\.href).joined(separator: "|"),
|
||||
@@ -72,7 +82,8 @@ extension RDEPUBWebView {
|
||||
String(format: "%.2f", request.contentInset.bottom),
|
||||
String(format: "%.2f", request.contentInset.right),
|
||||
request.backgroundColorCSS ?? "",
|
||||
request.fit.rawValue
|
||||
request.fit.rawValue,
|
||||
searchSignature
|
||||
].joined(separator: "#")
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,16 @@ extension RDEPUBWebView {
|
||||
let highlightSignature = request.highlights
|
||||
.map { [$0.id, $0.rangeInfo ?? "", $0.color, $0.style.rawValue].joined(separator: "|") }
|
||||
.joined(separator: ",")
|
||||
let searchSignature = [
|
||||
request.searchPresentation?.keyword ?? "",
|
||||
request.searchPresentation?.resources.map {
|
||||
[
|
||||
$0.href,
|
||||
String($0.matchCount),
|
||||
String($0.activeLocalMatchIndex ?? -1)
|
||||
].joined(separator: "|")
|
||||
}.joined(separator: ",") ?? ""
|
||||
].joined(separator: "#")
|
||||
let presentation = request.presentation
|
||||
return [
|
||||
publicationKey,
|
||||
@@ -121,7 +131,8 @@ extension RDEPUBWebView {
|
||||
presentation.themeBackgroundColor ?? "",
|
||||
presentation.themeTextColor ?? "",
|
||||
targetSignature,
|
||||
highlightSignature
|
||||
highlightSignature,
|
||||
searchSignature
|
||||
].joined(separator: "#")
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// RDEPUBWebView 的搜索高亮扩展
|
||||
// 从当前渲染请求中提取搜索展示信息,生成搜索高亮脚本并注入到 WebView 中执行。
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
extension RDEPUBWebView {
|
||||
@@ -31,7 +32,107 @@ extension RDEPUBWebView {
|
||||
if let error {
|
||||
self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription)
|
||||
}
|
||||
self.refreshNativeDecorationsIfNeeded(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshNativeDecorationsIfNeeded(completion: (() -> Void)? = nil) {
|
||||
guard let webView else {
|
||||
onDecorationsResolved?([])
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
webView.evaluateJavaScript(RDEPUBJavaScriptBridge.resolveDecorationsScript()) { [weak self] result, error in
|
||||
guard let self else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
if let error {
|
||||
self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription)
|
||||
self.onDecorationsResolved?([])
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
let decorations = self.nativeDecorations(from: result)
|
||||
self.onDecorationsResolved?(decorations)
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
private func nativeDecorations(from result: Any?) -> [RDEPUBTextOverlayDecoration] {
|
||||
guard let payload = result as? [String: Any] else { return [] }
|
||||
|
||||
let highlightDecorations = decorationList(
|
||||
from: payload["highlights"] as? [[String: Any]],
|
||||
defaultColor: overlayColor(hex: "#F8E16C", alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45)
|
||||
)
|
||||
let searchDecorations = decorationList(
|
||||
from: payload["search"] as? [[String: Any]],
|
||||
defaultColor: UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
|
||||
)
|
||||
return highlightDecorations + searchDecorations
|
||||
}
|
||||
|
||||
private func decorationList(
|
||||
from items: [[String: Any]]?,
|
||||
defaultColor: UIColor
|
||||
) -> [RDEPUBTextOverlayDecoration] {
|
||||
guard let items else { return [] }
|
||||
|
||||
return items.enumerated().compactMap { index, item -> RDEPUBTextOverlayDecoration? in
|
||||
guard let kindString = item["kind"] as? String,
|
||||
let kind = RDEPUBTextOverlayDecoration.Kind(rawValue: kindString),
|
||||
let rectPayloads = item["rects"] as? [[String: Any]] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rects = rectPayloads.compactMap { rectPayload -> CGRect? in
|
||||
guard let x = (rectPayload["x"] as? NSNumber)?.doubleValue,
|
||||
let y = (rectPayload["y"] as? NSNumber)?.doubleValue,
|
||||
let width = (rectPayload["width"] as? NSNumber)?.doubleValue,
|
||||
let height = (rectPayload["height"] as? NSNumber)?.doubleValue else {
|
||||
return nil
|
||||
}
|
||||
return CGRect(x: x, y: y, width: width, height: height)
|
||||
}
|
||||
guard !rects.isEmpty else { return nil }
|
||||
|
||||
let color: UIColor
|
||||
switch kind {
|
||||
case .underline:
|
||||
let hex = item["color"] as? String ?? "#F8E16C"
|
||||
color = overlayColor(hex: hex, alpha: 1) ?? defaultColor
|
||||
case .activeSearch:
|
||||
color = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75)
|
||||
case .search:
|
||||
color = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55)
|
||||
case .highlight:
|
||||
let hex = item["color"] as? String ?? "#F8E16C"
|
||||
color = overlayColor(hex: hex, alpha: 0.45) ?? defaultColor
|
||||
case .selection, .locate:
|
||||
color = defaultColor
|
||||
}
|
||||
|
||||
return RDEPUBTextOverlayDecoration(
|
||||
kind: kind,
|
||||
absoluteRange: NSRange(location: index, length: 1),
|
||||
rects: rects,
|
||||
color: color
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func overlayColor(hex: String, alpha: CGFloat) -> UIColor? {
|
||||
var value = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
value = value.replacingOccurrences(of: "#", with: "")
|
||||
guard value.count == 6, let hexValue = Int(value, radix: 16) else { return nil }
|
||||
return UIColor(
|
||||
red: CGFloat((hexValue >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((hexValue >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(hexValue & 0xFF) / 255,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ public final class RDEPUBWebView: UIView {
|
||||
public weak var delegate: RDEPUBWebViewDelegate?
|
||||
/// 渲染完成回调(与 delegate 并行使用)
|
||||
public var onRendered: (() -> Void)?
|
||||
/// 内部原生装饰解析回调
|
||||
var onDecorationsResolved: (([RDEPUBTextOverlayDecoration]) -> Void)?
|
||||
|
||||
/// 当前关联的出版物
|
||||
var publication: RDEPUBPublication?
|
||||
@@ -143,6 +145,7 @@ public final class RDEPUBWebView: UIView {
|
||||
public func reset() {
|
||||
delegate = nil
|
||||
onRendered = nil
|
||||
onDecorationsResolved = nil
|
||||
publication = nil
|
||||
currentRenderRequest = nil
|
||||
currentSpineIndex = 0
|
||||
|
||||
@@ -153,47 +153,195 @@
|
||||
var absoluteLeft = Math.max(0, rect.left + currentPageOffset());
|
||||
return Math.max(0, Math.floor(absoluteLeft / effectivePageStride()));
|
||||
}
|
||||
function unwrapHighlights() {
|
||||
document.querySelectorAll('mark.ss-reader-highlight').forEach(function(mark) {
|
||||
var parent = mark.parentNode;
|
||||
if (!parent) { return; }
|
||||
while (mark.firstChild) {
|
||||
parent.insertBefore(mark.firstChild, mark);
|
||||
}
|
||||
parent.removeChild(mark);
|
||||
});
|
||||
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) {
|
||||
unwrapHighlights();
|
||||
if (!Array.isArray(items)) { return; }
|
||||
items.forEach(function(item) {
|
||||
if (!item || !item.rangeInfo) { return; }
|
||||
var range = rangeFromInfo(item.rangeInfo);
|
||||
if (!range || range.collapsed) { return; }
|
||||
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 {
|
||||
var mark = document.createElement('mark');
|
||||
var style = item.style || 'highlight';
|
||||
mark.className = 'ss-reader-highlight ss-reader-highlight-' + style;
|
||||
mark.style.color = 'inherit';
|
||||
if (style === 'underline') {
|
||||
mark.style.background = 'transparent';
|
||||
mark.style.textDecorationLine = 'underline';
|
||||
mark.style.textDecorationStyle = 'solid';
|
||||
mark.style.textDecorationThickness = '2px';
|
||||
mark.style.textDecorationColor = item.color || '#F8E16C';
|
||||
} else {
|
||||
mark.style.background = item.color || '#F8E16C';
|
||||
}
|
||||
mark.dataset.highlightId = item.id || '';
|
||||
var fragment = range.extractContents();
|
||||
mark.appendChild(fragment);
|
||||
range.insertNode(mark);
|
||||
if (mark.parentNode && mark.parentNode.normalize) {
|
||||
mark.parentNode.normalize();
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 selection = window.getSelection();
|
||||
@@ -290,7 +438,9 @@
|
||||
this.scrollToPage(derivedPageIndex);
|
||||
},
|
||||
setHighlights: setHighlights,
|
||||
clearHighlights: unwrapHighlights,
|
||||
clearHighlights: clearHighlights,
|
||||
setSearchPresentation: setSearchPresentation,
|
||||
resolveDecorations: resolveDecorations,
|
||||
reportProgression: reportProgression,
|
||||
selectionPayload: selectionPayload
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* 适配搜狗百科 bar 里的 icon 被染色 */
|
||||
.header-home, .header-home:before, .header-logo, .loginBox, .header-search, .header-search:before, .header-more:before {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 适配搜狗百科 某些透明的 div 被染色导致挡住图片 */
|
||||
.abstract-img-none {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* 适配百度搜索图片丢失 */
|
||||
.c-touchable-feedback-no-default * {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
img, video {
|
||||
z-index: 1 !important;
|
||||
}
|
||||
|
||||
/*背景纯黑*/
|
||||
*, *:before, *:after {
|
||||
background-color: rgba(0, 0, 0, 1) !important;
|
||||
}
|
||||
|
||||
.wr-business-container,
|
||||
.wr-business-container *,
|
||||
.wr-business-container *:before,
|
||||
.wr-business-container *:after {
|
||||
background-color: rgb(28, 28, 29) !important;
|
||||
}
|
||||
|
||||
/*背景颜色和一般字体颜色*/
|
||||
div, h1, h2, h3, h4, h5, h6, p, body, em, html, link, textarea, form, select, input, span, button, em, menu, aside, table, tr, td, nav, dl, dt, dd, amp-iframe, main, section {
|
||||
color: rgba(180, 180, 182, 1) !important;
|
||||
border-color: #555555 !important;
|
||||
text-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
/*超链接*/
|
||||
a {
|
||||
color: rgba(84, 127, 176, 1) !important;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color : rgb(218, 220, 224);
|
||||
background-color : rgb(10, 14, 18) !important;
|
||||
border-left: 1px solid rgba(116, 120, 124, 1);
|
||||
}
|
||||
|
||||
img {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
ul li:before, ol li:before {
|
||||
background-color: rgba(196, 200, 204, 1) !important;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* this file is processed with xxd via a build rule and embedded in library */
|
||||
|
||||
/* these styles come from Safari */
|
||||
|
||||
/* note that comments are only permitted before selectors and before the styles */
|
||||
|
||||
/* DO NOT fiddle with this file if you want to have your own styles,
|
||||
pass your own stylesheet via the option parameter to override these defaults */
|
||||
|
||||
head {
|
||||
display:none;
|
||||
}
|
||||
|
||||
title {
|
||||
display:none;
|
||||
}
|
||||
|
||||
style {
|
||||
display:none;
|
||||
}
|
||||
|
||||
link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
meta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
script {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html {
|
||||
display:block;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
body {
|
||||
display:block;
|
||||
font-size:16px;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
article,aside,footer,header,hgroup,nav,section {
|
||||
display:block;
|
||||
}
|
||||
|
||||
p {
|
||||
display:block;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
img {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
ul,ol,menu,dir {
|
||||
display:block;
|
||||
margin:1em 0 1em 0;
|
||||
padding-left:24px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type:disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type:decimal;
|
||||
}
|
||||
|
||||
li {
|
||||
display:list-item;
|
||||
}
|
||||
|
||||
ul ul, ol ul {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
ol ol ul, ol ul ul, ul ol ul, ul ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family:Courier;
|
||||
}
|
||||
|
||||
pre, xmp, plaintext, listing {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color:#2262A3;
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color:#2262A3;
|
||||
}
|
||||
|
||||
center {
|
||||
text-align:center;
|
||||
display:block;
|
||||
}
|
||||
|
||||
strong,b {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
i,em {
|
||||
font-style:italic;
|
||||
}
|
||||
|
||||
u {
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
big {
|
||||
font-size:bigger;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size:smaller;
|
||||
}
|
||||
|
||||
sub {
|
||||
font-size:smaller;
|
||||
vertical-align:sub;
|
||||
}
|
||||
|
||||
sup {
|
||||
font-size:smaller;
|
||||
vertical-align:super;
|
||||
}
|
||||
|
||||
s,strike,del {
|
||||
text-decoration:line-through;
|
||||
}
|
||||
|
||||
tt,code,kbd,samp {
|
||||
font-family:monospace;
|
||||
}
|
||||
|
||||
pre,xmp,plaintext,listing {
|
||||
display:block;
|
||||
font-family:monospace;
|
||||
white-space:pre;
|
||||
margin-top:1em;
|
||||
margin-right:0;
|
||||
margin-bottom:1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color:rgba(0,0,0,.05);
|
||||
padding-top:1em;
|
||||
padding-bottom:1em;
|
||||
border-radius:0.3em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
display:block;
|
||||
font-size:1.5em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display:block;
|
||||
font-size:1.4em;
|
||||
margin-top: 0.83em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
display:block;
|
||||
font-size:1.3em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
display:block;
|
||||
font-size:1.2em;
|
||||
margin-top: 1.33em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
display:block;
|
||||
font-size:1.1em;
|
||||
margin-top: 1.67em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
display:block;
|
||||
font-size:1em;
|
||||
margin-top: 2.33em;
|
||||
}
|
||||
|
||||
div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
margin:0.5em auto 0.5em auto;
|
||||
border-style: inset;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
table {
|
||||
display: table;
|
||||
border-collapse: separate;
|
||||
border-spacing: 2px;
|
||||
border-color: gray;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/* <pre>代码块,注意必须写font-weight使字体生效 */
|
||||
pre {
|
||||
font-family: "Menlo";
|
||||
font-weight: normal;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
/*版权信息*/
|
||||
.copyRightTitle {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*图片说明文字*/
|
||||
.eepub-single-image-title {
|
||||
font-size: 0.75em;
|
||||
text-align: center;
|
||||
line-height: 1.4em;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
margin: 0.2em 0.4em 1em 0.4em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*标题*/
|
||||
.firstTitle, h1.firstTitle {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.secondTitle, h2.secondTitle {
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
line-height: 1.35em;
|
||||
}
|
||||
.thirdTitle, h3.thirdTitle {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.fourthTitle, h4.fourthTitle {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
line-height: 1.65em;
|
||||
}
|
||||
.fifthTitle, h5.fifthTitle {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
line-height: 1.85em;
|
||||
}
|
||||
.sixthTitle, h6.sixthTitle {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
/*首字加大*/
|
||||
/*浮动元素有默认的margin,所以这里会修复一下*/
|
||||
.ftext {
|
||||
float: left;
|
||||
margin: 0em;
|
||||
font-size: 2.38em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*引用内容*/
|
||||
.conQuot {
|
||||
font-weight: normal;
|
||||
margin: 0em 0em 0.2em 0em;
|
||||
}
|
||||
|
||||
/*标题下来可能会有一行subHead*/
|
||||
.subHead{
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
pre, pre span, pre code {
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size:.7em!important;
|
||||
}
|
||||
|
||||
.bodyPic {
|
||||
wr-vertical-center-style: 2;
|
||||
}
|
||||
|
||||
.qrbodyPic {
|
||||
page-break-inside: avoid;
|
||||
wr-vertical-center-style: 2;
|
||||
}
|
||||
|
||||
/* 注标图 */
|
||||
.qqreader-footnote {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
/* 私有垂直居中类 */
|
||||
.wr-vertical-center {
|
||||
wr-vertical-center-style: 1 !important;
|
||||
}
|
||||
|
||||
/* 翻译 */
|
||||
.wr-translation {
|
||||
line-height: 1.7em !important;
|
||||
}
|
||||
|
||||
/* 章节结尾工具 */
|
||||
.book-chapter-tool {
|
||||
display: block;
|
||||
height: 78px;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/* <pre>代码块,注意必须写font-weight使字体生效 */
|
||||
pre {
|
||||
font-family: "Menlo";
|
||||
font-weight: normal;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
/*版权信息*/
|
||||
.copyRightTitle {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*图片说明文字*/
|
||||
.eepub-single-image-title {
|
||||
font-size: 0.75em;
|
||||
text-align: center;
|
||||
line-height: 1.4em;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
margin: 0.2em 0.4em 1em 0.4em;
|
||||
font-family: "FZFSJW--GB1-0";
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*标题*/
|
||||
.firstTitle, h1.firstTitle {
|
||||
font-size: 1.5em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
.secondTitle, h2.secondTitle {
|
||||
font-size: 1.4em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 1.35em;
|
||||
}
|
||||
.thirdTitle, h3.thirdTitle {
|
||||
font-size: 1.3em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
.fourthTitle, h4.fourthTitle {
|
||||
font-size: 1.2em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 1.65em;
|
||||
}
|
||||
.fifthTitle, h5.fifthTitle {
|
||||
font-size: 1.1em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 1.85em;
|
||||
}
|
||||
.sixthTitle, h6.sixthTitle {
|
||||
font-size: 1em;
|
||||
font-family: "Source Han Serif CN";
|
||||
font-weight: bold;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
/*首字加大*/
|
||||
/*浮动元素有默认的margin,所以这里会修复一下*/
|
||||
.ftext {
|
||||
float: left;
|
||||
margin: 0em;
|
||||
font-size: 2.38em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*引用内容*/
|
||||
.conQuot {
|
||||
font-family: "FZFSJW--GB1-0";
|
||||
font-weight: normal;
|
||||
margin: 0em 0em 0.2em 0em;
|
||||
}
|
||||
|
||||
/*标题下来可能会有一行subHead*/
|
||||
.subHead{
|
||||
text-indent: 2em;
|
||||
}
|
||||
|
||||
pre, pre span, pre code {
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size:.7em!important;
|
||||
}
|
||||
|
||||
.bodyPic {
|
||||
wr-vertical-center-style: 2;
|
||||
}
|
||||
|
||||
.qrbodyPic {
|
||||
page-break-inside: avoid;
|
||||
wr-vertical-center-style: 2;
|
||||
}
|
||||
|
||||
/* 注标图 */
|
||||
.qqreader-footnote {
|
||||
width: 1em;
|
||||
}
|
||||
|
||||
/* 翻译 */
|
||||
.wr-translation {
|
||||
line-height: 1.7em !important;
|
||||
}
|
||||
|
||||
/*-------------------- 小文章相关 ----------------------*/
|
||||
|
||||
/*epub-custom-rule-6 如果 class="weread-page-relate" 在开头,需要把上一页末尾一行移到这一页*/
|
||||
.weread-page-relate {
|
||||
weread-page-relate:true;
|
||||
}
|
||||
|
||||
/*epub-custom-rule-29 小文章打赏的样式*/
|
||||
.chapter-reward {
|
||||
display: block;
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/*epub-custom-rule-7 小文章工具栏标签的样式*/
|
||||
.chapter-tool {
|
||||
display: block;
|
||||
height: 140px;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* 章节结尾工具 */
|
||||
.book-chapter-tool {
|
||||
display: block;
|
||||
height: 78px;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* 调试好样式后和安卓同步,交给后台随小文章 css 下发,并把下面的样式从replace.css 删除 */
|
||||
|
||||
/* 安卓的re_bookItem是通过原生view留出上下空间的,但是iOS是通过css来控制 */
|
||||
.re_bookItem{
|
||||
display: block;
|
||||
margin: 18px 0 18px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align:justify;
|
||||
}
|
||||
|
||||
img[data-image-size="large"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 文集公众号文章不支持控件的样式 */
|
||||
.unsupported_iframe {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffcdc0;
|
||||
background: #fee;
|
||||
padding: 12px 0px;
|
||||
color: #ff8d8d;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 私有垂直居中类 */
|
||||
.wr-vertical-center {
|
||||
wr-vertical-center-style: 1 !important;
|
||||
}
|
||||
Reference in New Issue
Block a user