88 lines
4.2 KiB
Swift
88 lines
4.2 KiB
Swift
import UIKit
|
|
|
|
enum RDEPUBTextRendererSupport {
|
|
static func injectFragmentMarkers(into html: String) -> String {
|
|
guard let regex = try? NSRegularExpression(pattern: #"(<[^>]+\sid="([^"]+)"[^>]*>)"#, options: [.caseInsensitive]) else {
|
|
return html
|
|
}
|
|
return regex.stringByReplacingMatches(
|
|
in: html,
|
|
options: [],
|
|
range: NSRange(location: 0, length: html.utf16.count),
|
|
withTemplate: "${id=$2}$1"
|
|
)
|
|
}
|
|
|
|
static func extractFragmentOffsets(from attributedString: NSMutableAttributedString) -> [String: Int] {
|
|
let markerPattern = #"\$\{id=([^}]+)\}"#
|
|
guard let regex = try? NSRegularExpression(pattern: markerPattern, options: []) else {
|
|
return [:]
|
|
}
|
|
|
|
let mutableString = NSMutableString(string: attributedString.string)
|
|
var fragmentOffsets: [String: Int] = [:]
|
|
var searchRange = NSRange(location: 0, length: mutableString.length)
|
|
var offsetAdjustment = 0
|
|
|
|
while let match = regex.firstMatch(in: mutableString as String, options: [], range: searchRange) {
|
|
let fullMatch = mutableString.substring(with: match.range) as NSString
|
|
let fragmentID = fullMatch
|
|
.replacingOccurrences(of: #"\$\{id="#, with: "", options: .regularExpression, range: NSRange(location: 0, length: fullMatch.length))
|
|
.replacingOccurrences(of: #"\}"#, with: "", options: .regularExpression)
|
|
|
|
let adjustedLocation = max(0, match.range.location + offsetAdjustment)
|
|
fragmentOffsets[fragmentID] = adjustedLocation
|
|
attributedString.deleteCharacters(in: match.range)
|
|
mutableString.deleteCharacters(in: match.range)
|
|
offsetAdjustment -= match.range.length
|
|
searchRange = NSRange(location: match.range.location, length: mutableString.length - match.range.location)
|
|
}
|
|
|
|
return fragmentOffsets
|
|
}
|
|
|
|
static func normalizeReadingAttributes(in attributedString: NSMutableAttributedString, style: RDEPUBTextRenderStyle) {
|
|
let fullRange = NSRange(location: 0, length: attributedString.length)
|
|
attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in
|
|
let sourceFont = attributes[.font] as? UIFont
|
|
let paragraph = (attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? paragraphStyle(lineSpacing: style.lineSpacing)
|
|
paragraph.lineSpacing = style.lineSpacing
|
|
paragraph.paragraphSpacing = max(paragraph.paragraphSpacing, style.lineSpacing / 2)
|
|
|
|
var updatedAttributes = attributes
|
|
updatedAttributes[.font] = normalizedFont(from: sourceFont, baseFont: style.font)
|
|
updatedAttributes[.paragraphStyle] = paragraph
|
|
if let textColor = style.textColor {
|
|
updatedAttributes[.foregroundColor] = textColor
|
|
}
|
|
attributedString.setAttributes(updatedAttributes, range: range)
|
|
}
|
|
}
|
|
|
|
static func fallbackAttributedString(for html: String, style: RDEPUBTextRenderStyle) -> NSMutableAttributedString {
|
|
let fallbackAttributes: [NSAttributedString.Key: Any] = [
|
|
.font: style.font,
|
|
.paragraphStyle: paragraphStyle(lineSpacing: style.lineSpacing),
|
|
.foregroundColor: style.textColor ?? UIColor.black
|
|
]
|
|
return NSMutableAttributedString(string: html, attributes: fallbackAttributes)
|
|
}
|
|
|
|
private static func normalizedFont(from sourceFont: UIFont?, baseFont: UIFont) -> UIFont {
|
|
guard let sourceFont else {
|
|
return baseFont
|
|
}
|
|
let traits = sourceFont.fontDescriptor.symbolicTraits.intersection([.traitBold, .traitItalic])
|
|
if let descriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) {
|
|
return UIFont(descriptor: descriptor, size: baseFont.pointSize)
|
|
}
|
|
return baseFont
|
|
}
|
|
|
|
private static func paragraphStyle(lineSpacing: CGFloat) -> NSMutableParagraphStyle {
|
|
let style = NSMutableParagraphStyle()
|
|
style.lineSpacing = lineSpacing
|
|
style.paragraphSpacing = max(6, lineSpacing / 2)
|
|
return style
|
|
}
|
|
} |