ReadViewSDK/Sources/RDEpubReaderView/EPUBCore/Notes/RDEPUBNoteResolver.swift
shenlei d7fcda345d 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
2026-07-10 19:44:53 +09:00

169 lines
6.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.

import Foundation
public final class RDEPUBNoteResolver {
private let resourceResolver: RDEPUBResourceResolver
public init(resourceResolver: RDEPUBResourceResolver) {
self.resourceResolver = resourceResolver
}
public func resolveInternalLink(
sourceHref: String,
sourceCFI: String?,
targetLocation: RDEPUBLocation,
sourceLocation: RDEPUBLocation? = nil,
relativeToSpineIndex spineIndex: Int?
) -> RDEPUBResolvedNote? {
guard let normalizedTarget = resourceResolver.normalizedLocation(
targetLocation,
relativeToSpineIndex: spineIndex,
bookIdentifier: targetLocation.bookIdentifier
) else {
return nil
}
let fragment = normalizedTarget.fragment
guard let elementHTML = extractElementHTML(href: normalizedTarget.href, fragment: fragment) else {
return nil
}
guard let reference = RDEPUBNoteDetector.makeReferenceIfLikelyNote(
sourceHref: sourceHref,
sourceCFI: sourceCFI,
targetHref: normalizedTarget.href,
targetFragment: fragment,
targetCFI: normalizedTarget.cfi,
targetElementHTML: elementHTML
) else {
return nil
}
let sanitizedHTML = stripBacklinks(from: elementHTML)
let plainText = plainText(from: sanitizedHTML)
guard !plainText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
return RDEPUBResolvedNote(
reference: reference,
sourceLocation: sourceLocation,
targetLocation: normalizedTarget,
title: title(for: reference),
html: sanitizedHTML,
plainText: plainText
)
}
public func extractElementHTML(href: String, fragment: String?) -> String? {
guard let fragment,
!fragment.isEmpty,
let fileURL = resourceResolver.fileURL(forRelativePath: href),
let html = chapterHTML(at: fileURL) else {
return nil
}
return elementHTML(withID: fragment, in: html)
}
/// HTML provider
private func chapterHTML(at fileURL: URL) -> String? {
if let provided = resourceResolver.providedResourceData(at: fileURL) {
return String(data: provided, encoding: .utf8)
?? String(data: provided, encoding: .utf16)
}
return try? String(contentsOf: fileURL, encoding: .utf8)
}
private func elementHTML(withID id: String, in html: String) -> String? {
let escapedID = NSRegularExpression.escapedPattern(for: id)
let pattern = #"<([A-Za-z][A-Za-z0-9:_-]*)(?=[^>]*(?:id|xml:id)\s*=\s*(['"])"# + escapedID + #"\2)[^>]*>"#
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
return nil
}
let nsRange = NSRange(html.startIndex..<html.endIndex, in: html)
guard let match = regex.firstMatch(in: html, options: [], range: nsRange),
let startRange = Range(match.range, in: html),
let tagRange = Range(match.range(at: 1), in: html) else {
return nil
}
let tagName = String(html[tagRange])
guard !String(html[startRange]).hasSuffix("/>") else {
return String(html[startRange])
}
guard let endRange = matchingEndTagRange(
tagName: tagName,
in: html,
after: startRange.upperBound
) else {
return String(html[startRange])
}
return String(html[startRange.lowerBound..<endRange.upperBound])
}
private func stripBacklinks(from html: String) -> String {
let backlinkPattern = #"<a\b[^>]*(?:rev\s*=\s*['"]footnote['"]|epub:type\s*=\s*['"][^'"]*(?:backlink|return)[^'"]*['"]|class\s*=\s*['"][^'"]*(?:backlink|return)[^'"]*['"])[^>]*>[\s\S]*?</a>"#
guard let regex = try? NSRegularExpression(pattern: backlinkPattern, options: [.caseInsensitive]) else {
return html
}
let nsRange = NSRange(html.startIndex..<html.endIndex, in: html)
return regex.stringByReplacingMatches(in: html, options: [], range: nsRange, withTemplate: "")
}
private func plainText(from html: String) -> String {
let withoutScripts = html
.replacingOccurrences(of: #"<script[\s\S]*?</script>"#, with: "", options: .regularExpression)
.replacingOccurrences(of: #"<style[\s\S]*?</style>"#, with: "", options: .regularExpression)
let withoutTags = withoutScripts.replacingOccurrences(of: #"<[^>]+>"#, with: " ", options: .regularExpression)
return withoutTags
.replacingOccurrences(of: "&nbsp;", with: " ")
.replacingOccurrences(of: "&lt;", with: "<")
.replacingOccurrences(of: "&gt;", with: ">")
.replacingOccurrences(of: "&amp;", with: "&")
.components(separatedBy: .whitespacesAndNewlines)
.filter { !$0.isEmpty }
.joined(separator: " ")
}
private func title(for reference: RDEPUBNoteReference) -> String {
switch reference.kind {
case .footnote:
return ""
case .endnote:
return ""
case .rearnote:
return ""
case .unknown:
return ""
}
}
private func matchingEndTagRange(
tagName: String,
in html: String,
after startIndex: String.Index
) -> Range<String.Index>? {
let escapedTagName = NSRegularExpression.escapedPattern(for: tagName)
let pattern = #"</?\s*"# + escapedTagName + #"\b[^>]*>"#
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
return nil
}
let searchRange = NSRange(startIndex..<html.endIndex, in: html)
var depth = 1
for match in regex.matches(in: html, options: [], range: searchRange) {
guard let range = Range(match.range, in: html) else { continue }
let tag = String(html[range])
if tag.hasPrefix("</") {
depth -= 1
} else if !tag.hasSuffix("/>") {
depth += 1
}
if depth == 0 {
return range
}
}
return nil
}
}