refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum RDEPUBJavaScriptBridgeMessage: String, CaseIterable {
|
||||
|
||||
case progressionChanged = "ssReaderProgressionChanged"
|
||||
|
||||
case selectionChanged = "ssReaderSelectionChanged"
|
||||
|
||||
case internalLink = "ssReaderInternalLink"
|
||||
|
||||
case externalLink = "ssReaderExternalLink"
|
||||
|
||||
case javaScriptError = "ssReaderJSError"
|
||||
|
||||
case fixedLayoutReady = "ssReaderFixedLayoutReady"
|
||||
|
||||
case imageDidTap = "ssReaderImageDidTap"
|
||||
|
||||
case footnoteDidTap = "ssReaderFootnoteDidTap"
|
||||
}
|
||||
|
||||
enum RDEPUBJavaScriptBridge {
|
||||
|
||||
static var messageNames: [String] {
|
||||
RDEPUBJavaScriptBridgeMessage.allCases.map(\.rawValue)
|
||||
}
|
||||
|
||||
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,
|
||||
"IMAGE_DID_TAP_MESSAGE": RDEPUBJavaScriptBridgeMessage.imageDidTap.rawValue,
|
||||
"FOOTNOTE_DID_TAP_MESSAGE": RDEPUBJavaScriptBridgeMessage.footnoteDidTap.rawValue
|
||||
]
|
||||
)
|
||||
].joined(separator: "\n\n")
|
||||
}
|
||||
|
||||
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),
|
||||
\(javaScriptStringLiteral(request.targetHighlightRangeInfo))
|
||||
);
|
||||
} 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;
|
||||
})();
|
||||
"""
|
||||
}
|
||||
|
||||
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();
|
||||
})();
|
||||
"""
|
||||
}
|
||||
|
||||
private static func escapedJavaScriptTemplateLiteral(_ string: String) -> String {
|
||||
string
|
||||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||||
.replacingOccurrences(of: "`", with: "\\`")
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// Encodes a string value as a safe JavaScript string literal.
|
||||
///
|
||||
/// Uses JSON serialization to handle escaping of quotes, backslashes, control
|
||||
/// characters, and Unicode, then strips the array wrapper to produce a standalone
|
||||
/// JS string literal (including surrounding quotes).
|
||||
///
|
||||
/// - Parameter value: The string to encode, or `nil` which produces the JS keyword `null`.
|
||||
/// - Returns: A JavaScript string literal safe for inline embedding in JS source.
|
||||
private static func javaScriptStringLiteral(_ value: String?) -> String {
|
||||
guard let value else { return "null" }
|
||||
guard JSONSerialization.isValidJSONObject([value]),
|
||||
let data = try? JSONSerialization.data(withJSONObject: [value], options: []),
|
||||
let arrayString = String(data: data, encoding: .utf8) else {
|
||||
return "null"
|
||||
}
|
||||
// JSON encodes ["value"] as `["value"]`. Drop the leading `[` and trailing `]`
|
||||
// to yield `"value"` — a valid JS string literal with all special characters escaped.
|
||||
return String(arrayString.dropFirst().dropLast())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user