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,217 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct RDEPUBPageBreakPolicy {
|
||||
|
||||
private let attributedString: NSAttributedString
|
||||
|
||||
init(attributedString: NSAttributedString) {
|
||||
self.attributedString = attributedString
|
||||
}
|
||||
|
||||
func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
|
||||
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||
return false
|
||||
}
|
||||
var found = false
|
||||
attributedString.enumerateAttributes(in: probeRange) { attributes, _, stop in
|
||||
guard shouldTreatAvoidHintAsBlockProtection(attributes) else {
|
||||
return
|
||||
}
|
||||
guard let rawValue = attributes[.rdPageSemanticHints] as? String else { return }
|
||||
let hints = rawValue
|
||||
.split(separator: ",")
|
||||
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||
if hints.contains(.avoidPageBreakInside) {
|
||||
found = true
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
|
||||
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||
return false
|
||||
}
|
||||
var found = false
|
||||
attributedString.enumerateAttribute(.rdPageSemanticHints, in: probeRange) { value, _, stop in
|
||||
guard let rawValue = value as? String else { return }
|
||||
let hints = rawValue
|
||||
.split(separator: ",")
|
||||
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||
if hints.contains(.keepWithNext) {
|
||||
found = true
|
||||
stop.pointee = true
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func adjustedRange(
|
||||
from proposedRange: NSRange,
|
||||
totalLength: Int,
|
||||
lineRanges: [NSRange],
|
||||
factory: RDEPUBCoreTextPageFrameFactory
|
||||
) -> (
|
||||
range: NSRange,
|
||||
breakReason: RDEPUBTextPageBreakReason,
|
||||
blockRange: NSRange?,
|
||||
attachmentRanges: [NSRange],
|
||||
attachmentKinds: [RDEPUBTextAttachmentKind],
|
||||
blockKinds: [RDEPUBTextBlockKind],
|
||||
semanticHints: [RDEPUBTextSemanticHint],
|
||||
attachmentPlacements: [RDEPUBTextAttachmentPlacement],
|
||||
diagnostics: [String]
|
||||
) {
|
||||
let pageEnd = proposedRange.location + proposedRange.length
|
||||
let proposedBlockKinds = factory.blockKinds(in: proposedRange)
|
||||
let proposedSemanticHints = factory.semanticHints(in: proposedRange)
|
||||
let proposedAttachmentPlacements = factory.attachmentPlacements(in: proposedRange)
|
||||
guard pageEnd < totalLength else {
|
||||
return (
|
||||
range: proposedRange,
|
||||
breakReason: .chapterEnd,
|
||||
blockRange: factory.blockRange(at: max(proposedRange.location, pageEnd - 1)),
|
||||
attachmentRanges: factory.attachmentRanges(in: proposedRange),
|
||||
attachmentKinds: factory.attachmentKinds(in: proposedRange),
|
||||
blockKinds: proposedBlockKinds,
|
||||
semanticHints: proposedSemanticHints,
|
||||
attachmentPlacements: proposedAttachmentPlacements,
|
||||
diagnostics: factory.diagnostics(
|
||||
reason: .chapterEnd,
|
||||
range: proposedRange,
|
||||
attachmentRanges: factory.attachmentRanges(in: proposedRange),
|
||||
blockRange: factory.blockRange(at: max(proposedRange.location, pageEnd - 1)),
|
||||
blockKinds: proposedBlockKinds,
|
||||
semanticHints: proposedSemanticHints,
|
||||
attachmentPlacements: proposedAttachmentPlacements
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let currentBlockRange = factory.blockRange(at: max(proposedRange.location, pageEnd - 1))
|
||||
let currentAttachmentRanges = factory.attachmentRanges(in: proposedRange)
|
||||
let currentAttachmentKinds = factory.attachmentKinds(in: proposedRange)
|
||||
let currentBlockKinds = proposedBlockKinds
|
||||
let currentSemanticHints = proposedSemanticHints
|
||||
let currentAttachmentPlacements = proposedAttachmentPlacements
|
||||
|
||||
if let pageRelateBoundary = preferredPageRelateBoundary(
|
||||
after: proposedRange,
|
||||
minimumEnd: proposedRange.location + 1,
|
||||
lineRanges: lineRanges,
|
||||
factory: factory
|
||||
) {
|
||||
let adjustedRange = NSRange(location: proposedRange.location, length: pageRelateBoundary - proposedRange.location)
|
||||
return (
|
||||
range: adjustedRange,
|
||||
breakReason: .semanticBoundary,
|
||||
blockRange: currentBlockRange,
|
||||
attachmentRanges: currentAttachmentRanges,
|
||||
attachmentKinds: currentAttachmentKinds,
|
||||
blockKinds: currentBlockKinds,
|
||||
semanticHints: currentSemanticHints,
|
||||
attachmentPlacements: currentAttachmentPlacements,
|
||||
diagnostics: factory.diagnostics(
|
||||
reason: .semanticBoundary,
|
||||
range: adjustedRange,
|
||||
attachmentRanges: currentAttachmentRanges,
|
||||
blockRange: currentBlockRange,
|
||||
blockKinds: currentBlockKinds,
|
||||
semanticHints: currentSemanticHints,
|
||||
attachmentPlacements: currentAttachmentPlacements,
|
||||
trigger: RDEPUBTextSemanticHint.pageRelate.rawValue
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
range: proposedRange,
|
||||
breakReason: .frameLimit,
|
||||
blockRange: currentBlockRange,
|
||||
attachmentRanges: currentAttachmentRanges,
|
||||
attachmentKinds: currentAttachmentKinds,
|
||||
blockKinds: currentBlockKinds,
|
||||
semanticHints: currentSemanticHints,
|
||||
attachmentPlacements: currentAttachmentPlacements,
|
||||
diagnostics: factory.diagnostics(
|
||||
reason: .frameLimit,
|
||||
range: proposedRange,
|
||||
attachmentRanges: currentAttachmentRanges,
|
||||
blockRange: currentBlockRange,
|
||||
blockKinds: currentBlockKinds,
|
||||
semanticHints: currentSemanticHints,
|
||||
attachmentPlacements: currentAttachmentPlacements
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func preferredPageRelateBoundary(
|
||||
after range: NSRange,
|
||||
minimumEnd: Int,
|
||||
lineRanges: [NSRange],
|
||||
factory: RDEPUBCoreTextPageFrameFactory
|
||||
) -> Int? {
|
||||
let pageStartOfNext = range.location + range.length
|
||||
guard pageStartOfNext > range.location,
|
||||
pageStartOfNext < attributedString.length,
|
||||
lineRanges.count >= 2,
|
||||
factory.semanticHints(at: pageStartOfNext).contains(.pageRelate) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let boundaryBlockStart = factory.blockRange(at: pageStartOfNext)?.location
|
||||
?? factory.paragraphRange(containing: pageStartOfNext).location
|
||||
guard boundaryBlockStart == pageStartOfNext else { return nil }
|
||||
|
||||
let lastLineStart = lineRanges[lineRanges.count - 1].location
|
||||
guard lastLineStart > range.location, lastLineStart >= minimumEnd else {
|
||||
return nil
|
||||
}
|
||||
return lastLineStart
|
||||
}
|
||||
|
||||
private func shouldTreatAvoidHintAsBlockProtection(_ attributes: [NSAttributedString.Key: Any]) -> Bool {
|
||||
guard let rawValue = attributes[.rdPageSemanticHints] as? String else {
|
||||
return false
|
||||
}
|
||||
let hints = rawValue
|
||||
.split(separator: ",")
|
||||
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||
guard hints.contains(.avoidPageBreakInside) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let placement = (attributes[.rdPageAttachmentPlacement] as? String)
|
||||
.flatMap(RDEPUBTextAttachmentPlacement.init(rawValue:))
|
||||
let blockKind = (attributes[.rdPageBlockKind] as? String)
|
||||
.flatMap(RDEPUBTextBlockKind.init(rawValue:))
|
||||
|
||||
// Inline attachments (footnote icons, rare-character images) flow with
|
||||
// the surrounding text, so their avoid hint must not lock the line to
|
||||
// the next page. Placement is checked besides blockKind because an
|
||||
// enclosing paragraph's semantics overwrite blockKind on the
|
||||
// attachment's range, while placement survives.
|
||||
if blockKind == .attachment || placement != nil, placement != .centered {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func clampedProbeRange(for lineRange: NSRange) -> NSRange? {
|
||||
clampedRange(NSRange(location: lineRange.location, length: max(lineRange.length, 1)))
|
||||
}
|
||||
|
||||
private func clampedRange(_ range: NSRange) -> NSRange? {
|
||||
guard range.location >= 0, range.length >= 0 else { return nil }
|
||||
guard attributedString.length > 0 else {
|
||||
return range.location == 0 ? NSRange(location: 0, length: 0) : nil
|
||||
}
|
||||
guard range.location < attributedString.length else { return nil }
|
||||
|
||||
let maxLength = attributedString.length - range.location
|
||||
return NSRange(location: range.location, length: min(range.length, maxLength))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user