feat(epub): complete wxread parity scope

This commit is contained in:
shen
2026-05-26 09:19:37 +08:00
parent 23182e8b5a
commit 0e7d952fe5
31 changed files with 2619 additions and 1533 deletions
@@ -7,6 +7,14 @@ import Foundation
/// EPUB
enum RDEPUBAsset: String {
/// Rangy core Web range/selection
case rangyCoreScript = "rangy-core"
/// Rangy serializer DOM range /
case rangySerializerScript = "rangy-serializer"
/// WXRead CSS
case cssInjectorScript = "cssInjector"
/// WXRead WeReadApi JS-Native
case weReadAPIScript = "WeReadApi"
/// JS epub-bridge.js WebView -JS
case bridgeScript = "epub-bridge"
/// HTML epub-fixed-layout.html pre-paginated EPUB
@@ -23,7 +31,7 @@ enum RDEPUBAsset: String {
///
var fileExtension: String {
switch self {
case .bridgeScript:
case .rangyCoreScript, .rangySerializerScript, .cssInjectorScript, .weReadAPIScript, .bridgeScript:
return "js"
case .fixedLayoutTemplate:
return "html"
@@ -31,17 +31,23 @@ enum RDEPUBJavaScriptBridge {
/// WebView document end
/// 使 JS window.webkit.messageHandlers
static var userScript: String {
RDEPUBAssetRepository.string(
for: .bridgeScript,
replacements: [
"PROGRESSION_CHANGED_MESSAGE": RDEPUBJavaScriptBridgeMessage.progressionChanged.rawValue,
"SELECTION_CHANGED_MESSAGE": RDEPUBJavaScriptBridgeMessage.selectionChanged.rawValue,
"INTERNAL_LINK_MESSAGE": RDEPUBJavaScriptBridgeMessage.internalLink.rawValue,
"EXTERNAL_LINK_MESSAGE": RDEPUBJavaScriptBridgeMessage.externalLink.rawValue,
"JAVASCRIPT_ERROR_MESSAGE": RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue,
"FIXED_LAYOUT_READY_MESSAGE": RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue
]
)
[
RDEPUBAssetRepository.string(for: .rangyCoreScript),
RDEPUBAssetRepository.string(for: .rangySerializerScript),
RDEPUBAssetRepository.string(for: .cssInjectorScript),
RDEPUBAssetRepository.string(for: .weReadAPIScript),
RDEPUBAssetRepository.string(
for: .bridgeScript,
replacements: [
"PROGRESSION_CHANGED_MESSAGE": RDEPUBJavaScriptBridgeMessage.progressionChanged.rawValue,
"SELECTION_CHANGED_MESSAGE": RDEPUBJavaScriptBridgeMessage.selectionChanged.rawValue,
"INTERNAL_LINK_MESSAGE": RDEPUBJavaScriptBridgeMessage.internalLink.rawValue,
"EXTERNAL_LINK_MESSAGE": RDEPUBJavaScriptBridgeMessage.externalLink.rawValue,
"JAVASCRIPT_ERROR_MESSAGE": RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue,
"FIXED_LAYOUT_READY_MESSAGE": RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue
]
)
].joined(separator: "\n\n")
}
///
@@ -55,23 +61,42 @@ enum RDEPUBJavaScriptBridge {
return """
(function() {
if (!window.RDReaderBridge) { return false; }
window.RDReaderBridge.applyPagination(`\(style)`);
window.RDReaderBridge.setPageMetrics(\(request.totalPagesInChapter), \(pageStride));
window.RDReaderBridge.clearHighlights();
window.RDReaderBridge.setHighlights(\(jsonString(from: highlightsPayload(request.highlights), fallback: "[]")));
if (!window.WeReadApi) { return false; }
window.WeReadApi.applySharedTheme({
backgroundColor: \(javaScriptStringLiteral(request.presentation.themeBackgroundColor)),
textColor: \(javaScriptStringLiteral(request.presentation.themeTextColor)),
includeFrames: false
});
window.WeReadApi.applyPagination(`\(style)`);
window.WeReadApi.setPageMetrics(\(request.totalPagesInChapter), \(pageStride));
window.WeReadApi.clearHighlights();
window.WeReadApi.setHighlights(\(jsonString(from: highlightsPayload(request.highlights), fallback: "[]")));
if (\(jsonString(from: targetLocationPayload(request.targetLocation), fallback: "null")) !== null) {
window.RDReaderBridge.scrollToLocation(\(jsonString(from: targetLocationPayload(request.targetLocation), fallback: "null")), \(request.pageIndex));
window.WeReadApi.scrollToLocation(\(jsonString(from: targetLocationPayload(request.targetLocation), fallback: "null")), \(request.pageIndex));
} else {
window.RDReaderBridge.scrollToPage(\(request.pageIndex));
window.WeReadApi.scrollToPage(\(request.pageIndex));
}
window.RDReaderBridge.reportProgression();
window.WeReadApi.reportProgression();
return true;
})();
\(searchScript)
"""
}
static func applyFixedPresentationScript(for request: RDEPUBFixedRenderRequest) -> String {
"""
(function() {
if (!window.WeReadApi) { return false; }
window.WeReadApi.applySharedTheme({
backgroundColor: \(javaScriptStringLiteral(request.backgroundColorCSS)),
textColor: null,
includeFrames: true
});
return true;
})();
"""
}
///
/// iframe span
/// 使
@@ -81,8 +106,8 @@ enum RDEPUBJavaScriptBridge {
let payload = jsonString(from: searchPayload(presentation), fallback: "null")
return """
(function() {
if (!window.RDReaderBridge) { return false; }
window.RDReaderBridge.setSearchPresentation(\(payload));
if (!window.WeReadApi) { return false; }
window.WeReadApi.setSearchPresentation(\(payload));
return true;
})();
"""
@@ -91,8 +116,8 @@ enum RDEPUBJavaScriptBridge {
static func resolveDecorationsScript() -> String {
"""
(function() {
if (!window.RDReaderBridge) { return { highlights: [], search: [] }; }
return window.RDReaderBridge.resolveDecorations();
if (!window.WeReadApi) { return { highlights: [], search: [] }; }
return window.WeReadApi.resolveDecorations();
})();
"""
}
@@ -152,4 +177,11 @@ enum RDEPUBJavaScriptBridge {
}
return string
}
private static func javaScriptStringLiteral(_ value: String?) -> String {
guard let value else { return "null" }
return jsonString(from: [value], fallback: "[null]")
.replacingOccurrences(of: "[", with: "")
.replacingOccurrences(of: "]", with: "")
}
}
@@ -25,12 +25,18 @@ public struct RDEPUBPreferences: Equatable {
public var fixedLayoutFit: RDEPUBFixedLayoutFit
/// never/always/automatic
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode
///
public var numberOfColumns: Int
///
public var columnGap: CGFloat
public init(
fontSize: CGFloat,
lineHeightMultiple: CGFloat,
reflowableContentInsets: UIEdgeInsets,
fixedContentInset: UIEdgeInsets,
numberOfColumns: Int = 1,
columnGap: CGFloat = 20,
themeBackgroundColor: String? = nil,
themeTextColor: String? = nil,
fixedBackgroundColor: String? = nil,
@@ -41,6 +47,8 @@ public struct RDEPUBPreferences: Equatable {
self.lineHeightMultiple = lineHeightMultiple
self.reflowableContentInsets = reflowableContentInsets
self.fixedContentInset = fixedContentInset
self.numberOfColumns = max(1, numberOfColumns)
self.columnGap = max(0, columnGap)
self.themeBackgroundColor = themeBackgroundColor
self.themeTextColor = themeTextColor
self.fixedBackgroundColor = fixedBackgroundColor
@@ -55,6 +63,8 @@ public struct RDEPUBPreferences: Equatable {
contentInsets: reflowableContentInsets,
fontSize: fontSize,
lineHeightMultiple: lineHeightMultiple,
numberOfColumns: numberOfColumns,
columnGap: columnGap,
themeBackgroundColor: themeBackgroundColor,
themeTextColor: themeTextColor
)
@@ -99,4 +109,4 @@ public struct RDEPUBPreferences: Equatable {
)
)
}
}
}
@@ -179,6 +179,13 @@ public enum RDEPUBHighlightStyle: String, Codable {
case underline
}
/// WXRead WRBookmark
public enum RDEPUBAnnotationKind: String, Codable, Equatable {
case bookmark
case highlight
case underline
}
///
public enum RDEPUBAnnotationMenuAction: Equatable {
case copy
@@ -396,6 +403,10 @@ public struct RDEPUBHighlight: Codable, Equatable {
try container.encodeIfPresent(note, forKey: .note)
try container.encode(createdAt, forKey: .createdAt)
}
public var annotation: RDEPUBAnnotation {
RDEPUBAnnotation(highlight: self)
}
}
///
@@ -433,6 +444,108 @@ public struct RDEPUBBookmark: Codable, Equatable {
public var hasNote: Bool {
note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
}
public var annotation: RDEPUBAnnotation {
RDEPUBAnnotation(bookmark: self)
}
}
/// WXRead WRBookmark
public struct RDEPUBAnnotation: Codable, Equatable {
public var id: String
public var bookIdentifier: String?
public var kind: RDEPUBAnnotationKind
public var location: RDEPUBLocation
public var text: String?
public var rangeInfo: String?
public var color: String?
public var chapterTitle: String?
public var note: String?
public var createdAt: Date
public init(
id: String = UUID().uuidString,
bookIdentifier: String? = nil,
kind: RDEPUBAnnotationKind,
location: RDEPUBLocation,
text: String? = nil,
rangeInfo: String? = nil,
color: String? = nil,
chapterTitle: String? = nil,
note: String? = nil,
createdAt: Date = Date()
) {
self.id = id
self.bookIdentifier = bookIdentifier
self.kind = kind
self.location = location
self.text = text?.nilIfEmpty
self.rangeInfo = rangeInfo?.nilIfEmpty
self.color = color?.nilIfEmpty
self.chapterTitle = chapterTitle?.nilIfEmpty
self.note = note?.nilIfEmpty
self.createdAt = createdAt
}
public init(highlight: RDEPUBHighlight) {
self.init(
id: highlight.id,
bookIdentifier: highlight.bookIdentifier,
kind: highlight.style == .underline ? .underline : .highlight,
location: highlight.location,
text: highlight.text,
rangeInfo: highlight.rangeInfo,
color: highlight.color,
chapterTitle: nil,
note: highlight.note,
createdAt: highlight.createdAt
)
}
public init(bookmark: RDEPUBBookmark) {
self.init(
id: bookmark.id,
bookIdentifier: bookmark.bookIdentifier,
kind: .bookmark,
location: bookmark.location,
text: nil,
rangeInfo: nil,
color: nil,
chapterTitle: bookmark.chapterTitle,
note: bookmark.note,
createdAt: bookmark.createdAt
)
}
public var bookmark: RDEPUBBookmark? {
guard kind == .bookmark else { return nil }
return RDEPUBBookmark(
id: id,
bookIdentifier: bookIdentifier,
location: location,
chapterTitle: chapterTitle,
note: note,
createdAt: createdAt
)
}
public var highlight: RDEPUBHighlight? {
guard kind == .highlight || kind == .underline,
let text else {
return nil
}
return RDEPUBHighlight(
id: id,
bookIdentifier: bookIdentifier,
location: location,
text: text,
rangeInfo: rangeInfo,
style: kind == .underline ? .underline : .highlight,
color: color ?? "#F8E16C",
note: note,
createdAt: createdAt
)
}
}
///
@@ -41,6 +41,10 @@ public struct RDEPUBPresentationStyle: Equatable {
public var fontSize: CGFloat
/// 1.0
public var lineHeightMultiple: CGFloat
/// WXRead
public var numberOfColumns: Int
/// WXRead columnGap
public var columnGap: CGFloat
/// CSS "#FFFFFF"
public var themeBackgroundColor: String?
/// CSS "#000000"
@@ -51,6 +55,8 @@ public struct RDEPUBPresentationStyle: Equatable {
contentInsets: UIEdgeInsets,
fontSize: CGFloat,
lineHeightMultiple: CGFloat,
numberOfColumns: Int = 1,
columnGap: CGFloat = 20,
themeBackgroundColor: String? = nil,
themeTextColor: String? = nil
) {
@@ -58,6 +64,8 @@ public struct RDEPUBPresentationStyle: Equatable {
self.contentInsets = contentInsets
self.fontSize = fontSize
self.lineHeightMultiple = lineHeightMultiple
self.numberOfColumns = max(1, numberOfColumns)
self.columnGap = max(0, columnGap)
self.themeBackgroundColor = themeBackgroundColor
self.themeTextColor = themeTextColor
}
@@ -170,4 +178,4 @@ public enum RDEPUBRenderRequest: Equatable {
return request.spread.primaryResource.href
}
}
}
}
@@ -28,6 +28,7 @@ public enum RDEPUBStyleSheetBuilder {
let values = cssValues(for: presentation)
let backgroundCSS = presentation.themeBackgroundColor.map { "background: \($0) !important;" } ?? ""
let textCSS = presentation.themeTextColor.map { "color: \($0) !important;" } ?? ""
let columnCountCSS = values.numberOfColumns > 1 ? "-webkit-column-count: \(values.numberOfColumns) !important; column-count: \(values.numberOfColumns) !important;" : ""
return """
html {
@@ -70,6 +71,7 @@ public enum RDEPUBStyleSheetBuilder {
line-height: \(values.lineHeight) !important;
-webkit-column-width: \(values.contentWidth)px !important;
column-width: \(values.contentWidth)px !important;
\(columnCountCSS)
-webkit-column-gap: \(values.columnGap)px !important;
column-gap: \(values.columnGap)px !important;
-webkit-column-fill: auto !important;
@@ -109,6 +111,7 @@ public enum RDEPUBStyleSheetBuilder {
let values = cssValues(for: presentation)
let backgroundCSS = presentation.themeBackgroundColor.map { "background: \($0) !important;" } ?? ""
let textCSS = presentation.themeTextColor.map { "color: \($0) !important;" } ?? ""
let columnCountCSS = values.numberOfColumns > 1 ? "-webkit-column-count: \(values.numberOfColumns) !important; column-count: \(values.numberOfColumns) !important;" : ""
return """
html {
@@ -132,6 +135,7 @@ public enum RDEPUBStyleSheetBuilder {
line-height: \(values.lineHeight) !important;
-webkit-column-width: \(values.contentWidth)px !important;
column-width: \(values.contentWidth)px !important;
\(columnCountCSS)
-webkit-column-gap: \(values.columnGap)px !important;
column-gap: \(values.columnGap)px !important;
-webkit-column-fill: auto !important;
@@ -187,6 +191,7 @@ public enum RDEPUBStyleSheetBuilder {
viewportHeight: String,
contentWidth: String,
columnGap: String,
numberOfColumns: Int,
paddingTop: String,
paddingRight: String,
paddingBottom: String,
@@ -197,14 +202,18 @@ public enum RDEPUBStyleSheetBuilder {
let viewportWidth = max(1, presentation.viewportSize.width)
let viewportHeight = max(1, presentation.viewportSize.height)
let contentInsets = presentation.contentInsets
let contentWidth = max(1, viewportWidth - contentInsets.left - contentInsets.right)
let columnGap = max(0, contentInsets.left + contentInsets.right)
let numberOfColumns = max(1, presentation.numberOfColumns)
let availableWidth = max(1, viewportWidth - contentInsets.left - contentInsets.right)
let totalGap = CGFloat(numberOfColumns - 1) * max(0, presentation.columnGap)
let contentWidth = max(1, (availableWidth - totalGap) / CGFloat(numberOfColumns))
let columnGap = max(0, presentation.columnGap)
return (
viewportWidth: String(format: "%.3f", viewportWidth),
viewportHeight: String(format: "%.3f", viewportHeight),
contentWidth: String(format: "%.3f", contentWidth),
columnGap: String(format: "%.3f", columnGap),
numberOfColumns: numberOfColumns,
paddingTop: String(format: "%.3f", contentInsets.top),
paddingRight: String(format: "%.3f", contentInsets.right),
paddingBottom: String(format: "%.3f", contentInsets.bottom),
@@ -91,8 +91,10 @@ extension RDEPUBWebView {
func scheduleFixedLayoutReadyFallback() {
let workItem = DispatchWorkItem { [weak self] in
RDEPUBWebViewDebug.log(self?.debugScope ?? "ReaderWebView", message: "fixed ready fallback fired")
self?.applySearchDecorationsIfNeeded {
self?.rendered()
self?.applyFixedPresentationIfNeeded {
self?.refreshNativeDecorationsIfNeeded {
self?.rendered()
}
}
}
fixedLayoutReadyWorkItem = workItem
@@ -104,4 +106,25 @@ extension RDEPUBWebView {
fixedLayoutReadyWorkItem?.cancel()
fixedLayoutReadyWorkItem = nil
}
func applyFixedPresentationIfNeeded(completion: (() -> Void)? = nil) {
guard let webView,
let currentRenderRequest,
case .fixed(let request) = currentRenderRequest else {
completion?()
return
}
let script = RDEPUBJavaScriptBridge.applyFixedPresentationScript(for: request)
webView.evaluateJavaScript(script) { [weak self] _, error in
guard let self else {
completion?()
return
}
if let error {
self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription)
}
self.applySearchDecorationsIfNeeded(completion: completion)
}
}
}
@@ -21,7 +21,8 @@ extension RDEPUBWebView {
let progression = (body["progression"] as? NSNumber)?.doubleValue ?? 0
let lastProgression = (body["lastProgression"] as? NSNumber)?.doubleValue
let fragment = body["fragment"] as? String
return RDEPUBLocation(href: currentHref, progression: progression, lastProgression: lastProgression, fragment: fragment)
let href = body["href"] as? String ?? currentHref
return RDEPUBLocation(href: href, progression: progression, lastProgression: lastProgression, fragment: fragment)
}
}
@@ -108,11 +109,13 @@ extension RDEPUBWebView: WKScriptMessageHandler {
case RDEPUBJavaScriptBridgeMessage.javaScriptError.rawValue:
delegate?.epubWebView(self, didLogJavaScriptError: "\(message.body)")
case RDEPUBJavaScriptBridgeMessage.fixedLayoutReady.rawValue:
applySearchDecorationsIfNeeded {
self.rendered()
applyFixedPresentationIfNeeded {
self.refreshNativeDecorationsIfNeeded {
self.rendered()
}
}
default:
break
}
}
}
}
@@ -128,6 +128,8 @@ extension RDEPUBWebView {
String(format: "%.2f", presentation.contentInsets.right),
String(format: "%.2f", presentation.fontSize),
String(format: "%.2f", presentation.lineHeightMultiple),
String(presentation.numberOfColumns),
String(format: "%.2f", presentation.columnGap),
presentation.themeBackgroundColor ?? "",
presentation.themeTextColor ?? "",
targetSignature,
@@ -0,0 +1,67 @@
(function() {
if (window.WeReadApi) { return; }
function bridge() {
return window.RDReaderBridge || null;
}
function sharedThemeCSS(theme) {
if (!theme) { return ''; }
var backgroundColor = theme.backgroundColor || 'transparent';
var textColor = theme.textColor || 'inherit';
return [
':root {',
' color-scheme: light;',
'}',
'html, body {',
' background: ' + backgroundColor + ' !important;',
' color: ' + textColor + ' !important;',
'}',
'a {',
' -webkit-tap-highlight-color: transparent;',
'}'
].join('\n');
}
window.WeReadApi = {
applySharedTheme: function(theme) {
var css = sharedThemeCSS(theme);
if (window.RDInjectedCSS) {
window.RDInjectedCSS.setStyle('ss-reader-theme', css, {
includeFrames: !!(theme && theme.includeFrames)
});
}
return true;
},
applyPagination: function(styleText) {
return !!(bridge() && bridge().applyPagination(styleText));
},
setPageMetrics: function(pageCount, pageStride) {
return !!(bridge() && bridge().setPageMetrics(pageCount, pageStride));
},
scrollToPage: function(pageIndex) {
return !!(bridge() && bridge().scrollToPage(pageIndex));
},
scrollToLocation: function(location, fallbackPageIndex) {
return !!(bridge() && bridge().scrollToLocation(location, fallbackPageIndex));
},
setHighlights: function(items) {
return !!(bridge() && bridge().setHighlights(items));
},
clearHighlights: function() {
return !!(bridge() && bridge().clearHighlights());
},
setSearchPresentation: function(payload) {
return !!(bridge() && bridge().setSearchPresentation(payload));
},
resolveDecorations: function() {
return bridge() ? bridge().resolveDecorations() : { highlights: [], search: [] };
},
reportProgression: function(fragment) {
return !!(bridge() && bridge().reportProgression(fragment));
},
selectionPayload: function() {
return bridge() ? bridge().selectionPayload() : null;
}
};
})();
@@ -0,0 +1,45 @@
(function() {
if (window.RDInjectedCSS) { return; }
function applyStyle(doc, identifier, cssText) {
if (!doc || !doc.head) { return; }
var style = doc.getElementById(identifier);
if (!style) {
style = doc.createElement('style');
style.id = identifier;
doc.head.appendChild(style);
}
style.textContent = cssText || '';
}
function walkDocuments(rootDoc, includeFrames, visitor) {
if (!rootDoc) { return; }
visitor(rootDoc);
if (!includeFrames || !rootDoc.querySelectorAll) { return; }
Array.prototype.forEach.call(rootDoc.querySelectorAll('iframe'), function(frame) {
try {
if (!frame.contentDocument) { return; }
walkDocuments(frame.contentDocument, includeFrames, visitor);
} catch (error) {
}
});
}
window.RDInjectedCSS = {
setStyle: function(identifier, cssText, options) {
var includeFrames = !!(options && options.includeFrames);
walkDocuments(document, includeFrames, function(doc) {
applyStyle(doc, identifier, cssText);
});
},
removeStyle: function(identifier, options) {
var includeFrames = !!(options && options.includeFrames);
walkDocuments(document, includeFrames, function(doc) {
var style = doc.getElementById(identifier);
if (style && style.parentNode) {
style.parentNode.removeChild(style);
}
});
}
};
})();
@@ -7,48 +7,17 @@
} catch (error) {
}
});
function nodePath(node) {
var path = [];
var current = node;
while (current && current !== document) {
var parent = current.parentNode;
if (!parent) { break; }
var index = Array.prototype.indexOf.call(parent.childNodes, current);
path.unshift(index);
current = parent;
}
return path;
}
function nodeFromPath(path) {
var current = document;
for (var i = 0; i < path.length; i += 1) {
if (!current || !current.childNodes || current.childNodes.length <= path[i]) { return null; }
current = current.childNodes[path[i]];
}
return current;
}
function serializeRange(range) {
return JSON.stringify({
kind: 'dom-range',
startPath: nodePath(range.startContainer),
startOffset: range.startOffset,
endPath: nodePath(range.endContainer),
endOffset: range.endOffset
});
if (window.rangy && typeof window.rangy.serializeRange === 'function') {
return window.rangy.serializeRange(range, range && range.startContainer && range.startContainer.ownerDocument);
}
return null;
}
function rangeFromInfo(rangeInfo) {
try {
var payload = typeof rangeInfo === 'string' ? JSON.parse(rangeInfo) : rangeInfo;
var startNode = nodeFromPath(payload.startPath || []);
var endNode = nodeFromPath(payload.endPath || []);
if (!startNode || !endNode) { return null; }
var range = document.createRange();
range.setStart(startNode, payload.startOffset || 0);
range.setEnd(endNode, payload.endOffset || 0);
return range;
} catch (error) {
return null;
if (window.rangy && typeof window.rangy.deserializeRange === 'function') {
return window.rangy.deserializeRange(rangeInfo, document);
}
return null;
}
var visiblePageIndex = 0;
var configuredPageCount = 1;
@@ -153,6 +122,22 @@
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) {
@@ -228,6 +213,36 @@
});
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 = [];
@@ -344,17 +359,18 @@
};
}
function selectionPayload() {
var selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
var payloadContext = currentSelectionContext();
if (!payloadContext) {
return null;
}
var range = selection.getRangeAt(0);
var text = selection.toString();
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,
@@ -362,13 +378,54 @@
};
}
var selectionTimer = null;
document.addEventListener('selectionchange', function() {
function notifySelectionChange() {
clearTimeout(selectionTimer);
selectionTimer = setTimeout(function() {
var payload = selectionPayload();
window.webkit.messageHandlers.{{SELECTION_CHANGED_MESSAGE}}.postMessage(payload);
}, 120);
});
}
function handleDocumentClick(event) {
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);
@@ -384,30 +441,22 @@
node.addEventListener('error', scheduleRelayout, { passive: true });
});
}
document.addEventListener('click', function(event) {
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 });
}, true);
window.RDReaderBridge = {
applyPagination: function(styleText) {
if (isFixedLayoutDocument()) { return; }
var style = document.getElementById('ss-reader-pagination');
if (!style) {
style = document.createElement('style');
style.id = 'ss-reader-pagination';
document.head.appendChild(style);
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;
}
style.textContent = styleText;
ensurePaginationStructure();
installFrameHooks(document);
},
setPageMetrics: function(pageCount, pageStride) {
if (isFixedLayoutDocument()) { return; }
@@ -423,9 +472,15 @@
},
scrollToLocation: function(location, fallbackPageIndex) {
if (location && location.fragment) {
var target = document.getElementById(location.fragment) || document.querySelector('[name="' + location.fragment.replace(/"/g, '\\"') + '"]');
if (target) {
setVisiblePageIndex(pageIndexForElement(target));
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;
}
@@ -0,0 +1,13 @@
(function() {
if (window.rangy) { return; }
window.rangy = {
initialized: true,
init: function() {
return window.rangy;
},
createRange: function(doc) {
return (doc || document).createRange();
}
};
})();
@@ -0,0 +1,93 @@
(function() {
if (!window.rangy) {
window.rangy = {};
}
if (window.rangy.serializeRange && window.rangy.deserializeRange) {
return;
}
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 nodePath(node) {
var path = [];
var current = node;
while (current && current !== current.ownerDocument) {
var parent = current.parentNode;
if (!parent) { break; }
var index = Array.prototype.indexOf.call(parent.childNodes, current);
path.unshift(index);
current = parent;
}
return path;
}
function nodeFromPath(doc, path) {
var current = doc;
for (var index = 0; index < path.length; index += 1) {
if (!current || !current.childNodes || current.childNodes.length <= path[index]) {
return null;
}
current = current.childNodes[path[index]];
}
return current;
}
function documentContexts(rootDoc) {
var items = [];
if (!rootDoc) { return items; }
items.push(rootDoc);
var frames = rootDoc.querySelectorAll ? rootDoc.querySelectorAll('iframe') : [];
Array.prototype.forEach.call(frames, function(frame) {
try {
if (!frame.contentDocument) { return; }
documentContexts(frame.contentDocument).forEach(function(item) {
items.push(item);
});
} catch (error) {
}
});
return items;
}
window.rangy.serializeRange = function(range, doc) {
var ownerDocument = doc || (range && range.startContainer && range.startContainer.ownerDocument) || document;
return JSON.stringify({
kind: 'dom-range',
documentHref: normalizeHref(ownerDocument.location && ownerDocument.location.href),
startPath: nodePath(range.startContainer),
startOffset: range.startOffset,
endPath: nodePath(range.endContainer),
endOffset: range.endOffset
});
};
window.rangy.deserializeRange = function(rangeInfo, rootDoc) {
try {
var payload = typeof rangeInfo === 'string' ? JSON.parse(rangeInfo) : rangeInfo;
var documentHref = normalizeHref(payload.documentHref);
var resolvedDocument = (documentContexts(rootDoc || document).find(function(candidate) {
return normalizeHref(candidate.location && candidate.location.href) === documentHref;
})) || (rootDoc || document);
var startNode = nodeFromPath(resolvedDocument, payload.startPath || []);
var endNode = nodeFromPath(resolvedDocument, payload.endPath || []);
if (!startNode || !endNode) { return null; }
var range = resolvedDocument.createRange();
range.setStart(startNode, payload.startOffset || 0);
range.setEnd(endNode, payload.endOffset || 0);
return range;
} catch (error) {
return null;
}
};
})();