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
@@ -30,8 +30,19 @@ public final class RDEPUBChapterData {
public var attributedContent: NSAttributedString { chapter.attributedContent }
///
public var pages: [RDEPUBTextPage] { chapter.pages }
///
public var pageCount: Int { chapter.pages.count }
/// fragment ID
public var fragmentOffsets: [String: Int] { chapter.fragmentOffsets }
/// /
public var chapterInfo: EPUBChapterInfo {
EPUBChapterInfo(spineIndex: spineIndex, title: title, pageCount: pageCount)
}
///
public var absolutePageRange: ClosedRange<Int>? {
guard let firstPage = pages.first, let lastPage = pages.last else { return nil }
return firstPage.absolutePageIndex...lastPage.absolutePageIndex
}
// MARK: -
@@ -52,20 +63,36 @@ public final class RDEPUBChapterData {
chapter.pages.first { $0.absolutePageIndex == absolutePageIndex }
}
/// 1
public func page(atPageNumber pageNumber: Int) -> RDEPUBTextPage? {
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else { return nil }
return pages[pageNumber - 1]
}
// MARK: -
/// fileIndex/row/column
/// fileIndex/row/column
public func anchor(forAbsoluteIndex index: Int) -> RDEPUBTextAnchor {
indexTable.anchor(forAbsoluteIndex: index, in: chapter)
}
///
///
public func anchor(forGlobalIndex index: Int) -> RDEPUBTextAnchor? {
indexTable.anchor(forGlobalIndex: index)
}
///
public func rangeAnchor(for absoluteRange: NSRange) -> RDEPUBTextRangeAnchor {
let start = anchor(forAbsoluteIndex: absoluteRange.location)
let end = anchor(forAbsoluteIndex: absoluteRange.location + absoluteRange.length)
return RDEPUBTextRangeAnchor(start: start, end: end)
}
///
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
indexTable.globalRange(for: rangeAnchor)
}
/// /
/// - Parameters:
/// - absoluteRange:
@@ -102,9 +129,21 @@ public final class RDEPUBChapterData {
location(for: page.contentRange, bookIdentifier: bookIdentifier)
}
///
public func page(for location: RDEPUBLocation) -> RDEPUBTextPage? {
guard let range = absoluteRange(for: location) else { return nil }
return page(containing: range.location)
}
///
public func page(for searchMatch: RDEPUBSearchMatch) -> RDEPUBTextPage? {
guard let range = absoluteRange(for: searchMatch) else { return nil }
return page(containing: range.location)
}
// MARK: - Location
///
///
///
///
/// 1. rangeAnchor
@@ -112,7 +151,7 @@ public final class RDEPUBChapterData {
/// 3. navigationProgression退
public func absoluteRange(for location: RDEPUBLocation) -> NSRange? {
if let rangeAnchor = location.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
if let fragment = location.fragment,
@@ -128,7 +167,7 @@ public final class RDEPUBChapterData {
///
public func absoluteRange(for highlight: RDEPUBHighlight) -> NSRange? {
if let rangeAnchor = highlight.location.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
return RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange
}
@@ -136,7 +175,7 @@ public final class RDEPUBChapterData {
///
public func absoluteRange(for searchMatch: RDEPUBSearchMatch) -> NSRange? {
if let rangeAnchor = searchMatch.rangeAnchor {
return indexTable.absoluteRange(for: rangeAnchor)
return indexTable.chapterRange(for: rangeAnchor)
}
if let location = searchMatch.rangeLocation {
return NSRange(location: location, length: searchMatch.rangeLength)
@@ -144,6 +183,42 @@ public final class RDEPUBChapterData {
return nil
}
///
public func globalRange(for location: RDEPUBLocation) -> NSRange? {
if let rangeAnchor = location.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: location),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
///
public func globalRange(for highlight: RDEPUBHighlight) -> NSRange? {
if let rangeAnchor = highlight.location.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: highlight),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
///
public func globalRange(for searchMatch: RDEPUBSearchMatch) -> NSRange? {
if let rangeAnchor = searchMatch.rangeAnchor {
return indexTable.globalRange(for: rangeAnchor)
}
guard let chapterRange = absoluteRange(for: searchMatch),
let chapterStart = indexTable.chapterStartOffset(forFileIndex: spineIndex) else {
return nil
}
return NSRange(location: chapterStart + chapterRange.location, length: chapterRange.length)
}
// MARK: - /
///
@@ -163,7 +238,6 @@ public final class RDEPUBChapterData {
///
public func searchMatches(on page: RDEPUBTextPage, from matches: [RDEPUBSearchMatch]) -> [RDEPUBSearchMatch] {
let pageRange = absoluteOffsetRange(for: page)
return matches.filter { match in
guard match.href == chapter.href else { return false }
if let range = absoluteRange(for: match) {
@@ -173,12 +247,54 @@ public final class RDEPUBChapterData {
}
}
/// `searchMatches(on:from:)` /WXRead
public func searchResults(on page: RDEPUBTextPage, from matches: [RDEPUBSearchMatch]) -> [RDEPUBSearchMatch] {
searchMatches(on: page, from: matches)
}
/// 1
public func pageNumber(for location: RDEPUBLocation) -> Int? {
if let range = absoluteRange(for: location) {
return pageNumber(containing: range.location).map { $0 + 1 }
guard let page = page(for: location) else { return nil }
return page.absolutePageIndex + 1
}
/// 1
public func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? {
guard let page = page(for: searchMatch) else { return nil }
return page.absolutePageIndex + 1
}
// MARK: -
/// TOC
public func contains(
tableOfContentsItem item: EPUBTableOfContentsItem,
normalizer: (String) -> String?
) -> Bool {
guard let chapterHref = normalizer(href),
let itemHref = normalizer(item.href.components(separatedBy: "#").first ?? item.href) else {
return false
}
return nil
return chapterHref == itemHref
}
///
public func tableOfContentsItems(
from items: [EPUBTableOfContentsItem],
normalizer: (String) -> String?
) -> [EPUBTableOfContentsItem] {
items.flatMap { item -> [EPUBTableOfContentsItem] in
let descendants = tableOfContentsItems(from: item.children, normalizer: normalizer)
return contains(tableOfContentsItem: item, normalizer: normalizer) ? [item] + descendants : descendants
}
}
///
public func primaryTableOfContentsItem(
from items: [EPUBTableOfContentsItem],
normalizer: (String) -> String?
) -> EPUBTableOfContentsItem? {
tableOfContentsItems(from: items, normalizer: normalizer).first
}
// MARK: -
@@ -190,12 +306,8 @@ public final class RDEPUBChapterData {
return lowerBound..<max(upperBound, lowerBound)
}
/// 退 chapterOffset
/// 退 chapterOffset
private func absoluteOffset(for anchor: RDEPUBTextAnchor) -> Int {
indexTable.absoluteIndex(
fileIndex: anchor.fileIndex,
row: anchor.row,
column: anchor.column
) ?? anchor.chapterOffset
indexTable.chapterOffset(for: anchor)
}
}
@@ -76,6 +76,10 @@ public struct RDEPUBTextBook {
public var pages: [RDEPUBTextPage]
/// fileIndex/row/column
public let indexTable: RDEPUBTextIndexTable
/// WXRead <-> <->
public var positionConverter: RDEPUBTextPositionConverter {
RDEPUBTextPositionConverter(book: self)
}
public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage]) {
self.chapters = chapters
@@ -93,12 +97,47 @@ public struct RDEPUBTextBook {
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
}
/// spine 访
public func chapterData(forSpineIndex spineIndex: Int) -> RDEPUBChapterData? {
guard let chapter = chapters.first(where: { $0.spineIndex == spineIndex }) else { return nil }
return RDEPUBChapterData(chapter: chapter, indexTable: indexTable)
}
/// 访
public func chapterData(atChapterIndex index: Int) -> RDEPUBChapterData? {
guard chapters.indices.contains(index) else { return nil }
return RDEPUBChapterData(chapter: chapters[index], indexTable: indexTable)
}
/// 1 访
public func chapterData(forPageNumber pageNumber: Int) -> RDEPUBChapterData? {
guard let page = page(at: pageNumber) else { return nil }
return chapterData(forSpineIndex: page.spineIndex)
}
/// 访
public func chapterData(
for location: RDEPUBLocation,
resolver: RDEPUBResourceResolver,
bookIdentifier: String?
) -> RDEPUBChapterData? {
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier) else {
return nil
}
return chapterData(for: normalizedLocation.href)
}
/// WXRead
public var chapterInfos: [EPUBChapterInfo] {
chapters.map { chapter in
EPUBChapterInfo(
spineIndex: chapter.spineIndex,
title: chapter.title,
pageCount: chapter.pages.count
)
}
}
/// 1
public func page(at pageNumber: Int) -> RDEPUBTextPage? {
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else {
@@ -115,51 +154,30 @@ public struct RDEPUBTextBook {
/// 3. navigationProgression 退
public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? {
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier),
let chapter = chapters.first(where: { $0.href == normalizedLocation.href }) else {
let chapterData = chapterData(for: normalizedLocation.href) else {
return nil
}
if let anchor = normalizedLocation.rangeAnchor?.start,
let page = indexTable.pageNumber(for: anchor, in: self) {
return page + 1
let page = positionConverter.pageNumber(for: anchor) {
return page
}
let targetOffset: Int
if let fragment = normalizedLocation.fragment, let fragmentOffset = chapter.fragmentOffsets[fragment] {
targetOffset = fragmentOffset
} else {
let lastOffset = max(chapter.attributedContent.length - 1, 0)
targetOffset = min(lastOffset, max(0, Int(round(Double(lastOffset) * normalizedLocation.navigationProgression))))
if let anchor = indexTable.anchor(for: normalizedLocation),
let page = positionConverter.pageNumber(for: anchor) {
return page
}
if let page = chapter.pages.first(where: { targetOffset >= $0.pageStartOffset && targetOffset <= $0.pageEndOffset }) {
return page.absolutePageIndex + 1
}
return chapter.pages.last.map { $0.absolutePageIndex + 1 }
return chapterData.pageNumber(for: normalizedLocation)
}
/// RDEPUBLocation
public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
guard let page = page(at: pageNumber),
let chapter = chapters.first(where: { $0.chapterIndex == page.chapterIndex }) else {
guard let chapterData = chapterData(forPageNumber: pageNumber),
let page = page(at: pageNumber) else {
return nil
}
let totalLength = max(chapter.attributedContent.length - 1, 1)
let startAnchor = indexTable.anchor(forAbsoluteIndex: page.pageStartOffset, in: chapter)
let endAnchor = indexTable.anchor(forAbsoluteIndex: page.pageEndOffset, in: chapter)
let rangeAnchor = RDEPUBTextRangeAnchor(start: startAnchor, end: endAnchor)
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: page.href,
progression: Double(page.pageStartOffset) / Double(totalLength),
lastProgression: Double(page.pageEndOffset) / Double(totalLength),
fragment: nil,
rangeAnchor: rangeAnchor
)
return chapterData.location(forPage: page, bookIdentifier: bookIdentifier)
}
}
@@ -44,6 +44,15 @@ public struct RDEPUBTextIndexTable {
/// spine
public let fileRowColumnMap: [Int: [RDEPUBRowColumnIndex]]
///
public var totalCharacterCount: Int {
guard let lastIndex = chapterStartOffsets.indices.last,
chapterLengths.indices.contains(lastIndex) else {
return 0
}
return chapterStartOffsets[lastIndex] + chapterLengths[lastIndex]
}
///
public init(chapters: [RDEPUBTextChapter]) {
var offsets: [Int] = []
@@ -118,11 +127,29 @@ public struct RDEPUBTextIndexTable {
)
}
///
public func anchor(forGlobalIndex index: Int) -> RDEPUBTextAnchor? {
guard let fileIndex = fileIndex(forCharacterPosition: index),
let href = href(for: fileIndex) else {
return nil
}
let chapterOffset = localOffsetInFile(at: fileIndex, forGlobalPosition: index) ?? 0
let fragmentID = nearestFragmentID(beforeOrAt: chapterOffset, inHref: href)
return RDEPUBTextAnchor(
fileIndex: fileIndex,
row: row(forAbsoluteIndex: chapterOffset, inFileIndex: fileIndex),
column: column(forAbsoluteIndex: chapterOffset, inFileIndex: fileIndex),
chapterOffset: chapterOffset,
fragmentID: fragmentID
)
}
///
public func pageNumber(for anchor: RDEPUBTextAnchor, in book: RDEPUBTextBook) -> Int? {
guard let chapter = book.chapters.first(where: { $0.spineIndex == anchor.fileIndex }) else { return nil }
let basePageIndex = chapter.pages.first?.absolutePageIndex ?? 0
let resolvedOffset = absoluteIndex(for: anchor)
let resolvedOffset = chapterOffset(for: anchor)
return chapter.pages.firstIndex { page in
NSLocationInRange(resolvedOffset, page.contentRange)
}.map { $0 + basePageIndex }
@@ -138,31 +165,77 @@ public struct RDEPUBTextIndexTable {
hrefToChapterIndex[href]
}
///
public func absoluteIndex(for anchor: RDEPUBTextAnchor) -> Int {
absoluteIndex(
///
public func chapterStartOffset(forFileIndex fileIndex: Int) -> Int? {
guard let href = href(for: fileIndex),
let chapterIndex = hrefToChapterIndex[href],
chapterStartOffsets.indices.contains(chapterIndex) else {
return nil
}
return chapterStartOffsets[chapterIndex]
}
///
public func chapterLength(forFileIndex fileIndex: Int) -> Int? {
guard let href = href(for: fileIndex),
let chapterIndex = hrefToChapterIndex[href],
chapterLengths.indices.contains(chapterIndex) else {
return nil
}
return chapterLengths[chapterIndex]
}
///
public func chapterOffset(for anchor: RDEPUBTextAnchor) -> Int {
chapterOffset(
fileIndex: anchor.fileIndex,
row: anchor.row,
column: anchor.column
) ?? anchor.chapterOffset
}
/// NSRange
public func absoluteRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
let start = absoluteIndex(for: rangeAnchor.start)
let end = max(start, absoluteIndex(for: rangeAnchor.end))
///
public func globalIndex(for anchor: RDEPUBTextAnchor) -> Int {
let chapterOffset = chapterOffset(for: anchor)
let baseOffset = chapterStartOffset(forFileIndex: anchor.fileIndex) ?? 0
let chapterLength = max(chapterLength(forFileIndex: anchor.fileIndex) ?? 0, 0)
let lastOffset = max(chapterLength - 1, 0)
return baseOffset + min(max(chapterOffset, 0), lastOffset)
}
///
public func absoluteIndex(for anchor: RDEPUBTextAnchor) -> Int {
globalIndex(for: anchor)
}
/// NSRange
public func chapterRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
let start = chapterOffset(for: rangeAnchor.start)
let end = max(start, chapterOffset(for: rangeAnchor.end))
return NSRange(location: start, length: max(end - start, 0))
}
/// NSRange
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
let start = globalIndex(for: rangeAnchor.start)
let end = max(start, globalIndex(for: rangeAnchor.end))
return NSRange(location: start, length: max(end - start, 0))
}
///
public func absoluteRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
globalRange(for: rangeAnchor)
}
/// progression
public func location(
for anchor: RDEPUBTextAnchor,
in chapter: RDEPUBTextChapter,
bookIdentifier: String?
) -> RDEPUBLocation {
let absoluteOffset = absoluteIndex(for: anchor)
let chapterOffset = self.chapterOffset(for: anchor)
let totalLength = max(chapter.attributedContent.length - 1, 1)
let progression = Double(min(max(absoluteOffset, 0), totalLength)) / Double(totalLength)
let progression = Double(min(max(chapterOffset, 0), totalLength)) / Double(totalLength)
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: chapter.href,
@@ -179,8 +252,8 @@ public struct RDEPUBTextIndexTable {
in chapter: RDEPUBTextChapter,
bookIdentifier: String?
) -> RDEPUBLocation {
let start = absoluteIndex(for: rangeAnchor.start)
let end = max(start, absoluteIndex(for: rangeAnchor.end))
let start = chapterOffset(for: rangeAnchor.start)
let end = max(start, chapterOffset(for: rangeAnchor.end))
let totalLength = max(chapter.attributedContent.length - 1, 1)
let clampedStart = min(max(start, 0), totalLength)
let clampedEnd = min(max(end, clampedStart), totalLength)
@@ -213,8 +286,8 @@ public struct RDEPUBTextIndexTable {
return max(index - lastRow.startOffset, 0)
}
///
public func absoluteIndex(fileIndex: Int, row: Int, column: Int) -> Int? {
///
public func chapterOffset(fileIndex: Int, row: Int, column: Int) -> Int? {
guard let rows = fileRowColumnMap[fileIndex], !rows.isEmpty else { return nil }
let normalizedRow = min(max(row, 0), rows.count - 1)
let rowEntry = rows[normalizedRow]
@@ -222,6 +295,46 @@ public struct RDEPUBTextIndexTable {
return rowEntry.startOffset + min(max(column, 0), maxColumn)
}
///
public func absoluteIndex(fileIndex: Int, row: Int, column: Int) -> Int? {
guard let chapterOffset = chapterOffset(fileIndex: fileIndex, row: row, column: column),
let baseOffset = chapterStartOffset(forFileIndex: fileIndex) else {
return nil
}
return baseOffset + chapterOffset
}
///
public func fileIndex(forCharacterPosition index: Int) -> Int? {
guard totalCharacterCount > 0 else { return nil }
let normalized = min(max(index, 0), max(totalCharacterCount - 1, 0))
for (href, chapterIndex) in hrefToChapterIndex {
guard chapterStartOffsets.indices.contains(chapterIndex),
chapterLengths.indices.contains(chapterIndex),
let fileIndex = hrefToFileIndex[href] else {
continue
}
let start = chapterStartOffsets[chapterIndex]
let length = chapterLengths[chapterIndex]
let endExclusive = start + max(length, 1)
if normalized >= start && normalized < endExclusive {
return fileIndex
}
}
return fileIndexToHref.keys.sorted().last
}
///
public func localOffsetInFile(at fileIndex: Int, forGlobalPosition index: Int) -> Int? {
guard let start = chapterStartOffset(forFileIndex: fileIndex),
let chapterLength = chapterLength(forFileIndex: fileIndex) else {
return nil
}
let normalized = min(max(index, start), start + max(chapterLength - 1, 0))
return normalized - start
}
///
private func clampedOffset(_ index: Int, in chapter: RDEPUBTextChapter) -> Int {
let lastOffset = max(chapter.attributedContent.length - 1, 0)
@@ -243,6 +356,21 @@ public struct RDEPUBTextIndexTable {
return bestID
}
/// href fragment ID
private func nearestFragmentID(beforeOrAt offset: Int, inHref href: String) -> String? {
guard let fragmentOffsets = fragmentOffsetsByHref[href] else {
return nil
}
var bestID: String?
var bestOffset = -1
for (id, fragmentOffset) in fragmentOffsets where fragmentOffset <= offset && fragmentOffset > bestOffset {
bestOffset = fragmentOffset
bestID = id
}
return bestID
}
///
private static func makeRowColumnIndices(for text: String) -> [RDEPUBRowColumnIndex] {
let nsText = text as NSString
@@ -0,0 +1,75 @@
import Foundation
/// WXRead `WREpubPositionConverter`
///
///
/// - `(fileIndex, row, column)`
/// -
/// -
/// - / `RDEPUBLocation`
public struct RDEPUBTextPositionConverter {
public let book: RDEPUBTextBook
public init(book: RDEPUBTextBook) {
self.book = book
}
public var totalCharacterCount: Int {
book.indexTable.totalCharacterCount
}
public func globalIndex(for anchor: RDEPUBTextAnchor) -> Int {
book.indexTable.globalIndex(for: anchor)
}
public func globalRange(for rangeAnchor: RDEPUBTextRangeAnchor) -> NSRange {
book.indexTable.globalRange(for: rangeAnchor)
}
public func fileIndex(forCharacterPosition index: Int) -> Int? {
book.indexTable.fileIndex(forCharacterPosition: index)
}
public func localOffsetInFile(at fileIndex: Int, forGlobalPosition index: Int) -> Int? {
book.indexTable.localOffsetInFile(at: fileIndex, forGlobalPosition: index)
}
public func anchor(forCharacterPosition index: Int) -> RDEPUBTextAnchor? {
book.indexTable.anchor(forGlobalIndex: index)
}
public func anchor(for location: RDEPUBLocation) -> RDEPUBTextAnchor? {
book.indexTable.anchor(for: location)
}
public func pageNumber(for anchor: RDEPUBTextAnchor) -> Int? {
book.indexTable.pageNumber(for: anchor, in: book).map { $0 + 1 }
}
public func pageNumber(forCharacterPosition index: Int) -> Int? {
guard let anchor = anchor(forCharacterPosition: index) else {
return nil
}
return pageNumber(for: anchor)
}
public func location(
for anchor: RDEPUBTextAnchor,
bookIdentifier: String?
) -> RDEPUBLocation? {
guard let chapter = book.chapters.first(where: { $0.spineIndex == anchor.fileIndex }) else {
return nil
}
return book.indexTable.location(for: anchor, in: chapter, bookIdentifier: bookIdentifier)
}
public func location(
for rangeAnchor: RDEPUBTextRangeAnchor,
bookIdentifier: String?
) -> RDEPUBLocation? {
guard let chapter = book.chapters.first(where: { $0.spineIndex == rangeAnchor.start.fileIndex }) else {
return nil
}
return book.indexTable.location(for: rangeAnchor, in: chapter, bookIdentifier: bookIdentifier)
}
}
@@ -1,4 +1,5 @@
import UIKit
import CoreText
#if canImport(DTCoreText)
import DTCoreText
@@ -27,6 +28,8 @@ enum RDEPUBTextRendererSupport {
private static var didLogFootnoteAttachment = false
///
private static var didLogCoverAttachment = false
/// CTFontManager
private static var registeredFontPaths = Set<String>()
// MARK: - Fragment /
@@ -209,6 +212,11 @@ enum RDEPUBTextRendererSupport {
contentLanguageCode: contentLanguageCode,
sourceHTML: rawHTML
)
registerEmbeddedFonts(
in: stylesheetHrefReplacements.inlinedCSS + "\n" + inlineStyleCSS(in: normalizedHTML),
chapterHref: href,
resourceResolver: resourceResolver
)
let htmlWithBase = injectBaseHref(into: stylesheetHrefReplacements.html, baseURL: baseURL)
let htmlWithDefaultLayers = injectStyleTag(
into: htmlWithBase,
@@ -327,6 +335,58 @@ enum RDEPUBTextRendererSupport {
return RDEPUBAssetRepository.string(for: asset)
}
private static func inlineStyleCSS(in html: String) -> String {
guard let regex = try? NSRegularExpression(pattern: #"<style\b[^>]*>([\s\S]*?)</style>"#, options: [.caseInsensitive]) else {
return ""
}
let nsHTML = html as NSString
return regex.matches(in: html, range: NSRange(location: 0, length: nsHTML.length))
.compactMap { match in
guard match.numberOfRanges > 1 else { return nil }
return nsHTML.substring(with: match.range(at: 1))
}
.joined(separator: "\n")
}
private static func registerEmbeddedFonts(
in css: String,
chapterHref: String,
resourceResolver: RDEPUBResourceResolver?
) {
guard let resourceResolver,
let faceRegex = try? NSRegularExpression(pattern: #"@font-face\s*\{([\s\S]*?)\}"#, options: [.caseInsensitive]),
let urlRegex = try? NSRegularExpression(pattern: #"url\(([^)]+)\)"#, options: [.caseInsensitive]) else {
return
}
let nsCSS = css as NSString
for faceMatch in faceRegex.matches(in: css, range: NSRange(location: 0, length: nsCSS.length)) {
guard faceMatch.numberOfRanges > 1 else { continue }
let block = nsCSS.substring(with: faceMatch.range(at: 1))
let nsBlock = block as NSString
for urlMatch in urlRegex.matches(in: block, range: NSRange(location: 0, length: nsBlock.length)) {
guard urlMatch.numberOfRanges > 1 else { continue }
let rawReference = nsBlock.substring(with: urlMatch.range(at: 1))
.trimmingCharacters(in: CharacterSet(charactersIn: "\"' \n\r\t"))
guard !rawReference.isEmpty,
!rawReference.hasPrefix("data:"),
!rawReference.hasPrefix("http://"),
!rawReference.hasPrefix("https://"),
let fileURL = resourceResolver.fileURL(forReference: rawReference, relativeToHref: chapterHref) else {
continue
}
registerFontIfNeeded(at: fileURL)
}
}
}
private static func registerFontIfNeeded(at fileURL: URL) {
let standardizedPath = fileURL.standardizedFileURL.path
guard !registeredFontPaths.contains(standardizedPath) else { return }
CTFontManagerRegisterFontsForURL(fileURL as CFURL, .process, nil)
registeredFontPaths.insert(standardizedPath)
}
private static func normalizeAttachmentHTMLMarkers(in html: String) -> String {
var normalized = html