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..") 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.. String {
let backlinkPattern = #"]*(?:rev\s*=\s*['"]footnote['"]|epub:type\s*=\s*['"][^'"]*(?:backlink|return)[^'"]*['"]|class\s*=\s*['"][^'"]*(?:backlink|return)[^'"]*['"])[^>]*>[\s\S]*?"#
guard let regex = try? NSRegularExpression(pattern: backlinkPattern, options: [.caseInsensitive]) else {
return html
}
let nsRange = NSRange(html.startIndex.. String {
let withoutScripts = html
.replacingOccurrences(of: #"