ReadViewSDK/Sources/RDReaderView/EPUBCore/RDEPUBJavaScriptBridge.swift

188 lines
8.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// RDEPUBJavaScriptBridge.swift
// WebView JavaScript
// JS progression/selection/link/error
// WebView
import Foundation
/// JS WebView
enum RDEPUBJavaScriptBridgeMessage: String, CaseIterable {
///
case progressionChanged = "ssReaderProgressionChanged"
///
case selectionChanged = "ssReaderSelectionChanged"
/// ss-reader://
case internalLink = "ssReaderInternalLink"
/// http/https/mailto/tel
case externalLink = "ssReaderExternalLink"
/// JavaScript
case javaScriptError = "ssReaderJSError"
///
case fixedLayoutReady = "ssReaderFixedLayoutReady"
}
/// JavaScript
enum RDEPUBJavaScriptBridge {
/// JS WKUserContentController
static var messageNames: [String] {
RDEPUBJavaScriptBridgeMessage.allCases.map(\.rawValue)
}
/// WebView document end
/// 使 JS window.webkit.messageHandlers
static var userScript: String {
[
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")
}
///
/// CSS
/// - Parameter request:
/// - Returns: evaluateJavaScript
static func applyPresentationScript(for request: RDEPUBReflowableRenderRequest) -> String {
let style = escapedJavaScriptTemplateLiteral(RDEPUBStyleSheetBuilder.renderCSS(for: request.presentation))
let pageStride = max(1, request.presentation.viewportSize.width)
let searchScript = applySearchScript(for: request.searchPresentation)
return """
(function() {
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.WeReadApi.scrollToLocation(\(jsonString(from: targetLocationPayload(request.targetLocation), fallback: "null")), \(request.pageIndex));
} else {
window.WeReadApi.scrollToPage(\(request.pageIndex));
}
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
/// 使
/// - Parameter presentation:
/// - Returns:
static func applySearchScript(for presentation: RDEPUBSearchPresentation?) -> String {
let payload = jsonString(from: searchPayload(presentation), fallback: "null")
return """
(function() {
if (!window.WeReadApi) { return false; }
window.WeReadApi.setSearchPresentation(\(payload));
return true;
})();
"""
}
static func resolveDecorationsScript() -> String {
"""
(function() {
if (!window.WeReadApi) { return { highlights: [], search: [] }; }
return window.WeReadApi.resolveDecorations();
})();
"""
}
/// JS
private static func escapedJavaScriptTemplateLiteral(_ string: String) -> String {
string
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "`", with: "\\`")
}
/// JS
private static func highlightsPayload(_ highlights: [RDEPUBHighlight]) -> [[String: String]] {
highlights.compactMap { highlight in
guard let rangeInfo = highlight.rangeInfo, !rangeInfo.isEmpty else { return nil }
return [
"id": highlight.id,
"rangeInfo": rangeInfo,
"color": highlight.color,
"style": highlight.style.rawValue
]
}
}
/// JS progressionfragment
private static func targetLocationPayload(_ location: RDEPUBLocation?) -> [String: Any]? {
guard let location else { return nil }
return [
"progression": location.progression,
"lastProgression": location.lastProgression ?? location.progression,
"fragment": location.fragment as Any
]
}
/// JS
private static func searchPayload(_ presentation: RDEPUBSearchPresentation?) -> [String: Any]? {
guard let presentation else { return nil }
return [
"keyword": presentation.keyword,
"resources": presentation.resources.map { resource in
[
"href": resource.href,
"matchCount": resource.matchCount,
"activeLocalMatchIndex": resource.activeLocalMatchIndex as Any
]
}
]
}
/// JSON fallback
private static func jsonString(from object: Any?, fallback: String) -> String {
guard let object else { return fallback }
guard JSONSerialization.isValidJSONObject(object),
let data = try? JSONSerialization.data(withJSONObject: object, options: []),
let string = String(data: data, encoding: .utf8) else {
return fallback
}
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: "")
}
}