From 44202357c0b437813c7a8ad2f72f39d02a33f818 Mon Sep 17 00:00:00 2001 From: shen <> Date: Sun, 31 May 2026 21:47:54 +0800 Subject: [PATCH] refactor: split reader architecture and chrome handling --- .../Pods/Pods.xcodeproj/project.pbxproj | 2878 +++++++++-------- .../ReadViewDemo.xcodeproj/project.pbxproj | 4 - .../Models/RDEPUBAnnotationModels.swift | 350 ++ .../Models/RDEPUBPaginationModels.swift | 229 ++ .../Models/RDEPUBReadingLocationModels.swift | 135 + .../EPUBCore/RDEPUBReadingModels.swift | 719 ---- .../RDEPUBBuildDiagnosticsReporter.swift | 57 + .../RDEPUBChapterTailNormalizer.swift | 153 + .../RDEPUBPaginationCacheCoordinator.swift | 45 + .../BuildPipeline/RDEPUBTextBookBuilder.swift | 395 +++ .../RDEPUBTextBookCache.swift | 0 .../BuildPipeline/RDEPUBTextBookModels.swift | 193 ++ .../RDEPUBTextBuildPipelineInterfaces.swift | 46 + .../RDEPUBTextPerformanceSampler.swift | 0 .../Pagination/RDEPUBChapterPageCounter.swift | 270 ++ .../RDEPUBCoreTextPageFrameFactory.swift | 389 +++ .../Pagination/RDEPUBPageBreakPolicy.swift | 308 ++ .../RDEPUBTextLayoutFrame.swift | 0 .../Pagination/RDEPUBTextLayouter.swift | 26 + .../RDEPUBTextPaginationInterfaces.swift | 28 + .../RDEPUBTextPaginationSupport.swift | 5 +- .../RDEPUBDTCoreTextRenderer.swift | 57 +- .../RDEPUBTextBookBuilder.swift | 798 ----- .../RDEPUBTextLayouter.swift | 946 ------ .../RDEPUBTextRenderer.swift | 10 +- .../RDEPUBTextRendererSupport.swift | 1304 -------- .../RDPlainTextBookBuilder.swift | 15 +- .../RDEPUBAttachmentNormalizer.swift | 187 ++ .../Typesetter/RDEPUBFontNormalizer.swift | 89 + .../RDEPUBFragmentMarkerInjector.swift | 59 + .../Typesetter/RDEPUBHTMLNormalizer.swift | 214 ++ .../RDEPUBRenderDiagnosticsCollector.swift | 162 + .../RDEPUBSemanticMarkerInjector.swift | 332 ++ .../Typesetter/RDEPUBStyleSheetComposer.swift | 288 ++ .../RDEPUBTextRendererSupport.swift | 167 + .../RDEPUBTypesettingPipeline.swift | 65 + ...PUBReaderController+ContentDelegates.swift | 176 + .../RDEPUBReaderController+DataSource.swift | 133 + .../RDEPUBReaderController+PublicAPI.swift | 176 + ...RDEPUBReaderController+RenderSupport.swift | 80 + ...RDEPUBReaderController+RuntimeBridge.swift | 234 ++ ...EPUBReaderController+TableOfContents.swift | 84 + .../EPUBUI/RDEPUBReaderController.swift | 2003 +----------- .../EPUBUI/RDEPUBReaderTopToolView.swift | 1 + .../EPUBUI/RDEPUBTextContentView.swift | 752 ----- .../EPUBUI/RDEPUBViewportTypes.swift | 29 + .../RDEPUBReaderAnnotationCoordinator.swift | 496 +++ .../RDEPUBReaderAssemblyCoordinator.swift | 71 + .../RDEPUBReaderChromeCoordinator.swift | 168 + .../RDEPUBReaderContext.swift | 209 ++ .../RDEPUBReaderDependencies.swift | 64 + .../RDEPUBReaderLoadCoordinator.swift | 84 + .../RDEPUBReaderLocationCoordinator.swift | 60 + .../RDEPUBReaderPaginationCoordinator.swift | 156 + .../RDEPUBReaderRuntime.swift | 281 ++ .../RDEPUBReaderSearchCoordinator.swift | 183 ++ .../RDEPUBReaderViewportMonitor.swift | 125 + .../RDEPUBReaderConfiguration.swift | 0 .../{ => Settings}/RDEPUBReaderSettings.swift | 0 .../RDEPUBReaderSettingsViewController.swift | 0 .../{ => Settings}/RDEPUBReaderTheme.swift | 0 .../RDEPUBPageInteractionController.swift | 0 .../RDEPUBPageLayoutSnapshot.swift | 0 .../TextPage/RDEPUBSelectableTextView.swift | 30 + .../RDEPUBSelectionOverlayView.swift | 2 +- .../RDEPUBTextAnnotationOverlay.swift | 136 + .../TextPage/RDEPUBTextContentView.swift | 438 +++ .../RDEPUBTextPageDecorationView.swift | 4 + .../TextPage/RDEPUBTextPageRenderView.swift | 41 + .../RDEPUBTextSelectionController.swift | 189 ++ .../Paging/RDReaderPagingController.swift | 59 + .../Paging/RDReaderPreloadController.swift | 256 ++ .../Paging/RDReaderSpreadResolver.swift | 72 + .../Paging/RDReaderTapRegionHandler.swift | 27 + .../RDReaderView+CollectionView.swift | 49 + .../RDReaderView+ContentAccess.swift | 75 + .../ReaderView/RDReaderView+PageCurl.swift | 138 + .../ReaderView/RDReaderView+ToolView.swift | 115 + .../ReaderView/RDReaderView.swift | 948 ++---- .../ReaderView/RDReaderViewProtocols.swift | 90 + 80 files changed, 10635 insertions(+), 8522 deletions(-) create mode 100644 Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift create mode 100644 Sources/RDReaderView/EPUBCore/Models/RDEPUBPaginationModels.swift create mode 100644 Sources/RDReaderView/EPUBCore/Models/RDEPUBReadingLocationModels.swift delete mode 100644 Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBBuildDiagnosticsReporter.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBChapterTailNormalizer.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBPaginationCacheCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift rename Sources/RDReaderView/EPUBTextRendering/{ => BuildPipeline}/RDEPUBTextBookCache.swift (100%) create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBuildPipelineInterfaces.swift rename Sources/RDReaderView/EPUBTextRendering/{ => BuildPipeline}/RDEPUBTextPerformanceSampler.swift (100%) create mode 100644 Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift rename Sources/RDReaderView/EPUBTextRendering/{ => Pagination}/RDEPUBTextLayoutFrame.swift (100%) create mode 100644 Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayouter.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationInterfaces.swift rename Sources/RDReaderView/EPUBTextRendering/{ => Pagination}/RDEPUBTextPaginationSupport.swift (86%) delete mode 100644 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift delete mode 100644 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift delete mode 100644 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBAttachmentNormalizer.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFontNormalizer.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFragmentMarkerInjector.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBHTMLNormalizer.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBRenderDiagnosticsCollector.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTextRendererSupport.swift create mode 100644 Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTypesettingPipeline.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+PublicAPI.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RenderSupport.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RuntimeBridge.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBReaderController+TableOfContents.swift delete mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift create mode 100644 Sources/RDReaderView/EPUBUI/RDEPUBViewportTypes.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAssemblyCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderDependencies.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLoadCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderSearchCoordinator.swift create mode 100644 Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderViewportMonitor.swift rename Sources/RDReaderView/EPUBUI/{ => Settings}/RDEPUBReaderConfiguration.swift (100%) rename Sources/RDReaderView/EPUBUI/{ => Settings}/RDEPUBReaderSettings.swift (100%) rename Sources/RDReaderView/EPUBUI/{ => Settings}/RDEPUBReaderSettingsViewController.swift (100%) rename Sources/RDReaderView/EPUBUI/{ => Settings}/RDEPUBReaderTheme.swift (100%) rename Sources/RDReaderView/EPUBUI/{ => TextPage}/RDEPUBPageInteractionController.swift (100%) rename Sources/RDReaderView/EPUBUI/{ => TextPage}/RDEPUBPageLayoutSnapshot.swift (100%) create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectableTextView.swift rename Sources/RDReaderView/EPUBUI/{ => TextPage}/RDEPUBSelectionOverlayView.swift (99%) create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextAnnotationOverlay.swift create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swift create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageDecorationView.swift create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift create mode 100644 Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift create mode 100644 Sources/RDReaderView/ReaderView/Paging/RDReaderPagingController.swift create mode 100644 Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift create mode 100644 Sources/RDReaderView/ReaderView/Paging/RDReaderSpreadResolver.swift create mode 100644 Sources/RDReaderView/ReaderView/Paging/RDReaderTapRegionHandler.swift create mode 100644 Sources/RDReaderView/ReaderView/RDReaderView+CollectionView.swift create mode 100644 Sources/RDReaderView/ReaderView/RDReaderView+ContentAccess.swift create mode 100644 Sources/RDReaderView/ReaderView/RDReaderView+PageCurl.swift create mode 100644 Sources/RDReaderView/ReaderView/RDReaderView+ToolView.swift create mode 100644 Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift diff --git a/ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj b/ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj index 357f026..b4bfd12 100644 --- a/ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj +++ b/ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj @@ -8,493 +8,546 @@ /* Begin PBXBuildFile section */ 002685407B9C4D548571DA24AFD6E8F1 /* NSString+Paragraphs.m in Sources */ = {isa = PBXBuildFile; fileRef = F53CCB60DE5D06A43E4A52EDB274E7D2 /* NSString+Paragraphs.m */; }; - 00C49E16B834531525EA78BB63A6E504 /* UIImage+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = 15941683D394FB8D7EAE91CD2CF03FF1 /* UIImage+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 012337AC75CC2DE92285DA91F07B1CC5 /* RDEPUBNavigatorLayoutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 521E0E2A04F1C8B2E1AB5D30420A010B /* RDEPUBNavigatorLayoutContext.swift */; }; 014449AE560FFF3C628FAE639D043004 /* DTAnchorHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D63AE1801C7AB149219317BE7529B26 /* DTAnchorHTMLElement.m */; }; - 018208E7B5B1C22398872119C18BD05B /* FileManager+ZIPDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F9B298989305B9569B71E448E8C1D4 /* FileManager+ZIPDeprecated.swift */; }; - 0279DC0CB56DFD659743755DF5932E28 /* ConstraintLayoutSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50297AAB8278F2BB6FCFE82B20C7A049 /* ConstraintLayoutSupport.swift */; }; - 02BD5B00D86A7A544629B90103ACB148 /* DTBlockFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 375179DD3A4576CEF947E1760BBDC69B /* DTBlockFunctions.m */; }; - 03B6C4C7DA368F0B8C15FAFD93358E87 /* URL+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537BB06317FA188DF7A8A960152520E8 /* URL+ZIP.swift */; }; + 0220B94FD97D36708E40E33DB89C76C8 /* RDEPUBParser+Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE4740D05411538E3715704ED63C6E76 /* RDEPUBParser+Resources.swift */; }; + 023CD43B36E7FC2AEEB3E546121AD89F /* RDEPUBWebViewDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D01284125120D95F9E41743D2CE9F81 /* RDEPUBWebViewDebug.swift */; }; + 03AA3739E62E025785FFB6EB6828B7C0 /* ConstraintMakerEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392A75EAF280876545D14AA00517BA4C /* ConstraintMakerEditable.swift */; }; 03D90C831397E8636A33CE557024A136 /* NSMutableAttributedString+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = A2C299CFB47AA6ECF86F7348E1EB751F /* NSMutableAttributedString+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 04AC4C71A95C08C9A1158AEA6B5B04B5 /* ConstraintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02EA0374C693F881AB36202FB276DCD /* ConstraintView.swift */; }; + 04138A4A20D11E47461B6CE8605C8080 /* ConstraintOffsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F950CEF31AC0065C0BEAB26BA2CEFA1 /* ConstraintOffsetTarget.swift */; }; 0561DF6793C8516F469D9813F5CE3211 /* DTCoreTextLayouter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D6DA580F40DF113709E15CF6542A35C /* DTCoreTextLayouter.m */; }; - 0622935FFD2ADD637FBDC5278F8D5E92 /* DTAnimatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = BC332335B380BC51C4F05C5CCDFB9189 /* DTAnimatedGIF.m */; }; - 06675C3F0CB6A5662061DB48B55B7DF3 /* Archive+MemoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174FC0CA05D20BB995E211DE5A0857DB /* Archive+MemoryFile.swift */; }; - 072E9480C779CFF4764E08D82F4B67CF /* wxread-replace.css in Resources */ = {isa = PBXBuildFile; fileRef = 6009A6FAECBFC4615F869A0D90D8A460 /* wxread-replace.css */; }; - 07512D0A383FA3C65725DB0BE97A97A6 /* RDEPUBResourceURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32769058982DA3B3ADB0051377B41916 /* RDEPUBResourceURLSchemeHandler.swift */; }; - 086B6FE0EFE46CBAA9CA4A74720B6C52 /* DTBase64Coding.m in Sources */ = {isa = PBXBuildFile; fileRef = 061BD3911E4E530C792E65CED7868C33 /* DTBase64Coding.m */; }; - 08E897E7340B3D5708F3520FE146F124 /* UIView+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4219E7C32019899890CAB66E5FBDF524 /* UIView+DTFoundation.m */; }; - 09F91906617C975ABCF9D9C11BDA8C4F /* DTCustomColoredAccessory.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C6BE3B2DA9A8C4CD3760CDAD6F305A5 /* DTCustomColoredAccessory.m */; }; - 0A64A09E852BBF52677A7BC5CC7691C0 /* NSArray+DTError.m in Sources */ = {isa = PBXBuildFile; fileRef = 73940D215D644B6815A09DA802CFBDDF /* NSArray+DTError.m */; }; + 077A7464B5BCC77F710DC721FF09B515 /* RDEPUBWebView+JavaScriptBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9FC3B2914C05EFDDFF68ABE5C7A83F0 /* RDEPUBWebView+JavaScriptBridge.swift */; }; + 07C1A54BF3FAE709C8CD683DC9199B17 /* RDEPUBReaderSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55B9A91FBFB85980E2E199DC0011868 /* RDEPUBReaderSettings.swift */; }; + 096932721786FDD80CE0F45A097E7162 /* DTCustomColoredAccessory.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C6BE3B2DA9A8C4CD3760CDAD6F305A5 /* DTCustomColoredAccessory.m */; }; + 0A37B6C1B26290A715DB47A6D750293D /* RDEPUBStyleSheetBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164CFAED45E63A7592580A342D44A43E /* RDEPUBStyleSheetBuilder.swift */; }; 0AAA5BB1DBF584102B15CF8738F1815E /* NSScanner+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = CA538A48DD3691701EC6B5021D7B7381 /* NSScanner+HTML.m */; }; 0ABC4D62588EABAA636441FD9A82C940 /* CTLineUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DD58A1908F3C532210EA65ACE410A44D /* CTLineUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0B6FA1F22150D7F8B1865F41FF70C922 /* DTImage+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AB9016408894675D11069CDAACEDE94 /* DTImage+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 0BFC402A79CB21B8CF1A99D7258961F7 /* RDEPUBTextLayouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73C71810144AB20A3D87FADF45109EF /* RDEPUBTextLayouter.swift */; }; - 0D5E63AF74BC876DEE822780271F8773 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0834E84632886CDD9911CAB76183F1CE /* QuartzCore.framework */; }; + 0B834BEE7479E0F78E3A7D8235A2EC95 /* RDEPUBReaderController+RuntimeBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D4FDB3A066F397D1FF9727C43A0777 /* RDEPUBReaderController+RuntimeBridge.swift */; }; + 0CD6D54912E8E2787802A5EEE4773070 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; + 0DC397CC2F255374FD7DD584D742622E /* RDReaderPreloadController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57C7D26C0F345A62E14DAB51521E69 /* RDReaderPreloadController.swift */; }; + 0F79C2851ACB90CAE1412D895C7010D7 /* DTExtendedFileAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = F5389D4E96A4AFE175691B4D3D4524E7 /* DTExtendedFileAttributes.m */; }; + 0F7F1426A2C194B170E82DE115A66DB1 /* NSURL+DTComparing.h in Headers */ = {isa = PBXBuildFile; fileRef = 731C0C61E3AD227D691FBA1C2C17BF7B /* NSURL+DTComparing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F9CC95163443852A6C43FB306CBFCCA /* ConstraintPriorityTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882FA707ED3513D263A1E0656AAD2D33 /* ConstraintPriorityTarget.swift */; }; 0FCBC8C64D8862F2A0E8794EDADDECBD /* UIFont+DTCoreText.h in Headers */ = {isa = PBXBuildFile; fileRef = 647FB0C5AAF84C71A7784DD277E48910 /* UIFont+DTCoreText.h */; settings = {ATTRIBUTES = (Public, ); }; }; 100BFFBD24F234C095EDB001B8088DB1 /* DTCoreTextLayoutFrameAccessibilityElementGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = D1599698B5A62671C488F13E07272B09 /* DTCoreTextLayoutFrameAccessibilityElementGenerator.m */; }; - 10891A3745ED879C322F4EAFB6A24F36 /* ConstraintMakerExtendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5245010774F8F1C4857763CC7B96E7B4 /* ConstraintMakerExtendable.swift */; }; + 106FD56F563F6B4531E0C35C1680358E /* Data+CompressionDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D641809A99849F80FDCB02F8CE8D6A0 /* Data+CompressionDeprecated.swift */; }; 109458F9D48FD4F381F0E40E2DB3D91C /* DTLazyImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = E5F69A1E1260BAD7737BEB533F643A15 /* DTLazyImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 113D2805F027DF665F9FEE905F6F9D0F /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D4549E0B9168D7DDB910DE874B874 /* Typealiases.swift */; }; + 1163FCAE7A2F60099BF3A9675E47E8E2 /* wxread-default.css in Resources */ = {isa = PBXBuildFile; fileRef = 5D8DA3839256D5A245E96F2C67A5E232 /* wxread-default.css */; }; 11AB1902A68385851FB81E870991F9A0 /* DTImageTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = FEEB15F87BA6575BBFE1C5FD19B17CD7 /* DTImageTextAttachment.m */; }; - 11C954E3357C1EEB7CC99D689FF2EF01 /* NSData+DTCrypto.h in Headers */ = {isa = PBXBuildFile; fileRef = 02EDFE0EEA47398F3E1A361A73AF6A6C /* NSData+DTCrypto.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 12840AD82C5E66B9DAABE907647D0961 /* RDEPUBReaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64B372E68C6C74223A76117BFA80B15 /* RDEPUBReaderController.swift */; }; 12B4A35B8E40C112E18A7C5511F023E9 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4472755F83C121DE5E1E505D145F6DBE /* CoreText.framework */; }; + 12E1D9F1AA8C4E17DC54886C3CE410C2 /* RDEPUBViewportTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B4304B138A25135B0946C6A7A3F4A8 /* RDEPUBViewportTypes.swift */; }; 130C3A85D9F9F445222C2E31EC245B0A /* NSScanner+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = A0927A875CA1F11DBCE60DFC6F0CA0B6 /* NSScanner+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 14429631792B0E8330AB420662AFE7D6 /* RDReaderFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4134DC0BA1A654610741FB33CA9BA7 /* RDReaderFlowLayout.swift */; }; + 13E76CEF04DCA3C012D6BE385BD843F9 /* DTTiledLayerWithoutFade.h in Headers */ = {isa = PBXBuildFile; fileRef = DF5EDCA3B21964D0462BE0982E2F0BF8 /* DTTiledLayerWithoutFade.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14BDD9FB7A7AC5C8ABFF8D24C381F3DA /* NSAttributedString+DTCoreText.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC9280BA00935565E9658775DD7404D /* NSAttributedString+DTCoreText.m */; }; - 1509118FA9E9AD657EAC9BEE5C49BE6E /* RDEPUBTextLayoutFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CF54683B145156F178EF99B0C7046A /* RDEPUBTextLayoutFrame.swift */; }; + 154E19088C6CC946DD47CB238C4C9BCC /* NSFileWrapper+DTCopying.m in Sources */ = {isa = PBXBuildFile; fileRef = 1756C6FFCA695290BA62DA4E7B6DD20B /* NSFileWrapper+DTCopying.m */; }; 15A7ED15DA03DD3B9F26A017D270D1C2 /* DTCSSListStyle.h in Headers */ = {isa = PBXBuildFile; fileRef = FDD333B409D528E5006FA48F4D70A6F5 /* DTCSSListStyle.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 18A69B8D310DB6279273A14E9DB1A1DF /* UIImage+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D85A85D891A320165843EF9F489178F7 /* UIImage+DTFoundation.m */; }; - 18E05C9D1D63A2AD105D6CC0E0C64C57 /* NSData+DTCrypto.m in Sources */ = {isa = PBXBuildFile; fileRef = 17F750F38183302BF345F40F5A4CDD43 /* NSData+DTCrypto.m */; }; + 1693DD61EFC198586623CFC4CFCAD1DB /* Archive+ZIP64.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5693BD171B879406EA14C97F4AD8D8 /* Archive+ZIP64.swift */; }; + 16F7D312910FD9A6F8CCE8C7128390EB /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0EBAC7714A5F73B75AAB47D24B08B71 /* Constraint.swift */; }; + 18EE808287AF9003259B828F3C4A6B3F /* RDEPUBPaginationCacheCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20AA41D781113C9A30CD0E9EDD6835D5 /* RDEPUBPaginationCacheCoordinator.swift */; }; 191824B2B1E1B0A7DD3B6298100B4B8A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE4821B98DDE6C71411EC139EC6D2762 /* CoreGraphics.framework */; }; 19649ED312D92A6F5D2D6AF4E540CE75 /* DTHorizontalRuleHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E28CA95D89DBAB2FE56BC8513D5838D7 /* DTHorizontalRuleHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 1AD6564F00D7B59D818A6AF35FCFDB3C /* RDEPUBPageLayoutSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7863FD91C5BD0B2EE358458E09C81194 /* RDEPUBPageLayoutSnapshot.swift */; }; - 1CAB795ED271F58E3A9321F3562CFFD0 /* RDReaderContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 609F81945B589AC45E4E531C01F6EE3E /* RDReaderContentCell.swift */; }; + 197770F407C11D14237B98158E119C55 /* RDEPUBReaderController+TableOfContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173FEADB0A73EA7EB5B3B7CADB74C3A4 /* RDEPUBReaderController+TableOfContents.swift */; }; + 19BA94EB41DF632F761221B74DAD526A /* DTBlockFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = A0BD48DAF5AE9DB087F2552E99069709 /* DTBlockFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 19F07715D355FB1869128B129D015C90 /* WeReadApi.js in Resources */ = {isa = PBXBuildFile; fileRef = ECDEDEE1D78BA473378CB3D26C486E5B /* WeReadApi.js */; }; + 1A35329A88FF592894E7396831A4C88B /* RDEPUBReaderBottomToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F274F6DE3095EAF4042CC6BF0F3E566 /* RDEPUBReaderBottomToolView.swift */; }; + 1AF5422FD56B541040966046B479CE53 /* ZIPFoundation-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = D927F128EBF26AD2C61FD0E6B6F6F5ED /* ZIPFoundation-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1CB67E5F08E9C847E38ED61A79A1F656 /* Archive+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2659DB0685E16ABAE0382F726DA8C8 /* Archive+Helpers.swift */; }; 1D460603DCA4A4E5ECFE8FF1F1CACC10 /* DTColorFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = AFC4C9F69BF2D0CAEBAA83896C139D2C /* DTColorFunctions.m */; }; - 1DB7CE877F9241539B365C9FA1D26217 /* wxread-dark.css in Resources */ = {isa = PBXBuildFile; fileRef = 57DE6F8B9CE9F488188CFE16DDF7929B /* wxread-dark.css */; }; - 213651A172BE115A3EB6597D4EF16EF9 /* SSAlertCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EE750340347ED584C5A2C1164331A /* SSAlertCommonView.swift */; }; + 20BA812F7892018A2905DFB5E9195B33 /* UIImage+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D85A85D891A320165843EF9F489178F7 /* UIImage+DTFoundation.m */; }; + 20CBAD9874EBC0BECD8003634E76CC12 /* ConstraintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02EA0374C693F881AB36202FB276DCD /* ConstraintView.swift */; }; + 21A7DDFC2A430E9105419DF3DC2D4AA5 /* RDEPUBTextBuildPipelineInterfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2055F08A4A307EA51828460481FE8276 /* RDEPUBTextBuildPipelineInterfaces.swift */; }; 2203356EEA94A9172BC7902923E43CBE /* DTCoreTextFontCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B7771FDE9AB1688166584C0AA03F12A /* DTCoreTextFontCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 239D541A0D14415051D9B84A6FAC4396 /* DTCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = B5FCE04EBB553439D1C2BA1001936293 /* DTCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 23C510B1C699288E3E15795A6C0835DB /* ConstraintLayoutGuide+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9A82CB094636AFB53D6D130D6219BA /* ConstraintLayoutGuide+Extensions.swift */; }; - 23EA3FF5A4302C09BE16C4F0EFEBF0D3 /* Archive+ReadingDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934F9BB2B3A313294F121484C3A97A14 /* Archive+ReadingDeprecated.swift */; }; + 22AEC3422EFA234293F90AB3738CE694 /* RDEPUBReaderTableOfContentsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F09F20128209EC507BBD2E73457DCD /* RDEPUBReaderTableOfContentsItem.swift */; }; + 22FD1F39B4B94E9E33FDDF0C6880AA53 /* RDEPUBSelectableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A65FCB87389CF471A9A4BD659D58346 /* RDEPUBSelectableTextView.swift */; }; 24395BEB3C360D9DA5180BFB955B04A0 /* DTAttributedLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 21BAA93E67D8CBC5297302D798FB4F7A /* DTAttributedLabel.m */; }; 2445709667CB3820E51CD4AA915E6943 /* DTHTMLParserTextNode.h in Headers */ = {isa = PBXBuildFile; fileRef = C30D143134EEC5D4BA99459104B76A2E /* DTHTMLParserTextNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 249683EC17AB72DA0F517756C7657A79 /* NSString+DTUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = BE547E464E5D34F9FFB24BAC81D1E4CC /* NSString+DTUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24C5B52FD2AA9C49731DC6859ABA7CDE /* NSDictionary+DTCoreText.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C50CAB4241C141EF6EC9ED22AB3A7E8 /* NSDictionary+DTCoreText.m */; }; - 25E47E6CE8DF5BE732774ACEAE47BBF8 /* Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C8F0DCEAA5A3DF38C503CA30DC94C1C /* Archive.swift */; }; + 2617C9F41307C3F9C95A8D3A226B4F9F /* DTWeakSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = C422BCADBB48E28B549495663705A0FB /* DTWeakSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 270C5C10028F0BC5A62548326CCB379B /* DTColor+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = E196F56E68578181D5BDE787DE5C0F3A /* DTColor+Compatibility.m */; }; - 273EEB3E0A49E67E561DF3C5EE3E67C2 /* ConstraintMakerFinalizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B32EA4B7A492EEF26CE260E9AEFA0B /* ConstraintMakerFinalizable.swift */; }; - 2952D6997F5580919F4825DCAA1A4EFA /* DTExtendedFileAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = F5389D4E96A4AFE175691B4D3D4524E7 /* DTExtendedFileAttributes.m */; }; + 27B96826B55ACC5EBD0442CE693D0B49 /* DTFolderMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 2101691D5743105B2E3E40376584B91C /* DTFolderMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 27C72FB405DFBC4E9922AD382A2C35EB /* NSDictionary+DTError.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FC368BD2A8D266EB771E5FB9A5A5B3C /* NSDictionary+DTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 28BF24C2F4CD401C4C24E1DE0516746E /* DTActivityTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = 8272D0B59ABD6652D0CAE3DFCC2C5EE1 /* DTActivityTitleView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29D9B32DFFC59A157EDEF005DB92A5AC /* DTCoreTextLayoutLine.m in Sources */ = {isa = PBXBuildFile; fileRef = C71D83D6AEF198FA680B8A80BF2D9605 /* DTCoreTextLayoutLine.m */; }; - 2A875BEC32568EFA0FE0F3E58F0AA37C /* Archive+BackingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E36C4FE448DDDA03450847FCB785B6 /* Archive+BackingConfiguration.swift */; }; + 29DA9E401CE134D89FFFD1B7C2641DDA /* Entry+ZIP64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345CE45F696634CEA692A86D16F684C /* Entry+ZIP64.swift */; }; + 2A8AC1184CB8CD9ECA5115B5DB902490 /* RDEPUBReaderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BBE8424D2FE92DCAA8D2B1CA7D4D029 /* RDEPUBReaderConfiguration.swift */; }; 2AAF9361720FC2D8FFCA93B5FAE42019 /* NSMutableString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 31F3F91D5624C451E5D44D85B1DDE7C5 /* NSMutableString+HTML.m */; }; - 2D271CB0ADE6190C0835F90BC684CF0F /* RDReaderView-RDReaderViewAssets in Resources */ = {isa = PBXBuildFile; fileRef = 83410CC9CF2ABE63B90A92F2F988BF65 /* RDReaderView-RDReaderViewAssets */; }; - 2E477BA16E25771C05B852318212C523 /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945D4549E0B9168D7DDB910DE874B874 /* Typealiases.swift */; }; - 2E8C5F61C3B6A6D116BE6612C62C7370 /* Archive+Progress.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD987C2FA7C8FBC96FD024E421DA0E68 /* Archive+Progress.swift */; }; - 2EABBFC2F99A82EDEE15171F3F02126A /* RDEPUBReaderTableOfContentsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76374229228450DD3ADBF2DB618329F5 /* RDEPUBReaderTableOfContentsItem.swift */; }; - 2F39C838FD072D7CA4D3978C874DCF86 /* NSString+DTFormatNumbers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACA5E519F100582F0D772FCD76125C0 /* NSString+DTFormatNumbers.m */; }; + 2ACD02E15A8ABD8CD8D51AE3A6C29683 /* LayoutConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8C0D83EB6EDE71B264B6B0DD4644F5 /* LayoutConstraintItem.swift */; }; + 2B81BEF86D6B38359592B4576EB1851A /* ConstraintDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF362F88AB69253F1898E53D7A286A /* ConstraintDSL.swift */; }; + 2C4050D8A87BF13B3029E82DF0D712C5 /* SSAlertDefaultAnmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8F42440B802E6EC2B1FF7E3022F3A /* SSAlertDefaultAnmation.swift */; }; + 2D03767663B1C3251B80EDAB4F33D3CE /* Archive+Reading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0619F98DA80922E78A07321D0DC54F62 /* Archive+Reading.swift */; }; + 2DDC5E420C2953CB9DB36BCEB59C1F29 /* NSMutableArray+DTMoving.m in Sources */ = {isa = PBXBuildFile; fileRef = 53499F40E1790A6CBE8AFEB3AC5BEFC8 /* NSMutableArray+DTMoving.m */; }; + 2E26468A7766E7F228328A1B13FC92A8 /* DTBase64Coding.h in Headers */ = {isa = PBXBuildFile; fileRef = B8E40E302B85493BDE0FE91F829EC01E /* DTBase64Coding.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2FD7360250C2255A80583D4CF518FBFA /* DTTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = F35039A8D0AA1F7BAFC4E5E016057E15 /* DTTextAttachment.m */; }; + 30566C35A1D4F14A6A128B570E061108 /* RDEPUBReaderTopToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9D5BE3BC1B9E7C17553D6377DAA5D3 /* RDEPUBReaderTopToolView.swift */; }; + 30618822C1BD51F652D2F1A6ABA7E871 /* ConstraintLayoutSupportDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B34EE2160C27947D1EEFD42184CF5D /* ConstraintLayoutSupportDSL.swift */; }; + 308F8ADD0B8367CBD99AA8781FF1B580 /* RDEPUBTextRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01073FE593A6AE17D4376AF909753A8F /* RDEPUBTextRenderer.swift */; }; 30C01FE491C26314F776AD8B2AF136FA /* NSAttributedStringRunDelegates.m in Sources */ = {isa = PBXBuildFile; fileRef = E6F2E79CC33E0823C4E719ECDDFBBE08 /* NSAttributedStringRunDelegates.m */; }; - 3139FB597E4CA2C87278EAB2944CFD31 /* SnapKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CE078B465FDEF40EC0469626A2C2469 /* SnapKit-dummy.m */; }; - 317C3FF6D87E75F29CD74E44342734B3 /* RDEPUBParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5AA1752B9A4DA12829506699ACB35F /* RDEPUBParser.swift */; }; + 30FFF4A91F83509371C5B0CC18D5B919 /* NSData+DTCrypto.m in Sources */ = {isa = PBXBuildFile; fileRef = 17F750F38183302BF345F40F5A4CDD43 /* NSData+DTCrypto.m */; }; 31B805C50E0E187E6242B0C878C4D30D /* DTCoreTextMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F5D34047BAA894BEF1DFA23E6A9DAB1 /* DTCoreTextMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 31BE851901D14096E7AA66B5CDEE4D55 /* RDReaderPageChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC307B372D2CF490DBFE40A0328E8B0A /* RDReaderPageChildViewController.swift */; }; - 3257F2C96D06EA0B53E9CA9610A9137A /* Archive+Writing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF988440049800DB7824A94B81D20254 /* Archive+Writing.swift */; }; - 32A7AD3C6E3D5990746B47165F582A1D /* RDEPUBWebView+JavaScriptBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1A759F6D90A955E7DF86A14FEE1737 /* RDEPUBWebView+JavaScriptBridge.swift */; }; + 31EFA2212FA286AB73DE8ADE65F32224 /* RDReaderView+ContentAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E7E1D7707960ECBBBA26D5E04CE1A8 /* RDReaderView+ContentAccess.swift */; }; + 3238A1DC9C7C19F477F62081CEB1CDF6 /* RDEPUBWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268AF3AC14AC70E5C08F5EF60DBFC4F8 /* RDEPUBWebView.swift */; }; + 32B51B6FF5F874A18A0C55F9D785CB24 /* ConstraintRelatableTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885653999BD237A574B67DB279DE7E87 /* ConstraintRelatableTarget.swift */; }; + 3349AC669F17F9DE48E0CBEF213C71F7 /* RDReaderSpreadResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6422B9C1E04A7B5F008E4D4ED496CE6 /* RDReaderSpreadResolver.swift */; }; 343E794F88AA503F5139C2244DC1A2B1 /* DTTextAttachmentHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = C298199F5DC5390E8B6666F638B34C10 /* DTTextAttachmentHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 34BBABDE700AB81718A4752E7D1E0140 /* DTHTMLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FB2B1B0498DCBE7B79BDF49E963E3D /* DTHTMLParser.m */; }; + 359B41BE1F797CD1708D6E6473542222 /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5964AF1336E2F0DA0E2C32991E76C78C /* Data+Compression.swift */; }; + 35A8A9292DCF25CF8E47462B12D246EA /* RDEPUBRenderRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8BD060A6BD280BDB1107988B230DB1A /* RDEPUBRenderRequest.swift */; }; 35C5BE59CA712CCDF93D6E7010D93883 /* DTAttributedTextContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = C123C127CC539ACCB510F665DB774899 /* DTAttributedTextContentView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 36C2DB50F6A31A36A860540B4EF32E0F /* ConstraintMakerPrioritizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E138EC76D0DF24544D29D1A4D7429E3 /* ConstraintMakerPrioritizable.swift */; }; - 372E05B50EF2FD40BBF07BB08DA32E4A /* RDEPUBPaginator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24AD5C352BC2F291B84A39565644DE44 /* RDEPUBPaginator.swift */; }; + 36B1D51E18199A4B80157CED6D97C104 /* RDEPUBWebContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69E0662B2604A49919B5DDA504A39C4 /* RDEPUBWebContentView.swift */; }; 372E907DF08263D0E1AF904F1FCD66EB /* DTTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = D36ED8209C907E3E9B074DDB399F54FF /* DTTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3821BB7D496697C9F3CA8E00DD53115A /* ConstraintRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3593ADF561D422F73C8EABAD4A4F440 /* ConstraintRelation.swift */; }; 389F7C8400F9223C019F2D7825CD2369 /* NSCharacterSet+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 713C9FD0354D16AFCCFA630A97F2DA68 /* NSCharacterSet+HTML.m */; }; - 3B7E31D804C0208ECAEF8630D8CE6E8C /* ConstraintPriorityTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882FA707ED3513D263A1E0656AAD2D33 /* ConstraintPriorityTarget.swift */; }; - 3CDE626DA9038F9143A6004836F6284E /* RDEPUBWebDecorationOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F64F7254BE291374ECDC3E59BDB065 /* RDEPUBWebDecorationOverlayView.swift */; }; - 3CF8124B3C1C0C80E1784E221E564CC9 /* Entry+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F991240340FA026C71B68EB25B9F9C8 /* Entry+Serialization.swift */; }; - 3CFE245716B8EAC81022BB5543A419B4 /* SSAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F410ED25E4E84C3D5166261CDF4B58F2 /* SSAlertView.swift */; }; - 3D57FD29F6030DC9F461D1344BD78986 /* DTHTMLParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D75FEDE65722CF41F7F32A164376926 /* DTHTMLParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 38A65ED943B0C81B4EA40C50BBC63E50 /* UIApplication+DTNetworkActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 77213203802F44012346181FED2856F9 /* UIApplication+DTNetworkActivity.m */; }; + 38D8FC875F9F62E1F50B427EE02B2DA8 /* NSData+DTCrypto.h in Headers */ = {isa = PBXBuildFile; fileRef = 02EDFE0EEA47398F3E1A361A73AF6A6C /* NSData+DTCrypto.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 38F8914C748138F46D406222CA4E85AC /* epub-fixed-layout.html in Resources */ = {isa = PBXBuildFile; fileRef = 6CB839342BEE5213E1F9D59970EEC467 /* epub-fixed-layout.html */; }; + 3A6BC96AC902E04E85517D39B48F1FCD /* ConstraintDirectionalInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A651CEE8062C84A597DBC44ECA3DBCC1 /* ConstraintDirectionalInsetTarget.swift */; }; + 3AD8BF0401CF9D849805A1E3A57C5399 /* RDEPUBTextContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B789EEAB8791D76E537C6186EA1AC1D /* RDEPUBTextContentView.swift */; }; + 3AF7CE34BE9312F7027DC4763B33C4EB /* RDEPUBChapterTailNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B832537E4E508747319F7DF89BE256A /* RDEPUBChapterTailNormalizer.swift */; }; + 3C5EDCAB9E3A36C8F43A339B24ADCAB3 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0834E84632886CDD9911CAB76183F1CE /* QuartzCore.framework */; }; + 3CCC8FC7FF95E7A988B0F0B0468C12BF /* cssInjector.js in Resources */ = {isa = PBXBuildFile; fileRef = 4D9B8DA4629A86EE848CD980111120C3 /* cssInjector.js */; }; 3DC8F9474FC6A0BB7E81A703CE958966 /* NSAttributedString+DTDebug.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E2D85A099C99CD82FD1842EE7C52D2 /* NSAttributedString+DTDebug.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 3E10312523D443840CFBA72845ACCED9 /* ConstraintDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF362F88AB69253F1898E53D7A286A /* ConstraintDSL.swift */; }; - 3E8AFCDE8242393816FB4CFCA5048FCA /* ConstraintPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C7129757B54DFD9E445D0A17D73A83 /* ConstraintPriority.swift */; }; - 3EB94B3FE3C93EF534899C25C60E25EB /* RDEPUBReadingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67B49D37D02FC71C317CCA00B61EDB /* RDEPUBReadingSession.swift */; }; - 3F8EDE1AE8937FC335704E3F15C7C0B5 /* ConstraintInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C241AD9CEC9D6732CFBA6A55B49E64C9 /* ConstraintInsets.swift */; }; - 3FA49154704A5A890D4CF70CA5C57665 /* ConstraintMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60657D08CCDB38BFED30BD40F4720CE4 /* ConstraintMaker.swift */; }; - 41C4267B8D6E2C867C47F26F46116693 /* RDEPUBParser+ReadingProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 992AFC205F9539B89BF279A5EE6CB266 /* RDEPUBParser+ReadingProfile.swift */; }; - 42B5874545A810ADB3F168784542C3D4 /* RDEPUBReadingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49439BEDC4669BE8E3E9D3315C568645 /* RDEPUBReadingModels.swift */; }; + 3F9097BE7A6CA36DA12224F99D0D37BF /* RDEPUBTextRendererSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303F36CC1C0432C0F498EAED0D82A567 /* RDEPUBTextRendererSupport.swift */; }; + 415F0FDFD64679F7847A8E1E92277D68 /* ConstraintRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3593ADF561D422F73C8EABAD4A4F440 /* ConstraintRelation.swift */; }; + 416D5AD659DCF761E28F76F7E23B9F7F /* RDEPUBHTMLNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5612A9760A2FEC2682E417DF75F4C869 /* RDEPUBHTMLNormalizer.swift */; }; + 4195E8AE080906ABE5E4F962B53AF658 /* Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C8F0DCEAA5A3DF38C503CA30DC94C1C /* Archive.swift */; }; + 431305719D350F47AB85C2E09B3FB6D4 /* NSString+DTFormatNumbers.h in Headers */ = {isa = PBXBuildFile; fileRef = 6D86C7063D1128EAEB3A30EBEF214B3F /* NSString+DTFormatNumbers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 43C745CDBA3B1C734F9F93E63D48FF51 /* Archive+Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA251BAC8B430B3A4D4C4321046B990 /* Archive+Deprecated.swift */; }; + 43E3E9B7575ADB1B90A26C2879C6BD75 /* DTAnimatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = BC332335B380BC51C4F05C5CCDFB9189 /* DTAnimatedGIF.m */; }; + 441269EE3B2065102DE8D9F0D7EB461B /* RDEPUBResourceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615F9666D462FC95654FDC2732F700A6 /* RDEPUBResourceResolver.swift */; }; + 4432641D03459CFA21386E96E69D470A /* RDEPUBReaderAnnotationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7333A1BFCB9D7512A152B2B0EC6350 /* RDEPUBReaderAnnotationCoordinator.swift */; }; + 44688B5127920D2151DD116BB8709AE0 /* RDReaderView-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B92A6483AF878B266B8FE776A9A6F4B3 /* RDReaderView-dummy.m */; }; 44B1FBF893C06F314F8C9BD1405897D8 /* DTCoreTextGlyphRun.m in Sources */ = {isa = PBXBuildFile; fileRef = 36FCB869246FE45FD1C9609A34EB4E74 /* DTCoreTextGlyphRun.m */; }; + 44B918236776349CDE1D85C70EDD7328 /* RDEPUBSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185CD1A4E30E72715AF3129924F063E6 /* RDEPUBSelectionOverlayView.swift */; }; 45133E47CC5FBCF78BFCD38981BA027F /* DTDictationPlaceholderView.h in Headers */ = {isa = PBXBuildFile; fileRef = 982391EC8620A617B8788C8A6101C96A /* DTDictationPlaceholderView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 453D3B15ED40279A37CA0D74DBE4FFF2 /* SSAlertSwift-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 937CEFA81DA4227008DBFB114783A08B /* SSAlertSwift-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 45648E6F07341B6DC91D6E22FD4E253C /* RDURLReaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BCB7DFA6D42E5D07629E5E2F44FD0D /* RDURLReaderController.swift */; }; - 456926E08F81E14D4462225F8219D7B6 /* RDEPUBNavigatorLayoutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9197422E9FAD3EDDC5C5844350D497F7 /* RDEPUBNavigatorLayoutContext.swift */; }; + 453AA8CE0133392DB836780F707A41C1 /* Archive+MemoryFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174FC0CA05D20BB995E211DE5A0857DB /* Archive+MemoryFile.swift */; }; 45F90B98546FDBA03AB112DFE859A185 /* DTCSSStylesheet.m in Sources */ = {isa = PBXBuildFile; fileRef = E0D90EB06FC37D534934542F9CAB1D74 /* DTCSSStylesheet.m */; }; + 4626D775FF8050FC1B85AED719C6CCD3 /* RDEPUBReaderSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F47726654DBD3FAFF21FFA121D6EE7 /* RDEPUBReaderSettingsViewController.swift */; }; + 466AB83B99254DDBDF7ED5730D665FEC /* RDEPUBReaderController+PublicAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD33493F588BFD65E47EC9B8773C74C /* RDEPUBReaderController+PublicAPI.swift */; }; 46EF32D586FF590CE2C60C66364B9357 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEB14A8F8170777DB7CCC3F27C8EA57A /* ImageIO.framework */; }; - 47AA74DAF06F2F7A71F70FE9F6CC6CAE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; + 488A6649575C6F4DB55809630C3218B1 /* RDURLReaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECB9576AFBC841E4282C8091B9D1244 /* RDURLReaderController.swift */; }; + 48A82D1D9C23DC22512E4005270631D8 /* RDEPUBTextPageRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E030AC563D5D21F6792CA5056667F3F /* RDEPUBTextPageRenderView.swift */; }; + 48B2080D6CD3A7C757A1F69635D1446E /* RDReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A294C65CC6C154EF2122380953FEFE3 /* RDReaderView.swift */; }; + 48BE5A69C2B044534A947BC810B1DED4 /* RDReaderView-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 59444DC57CE3171F4EF0CD6481FACF30 /* RDReaderView-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 48C129F6559E80BA658FE6824B8BA94F /* ConstraintConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7176B677A350927FC670368313DC2CA1 /* ConstraintConfig.swift */; }; 4A13B4E5EA8616A8F09D90BC6EAEEA64 /* CTLineUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = D24FE8634A7AD2562DB8ADB9D4BBB36A /* CTLineUtils.m */; }; - 4AEC4012771A907108FD2D0C2237BCBD /* RDEPUBTextPositionConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA9133889FF0F7999C456AC029966C /* RDEPUBTextPositionConverter.swift */; }; - 4D981359F53E36D187F92A2EC9F23BEE /* RDEPUBSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16973940E1B38B29D620DAB562AB7396 /* RDEPUBSearchEngine.swift */; }; + 4B184BEB24237C483AE54DD52C6C107A /* NSString+DTPaths.m in Sources */ = {isa = PBXBuildFile; fileRef = A3565FEE19F19CB43A43409AFFF2BD2C /* NSString+DTPaths.m */; }; + 4B520B6C488D585307F8B6A48DC788B6 /* RDEPUBTextPageDecorationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DB5F45FEE54BD8D3F49BD345EDAB5D9 /* RDEPUBTextPageDecorationView.swift */; }; + 4D00F4DD1B8D720FFA715756C7D70D40 /* RDEPUBSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16C3C684271C3DE0964041A2D8701E58 /* RDEPUBSearchEngine.swift */; }; + 4D1B1254D155186FEE0796901E729627 /* RDEPUBWebView+FixedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71171943FCD1D8271266E2F6347D503B /* RDEPUBWebView+FixedLayout.swift */; }; 4DA6EC46F8968265B96C6D7B2260C4E5 /* DTTextBlock.m in Sources */ = {isa = PBXBuildFile; fileRef = ADC5AC8FD4EC45E9BA5654A38715BB93 /* DTTextBlock.m */; }; 4E4939F6592B06D2CA764DCACFB47871 /* NSString+Paragraphs.h in Headers */ = {isa = PBXBuildFile; fileRef = 83E37DC74BB53F90A5AD54842858A62F /* NSString+Paragraphs.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 4EEA1AF224E7966AC2BD3AD70903681A /* ZIPFoundation-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A06A36ADBE678DB44158D32E751FAFF /* ZIPFoundation-dummy.m */; }; 4F50C703427747B88405080B221349A1 /* DTLinkButton.h in Headers */ = {isa = PBXBuildFile; fileRef = D7B5E61166782ED8D585D4FCEE2074DB /* DTLinkButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 4FF142B3F09AA12AF220CB09D92F37D3 /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0EBAC7714A5F73B75AAB47D24B08B71 /* Constraint.swift */; }; - 50526E23A8AC98FB5502D6E22758A56D /* ConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D62D3200AC3D4E42C81CDFF7E884E8 /* ConstraintItem.swift */; }; + 4FFAECEE5100543DF0EE4113D9973AEC /* wxread-replace.css in Resources */ = {isa = PBXBuildFile; fileRef = 7F04C6EEE7B8C63B04236F35AE34B289 /* wxread-replace.css */; }; 5065D99E1588E8CC60677E74916A5C03 /* DTAttributedTextCell.m in Sources */ = {isa = PBXBuildFile; fileRef = E1F63A854BBF28E93AC1D3F71CD48CBE /* DTAttributedTextCell.m */; }; - 512C2FE338700D1FD6DFCFF2D1C96903 /* SSAlertPresentAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E481007A8174943868EBC81CF3B04B9F /* SSAlertPresentAnimation.swift */; }; - 5155756A1E960CE0786993DB461B11E9 /* DTPieProgressIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B443D035BA5652B03A41BE905C5AC17 /* DTPieProgressIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51A1B5800D23C355B1712E8D1103780C /* DTTextBlock.h in Headers */ = {isa = PBXBuildFile; fileRef = D497C3CA364E37DAAFF2C3B90CC7DBE8 /* DTTextBlock.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5271068D32AFE88CAC78B59ADED07EDB /* RDEPUBWebView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBCDE54D10D205F325580161FB9FC3 /* RDEPUBWebView+Configuration.swift */; }; 528DBAC7FCEFC7D6575C1F6CB4399AEA /* DTStylesheetHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 96FA99AA7D4AD0D4246C8DAC1CC0C24E /* DTStylesheetHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 52A961050B64F5F0985064C3BF7EBDBB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; - 52C2803D19934E29EC31F21B0808F3BC /* LayoutConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8C0D83EB6EDE71B264B6B0DD4644F5 /* LayoutConstraintItem.swift */; }; - 5486FFE1398EF5F6BBE4EE0871A21C39 /* NSDictionary+DTError.m in Sources */ = {isa = PBXBuildFile; fileRef = C18480913E002315E3F5EC6945371697 /* NSDictionary+DTError.m */; }; - 555E422A804DAB73D29F1E68F33BA9BD /* ZIPFoundation-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = D927F128EBF26AD2C61FD0E6B6F6F5ED /* ZIPFoundation-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 53240F8A0E98A48B59C6B2FA88270310 /* RDEPUBSearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8830746779660E908FADB2DEAD879AA2 /* RDEPUBSearchModels.swift */; }; + 53D7A11DAB7412522214EBA6CCB2ABE3 /* NSFileWrapper+DTCopying.h in Headers */ = {isa = PBXBuildFile; fileRef = 09C957F3199C596781EFA1DEA1DDCEC8 /* NSFileWrapper+DTCopying.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 53D8E4E9570BCD825E00CDD8A881054E /* RDEPUBModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2067EA81E19AA5F8E66DE4300DAB4A /* RDEPUBModels.swift */; }; + 53E8737EF21A600E0BFCD6F5DE467C5F /* RDEPUBReaderAssemblyCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72E8D63E9D57DA89DEE2774A89A61152 /* RDEPUBReaderAssemblyCoordinator.swift */; }; + 54634310A0DDCA6986A95D3A66CEECE0 /* Archive+WritingDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C7359A7C3FC7675FED220200A43A17 /* Archive+WritingDeprecated.swift */; }; + 5498E8AC301713ED5BEBDDA779B37AC9 /* ConstraintDirectionalInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91144FC099DD458E61625CD939577DAA /* ConstraintDirectionalInsets.swift */; }; + 54B9DDFA2E91F83BB5C9E2F5722884EE /* RDEPUBReaderChromeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BCD9B2E62051692362E404278683A18 /* RDEPUBReaderChromeCoordinator.swift */; }; + 552B25799A8AAD599BFAB1C6C3B4D454 /* RDReaderTapRegionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C637C7A440920FA0074A7ECF39D9C6F8 /* RDReaderTapRegionHandler.swift */; }; 5582735073EBB9F5078E6E8AEEFB646A /* DTCoreTextLayoutLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 64AB89EAB10148C39B726B26BE04F4F8 /* DTCoreTextLayoutLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 562A2DC2B45B453B2FD5C3F5F6EBE405 /* Archive+BackingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E36C4FE448DDDA03450847FCB785B6 /* Archive+BackingConfiguration.swift */; }; + 571B611A9DE148C57AF2C7EA53857C76 /* SSAlertAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97995C73297871C353364E685939DED9 /* SSAlertAnimationController.swift */; }; + 5720E398FFB4ECEDDE02A66A43FB901D /* DTSmartPagingScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB6B022E22A4ABC86D87469D4815B /* DTSmartPagingScrollView.m */; }; 57D5228C8A85628772D4D78E7FBEAF3C /* DTDictationPlaceholderTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 477A0FC2A1F0B7F7CCF72D201ECEBAB0 /* DTDictationPlaceholderTextAttachment.m */; }; + 584FC22D5B5F0120FCF9ACE0E570D137 /* UIView+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = 79A77584AB3B92B2508AE0BCFCB8FD87 /* UIView+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5894AF8B6F6022A84E3FB6F22494A364 /* RDEPUBCoreTextPageFrameFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C996E23A573AA37FB9B35A09725C1D1D /* RDEPUBCoreTextPageFrameFactory.swift */; }; + 58A61B581F5E418C86458C824089E6DC /* DTCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = B5FCE04EBB553439D1C2BA1001936293 /* DTCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 58B093EF9A7EA0373AD770DC0939D925 /* RDEPUBPaginationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B33931EE71CDEAAB6870A8DD00F441 /* RDEPUBPaginationModels.swift */; }; + 58EB34A71BF81209B8649B5D44C5458F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; + 58F5FFB8103C4733FF2F207057E60CE5 /* DTCoreGraphicsUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = B6425B2235BA00FE865AF58EF44F1144 /* DTCoreGraphicsUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 59272D5476F27119EF257930FE3D4722 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 53D473C6F07982D3D25781A343EC5A99 /* PrivacyInfo.xcprivacy */; }; + 597A2991F0147098A1A5F3555E7A4433 /* RDEPUBTextBookBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ED26E7B899A0B29A202F0CB011D5F09 /* RDEPUBTextBookBuilder.swift */; }; 597DE4CDF5C674E269F03F6AA0AFFCC1 /* NSAttributedString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 92460D196C5300656C9649784EBD3C58 /* NSAttributedString+HTML.m */; }; + 59FB4EE13B3168CADAAF65DC27802EFA /* RDEPUBTextPerformanceSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8607E8F20399521FFA80047941709E8C /* RDEPUBTextPerformanceSampler.swift */; }; + 5B36BCAC2EE5770917EE46A25443744F /* NSURL+DTUnshorten.h in Headers */ = {isa = PBXBuildFile; fileRef = 054674EB90C625BB4BEA9368ED92592B /* NSURL+DTUnshorten.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5BD388950720A80033201DCCC5B396B1 /* FileManager+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B7D3C6F0C845E3D42BBA613FFB0465 /* FileManager+ZIP.swift */; }; + 5C08CE24A3901A3E0F8B090F71A5871B /* RDReaderView+ToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9392F7CFCA8C63DD163AAB74B900D729 /* RDReaderView+ToolView.swift */; }; 5D03A5B9DC067875CFEC9D4EA7B059E8 /* DTCoreTextLayoutFrameAccessibilityElementGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = D863AE4F659A0F71B22DC9FF334DDF4E /* DTCoreTextLayoutFrameAccessibilityElementGenerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5D58B68AE7FBF43CF6F7B5AD1285C803 /* DTSmartPagingScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 96EEB6B022E22A4ABC86D87469D4815B /* DTSmartPagingScrollView.m */; }; + 5D8A0C9FDE40C0ECD438718CAE0080B4 /* DTTiledLayerWithoutFade.m in Sources */ = {isa = PBXBuildFile; fileRef = 70F5D03B9B3B7F182C8F5C1F5D4FA7E7 /* DTTiledLayerWithoutFade.m */; }; 5D8D65121E87E419D8D2F23F8AF43673 /* DTColor+Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 80B3D15ED0D19D020663EF055386291A /* DTColor+Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5E7FDA93607BDB800512A93BE294B12B /* Archive+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2659DB0685E16ABAE0382F726DA8C8 /* Archive+Helpers.swift */; }; + 5DCA3B311B4E46F3E96BF3D73119EB86 /* RDEPUBTextBookModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F7945AF94DCC77CB3E7F3F711BD4AB /* RDEPUBTextBookModels.swift */; }; 5E90EBD876ED4E50AB890FB56C4BFA57 /* DTHTMLParserNode.h in Headers */ = {isa = PBXBuildFile; fileRef = C8343CDCF82729A8EBCCA37A6650E34F /* DTHTMLParserNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 600D8BCA0F833306A821AE9963A2705F /* RDReaderGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45989DEA616D69D14306213B39E79FD6 /* RDReaderGestureController.swift */; }; 60532A2E06CCFF814933D2FAE6793AE9 /* DTCoreTextLayouter.h in Headers */ = {isa = PBXBuildFile; fileRef = C26DBE1150BC9A30B69839783E000358 /* DTCoreTextLayouter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 60974BD9B17F54CEB69CA6B80BB25730 /* NSURL+DTComparing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BFCAC7C2AEA867E67C63A852576774F /* NSURL+DTComparing.m */; }; 61BE3607EABD2657BC71AF8ACB1C25D4 /* DTHTMLAttributedStringBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B59C9A1B265393E4BA8383B308A5917 /* DTHTMLAttributedStringBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6214D10FFC4107B61C0A7D2124BE832D /* RDEPUBWebViewDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A3B34DE993508CAEBD18287344623E /* RDEPUBWebViewDebug.swift */; }; - 6268A3E8B23E586AE603A5F40F40F277 /* Archive+Reading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0619F98DA80922E78A07321D0DC54F62 /* Archive+Reading.swift */; }; - 62AD6F599831925D68B03800823FFE73 /* SSAlertAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2EC0B845131A88247902AEA26F40AE /* SSAlertAnimation.swift */; }; - 6439A2CC45B255D86F1BBF560BE64F7B /* RDEPUBReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659E6B7AFC5374BF3A56A306238C3E96 /* RDEPUBReaderDelegate.swift */; }; - 660A86A14D4B4568FA3DFABC53BA83ED /* NSString+DTURLEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 31BD7533867336DEA9B835F471767178 /* NSString+DTURLEncoding.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 636106852946C6686CE248B0722AE689 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1E89FE5F2996336E862E82C1E14E0EE7 /* PrivacyInfo.xcprivacy */; }; + 63A8D8CEE40C56246D84EB59FCC78AF6 /* RDEPUBReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFD8EDA8C70B31844B0A06CBCD0B403 /* RDEPUBReaderDelegate.swift */; }; + 64122D8C357DCDCBF0BD1BA6080A46FE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; 6634D568E49F5610242E1454680E5885 /* NSAttributedString+SmallCaps.h in Headers */ = {isa = PBXBuildFile; fileRef = DD189DA6054AAE2242F6644A2623B50E /* NSAttributedString+SmallCaps.h */; settings = {ATTRIBUTES = (Public, ); }; }; 67AE2AC8D9B961D45871077774E11FD3 /* DTObjectTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 073A7F9DF1A554BD1B324EA9E0090E91 /* DTObjectTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 68E20F956433EA0CEDAC3948339C74E1 /* ConstraintOffsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F950CEF31AC0065C0BEAB26BA2CEFA1 /* ConstraintOffsetTarget.swift */; }; + 67FC2E8BF2E51B5AEBE73C81BE0375C6 /* RDEPUBTextIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB1B8F778B5A8442BF121C13E5E460A /* RDEPUBTextIndexTable.swift */; }; + 68B185C109A9E448C95B2153715DBF41 /* SSAlertAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2EC0B845131A88247902AEA26F40AE /* SSAlertAnimation.swift */; }; 69674FEA70899F07BE6098AAEC849978 /* DTAccessibilityViewProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 43CFADE0F46014990052AD67F17C86FF /* DTAccessibilityViewProxy.m */; }; - 69B3DF49F4AB7646FD5A4584F9DCEF98 /* NSDictionary+DTError.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FC368BD2A8D266EB771E5FB9A5A5B3C /* NSDictionary+DTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69F197DAC284EBCD47A972FB1D112707 /* RDEPUBParser+TOC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AAB03A10169416E66E50562D9A57A0 /* RDEPUBParser+TOC.swift */; }; - 6AC9ABB942082B71200798D8E9E0AAEE /* DTSmartPagingScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = D2BA6D5DBD4C6A9285C4592721790A26 /* DTSmartPagingScrollView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6B22BC159183EB1F45E0EA101C69982A /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932FD688DE493323BA6B691BC2CB5094 /* LayoutConstraint.swift */; }; - 6C50D4014739F5FF607E4E9731191A16 /* DTFolderMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 2101691D5743105B2E3E40376584B91C /* DTFolderMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6C6BA0FF3D91F5121393BBF59DA93352 /* RDPlainTextBookBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D078643E7C9EA66EB922D04504AC413 /* RDPlainTextBookBuilder.swift */; }; - 6D8994F32576C73ECE67B2A7BC0DD084 /* ConstraintConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7176B677A350927FC670368313DC2CA1 /* ConstraintConfig.swift */; }; - 6E390B8259A9728AAA7EA8B864179418 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 53D473C6F07982D3D25781A343EC5A99 /* PrivacyInfo.xcprivacy */; }; + 699FFBEC77DD1CC3A54701AFC65FB2D0 /* RDEPUBReaderLocationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26BE3D7337938B1DB1B7610001CD169 /* RDEPUBReaderLocationCoordinator.swift */; }; + 69A2C721B126BE26E4BBAC1DF63858BB /* RDEPUBWebView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B55540809AED42C4B61333680CEC4CB /* RDEPUBWebView+Configuration.swift */; }; + 6AAD95B33C144808A58590F8EEC1812B /* DTFoundation-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = FCC718115E5C31C88A680522AE36B089 /* DTFoundation-dummy.m */; }; + 6AB30A0403FF95366915988914FB7D3C /* DTHTMLParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D75FEDE65722CF41F7F32A164376926 /* DTHTMLParser.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6D96D665BAD918861C54F9682459BE3E /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEB14A8F8170777DB7CCC3F27C8EA57A /* ImageIO.framework */; }; 6E3D0FDD78ECC20F4B3697ED21D64224 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C2B218B5EAA5831A036BBF7E1EFC1B9 /* MediaPlayer.framework */; }; - 6E90373CEFA7629B5FF48F610799E606 /* DTTiledLayerWithoutFade.m in Sources */ = {isa = PBXBuildFile; fileRef = 70F5D03B9B3B7F182C8F5C1F5D4FA7E7 /* DTTiledLayerWithoutFade.m */; }; + 6F6EE18D7F839AB238A8B47527737580 /* SSAlertPresentAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E481007A8174943868EBC81CF3B04B9F /* SSAlertPresentAnimation.swift */; }; 6FA0125F16DDEDAD341DB75DEB084F4B /* DTHTMLWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = E16D20FA492E085BD0871B107B3077B7 /* DTHTMLWriter.m */; }; - 701E081379C13353FC056435950394DE /* DTCustomColoredAccessory.h in Headers */ = {isa = PBXBuildFile; fileRef = 33DB57DF8C5AE334B1BBE9F989352F1D /* DTCustomColoredAccessory.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6FF9A524B248BAACCF8E3D4A00BF4CF1 /* NSArray+DTError.h in Headers */ = {isa = PBXBuildFile; fileRef = CEA93EDA6ED54D374265AA5D6B9EBBA9 /* NSArray+DTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7027EF8D77D4082C1BB9B80FF61FCADC /* DTCoreTextLayoutFrame+Cursor.h in Headers */ = {isa = PBXBuildFile; fileRef = 28043173CA3B3787DC401ACEB5398108 /* DTCoreTextLayoutFrame+Cursor.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 713D20A253E4AA77903E15DA2A9BA37F /* RDEPUBReaderSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40156267853001A18D40A4515A03C5A7 /* RDEPUBReaderSettingsViewController.swift */; }; - 72599431CE239793C684C877B4D23347 /* Date+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76A97683FB5E8E8B86EA4AD71F6F4B4 /* Date+ZIP.swift */; }; + 708B7BF70311E17A3B27C8AF36937ED9 /* NSString+DTPaths.h in Headers */ = {isa = PBXBuildFile; fileRef = FC5EC7C3D1808C9CF53354D7A3D69EE7 /* NSString+DTPaths.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7104D4CFBCF8831BEFD9EC0DD9AB678F /* DTVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BD9F2023FF826F67E69E3407A70A8A6 /* DTVersion.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 714C19C6756608FBFDD1DDBDBCBC294D /* ConstraintLayoutGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6BA0ECCB0ACF8580C3B5192CA9F11B /* ConstraintLayoutGuide.swift */; }; + 71624302D6B9222103A160FCC949D7B3 /* Archive+Writing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF988440049800DB7824A94B81D20254 /* Archive+Writing.swift */; }; + 7224AF6EDE2F24361DFAC2B54ADA9FE4 /* UIColor+RDEPUBHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933F9E11CFE9DD8A63C31D119B381969 /* UIColor+RDEPUBHex.swift */; }; 730ACB99580C5982DC08D50FC5084D41 /* DTAttributedTextView.h in Headers */ = {isa = PBXBuildFile; fileRef = A276DE925253460239BFAEC7663F60D3 /* DTAttributedTextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 73C9F968338A5A64F77CC8E763D05AB5 /* RDEPUBPublication.swift in Sources */ = {isa = PBXBuildFile; fileRef = F62A82E787C2CF458A244942F96F35A3 /* RDEPUBPublication.swift */; }; 7459A1656EDC29D685073B83561B783E /* NSNumber+RomanNumerals.h in Headers */ = {isa = PBXBuildFile; fileRef = 8552BE1A1F6A6D2879763DE3B2B8A2DD /* NSNumber+RomanNumerals.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 77DEFBEDEE73BB372A60ECB671AEE41E /* DTExtendedFileAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 538572A5C098252D629F2185F1B1C9E8 /* DTExtendedFileAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 78000F5A9639B1D9577CDD05682454A5 /* SnapKit-SnapKit_Privacy in Resources */ = {isa = PBXBuildFile; fileRef = B9DCB5EC0B1CDADD221717CADDF62359 /* SnapKit-SnapKit_Privacy */; }; + 755E82D4E33E3D24EBA3F426A58961D9 /* RDReaderViewProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31407369BDB36B40D253244CCF1F954A /* RDReaderViewProtocols.swift */; }; + 767F49D5DE536CA0332B88DAD52D3A5F /* RDEPUBReaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D479BB941D318A247DDD0A5C55B859C4 /* RDEPUBReaderController.swift */; }; + 768BBF03C64346E98F23D30DABEE3CAB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; 7821105EF320619993B3BF7FB90C7ADA /* DTCSSStylesheet.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B610E558C4917C4257DC198EAFEE2F6 /* DTCSSStylesheet.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 782BAE4F5C0FA3E6EB6EF30D83DB8653 /* RDEPUBRenderRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FAF02EDC6AC0DB9FA24A64E7B870CB /* RDEPUBRenderRequest.swift */; }; - 789D40DEE2CC4E0CBDFC873B9E30FE3F /* Data+Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5964AF1336E2F0DA0E2C32991E76C78C /* Data+Compression.swift */; }; - 78B7944A0F9E1A14F2787D72C0133A91 /* RDEPUBReaderTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA685920BF70A411A454D84901312521 /* RDEPUBReaderTheme.swift */; }; + 7877D94D10C10C9B7AC335B0A5864575 /* RDEPUBReaderPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8132548C8745F3AE7F3CBC78E1DDA9 /* RDEPUBReaderPersistence.swift */; }; 78D24D82D98D77E744FF73C9040618A8 /* DTHTMLParserNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 11B604FC3B0D5C91FEA763DCC0BE4DC5 /* DTHTMLParserNode.m */; }; + 7A31B9816036902B3A32A5C00CF641AC /* ConstraintMultiplierTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BE22EE711B7A38A4443E1C1C12BD79 /* ConstraintMultiplierTarget.swift */; }; 7A48CDFB40202EFD09A2A9AB0EE0770D /* DTCoreTextFontDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 56489D9BA834497E48438A4B4E1CCCFF /* DTCoreTextFontDescriptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7A6320A72DFF2C533B1CA8A927A9D516 /* NSString+DTPaths.m in Sources */ = {isa = PBXBuildFile; fileRef = A3565FEE19F19CB43A43409AFFF2BD2C /* NSString+DTPaths.m */; }; - 7A8961D4D3F7C2D7141C3AF9B2E60F0E /* RDEPUBPageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E2529CE2E852CB74565B24AC7B88965 /* RDEPUBPageInteractionController.swift */; }; - 7B6DFB4412FF0ACDAD4320AA4EEA8E63 /* NSURL+DTAppLinks.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D36F7CDCD7D5788CDCB3A1421DB242B /* NSURL+DTAppLinks.m */; }; - 7BBEA8E422292F56BFCE3A04391EBE01 /* RDEPUBReaderToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A6391BB8EAA2BA440A903CFBD508ED /* RDEPUBReaderToolView.swift */; }; - 7C046A2B1C3E09420711F10C0B5FED3D /* ConstraintMakerEditable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392A75EAF280876545D14AA00517BA4C /* ConstraintMakerEditable.swift */; }; + 7A7F0DE40D8B77129FC1E7FB54D79F7C /* RDEPUBParser+Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B142DE3191F790898FB028F3E6C91BA /* RDEPUBParser+Package.swift */; }; + 7AF352A0D8851E7032D631349459BADB /* ConstraintMakerRelatable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFE19508E12BF8B0B52E8CB4F724217 /* ConstraintMakerRelatable+Extensions.swift */; }; + 7B03A8945E4D03692C21B116D97DC74C /* SSAlertSwift-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 937CEFA81DA4227008DBFB114783A08B /* SSAlertSwift-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7B06E0624E131F4B4AE9CBA7C182DDD3 /* DTFolderMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = B245F7E143F3091652C64F6593E33F7A /* DTFolderMonitor.m */; }; + 7C026FA594F06339EAAEB774A776A030 /* RDEPUBResourceURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64644C3A8F17BE731AE00482BDEE9D86 /* RDEPUBResourceURLSchemeHandler.swift */; }; 7C05F56B62B15EFF344C5CDE4A52B8FB /* DTHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D4C9B7ED5E04C216AB28E3A4065C1C5 /* DTHTMLElement.m */; }; - 7C899BA1C93C579E0071B1C4AFF75521 /* RDEPUBWebView+FixedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 252896D6D97D0F06D5937D72DD041E65 /* RDEPUBWebView+FixedLayout.swift */; }; - 7CAA2C10A02EE7F4E6AF65DCB7F3772B /* RDEPUBWebContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1A53CCB5BC34FB44E35A4A29AA5591 /* RDEPUBWebContentView.swift */; }; - 7CCE1F11430F490242DCFFC47DD434A2 /* RDEPUBWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 929106407362D571E93A73D87EAB8107 /* RDEPUBWebView.swift */; }; + 7CB2AB8578F4A18DAEC3D738EC46F13A /* RDEPUBPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1FF16A8E3839BF599427F23328492C /* RDEPUBPreferences.swift */; }; 7D29C0A53AB1B4C1B257D46EBBDC9DA3 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; - 7F1244DE6C0ED5875B9149BDD300C1BC /* NSString+DTFormatNumbers.h in Headers */ = {isa = PBXBuildFile; fileRef = 6D86C7063D1128EAEB3A30EBEF214B3F /* NSString+DTFormatNumbers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7E2ACD5F2A6DB7B183F56A4B3F356AD0 /* ConstraintConstantTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F090A0289338B14050FAF16C22605B0 /* ConstraintConstantTarget.swift */; }; + 7E68A2D73CBE9B63C25ED43B6D33D494 /* ConstraintViewDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5637603691B9B35C2A886B686D3CE7 /* ConstraintViewDSL.swift */; }; 7F9CC306B17D409DB903DF850FEDA64C /* DTListItemHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = F7B0087DE7C394A677F5883541AB4FB7 /* DTListItemHTMLElement.m */; }; - 8093FDC6F3C2D5F6250492A548EA73A2 /* RDEPUBReaderBottomToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97FCA163E8AE5E977B12F9060F220252 /* RDEPUBReaderBottomToolView.swift */; }; 8099F1E613A15275EF5FC59ED3C73579 /* NSAttributedString+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D3D7C8444F58D9B3FCB4F3006D2CFBA /* NSAttributedString+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 81086493EE5742CF823461235F60AFD2 /* FileManager+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B7D3C6F0C845E3D42BBA613FFB0465 /* FileManager+ZIP.swift */; }; + 809E658A89FF98AF699C66D2429B29FD /* UIScreen+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = A3765B289CB7E6C5DF6160D8EBC83C3C /* UIScreen+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8115CAF95FA9A1ED63213498A1CA421E /* DTSmartPagingScrollView.h in Headers */ = {isa = PBXBuildFile; fileRef = D2BA6D5DBD4C6A9285C4592721790A26 /* DTSmartPagingScrollView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 811D7CBCD710AE45FF80A4B05BFE444C /* UIFont+DTCoreText.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E1548FC42118F1A4164739DE6BE4E9 /* UIFont+DTCoreText.m */; }; 814D0D63E5FD4AB7231E63064FE9EB5C /* NSAttributedString+DTDebug.m in Sources */ = {isa = PBXBuildFile; fileRef = BF4F3EC137168B92A5DA85AF50CD239F /* NSAttributedString+DTDebug.m */; }; 81905A7717925A17E6B98E934A75B8BA /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0834E84632886CDD9911CAB76183F1CE /* QuartzCore.framework */; }; 82119A0DC4981CC2AF389C7C4CC08186 /* DTListItemHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 98658510AA525496EBF5792E5D9884ED /* DTListItemHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8250FB5F79016BDCDD60AD731F0BD350 /* wxread-replace-latin.css in Resources */ = {isa = PBXBuildFile; fileRef = F159D5AE51CF38BA79429767DAA95194 /* wxread-replace-latin.css */; }; 83A4BC9FE4765C9B848DD4925D99F362 /* DTAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 99299A9FD6EF739D18352AC5B9325CB0 /* DTAccessibilityElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 83D0422FB78BED08AE09435CCB354F4D /* RDEPUBTextRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 749B6320F95F8F75FDF6B8ACC3E43D57 /* RDEPUBTextRenderer.swift */; }; - 8444C850D0A40CD0A8F32EF44197A6FF /* Data+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC78D0B0F37BF9AD1058CACA9AAF9BE /* Data+Serialization.swift */; }; - 84BEA3D5956D75FED777C15736EE787A /* wxread-default.css in Resources */ = {isa = PBXBuildFile; fileRef = 32D260607CE02388D7741193F901AF75 /* wxread-default.css */; }; - 8548C2C333CE758AF37B85D03AE4CFB7 /* RDEPUBReaderSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2C63A2462C2CA8751808A396C796F2 /* RDEPUBReaderSettings.swift */; }; - 8999804F6563D2105144FA9D821F3166 /* RDEPUBSearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A422EF35AF8F3EC0DCD8BCAF01A32E /* RDEPUBSearchModels.swift */; }; + 8483273E622F869454330F71D3824098 /* RDEPUBTextBookCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE13C3BA99810308E47EC53C68F6169A /* RDEPUBTextBookCache.swift */; }; + 855020CBFB65BB877255A61BAFFF7245 /* RDReaderPageChildViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6369702FFDE8929ED1931FC77B6B02A1 /* RDReaderPageChildViewController.swift */; }; + 88A854FEBA21BB5C77868E663A7C4346 /* RDEPUBReaderChapterListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6703BD7A2507028A11319843D0D7945F /* RDEPUBReaderChapterListController.swift */; }; + 89068EC008D095EA391E422D25604753 /* ConstraintLayoutSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50297AAB8278F2BB6FCFE82B20C7A049 /* ConstraintLayoutSupport.swift */; }; + 89A4857FB58483273A0954DA4935A558 /* DTPieProgressIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA1E8494B86FFD79C639DCB8C21E1BB /* DTPieProgressIndicator.m */; }; 8A0A0FB23DB0021ADC9AD0D90631883A /* NSMutableString+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C6293E97E128E98559FB74D9A342EAD /* NSMutableString+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A35CC67F0AA6B26397C10D756EA34E8 /* DTTextHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = FB2ABB1D297442D91AA42F1407DB4A58 /* DTTextHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8AA746B78ADA7C80D5BC28F8B53D8723 /* DTActivityTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = 8272D0B59ABD6652D0CAE3DFCC2C5EE1 /* DTActivityTitleView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8AB361C3413C66575600A33ED1EC699C /* RDEPUBReaderChapterListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDE24320E72FF6E77D1E402600E9753 /* RDEPUBReaderChapterListController.swift */; }; 8AC90941E71199090BCED76DE89EFB60 /* DTAttributedTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 77A613841EB90827AF321DD50B21DB5D /* DTAttributedTextView.m */; }; - 8AD34CC035AC1D89AB68C1327FAE3C62 /* DTFolderMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = B245F7E143F3091652C64F6593E33F7A /* DTFolderMonitor.m */; }; - 8B35034FEA4599E7AC24215B10A2CC54 /* ConstraintDirectionalInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91144FC099DD458E61625CD939577DAA /* ConstraintDirectionalInsets.swift */; }; 8B405CE13E7D718A9340A82B52766AB2 /* NSCharacterSet+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 75FA8E843D9225BD65EB9D17825EC0F4 /* NSCharacterSet+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8B81B840ED3E5B4E15887391DD7E5B87 /* wxread-replace-latin.css in Resources */ = {isa = PBXBuildFile; fileRef = C65067E704BE5EEEDE5FC6FF04C9C0AA /* wxread-replace-latin.css */; }; - 8B831C5DE899A4C6E6A9756FF359E6F3 /* NSArray+DTError.h in Headers */ = {isa = PBXBuildFile; fileRef = CEA93EDA6ED54D374265AA5D6B9EBBA9 /* NSArray+DTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 8BE4E5EF4D6B2F597605AE93CD3452D4 /* ConstraintDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6578DFAB51436E8DF266FD087700E26B /* ConstraintDescription.swift */; }; - 8CA81801E1B75ED092543CB44D5C29EC /* NSString+DTPaths.h in Headers */ = {isa = PBXBuildFile; fileRef = FC5EC7C3D1808C9CF53354D7A3D69EE7 /* NSString+DTPaths.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8E1EFA605CED65BF0D98CD48AD0DB08B /* RDEPUBChapterData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CEAB7724C9E71FFC8A368EECE6AADF9 /* RDEPUBChapterData.swift */; }; 8E8726381CBCFB1EDB350183456D4EF7 /* DTHTMLParserTextNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13AB6A32A293E3DC21A7047C6DB958CA /* DTHTMLParserTextNode.m */; }; - 8E9C2D9C827EA566D22F5E1131F794A9 /* ConstraintConstantTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F090A0289338B14050FAF16C22605B0 /* ConstraintConstantTarget.swift */; }; - 8EC243629BE685783F8B3E64FA924738 /* ConstraintViewDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5637603691B9B35C2A886B686D3CE7 /* ConstraintViewDSL.swift */; }; - 90F6FF9B3DA53C4B6C2D2FC588AA385E /* cssInjector.js in Resources */ = {isa = PBXBuildFile; fileRef = C6F199830BC382B6C9AEF29F421EE0C9 /* cssInjector.js */; }; - 9144EF4B8243782D693D9C3F18E4D968 /* DTPieProgressIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5DA1E8494B86FFD79C639DCB8C21E1BB /* DTPieProgressIndicator.m */; }; + 8F8C9BEEF983EFFB9D38B5A7C8BA5518 /* RDReaderPagingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F17B78960BAA0E4F63CBDDEAD953FE /* RDReaderPagingController.swift */; }; + 90BC61DF679C4367DAD09A5D02E0AB02 /* DTCustomColoredAccessory.h in Headers */ = {isa = PBXBuildFile; fileRef = 33DB57DF8C5AE334B1BBE9F989352F1D /* DTCustomColoredAccessory.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 90C189E1159D4DE5725F33E1E8ABF5D3 /* NSString+DTURLEncoding.m in Sources */ = {isa = PBXBuildFile; fileRef = CB7F3535507F56824BA21509302CF8B3 /* NSString+DTURLEncoding.m */; }; + 90C2D02582043D4CF91BDAD5FB0BC344 /* RDEPUBReaderContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA5A4277907D54250DB4BF7B95CE1A9 /* RDEPUBReaderContext.swift */; }; + 912F0C755DC31C3010FB5700F94193DD /* RDEPUBTextSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591B0B382F10A52D84F90D5C34893CB8 /* RDEPUBTextSearchEngine.swift */; }; 91FDA47810CE5CE2C48AED9205AE89B5 /* DTStylesheetHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = DA25968B7C9A40031DA45BB328EA8A2E /* DTStylesheetHTMLElement.m */; }; + 92594DF73D4E65DE3A257494085A7FA4 /* RDEPUBTextAnnotationOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F910EB56A7325328DCD94F082E99648 /* RDEPUBTextAnnotationOverlay.swift */; }; 92C59F96D443525E8833676479FA60C5 /* DTVideoTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 73A99927E52B24583A6A2CF7E6F23848 /* DTVideoTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 94A20C3CAA5381176EFE5636D8ECFFD3 /* ConstraintMakerRelatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD768C27E392C934C06DFB4AC401037C /* ConstraintMakerRelatable.swift */; }; + 93CDFBD7BE5E4EB5ED06820EB9A89794 /* NSURL+DTAppLinks.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D36F7CDCD7D5788CDCB3A1421DB242B /* NSURL+DTAppLinks.m */; }; + 9476BAD56597A02E230B9E4F8F6D306E /* RDEPUBReadingSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7429300C02F7E8453A429E08C9BD10EA /* RDEPUBReadingSession.swift */; }; 94F5846979DBC90E2B646E975F5ECBA8 /* DTHTMLWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B96B0998B49D8E149C995474DA5FE5F /* DTHTMLWriter.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 94FF947022AF6FE1F04D484EB69A2048 /* UIApplication+DTNetworkActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = 77213203802F44012346181FED2856F9 /* UIApplication+DTNetworkActivity.m */; }; - 95596341B01BC4C7E8E25D13DF885DE6 /* SSAlertDefaultAnmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B8F42440B802E6EC2B1FF7E3022F3A /* SSAlertDefaultAnmation.swift */; }; + 9504C9FFDCBBF6F619C349CB657093A1 /* default.css in Resources */ = {isa = PBXBuildFile; fileRef = 05D57D16346FAF830F1F82067307EA9F /* default.css */; }; 96507B8910C8C9DE0B221CFF0E8518D0 /* Pods-ReadViewDemo-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EBA0272559B0F7FAD8AC670AC7431188 /* Pods-ReadViewDemo-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 96C233ED331A3F6A71D589160B7668F3 /* RDEPUBTextRendererSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9654CB2B436E0FB30970E9FCF3870820 /* RDEPUBTextRendererSupport.swift */; }; - 96FB53A9630F4A8B5A772DDC36E52801 /* DTFoundationConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = FD03567681A79FFB6E81B3105503065D /* DTFoundationConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 9752BDBC8B255FC16E8DC9E10F407147 /* DTVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AF6156128A1A0CA24D9D06E346B9445 /* DTVersion.m */; }; - 982E91D1CAADCF6953B81B68AB159CEB /* DTFoundation-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5FAB3C7F19A511C2B1B7DFBA3CA308C4 /* DTFoundation-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 96AB626B9C20C3A6512835EC71A4F9FD /* RDEPUBPageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA31C1F2D2AF87B527C261FF3EF62C58 /* RDEPUBPageInteractionController.swift */; }; + 973D5F77AA6D4F77CEF20353736DA403 /* RDEPUBFontNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEE00544B5F5C69A1636474C8E76690 /* RDEPUBFontNormalizer.swift */; }; 9854DC3F1763B6272C2EA3FC6EC40B95 /* NSAttributedStringRunDelegates.h in Headers */ = {isa = PBXBuildFile; fileRef = E20272295EC253826AEE1F83A15D47C5 /* NSAttributedStringRunDelegates.h */; settings = {ATTRIBUTES = (Public, ); }; }; 987538B07E11741899878EA8BC947F51 /* NSCoder+DTCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 354C5DD0ACC742762200A640ADA859C6 /* NSCoder+DTCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 989258CEB05005E2A8BEA35661CA8591 /* DTTiledLayerWithoutFade.h in Headers */ = {isa = PBXBuildFile; fileRef = DF5EDCA3B21964D0462BE0982E2F0BF8 /* DTTiledLayerWithoutFade.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9917E9B804CD33AF3BE2A992A3510C97 /* NSNumber+RomanNumerals.m in Sources */ = {isa = PBXBuildFile; fileRef = B7672A26922DBD934FA1AC60D49EB322 /* NSNumber+RomanNumerals.m */; }; 992B7C8C824B5EA190EF1DB096064DB7 /* DTDictationPlaceholderTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = F9A2565B302334B662AB51DCF8E65EEE /* DTDictationPlaceholderTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 9959EBA37505B2AACF77057A32B8F7F0 /* Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD081272B47A3B73CE693D21DC26827 /* Entry.swift */; }; - 9A35B17A98BF4BA386549F582B9C8118 /* SSAlertViewExtention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2280F2302F54E90122747821FE681543 /* SSAlertViewExtention.swift */; }; - 9A57BBF6B1A6FEC692F444E92C508997 /* RDEPUBAssetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C4BF1170DCF38B8CC1E4979A0334FA /* RDEPUBAssetRepository.swift */; }; + 99BEFA2791CD385F40B63F36FDE54020 /* ConstraintAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9593E7B37569219F8D9A504B47707DC /* ConstraintAttributes.swift */; }; 9B199D71D2F48B2055A7FDF9BD7051CC /* NSString+CSS.m in Sources */ = {isa = PBXBuildFile; fileRef = E27C503C8128BC3CE109FFE077AEF325 /* NSString+CSS.m */; }; - 9BB5822506549A94B4A5A0B5F40ADC9A /* RDEPUBJavaScriptBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C425CE28DE9FA9CDD148A2986752D1 /* RDEPUBJavaScriptBridge.swift */; }; + 9B739D53E63B8810FC6F307C7997A363 /* ConstraintItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D62D3200AC3D4E42C81CDFF7E884E8 /* ConstraintItem.swift */; }; + 9BC6C98935EEE83035986E164F76BC47 /* DTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B007AEEACE9EA271DD73AFD80A74D83 /* DTLog.m */; }; 9BD146F72B29C703798001F138B069C2 /* NSCoder+DTCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 9129D37C7F89AD8FD530663BB4503FC0 /* NSCoder+DTCompatibility.m */; }; - 9D8CB20F75B0767AF187FAE3C595D790 /* UIApplication+DTNetworkActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = 957DB4675D5CCBD3CB1762F08619F3F7 /* UIApplication+DTNetworkActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9DE04C83A68C7512C0A860FFF5CEC113 /* DTCoreText.h in Headers */ = {isa = PBXBuildFile; fileRef = 53F98F0F060B066CAFBCD3C85C26B2B4 /* DTCoreText.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 9DF3F596DF4C655F29378648A7229D0A /* UIView+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = 79A77584AB3B92B2508AE0BCFCB8FD87 /* UIView+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9E4AB71DE8B3E7218B53B2C8EF6B843A /* DTAttributedTextContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = F0D72C5680EBE6CA716EE77281AA4436 /* DTAttributedTextContentView.m */; }; - 9E8DCCE70D492157FC50318D42742D5F /* ConstraintLayoutSupportDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B34EE2160C27947D1EEFD42184CF5D /* ConstraintLayoutSupportDSL.swift */; }; - 9FC9FAE8041266F3F85ACD5B0E1E7B17 /* RDEPUBDTCoreTextRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CBC86AC7AC17CD094469EC7DCE21724 /* RDEPUBDTCoreTextRenderer.swift */; }; - A0C9C636444B262AFF4C7AC91A9FF591 /* Archive+ZIP64.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5693BD171B879406EA14C97F4AD8D8 /* Archive+ZIP64.swift */; }; + A0A8F3BD0C99B4713511CC6D1691976E /* DTBase64Coding.m in Sources */ = {isa = PBXBuildFile; fileRef = 061BD3911E4E530C792E65CED7868C33 /* DTBase64Coding.m */; }; + A0C7E2ACBEA4D3103EB06870D860E2BE /* RDEPUBReaderLoadCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C94F70B03585FF87A0DEF959FD7E5A6 /* RDEPUBReaderLoadCoordinator.swift */; }; + A115BC82073566E4B9E4901AD77705B4 /* RDEPUBReaderHighlightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD245FC216DD480CF148530003BAA4BC /* RDEPUBReaderHighlightsViewController.swift */; }; + A118C8C3DDFA7F01BD8CD0BD57C74098 /* wxread-dark.css in Resources */ = {isa = PBXBuildFile; fileRef = 2FBA8F4FD78EAB6A6E4011D92577EA42 /* wxread-dark.css */; }; + A12A29C8B64DAB94E6013711400452B1 /* Data+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC78D0B0F37BF9AD1058CACA9AAF9BE /* Data+Serialization.swift */; }; A132A0C9EB13B65FB886B11AA2E6304C /* NSDictionary+DTCoreText.h in Headers */ = {isa = PBXBuildFile; fileRef = 27205D159B39090908CA0E9C3997B784 /* NSDictionary+DTCoreText.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A1FE48E415D345B47864748E6852C4D2 /* RDReaderView-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 875F2A69FD6767E29074114D638B2934 /* RDReaderView-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A15106734ACDCD5AA02C4F67C8F0F2B2 /* ConstraintMakerFinalizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B32EA4B7A492EEF26CE260E9AEFA0B /* ConstraintMakerFinalizable.swift */; }; A2CA9C27EB2B77637A12723F8CFE7AAB /* NSString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 4428633C8EC3F6289CCF376F588CE0C4 /* NSString+HTML.m */; }; - A2F28D0E1EE3C7B2135DF059CC5B8FBA /* DTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B007AEEACE9EA271DD73AFD80A74D83 /* DTLog.m */; }; + A2F0139EB87218A9361BB1F85800192A /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D495E2AB1223BD4D4909F8FF1FF47DAF /* Debugging.swift */; }; + A3045667F74EFACBD3F29F46BFB4EBC0 /* RDEPUBTextPositionConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 476A874AD38352C8F072379DDDEBE8CD /* RDEPUBTextPositionConverter.swift */; }; A30D5C1E413A3D319A83D3410A8A549F /* DTCoreTextParagraphStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F760D62F6C6F083374702146337F2CB /* DTCoreTextParagraphStyle.m */; }; - A323CECAFCDE8E4009A1E598D839BBD1 /* RDEPUBSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DEEEBC962A7BAB4EDB772B3A2279B2 /* RDEPUBSelectionOverlayView.swift */; }; - A33EB2CB8F875B027925A5CC9EEF8117 /* RDEPUBReaderPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B0E9F83D59A57EF00599DBE41952BF /* RDEPUBReaderPersistence.swift */; }; - A3FF25BFBC5DBC01FB99578E25B361E7 /* ConstraintLayoutGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6BA0ECCB0ACF8580C3B5192CA9F11B /* ConstraintLayoutGuide.swift */; }; - A667E54F636BD55EC03EACCB937AF1D9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; + A37500E37BE9E26DA5EE55916224DD7E /* FileManager+ZIPDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F9B298989305B9569B71E448E8C1D4 /* FileManager+ZIPDeprecated.swift */; }; + A43A87703C16488B17C5871E7398371B /* RDEPUBTextPaginationInterfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2412CF0CE63E83F2F0EC492D3256F9 /* RDEPUBTextPaginationInterfaces.swift */; }; + A6A66002BC974BBD05385DC49E9A4CD6 /* NSURL+DTAppLinks.h in Headers */ = {isa = PBXBuildFile; fileRef = 557DA029E15EC99631B647AE8922007F /* NSURL+DTAppLinks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A6C184213CB62062887604E95FCEC014 /* RDEPUBRenderDiagnosticsCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0484A107F418C2B5E2944F8BA3C1E6 /* RDEPUBRenderDiagnosticsCollector.swift */; }; A72E9F983E5BBFA7485EFB482B80E89E /* DTWeakSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 5155CCDD61E030184249A020FE5D74B0 /* DTWeakSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7AFFAFDE55A53049223F920254CAF86 /* ConstraintInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A5ABB8F3656A110B955B0306E9A8C3 /* ConstraintInsetTarget.swift */; }; A7D665BBDADBE3D64023FF52E218FCFA /* DTCoreTextConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = B2B3D0CB47B0A8E9EC5AF48B7F2DD6B0 /* DTCoreTextConstants.m */; }; - A92FACEFAAAFBCEF79747B917B280B85 /* RDEPUBWebView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB29D001CA71EAB0F6E4C7A8B0F04DA0 /* RDEPUBWebView+Search.swift */; }; - AA10BB78D6DCE8229EB4E146BCC9105A /* ConstraintRelatableTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885653999BD237A574B67DB279DE7E87 /* ConstraintRelatableTarget.swift */; }; + A7F239422F3768F1B4B3A696DD485D3B /* SSAlertSwift-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DE4FA5133891593CB45AF36B8EBF60C /* SSAlertSwift-dummy.m */; }; + A8BDFF208A28D6E52EDD9138290A3708 /* RDEPUBNavigatorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE2A1B83DCC99894642D65CE5EADFD4 /* RDEPUBNavigatorState.swift */; }; + A8F6AFC87576314D31AC08581890E4CD /* RDEPUBReaderController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8F6026114BC156F5EAC3906C3DAD24 /* RDEPUBReaderController+DataSource.swift */; }; + A9BDCA1E1104F38F9823FF2F9D06FE7E /* RDEPUBTextPaginationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D366C7AF049977D42FB1B20524EB0B15 /* RDEPUBTextPaginationSupport.swift */; }; + AA2ADBCF2640D8FF5BD8D071271284E2 /* SnapKit-SnapKit_Privacy in Resources */ = {isa = PBXBuildFile; fileRef = B9DCB5EC0B1CDADD221717CADDF62359 /* SnapKit-SnapKit_Privacy */; }; AA3722CC8B4992EDA8A9F5E9116835B7 /* DTCoreText-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 015F3EE69253C66E11B931E3360BA5FE /* DTCoreText-dummy.m */; }; - AAAC44275D726EADF484E946691E09EF /* ConstraintLayoutGuideDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4684B406B325B35F39322D75B59B6479 /* ConstraintLayoutGuideDSL.swift */; }; - AAFA34024F80407B268712A30687AE65 /* default.css in Resources */ = {isa = PBXBuildFile; fileRef = 05D57D16346FAF830F1F82067307EA9F /* default.css */; }; - AC190D36F6593D186EF5CA6E01C3016D /* RDReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70AA5021FC2962BB31E28C8BFDA797CD /* RDReaderView.swift */; }; - AC6220500850D0DB8142B44396FCA6BC /* RDEPUBModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2DE084B4CF376178F1AFB96BD84562 /* RDEPUBModels.swift */; }; + AB9713F8457EB1C14E53C76F0E442D82 /* DTExtendedFileAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 538572A5C098252D629F2185F1B1C9E8 /* DTExtendedFileAttributes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + ABB8DE00DCF34B2ED79C27585E97D5FB /* RDEPUBParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9B5052FF6F4941CE226FF90ED457DE /* RDEPUBParser.swift */; }; AC7000465258822B466855B12F1F97A3 /* DTCoreTextGlyphRun.h in Headers */ = {isa = PBXBuildFile; fileRef = 49A21AE1A0AB5383F895B34EA7D6973D /* DTCoreTextGlyphRun.h */; settings = {ATTRIBUTES = (Public, ); }; }; - ADD70654B7F6B32B2A687B2A7866027E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; - AE25127DC32DB2C11CF9136EC7E484E8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1E89FE5F2996336E862E82C1E14E0EE7 /* PrivacyInfo.xcprivacy */; }; - AF2E9051FD1821C3DC9C3AFBE674E0CF /* RDEPUBResourceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6002874EC82D09DCF64076F537D57EE4 /* RDEPUBResourceResolver.swift */; }; + AC787FA868EAD10F9929927B2AEBD247 /* RDEPUBJavaScriptBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C13EA061DD9866E60F683AD49EE945 /* RDEPUBJavaScriptBridge.swift */; }; + AD103B6A562A487B79DDB6617889D429 /* DTFoundation-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5FAB3C7F19A511C2B1B7DFBA3CA308C4 /* DTFoundation-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AD3F24E834B5215C632C984EFD959D10 /* RDReaderView+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7D36A8CADACEF369077B844EA911D0 /* RDReaderView+CollectionView.swift */; }; + AD4B914F1D725723EF1927B09FD6CC64 /* DTPieProgressIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B443D035BA5652B03A41BE905C5AC17 /* DTPieProgressIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AE2EA185BE47AA7636E4F927C1B220C7 /* RDEPUBReaderPaginationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D246461688CA3E0592B5536C95A624A /* RDEPUBReaderPaginationCoordinator.swift */; }; + AF261667F646799AC6B2D780044128B8 /* ConstraintMakerExtendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5245010774F8F1C4857763CC7B96E7B4 /* ConstraintMakerExtendable.swift */; }; AF7A1DC953992A149CB0815C90769529 /* DTIframeTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B931AB189BDDE2CCE8578B50BD60AF0 /* DTIframeTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; - AF9AE1A497A51A6C99E09D260EE8939F /* SSAlertSwift-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DE4FA5133891593CB45AF36B8EBF60C /* SSAlertSwift-dummy.m */; }; - B05D1543F8BF9AC00B000026C03067B9 /* RDEPUBPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8173B8E972A645B3A420B3C5C0DCDF6A /* RDEPUBPreferences.swift */; }; - B0BDB773F63288118EB1DE8DB294E53C /* RDEPUBPublication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF085139C084B6CB83EB511352CC385 /* RDEPUBPublication.swift */; }; - B189C3640D0AACE5C0B2513E932166F6 /* DTCoreGraphicsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = A8DFDCAB69B471EB5667C964865C26CF /* DTCoreGraphicsUtils.m */; }; + AFCBCBAE8B0C6A6EF6A6832A7213DC0D /* SSAlertViewExtention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2280F2302F54E90122747821FE681543 /* SSAlertViewExtention.swift */; }; + B00DB012B30976FE2972CC22585AE9DE /* ConstraintView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DB34ACB132E465CDF8191F7DB71EF6 /* ConstraintView+Extensions.swift */; }; + B16CC6DF8233C4E37B30A83420E10597 /* SnapKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CE078B465FDEF40EC0469626A2C2469 /* SnapKit-dummy.m */; }; + B18755975A9D04D3DB2732447D98CCB4 /* UIViewFrameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C351514678D41C58869BD057D5483F /* UIViewFrameExtension.swift */; }; + B2065C3E8DA14CE92675506BBC1A71F2 /* RDEPUBReaderTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 862E061063601D25DB09C808CA390C18 /* RDEPUBReaderTheme.swift */; }; B24FAD34A7915FC4B90FE9038E814FF2 /* DTLazyImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = A4E43DCDE0C8751557E7E65FEEB1C865 /* DTLazyImageView.m */; }; - B533B190B349CC90CA4814887EA7185F /* DTAnimatedGIF.h in Headers */ = {isa = PBXBuildFile; fileRef = A9FBC69C7DA29A7283920FDA9EBBCE8A /* DTAnimatedGIF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B4F34BAC839565D3D79B44FD9930560F /* RDEPUBAttachmentNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F132DA04F32D0155954A84A4467C95CC /* RDEPUBAttachmentNormalizer.swift */; }; + B678CD12611B4C83E3AF88FF80B8D092 /* RDReaderView+PageCurl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A525BB5B1708520D409C1A30CF1CD2D /* RDReaderView+PageCurl.swift */; }; + B686B485ACD5C39FEA878C32F4741AD9 /* RDEPUBReaderToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE03724A9B7893A98793AE13319755A1 /* RDEPUBReaderToolView.swift */; }; + B7A73A26B8B88977C3DDC07D559EAAEC /* UIScreen+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F051D813D50D20D53205D5FDCAA877C /* UIScreen+DTFoundation.m */; }; B7BDD79FC06A1E4E0E17592860844108 /* NSAttributedString+DTCoreText.h in Headers */ = {isa = PBXBuildFile; fileRef = FF3F7BA20A1213804048557D946CEEA9 /* NSAttributedString+DTCoreText.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8332F36582F7A001FE85A8D54B27C9E /* DTCoreTextParagraphStyle.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D62C5531667F364F14DB67D33FFA554 /* DTCoreTextParagraphStyle.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B85FE04FC7958BAB89406AD289D1D018 /* RDPlainTextBookBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5341C765E36D0B172F2F8D4812829521 /* RDPlainTextBookBuilder.swift */; }; B86493B996A888D1DA7814ECB6028E22 /* DTHorizontalRuleHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = A68D72764140BC3DF67A2B9E52CEC7A4 /* DTHorizontalRuleHTMLElement.m */; }; + B883A5F1131F3F9776604F3F9452AC65 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; B94CD41727DFCA8791EED8FB2CA397DD /* DTCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 2702E05AA7A266B7D8D0F881FD4E09DA /* DTCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B9F9B1F8E6FE6D22AAD7C2D9B8E660BB /* Entry+ZIP64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345CE45F696634CEA692A86D16F684C /* Entry+ZIP64.swift */; }; - BA174999E3413F5A45786FF54383240D /* NSFileWrapper+DTCopying.m in Sources */ = {isa = PBXBuildFile; fileRef = 1756C6FFCA695290BA62DA4E7B6DD20B /* NSFileWrapper+DTCopying.m */; }; - BA714218310D9BC815D508D7EBA90089 /* RDEPUBTextPaginationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B89D235A3ECE3B096FBB5E7A2626C9C /* RDEPUBTextPaginationSupport.swift */; }; - BBE7C12DD486B57DBAA98F86AC80994A /* RDEPUBParser+Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD993F901F35B7F58E0A7AC2F8962BA /* RDEPUBParser+Archive.swift */; }; + B96925D97DBA16A527D4FB87673AFF4E /* RDReaderView-RDReaderViewAssets in Resources */ = {isa = PBXBuildFile; fileRef = 83410CC9CF2ABE63B90A92F2F988BF65 /* RDReaderView-RDReaderViewAssets */; }; + BB782BBE8587EDB13C294A81E7F36225 /* SnapKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B10D3EC1701BBA4D69926915F942558 /* SnapKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BBF0C5F3F558E5C82C28F9F2885428B2 /* NSMutableArray+DTMoving.h in Headers */ = {isa = PBXBuildFile; fileRef = B0AC307F12521444C973EA0DE38B4608 /* NSMutableArray+DTMoving.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BC7BDDC881DED4124CD0C95A9B170A30 /* RDEPUBBuildDiagnosticsReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9D07EA99A4878E9F8A8839F19056654 /* RDEPUBBuildDiagnosticsReporter.swift */; }; + BCB700C605AF5689D526F24D02B97BFA /* RDEPUBReaderSearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3156D6DC256E20EF49691134C4342473 /* RDEPUBReaderSearchCoordinator.swift */; }; BE12CD112F82AD9050D7FB29395F27F9 /* DTVideoTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = 63A6EDF10DA5A051C6CDCBE5D97AF11E /* DTVideoTextAttachment.m */; }; - BE268016D8D21C191A2E82ED75A1F686 /* rangy-core.js in Resources */ = {isa = PBXBuildFile; fileRef = 49553EE889D9BB1C29A6AA69A857A8EB /* rangy-core.js */; }; - BFF4E73F7207C5C28837D4A6684055B1 /* ConstraintMultiplierTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BE22EE711B7A38A4443E1C1C12BD79 /* ConstraintMultiplierTarget.swift */; }; - C0CDCF140488BC1D63E83DBA74D30C2C /* RDEPUBTextAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA430D9A4A6EAE818564BB80309781D /* RDEPUBTextAnchor.swift */; }; - C341EF847BE944F959495C8145AE915D /* ConstraintInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32A5ABB8F3656A110B955B0306E9A8C3 /* ConstraintInsetTarget.swift */; }; - C346D582EB4C894FB4D2FFBC5FDD2D28 /* SnapKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B10D3EC1701BBA4D69926915F942558 /* SnapKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C41084FA13EB4E085E82258526EC3565 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; + BE67CF1248E014FD0397C32C8EC6D868 /* RDEPUBTextAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F083BF98854C4CB74C7C21DA34399A /* RDEPUBTextAnchor.swift */; }; + C078EB1EF2AD0AB675C64A3B920CDE8D /* RDEPUBChapterPageCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694DC251BF921B37330A35A6ACD9DF62 /* RDEPUBChapterPageCounter.swift */; }; + C0ADCA6F77E50FEDD72E2EF8FFA5B9A1 /* DTFoundationConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = FD03567681A79FFB6E81B3105503065D /* DTFoundationConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C100C86A98BE2D71D4DCC0C14BC9C933 /* DTBlockFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 375179DD3A4576CEF947E1760BBDC69B /* DTBlockFunctions.m */; }; + C107081396894ED1035195055E1D03D4 /* RDEPUBReaderController+RenderSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F9C4D562F4650166F650997ADA132B /* RDEPUBReaderController+RenderSupport.swift */; }; + C10B9F0291FFE41EBE7AF0CE975B3EB8 /* RDEPUBTextLayoutFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141325B9AB28E266F40552E4F82F952D /* RDEPUBTextLayoutFrame.swift */; }; + C1730B2336E26354E273E454C18E48A5 /* ConstraintMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60657D08CCDB38BFED30BD40F4720CE4 /* ConstraintMaker.swift */; }; + C252D4D3D440861EA6122DF66EB33500 /* DTHTMLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 32FB2B1B0498DCBE7B79BDF49E963E3D /* DTHTMLParser.m */; }; + C37E74CF4A213BB3E7AE31B676812BD9 /* RDEPUBPageBreakPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 117036F7DB9A22578522D0B397C28F26 /* RDEPUBPageBreakPolicy.swift */; }; + C48E39E0973F3D272FF1B3A23A43A4A0 /* Entry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD081272B47A3B73CE693D21DC26827 /* Entry.swift */; }; C5EE8E7373FBD24E66C678356393D871 /* DTCoreTextFontCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E791886E59DC0FAB02C6BBAB5BE5B6B /* DTCoreTextFontCollection.m */; }; C6B65C0EA759DF426FBBBB113BA3C3E8 /* DTBreakHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = F734214E067BDBDB581911E1A5CDAADB /* DTBreakHTMLElement.m */; }; C72CE4B974DFAE2C3037D425EB559314 /* DTCoreTextLayoutFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = CE6907AC04870132CCE3177E894607DD /* DTCoreTextLayoutFrame.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C78E9E8C277379E5FA7E49DD9C3A730B /* RDEPUBTextBookBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763D7B1705C9113C0A37E0775C4F9FEC /* RDEPUBTextBookBuilder.swift */; }; - C7B4B692AC1E9B6F6A844F171DE1BEC4 /* DTActivityTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEFAE80193E1F0DA53400C9AD641410 /* DTActivityTitleView.m */; }; + C75B12810764BCA70B81F9A3C4A562C1 /* RDEPUBAssetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFF67943C8CABB6171A6EDC7639F82 /* RDEPUBAssetRepository.swift */; }; + C86AF629D40E46FD947662786FAEA40C /* DTVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AF6156128A1A0CA24D9D06E346B9445 /* DTVersion.m */; }; C8F753B56A6C4D2A938B6EA68CF522F8 /* DTColorFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 86A0361390AA0C29BC1A5487083A12A7 /* DTColorFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; C9DD05EF2D27FFEBFA7BF6F437168501 /* DTCoreTextConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = C8C20D9A71E6605DEE80342E7122B683 /* DTCoreTextConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CAA01BF56A0A87A5C9260A864755863F /* RDEPUBTextBookCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D52166703BA10418F79BCD397B07DB /* RDEPUBTextBookCache.swift */; }; - CAD07B5D2FADB9B4A8F807CB4EB4F92A /* DTBlockFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = A0BD48DAF5AE9DB087F2552E99069709 /* DTBlockFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CA0C55A7CB67DBA313171CF5AF49A349 /* RDEPUBDTCoreTextRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A5FB560E84A3FE29C291466EAE9E22 /* RDEPUBDTCoreTextRenderer.swift */; }; + CC52D4A2D391BFE5D9F6F39DB6913757 /* DTCoreGraphicsUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = A8DFDCAB69B471EB5667C964865C26CF /* DTCoreGraphicsUtils.m */; }; CC8B8F3D758E98B51F69EDFAA69663A4 /* NSString+HTML.h in Headers */ = {isa = PBXBuildFile; fileRef = 8ABDEFEA94DC6408D92AC66DA3625DE5 /* NSString+HTML.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CCC981AE077E9F3E0BCFD8914E7D6F0B /* DTWeakSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = C422BCADBB48E28B549495663705A0FB /* DTWeakSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDA8EE4B287129954949825C50EBD3EB /* epub-fixed-layout.html in Resources */ = {isa = PBXBuildFile; fileRef = 353AB6D63236BF3C98B630EAB53AC742 /* epub-fixed-layout.html */; }; + CCF3600DBACB7361D2130AAFD7A6A83D /* UIImage+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = 15941683D394FB8D7EAE91CD2CF03FF1 /* UIImage+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE5C346EF640EA27FE463DBC8D1E1C88 /* RDReaderFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3346B9CF2882D4D658CE30CDCCC93CA /* RDReaderFlowLayout.swift */; }; CFC49467FE75BFABC22A1CE2FFA26570 /* DTImage+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = 12A5EA26EE7539EDDA90649D22FB675D /* DTImage+HTML.m */; }; - D0DEF92032CC5203F7CA51CCE03979FD /* UIColor+RDEPUBHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5DF77100B1D534940D2F5CEFCBB9F01 /* UIColor+RDEPUBHex.swift */; }; - D1792FAE89B67B4379A9B4157E22290F /* NSURL+DTUnshorten.m in Sources */ = {isa = PBXBuildFile; fileRef = C80065DBA5634DAA71129404975D7349 /* NSURL+DTUnshorten.m */; }; + CFCACF1D9A6846A81C7F50C8E6A23F70 /* RDEPUBTextSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42EA2D403B179C805701DD3638696B0 /* RDEPUBTextSelectionController.swift */; }; + D02DF9F9FB5549E3D0A75B88BF9A30A8 /* DTLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A1EBB9497F4846C84CA0BF5E4F84FC7 /* DTLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D031693A1CC970AC82EF0835E9DAD65D /* RDReaderContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291992715B24D4645FF6E45D0881635F /* RDReaderContentCell.swift */; }; + D0F53E98858850D4E7537E7BD40E8796 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932FD688DE493323BA6B691BC2CB5094 /* LayoutConstraint.swift */; }; D1A93B2D7F1B312755C48790338D7712 /* DTIframeTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = AA3CFD777AD76F97EBA05B3920F5786A /* DTIframeTextAttachment.m */; }; - D275929B8625E9F1E4A8306279B8B916 /* RDReaderView-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8432F1648F7FDA0F1A736F8A53739308 /* RDReaderView-dummy.m */; }; - D275D1EAFB36E5273169E7A4417E1219 /* rangy-serializer.js in Resources */ = {isa = PBXBuildFile; fileRef = 129DD2A43FC15DC9844C508DFE51D24A /* rangy-serializer.js */; }; - D2FC00885DA4EA4587ABF3384C82062B /* RDEPUBTextContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815D5A48F693355A35C89FC9E51D336 /* RDEPUBTextContentView.swift */; }; - D38A633998460910E98BF5E93841DAD6 /* NSMutableArray+DTMoving.h in Headers */ = {isa = PBXBuildFile; fileRef = B0AC307F12521444C973EA0DE38B4608 /* NSMutableArray+DTMoving.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D47B1A18F9830B10F0BE5D3E7B13C13D /* RDEPUBChapterData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967506B2C9A33F6E902BDA98940C603F /* RDEPUBChapterData.swift */; }; - D47F4852A393C8731EA395FE7CB059F4 /* UIScreen+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F051D813D50D20D53205D5FDCAA877C /* UIScreen+DTFoundation.m */; }; - D4EAEB6CFB1CB5C3B2456D475B2A460F /* RDEPUBTextSearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F8C4EB13F9C6442DAD004816726B09A /* RDEPUBTextSearchEngine.swift */; }; + D1B22560078A0D3A65D0427B3A0A38DF /* RDEPUBPaginator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA479EAA368CC8B528282E49B6C96BC8 /* RDEPUBPaginator.swift */; }; + D26C59ED1011B3D0A364E09AD764683C /* RDEPUBParser+TOC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137B4BC4EAE7AE0D9567E53EBBBA687E /* RDEPUBParser+TOC.swift */; }; + D2EC76E48BBF742006A8DE10F31F1A25 /* NSDictionary+DTError.m in Sources */ = {isa = PBXBuildFile; fileRef = C18480913E002315E3F5EC6945371697 /* NSDictionary+DTError.m */; }; + D39C3766E7B664581FAD2688E295BAB3 /* RDEPUBReaderViewportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FD217FE3F87580A8BA3BD7D8992D4EB /* RDEPUBReaderViewportMonitor.swift */; }; D526E03C098DA6CF8F01C1BC126C9BBA /* DTCoreText-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = CE7768E097F8BDE9FBE3752B98CD0101 /* DTCoreText-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D68CDB4A9AFFBBDE8383EB3B4D27D7C2 /* RDEPUBReaderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89ED8313799E7A012F5A06BCEACF09B8 /* RDEPUBReaderConfiguration.swift */; }; - D7159CF29AB1F8821F9D68A1412B0BD7 /* epub-bridge.js in Resources */ = {isa = PBXBuildFile; fileRef = 33D8E2EC0A0BDF6FCA33CA829E2C6FCF /* epub-bridge.js */; }; - D7972A2540E20B28E2C9D299AAE8AF46 /* NSMutableArray+DTMoving.m in Sources */ = {isa = PBXBuildFile; fileRef = 53499F40E1790A6CBE8AFEB3AC5BEFC8 /* NSMutableArray+DTMoving.m */; }; + D578954B277D170B2054CBA6081BEC4D /* DTAnimatedGIF.h in Headers */ = {isa = PBXBuildFile; fileRef = A9FBC69C7DA29A7283920FDA9EBBCE8A /* DTAnimatedGIF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D5F33E813692FC371616921D7C4AD1FF /* RDEPUBFragmentMarkerInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58EB90E2A4610839F356FE9B2A97486 /* RDEPUBFragmentMarkerInjector.swift */; }; + D5F9B30BDC204B4148E3BA2D6D69B90B /* UILayoutSupport+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C647A2EB2720BCBF88B160D124B57BF /* UILayoutSupport+Extensions.swift */; }; + D6104499A1F639473F78E573D24672F1 /* ConstraintMakerPrioritizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E138EC76D0DF24544D29D1A4D7429E3 /* ConstraintMakerPrioritizable.swift */; }; + D61D512B689DBF87FCF457C01594871A /* RDEPUBWebView+Reflowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8067F377315BFA008F9F0795CCB68D5F /* RDEPUBWebView+Reflowable.swift */; }; + D77ADB50A40D2C35FAA905E24838929B /* RDEPUBWebDecorationOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F616DAE29591FE9EC5FF94F4A922B115 /* RDEPUBWebDecorationOverlayView.swift */; }; D82DDA4C1E5C4D5378A9E5EA3F8E9DFE /* DTBreakHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 533CFFF04BE7BC4BDEAE11DF675A84CA /* DTBreakHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D8435365E01789A7EACBA963C1C92C6D /* RDEPUBTextPerformanceSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3119842637EAE995A0DAEBEB1170E363 /* RDEPUBTextPerformanceSampler.swift */; }; - D8F2872A4205075B286FE7718FC696CE /* RDEPUBWebView+Reflowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6861909964EF2CEDB7ED884A3B410A7D /* RDEPUBWebView+Reflowable.swift */; }; + D886581273ADE0CC6CA73210AA2A8740 /* NSString+DTUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = DCECDBD73595CA88A2208000B4B68352 /* NSString+DTUtilities.m */; }; + D9B2103AEF9770DFFED080731A236489 /* DTActivityTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEFAE80193E1F0DA53400C9AD641410 /* DTActivityTitleView.m */; }; + DA009A1B7AF4A6BB397EF6A0D54199EB /* ConstraintMakerRelatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD768C27E392C934C06DFB4AC401037C /* ConstraintMakerRelatable.swift */; }; DA5B51E5D80C161D2902B6FC60DA5EC8 /* DTImageTextAttachment.h in Headers */ = {isa = PBXBuildFile; fileRef = 426D442C01742E4361F5BC34D43152A6 /* DTImageTextAttachment.h */; settings = {ATTRIBUTES = (Public, ); }; }; DAD16D3E6EBB32FF96C63BFE6037065F /* DTLinkButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A1B75050E71BDD96FC19EC6BA129E6A /* DTLinkButton.m */; }; DBCA42F7E1697F4EC82E0E7C9657851D /* DTAttributedTextCell.h in Headers */ = {isa = PBXBuildFile; fileRef = D4093D30B372960168C1349100DF7DE0 /* DTAttributedTextCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DBF7631687BF37FCD33C20E23D6C291C /* UIApplication+DTNetworkActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = 957DB4675D5CCBD3CB1762F08619F3F7 /* UIApplication+DTNetworkActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC3E2932A82AD5D88D8B1EFBF004BEC8 /* DTCSSListStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = B509CD4B498635D4536245E43D802ED9 /* DTCSSListStyle.m */; }; - DCD33DEBC59EAAB25D3B3F34EA206133 /* Archive+Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA251BAC8B430B3A4D4C4321046B990 /* Archive+Deprecated.swift */; }; - DD4857F182195748AB22953A3AD5CFBC /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = D495E2AB1223BD4D4909F8FF1FF47DAF /* Debugging.swift */; }; - DD55868C1A308AE1C5D56E5E3FA0AE44 /* RDEPUBFixedLayoutTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660901C91F4E1D354689FF2F8B6E1679 /* RDEPUBFixedLayoutTemplate.swift */; }; + DC5C1FECCEB131BAD26561FEBE22745C /* RDEPUBStyleSheetComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16842E6F1C3E8526D28C7DABA1D6898 /* RDEPUBStyleSheetComposer.swift */; }; + DD098AE376D237EBCD0602DC493FD49E /* RDEPUBParser+ReadingProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 667442978548AAC9B24B14B6F2597536 /* RDEPUBParser+ReadingProfile.swift */; }; + DD5647950937A07ACA022C75387CF527 /* ZIPFoundation-ZIPFoundation_Privacy in Resources */ = {isa = PBXBuildFile; fileRef = CA4A114775B98CEA4A566C18123B145F /* ZIPFoundation-ZIPFoundation_Privacy */; }; DD73CD0E5148961D1ABD80E050122314 /* DTCoreTextFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B031E2B903C2F177195C1C3D1D1EDB /* DTCoreTextFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DD75D7D90DE6DB0FC4722C6A267B173F /* ConstraintAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9593E7B37569219F8D9A504B47707DC /* ConstraintAttributes.swift */; }; + DDFCE8D603027FA6C70FCED85A181CA8 /* ConstraintPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C7129757B54DFD9E445D0A17D73A83 /* ConstraintPriority.swift */; }; DE68AD2F426FD462BC70F968E5AF5553 /* DTTextAttachmentHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC2F552388CD08EC1EFDEF606AE222 /* DTTextAttachmentHTMLElement.m */; }; - DF8D260A10033EBCFE8C5413EFF1535F /* UIViewFrameExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C351514678D41C58869BD057D5483F /* UIViewFrameExtension.swift */; }; + DF39E502984126B7FFA216FC647FDB02 /* RDEPUBPageLayoutSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19F9801AD3675102B14F64AFAFB99D89 /* RDEPUBPageLayoutSnapshot.swift */; }; + DF7B0A62AD5A1F39463B55A890F32E50 /* UIView+DTFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = 4219E7C32019899890CAB66E5FBDF524 /* UIView+DTFoundation.m */; }; DFB5A1C64C7908FD05BEFF7C7B571353 /* DTAttributedLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = 5679D281C282896E6FC352C48E0AF439 /* DTAttributedLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E0021B223A2E2D11206F395CCFA31A02 /* RDEPUBNavigatorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A800873D793225974FA75561B0810DF /* RDEPUBNavigatorState.swift */; }; - E0DA92D28A9D84B483111D6C0D35A118 /* RDReaderGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2682E7883D110ECC2F46C6270375D24 /* RDReaderGestureController.swift */; }; - E1A1B8B02C3EF2EF0008C7DAF6608192 /* UIScreen+DTFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = A3765B289CB7E6C5DF6160D8EBC83C3C /* UIScreen+DTFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E005DCCFE4D00062C0593F16CB0FB7C9 /* RDEPUBReadingLocationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0311E22DAAE8E4ECEE12DFDB8A092A9 /* RDEPUBReadingLocationModels.swift */; }; + E0A64565947C7016CD15A87A29C06FE1 /* RDEPUBTypesettingPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EADCA56DD969174A20085845D93C409 /* RDEPUBTypesettingPipeline.swift */; }; E1DA58105A72D6C0B77A8A9F7822BD68 /* DTCoreTextLayoutFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 029130886CE8E422D16ADF97468CB1B0 /* DTCoreTextLayoutFrame.m */; }; - E2731FCFCF96868342E633F7962FD9F5 /* SSAlertAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97995C73297871C353364E685939DED9 /* SSAlertAnimationController.swift */; }; - E3726D55A7B60C3DC86EC4D90370426A /* DTFoundation-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = FCC718115E5C31C88A680522AE36B089 /* DTFoundation-dummy.m */; }; + E2EE910F549C5D233EB236BB480CC09D /* NSString+DTURLEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 31BD7533867336DEA9B835F471767178 /* NSString+DTURLEncoding.h */; settings = {ATTRIBUTES = (Public, ); }; }; E38FC7829B5BDD1588FA487E0FBAC208 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */; }; - E5926D0035C1FE12AD81A4441276CB8C /* RDEPUBParser+Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE8194BF851EE58EA61CBBA1710B1 /* RDEPUBParser+Package.swift */; }; - E61C54D454FC27FD101B58B9BC395466 /* DTLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A1EBB9497F4846C84CA0BF5E4F84FC7 /* DTLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E8AB2FEE8443436B10DC3BD74A8226FD /* DTFoundationConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD568D38C209BA1CC0EBFA809A35204 /* DTFoundationConstants.m */; }; + E3B9A9854AB1AB2154E1CA2C6B9E61E2 /* URL+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537BB06317FA188DF7A8A960152520E8 /* URL+ZIP.swift */; }; + E454A390799FA2F7E988A23592713FCE /* DTFoundationConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BD568D38C209BA1CC0EBFA809A35204 /* DTFoundationConstants.m */; }; + E55D6001C2F52C1C47E541F84B4C5A8A /* ConstraintDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6578DFAB51436E8DF266FD087700E26B /* ConstraintDescription.swift */; }; + E5AB0AA1CD3062159407E53FD7365145 /* NSString+DTUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = BE547E464E5D34F9FFB24BAC81D1E4CC /* NSString+DTUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E6C30E9127E9DCDE49BDD8BBB7F81507 /* Archive+Progress.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD987C2FA7C8FBC96FD024E421DA0E68 /* Archive+Progress.swift */; }; E8AB5CE26A5224EF09E9E363D70C7775 /* DTDictationPlaceholderView.m in Sources */ = {isa = PBXBuildFile; fileRef = AFE0B56638AFB77F4455784A7ED59876 /* DTDictationPlaceholderView.m */; }; - E94DC7964A5276288537BA9B36377647 /* NSFileWrapper+DTCopying.h in Headers */ = {isa = PBXBuildFile; fileRef = 09C957F3199C596781EFA1DEA1DDCEC8 /* NSFileWrapper+DTCopying.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E8C0D5929CEA330C30F63AA5502723DD /* RDEPUBReaderController+ContentDelegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127FB4B3B74B022CBE4054BA33CB24A0 /* RDEPUBReaderController+ContentDelegates.swift */; }; EA0040823CCF8E97363DB5891F5DA6D0 /* DTCoreTextFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F2D44B82815CA3397BE2C9F64819B88 /* DTCoreTextFunctions.m */; }; - EABC41A8E3D4525FEEC80C2F6CF0C108 /* NSString+DTURLEncoding.m in Sources */ = {isa = PBXBuildFile; fileRef = CB7F3535507F56824BA21509302CF8B3 /* NSString+DTURLEncoding.m */; }; + EA3DDFFEE504AA55AD089305ACA3D763 /* RDEPUBTextLayouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A32D83CDE7A6494252691BE7E99638 /* RDEPUBTextLayouter.swift */; }; + EA42097748117159D15207C9323ABDED /* RDEPUBParser+Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9087CF398AFBBC8FFB5C60E6389EAB1 /* RDEPUBParser+Archive.swift */; }; + EA53228A41C6502A614D69F15A3B408A /* RDEPUBReaderDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851EF2369FB225D981E94F4BB2E9F65F /* RDEPUBReaderDependencies.swift */; }; EBB7E4D4DB253B5F1267CF9B4DDB4E93 /* DTCoreTextFontDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = BCCA590DB72AA93275262A414F16578A /* DTCoreTextFontDescriptor.m */; }; + EBDF8A9891119113AA5CA45851DBD1B7 /* RDEPUBReaderRuntime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413B561A626E7BC1C78EFEFA131B92B0 /* RDEPUBReaderRuntime.swift */; }; EDA269B595D70EC2B9D81DC890157D63 /* DTAccessibilityViewProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = F37563A56C180CE18EEFADE56FA3C17F /* DTAccessibilityViewProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EDE07C2D89DEC86AF5B3BDB830DFDFDB /* DTVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BD9F2023FF826F67E69E3407A70A8A6 /* DTVersion.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EDAD4A9457EBB9E65A2381991AB738F7 /* NSArray+DTError.m in Sources */ = {isa = PBXBuildFile; fileRef = 73940D215D644B6815A09DA802CFBDDF /* NSArray+DTError.m */; }; + EE18BE5AFEFAA1893D26EFBA4AFA7193 /* Date+ZIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76A97683FB5E8E8B86EA4AD71F6F4B4 /* Date+ZIP.swift */; }; EE4F0992A0F597320FF7BDE4691D0FD4 /* NSString+CSS.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D6C48939A8498A09DCFC7F41ECB8A25 /* NSString+CSS.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EFDDBC3AC88A2F9843346E3A0A5389F0 /* DTCoreGraphicsUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = B6425B2235BA00FE865AF58EF44F1144 /* DTCoreGraphicsUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EF09C17C1A9226B1D3377A4186237715 /* RDEPUBWebView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DD2563775FC83493319BFC4E953F00 /* RDEPUBWebView+Search.swift */; }; F006F73F17F2E3CC0E7852E9357BA946 /* DTAnchorHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 424F12BEB0EDAAF1C719B5103D30C653 /* DTAnchorHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F045CE2C2618CE50C24CD7337B7C69A3 /* UILayoutSupport+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C647A2EB2720BCBF88B160D124B57BF /* UILayoutSupport+Extensions.swift */; }; - F0561A69C416F2368A40D6BAF663B2E1 /* Data+CompressionDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D641809A99849F80FDCB02F8CE8D6A0 /* Data+CompressionDeprecated.swift */; }; F0F631056BADA32253528247DF493272 /* DTHTMLElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9D4A455723797A27F04A15C4E47823 /* DTHTMLElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F1E8826F0590E0CC1F5A0128D99D7AA3 /* ConstraintMakerRelatable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFE19508E12BF8B0B52E8CB4F724217 /* ConstraintMakerRelatable+Extensions.swift */; }; F1E9B6D0728711AE4D7154616DBC17EC /* Pods-ReadViewDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = AA0811310099A4BCB3D52683B70DB3D1 /* Pods-ReadViewDemo-dummy.m */; }; - F2376D242202DBE8F1E8ED36A43FB5D3 /* ZIPFoundation-ZIPFoundation_Privacy in Resources */ = {isa = PBXBuildFile; fileRef = CA4A114775B98CEA4A566C18123B145F /* ZIPFoundation-ZIPFoundation_Privacy */; }; - F25AD7EBA6F5DF35A82207F6D7E7A25C /* NSURL+DTUnshorten.h in Headers */ = {isa = PBXBuildFile; fileRef = 054674EB90C625BB4BEA9368ED92592B /* NSURL+DTUnshorten.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F27B7E0ECDB515E97953535C268DFC0E /* RDEPUBStyleSheetBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C1467D2571E3541EC221A49C90B110 /* RDEPUBStyleSheetBuilder.swift */; }; + F2BAD19B5E842273FA3B4FB464F24EBA /* ZIPFoundation-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A06A36ADBE678DB44158D32E751FAFF /* ZIPFoundation-dummy.m */; }; + F367BD50B50B680A2DA23005EAC2A7BD /* SSAlertCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE4EE750340347ED584C5A2C1164331A /* SSAlertCommonView.swift */; }; + F4A25EE18B07DECDA92E9D539D70A76B /* ConstraintLayoutGuideDSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4684B406B325B35F39322D75B59B6479 /* ConstraintLayoutGuideDSL.swift */; }; F4A2E3C4237B8931F61B72FF0BDD6D69 /* DTObjectTextAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = F038FB491B9A4A53447EF0DC333301AE /* DTObjectTextAttachment.m */; }; - F588926C35B92A750FB13835C0111BB1 /* NSURL+DTAppLinks.h in Headers */ = {isa = PBXBuildFile; fileRef = 557DA029E15EC99631B647AE8922007F /* NSURL+DTAppLinks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F5142E93B76DB1917D507EECB36E5DED /* rangy-serializer.js in Resources */ = {isa = PBXBuildFile; fileRef = FD2D305B938F31ABC9EC9369E230A62B /* rangy-serializer.js */; }; F58ABA106C6200175DF574DF05A75BA8 /* DTAccessibilityElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F0486D3B46DC92AB254D6EE9CC233BF /* DTAccessibilityElement.m */; }; F5A424E67F41C4F7B830CE2F7E71570D /* DTCoreText-Resources in Resources */ = {isa = PBXBuildFile; fileRef = F4E2B1AC73977F10254628C0B7EBE38F /* DTCoreText-Resources */; }; F6036BBDFE7484F5A453F6876C97C098 /* NSAttributedString+SmallCaps.m in Sources */ = {isa = PBXBuildFile; fileRef = 76F99C8130EE83A22CD223C7F5E6E66E /* NSAttributedString+SmallCaps.m */; }; F61102C9BF3D6296CB6C6B754ABF98C5 /* DTTextHTMLElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CFEE298DD52246B46E6FA62B16B38DF /* DTTextHTMLElement.m */; }; - F6511F62871012278880F32E36DC1B20 /* RDEPUBParser+Resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4EE1E0ACE64032C6C8CB26A91235993 /* RDEPUBParser+Resources.swift */; }; - F65F1F57FE114E39EDF8BD657EF8565B /* NSString+DTUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = DCECDBD73595CA88A2208000B4B68352 /* NSString+DTUtilities.m */; }; - F675FAE700CEDACDC117FD78CBC91B9E /* ConstraintView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DB34ACB132E465CDF8191F7DB71EF6 /* ConstraintView+Extensions.swift */; }; - F7B3249FC969AB2C684558D696807F42 /* ConstraintDirectionalInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A651CEE8062C84A597DBC44ECA3DBCC1 /* ConstraintDirectionalInsetTarget.swift */; }; - F9C5409DC727419DBFA6AD6701108F13 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEB14A8F8170777DB7CCC3F27C8EA57A /* ImageIO.framework */; }; - F9D7FD379D25CE2B854236C1051D4B1C /* WeReadApi.js in Resources */ = {isa = PBXBuildFile; fileRef = FA510A2ECD919C8E8C8EAA7770D37E49 /* WeReadApi.js */; }; - F9F8A1BE8710CBCA9E2DEA97D35B17C0 /* RDEPUBReaderHighlightsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C6DCCE49AF60A4CD752939A15DB118 /* RDEPUBReaderHighlightsViewController.swift */; }; - FA5321384E837B212BA8DBD3FBE3FB04 /* NSURL+DTComparing.h in Headers */ = {isa = PBXBuildFile; fileRef = 731C0C61E3AD227D691FBA1C2C17BF7B /* NSURL+DTComparing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F638D941A6B83EE3D0FA3116BFE9CE52 /* RDEPUBAnnotationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C274719939F1335BCB8FA97636A9D2DD /* RDEPUBAnnotationModels.swift */; }; + F677C12701262AFF0F6FCCE4C2150CF9 /* rangy-core.js in Resources */ = {isa = PBXBuildFile; fileRef = A2BD2727B338CE5C2C3BC6E8B8A23DE9 /* rangy-core.js */; }; + F68D834D874BA6D351EA0BAE720DB1A6 /* NSURL+DTUnshorten.m in Sources */ = {isa = PBXBuildFile; fileRef = C80065DBA5634DAA71129404975D7349 /* NSURL+DTUnshorten.m */; }; + F6E6CAC7A3C273A7C4227BD6F0E36EE1 /* SSAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F410ED25E4E84C3D5166261CDF4B58F2 /* SSAlertView.swift */; }; + F77B604D4BCCE6CBE8520E588343F0A1 /* RDEPUBSemanticMarkerInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E2F4C746E0B7479BE677A20358CB93B /* RDEPUBSemanticMarkerInjector.swift */; }; + F8D652BDA6DF2BCEECB48009D34C6834 /* RDEPUBFixedLayoutTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E247404E492652921952ECE5BA420E /* RDEPUBFixedLayoutTemplate.swift */; }; + F981518F2DA0E5B53C2560759D14986B /* Archive+ReadingDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934F9BB2B3A313294F121484C3A97A14 /* Archive+ReadingDeprecated.swift */; }; + FB2BFDE8FC140377D92952992080A8DB /* ConstraintLayoutGuide+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9A82CB094636AFB53D6D130D6219BA /* ConstraintLayoutGuide+Extensions.swift */; }; + FC24AD03BC7583FF043A6723AC086232 /* NSString+DTFormatNumbers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACA5E519F100582F0D772FCD76125C0 /* NSString+DTFormatNumbers.m */; }; FC5F52919451A15639442B46B38E4C67 /* DTCoreTextLayoutFrame+Cursor.m in Sources */ = {isa = PBXBuildFile; fileRef = E62C5E593037EF8A45E7FAA847D0277C /* DTCoreTextLayoutFrame+Cursor.m */; }; - FCDDEA00D578F661AEFF4B75CFF80B60 /* RDEPUBTextIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAB3488CD710ECC37CE9B0F10A10545 /* RDEPUBTextIndexTable.swift */; }; - FD036CA8E474138A8DAC47527185B5FA /* DTBase64Coding.h in Headers */ = {isa = PBXBuildFile; fileRef = B8E40E302B85493BDE0FE91F829EC01E /* DTBase64Coding.h */; settings = {ATTRIBUTES = (Public, ); }; }; - FD0BD9D4FCDB12FAC03FC94901DB81B6 /* RDEPUBReaderTopToolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BAAD948F35A4788F3441FA024D937B /* RDEPUBReaderTopToolView.swift */; }; + FD4B6F54758FB3894484A092B0A1B2AC /* Entry+Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F991240340FA026C71B68EB25B9F9C8 /* Entry+Serialization.swift */; }; FD5D68887297CFA94F543C49DC68A156 /* DTHTMLAttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C496AB6A73C2D0BD0FF340A6A2960D5 /* DTHTMLAttributedStringBuilder.m */; }; - FE80BC2F78769407CABD426D8569A855 /* Archive+WritingDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C7359A7C3FC7675FED220200A43A17 /* Archive+WritingDeprecated.swift */; }; + FD6F8A543C3241B08E21FBBD47911F6D /* epub-bridge.js in Resources */ = {isa = PBXBuildFile; fileRef = 75838C8BB89B6B0B142EC3F6922D925B /* epub-bridge.js */; }; + FDAAF8F1F0F13A657B57B92F75B5F2A0 /* ConstraintInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C241AD9CEC9D6732CFBA6A55B49E64C9 /* ConstraintInsets.swift */; }; FEB00C614DCB6A8A7BDF8DEE11260984 /* NSMutableAttributedString+HTML.m in Sources */ = {isa = PBXBuildFile; fileRef = B0500CA69405E23B09291359EB28331E /* NSMutableAttributedString+HTML.m */; }; - FF6E330BB1C384D79EA36415F5415511 /* NSURL+DTComparing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BFCAC7C2AEA867E67C63A852576774F /* NSURL+DTComparing.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 2DEC0E6E6DD76FFDDCA8167EC51975D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8F6E5A5BF72D62CDFD25F91A7CFA3309; - remoteInfo = DTFoundation; - }; - 37F6C824B43C269AA070A9970F478E23 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = AA2E57587AA8EECA63C4BE08EA3CB6D2; - remoteInfo = RDReaderView; - }; - 54FE0EFC4BD35EB51504F9D17F65C630 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = C7A8D82E407CD3FDC3BA55CEE519B252; - remoteInfo = "ZIPFoundation-ZIPFoundation_Privacy"; - }; - 57FCFD9D0AF20D20A8404315B4634FEB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8619D5ADECF2B26CFD9A9826D61D289A; - remoteInfo = SSAlertSwift; - }; - 5A688446F3834F916B644BF13086903B /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B88F4EA0695B6B3165C64594850D72C7; - remoteInfo = DTCoreText; - }; - 5C2DF152EE7F8EAB792D96A02ED313A6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8A8DB685241263AFDF5E6B20FE67B93A; - remoteInfo = "SnapKit-SnapKit_Privacy"; - }; - 6FEB3EC13F3780DE28D9E3A7027F5EF4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 8619D5ADECF2B26CFD9A9826D61D289A; - remoteInfo = SSAlertSwift; - }; - 87730E6F16E8FFBF6898B9EBD0AD14C5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B88F4EA0695B6B3165C64594850D72C7; - remoteInfo = DTCoreText; - }; - AD170DC0B755D9C13E44D4703F93ECAC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = AA15C8469D67684160CC2A7098EB841C; - remoteInfo = ZIPFoundation; - }; - B1F14D48BA2B3361AB7CF6CFB4248E92 /* PBXContainerItemProxy */ = { + 24BA72D0D912AE5A45EAC25AC27B6B7F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 19622742EBA51E823D6DAE3F8CDBFAD4; remoteInfo = SnapKit; }; - B27CFAAF383D62C8A184778C6B4667E7 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = AA15C8469D67684160CC2A7098EB841C; - remoteInfo = ZIPFoundation; - }; - BDB86097405AC17908E1D824C764DF29 /* PBXContainerItemProxy */ = { + 2B7231DD02DE6D67ED0FF347C2D2831A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 0C24CB0E87A728A11AA1124CB360D6A1; remoteInfo = "DTCoreText-Resources"; }; - CAB04B4C082D733C23D698A32EF0BD3C /* PBXContainerItemProxy */ = { + 41C358828795BDBE26E60DEDDB6F5A31 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; - remoteGlobalIDString = 8F6E5A5BF72D62CDFD25F91A7CFA3309; - remoteInfo = DTFoundation; + remoteGlobalIDString = C7A8D82E407CD3FDC3BA55CEE519B252; + remoteInfo = "ZIPFoundation-ZIPFoundation_Privacy"; }; - E02332EF5B2CCE697593D8FC99DCEBB2 /* PBXContainerItemProxy */ = { + 51F9BB05FB5362314AABDBF12FD0F619 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = AE7F393FB7805DE2664AB4111873F907; remoteInfo = "RDReaderView-RDReaderViewAssets"; }; - F1618E89E8894E616222D6212EEB9BE2 /* PBXContainerItemProxy */ = { + 5BA4345CC5FD80F403AD69F002484017 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B88F4EA0695B6B3165C64594850D72C7; + remoteInfo = DTCoreText; + }; + 7C28D11939EC5ABB1D98898772B52AED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8619D5ADECF2B26CFD9A9826D61D289A; + remoteInfo = SSAlertSwift; + }; + 8EC7B58761479718591F7FDFB9BC34E2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA15C8469D67684160CC2A7098EB841C; + remoteInfo = ZIPFoundation; + }; + 962A98D094FBC182A4454FA956B2E681 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA15C8469D67684160CC2A7098EB841C; + remoteInfo = ZIPFoundation; + }; + A0BFC7B494DC59D52F43761C722DAB2D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8619D5ADECF2B26CFD9A9826D61D289A; + remoteInfo = SSAlertSwift; + }; + A0CA6E77551A041C81844E5901F24EFC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B88F4EA0695B6B3165C64594850D72C7; + remoteInfo = DTCoreText; + }; + A7A366C3CFD62C32F5B8E5EB9A8310BB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 19622742EBA51E823D6DAE3F8CDBFAD4; remoteInfo = SnapKit; }; + B1D7453B0F93836CC086060595CB753A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8A8DB685241263AFDF5E6B20FE67B93A; + remoteInfo = "SnapKit-SnapKit_Privacy"; + }; + CE5F9B32E7622F85FC1F741CE5013575 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8F6E5A5BF72D62CDFD25F91A7CFA3309; + remoteInfo = DTFoundation; + }; + EA5C5FB405F5479FFA8A607CD33F3ECF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA2E57587AA8EECA63C4BE08EA3CB6D2; + remoteInfo = RDReaderView; + }; + F24E133D0B4B0D43780A5C9975FA4EF9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8F6E5A5BF72D62CDFD25F91A7CFA3309; + remoteInfo = DTFoundation; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 01073FE593A6AE17D4376AF909753A8F /* RDEPUBTextRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextRenderer.swift; sourceTree = ""; }; 015F3EE69253C66E11B931E3360BA5FE /* DTCoreText-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "DTCoreText-dummy.m"; sourceTree = ""; }; 029130886CE8E422D16ADF97468CB1B0 /* DTCoreTextLayoutFrame.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextLayoutFrame.m; path = Core/Source/DTCoreTextLayoutFrame.m; sourceTree = ""; }; 02CF362F88AB69253F1898E53D7A286A /* ConstraintDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDSL.swift; path = Sources/ConstraintDSL.swift; sourceTree = ""; }; 02EDFE0EEA47398F3E1A361A73AF6A6C /* NSData+DTCrypto.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSData+DTCrypto.h"; path = "Core/Source/NSData+DTCrypto.h"; sourceTree = ""; }; 045ADBF2087ADD914A9CA0B62EBF2A9F /* Pods-ReadViewDemo.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-ReadViewDemo.modulemap"; sourceTree = ""; }; + 04C13EA061DD9866E60F683AD49EE945 /* RDEPUBJavaScriptBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBJavaScriptBridge.swift; sourceTree = ""; }; 04F9B298989305B9569B71E448E8C1D4 /* FileManager+ZIPDeprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "FileManager+ZIPDeprecated.swift"; path = "Sources/ZIPFoundation/FileManager+ZIPDeprecated.swift"; sourceTree = ""; }; 054674EB90C625BB4BEA9368ED92592B /* NSURL+DTUnshorten.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSURL+DTUnshorten.h"; path = "Core/Source/NSURL+DTUnshorten.h"; sourceTree = ""; }; 05D57D16346FAF830F1F82067307EA9F /* default.css */ = {isa = PBXFileReference; includeInIndex = 1; name = default.css; path = Core/Source/default.css; sourceTree = ""; }; @@ -505,345 +558,383 @@ 09BE22EE711B7A38A4443E1C1C12BD79 /* ConstraintMultiplierTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMultiplierTarget.swift; path = Sources/ConstraintMultiplierTarget.swift; sourceTree = ""; }; 09C2D4A3D4F602D125FF38B56C91CF84 /* ZIPFoundation.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ZIPFoundation.modulemap; sourceTree = ""; }; 09C957F3199C596781EFA1DEA1DDCEC8 /* NSFileWrapper+DTCopying.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSFileWrapper+DTCopying.h"; path = "Core/Source/NSFileWrapper+DTCopying.h"; sourceTree = ""; }; + 0A294C65CC6C154EF2122380953FEFE3 /* RDReaderView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderView.swift; sourceTree = ""; }; + 0A65FCB87389CF471A9A4BD659D58346 /* RDEPUBSelectableTextView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSelectableTextView.swift; sourceTree = ""; }; 0B10D3EC1701BBA4D69926915F942558 /* SnapKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SnapKit-umbrella.h"; sourceTree = ""; }; - 0B1A759F6D90A955E7DF86A14FEE1737 /* RDEPUBWebView+JavaScriptBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+JavaScriptBridge.swift"; sourceTree = ""; }; 0B7771FDE9AB1688166584C0AA03F12A /* DTCoreTextFontCollection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextFontCollection.h; path = Core/Source/DTCoreTextFontCollection.h; sourceTree = ""; }; + 0B832537E4E508747319F7DF89BE256A /* RDEPUBChapterTailNormalizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBChapterTailNormalizer.swift; sourceTree = ""; }; 0C6BE3B2DA9A8C4CD3760CDAD6F305A5 /* DTCustomColoredAccessory.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCustomColoredAccessory.m; path = Core/Source/iOS/DTCustomColoredAccessory.m; sourceTree = ""; }; + 0C7333A1BFCB9D7512A152B2B0EC6350 /* RDEPUBReaderAnnotationCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderAnnotationCoordinator.swift; sourceTree = ""; }; 0D75FEDE65722CF41F7F32A164376926 /* DTHTMLParser.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLParser.h; path = Core/Source/DTHTMLParser/DTHTMLParser.h; sourceTree = ""; }; 0E138EC76D0DF24544D29D1A4D7429E3 /* ConstraintMakerPrioritizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerPrioritizable.swift; path = Sources/ConstraintMakerPrioritizable.swift; sourceTree = ""; }; 0E8C0D83EB6EDE71B264B6B0DD4644F5 /* LayoutConstraintItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LayoutConstraintItem.swift; path = Sources/LayoutConstraintItem.swift; sourceTree = ""; }; + 0ECB9576AFBC841E4282C8091B9D1244 /* RDURLReaderController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDURLReaderController.swift; sourceTree = ""; }; 0F5D34047BAA894BEF1DFA23E6A9DAB1 /* DTCoreTextMacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextMacros.h; path = Core/Source/DTCoreTextMacros.h; sourceTree = ""; }; 0FC368BD2A8D266EB771E5FB9A5A5B3C /* NSDictionary+DTError.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+DTError.h"; path = "Core/Source/NSDictionary+DTError.h"; sourceTree = ""; }; + 117036F7DB9A22578522D0B397C28F26 /* RDEPUBPageBreakPolicy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPageBreakPolicy.swift; sourceTree = ""; }; 11B604FC3B0D5C91FEA763DCC0BE4DC5 /* DTHTMLParserNode.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHTMLParserNode.m; path = Core/Source/DTHTMLParserNode.m; sourceTree = ""; }; - 129DD2A43FC15DC9844C508DFE51D24A /* rangy-serializer.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "rangy-serializer.js"; path = "Sources/RDReaderView/EPUBCore/Resources/rangy-serializer.js"; sourceTree = ""; }; + 127FB4B3B74B022CBE4054BA33CB24A0 /* RDEPUBReaderController+ContentDelegates.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+ContentDelegates.swift"; sourceTree = ""; }; 12A5EA26EE7539EDDA90649D22FB675D /* DTImage+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "DTImage+HTML.m"; path = "Core/Source/DTImage+HTML.m"; sourceTree = ""; }; + 136BC2B3FC110EA496A8803FF547F72D /* RDReaderView-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "RDReaderView-Info.plist"; sourceTree = ""; }; + 137B4BC4EAE7AE0D9567E53EBBBA687E /* RDEPUBParser+TOC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+TOC.swift"; sourceTree = ""; }; 13AB6A32A293E3DC21A7047C6DB958CA /* DTHTMLParserTextNode.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHTMLParserTextNode.m; path = Core/Source/DTHTMLParserTextNode.m; sourceTree = ""; }; 13E1548FC42118F1A4164739DE6BE4E9 /* UIFont+DTCoreText.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIFont+DTCoreText.m"; path = "Core/Source/UIFont+DTCoreText.m"; sourceTree = ""; }; + 141325B9AB28E266F40552E4F82F952D /* RDEPUBTextLayoutFrame.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextLayoutFrame.swift; sourceTree = ""; }; 15941683D394FB8D7EAE91CD2CF03FF1 /* UIImage+DTFoundation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+DTFoundation.h"; path = "Core/Source/iOS/UIImage+DTFoundation.h"; sourceTree = ""; }; - 16973940E1B38B29D620DAB562AB7396 /* RDEPUBSearchEngine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSearchEngine.swift; sourceTree = ""; }; - 16D52166703BA10418F79BCD397B07DB /* RDEPUBTextBookCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBookCache.swift; sourceTree = ""; }; + 164CFAED45E63A7592580A342D44A43E /* RDEPUBStyleSheetBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBStyleSheetBuilder.swift; sourceTree = ""; }; + 16C3C684271C3DE0964041A2D8701E58 /* RDEPUBSearchEngine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSearchEngine.swift; sourceTree = ""; }; + 173FEADB0A73EA7EB5B3B7CADB74C3A4 /* RDEPUBReaderController+TableOfContents.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+TableOfContents.swift"; sourceTree = ""; }; 174FC0CA05D20BB995E211DE5A0857DB /* Archive+MemoryFile.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+MemoryFile.swift"; path = "Sources/ZIPFoundation/Archive+MemoryFile.swift"; sourceTree = ""; }; 1756C6FFCA695290BA62DA4E7B6DD20B /* NSFileWrapper+DTCopying.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSFileWrapper+DTCopying.m"; path = "Core/Source/NSFileWrapper+DTCopying.m"; sourceTree = ""; }; 17F750F38183302BF345F40F5A4CDD43 /* NSData+DTCrypto.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSData+DTCrypto.m"; path = "Core/Source/NSData+DTCrypto.m"; sourceTree = ""; }; 18387159515E0523AFB4204B1AF940ED /* SSAlertSwift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SSAlertSwift.debug.xcconfig; sourceTree = ""; }; + 185CD1A4E30E72715AF3129924F063E6 /* RDEPUBSelectionOverlayView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSelectionOverlayView.swift; sourceTree = ""; }; + 19F9801AD3675102B14F64AFAFB99D89 /* RDEPUBPageLayoutSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPageLayoutSnapshot.swift; sourceTree = ""; }; 1ABC2F552388CD08EC1EFDEF606AE222 /* DTTextAttachmentHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTTextAttachmentHTMLElement.m; path = Core/Source/DTTextAttachmentHTMLElement.m; sourceTree = ""; }; 1ACA5E519F100582F0D772FCD76125C0 /* NSString+DTFormatNumbers.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+DTFormatNumbers.m"; path = "Core/Source/NSString+DTFormatNumbers.m"; sourceTree = ""; }; 1AD502A1FA6EB1E38200B5283F979504 /* Pods-ReadViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ReadViewDemo.release.xcconfig"; sourceTree = ""; }; 1B443D035BA5652B03A41BE905C5AC17 /* DTPieProgressIndicator.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTPieProgressIndicator.h; path = Core/Source/iOS/DTPieProgressIndicator.h; sourceTree = ""; }; - 1C17D914B39D21C0A4943F60349040BA /* RDReaderView-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "RDReaderView-prefix.pch"; sourceTree = ""; }; + 1B8132548C8745F3AE7F3CBC78E1DDA9 /* RDEPUBReaderPersistence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderPersistence.swift; sourceTree = ""; }; + 1BD33493F588BFD65E47EC9B8773C74C /* RDEPUBReaderController+PublicAPI.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+PublicAPI.swift"; sourceTree = ""; }; 1C8F0DCEAA5A3DF38C503CA30DC94C1C /* Archive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Archive.swift; path = Sources/ZIPFoundation/Archive.swift; sourceTree = ""; }; - 1CF085139C084B6CB83EB511352CC385 /* RDEPUBPublication.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPublication.swift; sourceTree = ""; }; + 1CA5A4277907D54250DB4BF7B95CE1A9 /* RDEPUBReaderContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderContext.swift; sourceTree = ""; }; + 1D01284125120D95F9E41743D2CE9F81 /* RDEPUBWebViewDebug.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebViewDebug.swift; sourceTree = ""; }; 1D4C9B7ED5E04C216AB28E3A4065C1C5 /* DTHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHTMLElement.m; path = Core/Source/DTHTMLElement.m; sourceTree = ""; }; 1D6DA580F40DF113709E15CF6542A35C /* DTCoreTextLayouter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextLayouter.m; path = Core/Source/DTCoreTextLayouter.m; sourceTree = ""; }; 1E89FE5F2996336E862E82C1E14E0EE7 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sources/ZIPFoundation/Resources/PrivacyInfo.xcprivacy; sourceTree = ""; }; 1F090A0289338B14050FAF16C22605B0 /* ConstraintConstantTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintConstantTarget.swift; path = Sources/ConstraintConstantTarget.swift; sourceTree = ""; }; 2047430DF5C84E0A8FC8874A2B915FC3 /* Pods-ReadViewDemo-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-ReadViewDemo-acknowledgements.markdown"; sourceTree = ""; }; + 2055F08A4A307EA51828460481FE8276 /* RDEPUBTextBuildPipelineInterfaces.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBuildPipelineInterfaces.swift; sourceTree = ""; }; + 20AA41D781113C9A30CD0E9EDD6835D5 /* RDEPUBPaginationCacheCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPaginationCacheCoordinator.swift; sourceTree = ""; }; 2101691D5743105B2E3E40376584B91C /* DTFolderMonitor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTFolderMonitor.h; path = Core/Source/DTFolderMonitor.h; sourceTree = ""; }; 21BAA93E67D8CBC5297302D798FB4F7A /* DTAttributedLabel.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAttributedLabel.m; path = Core/Source/DTAttributedLabel.m; sourceTree = ""; }; 2280F2302F54E90122747821FE681543 /* SSAlertViewExtention.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertViewExtention.swift; path = Sources/SSAlertSwift/SSAlertViewExtention.swift; sourceTree = ""; }; 238893C9CBD95CB9BDF069EB5A303E17 /* RDReaderView */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = RDReaderView; path = RDReaderView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 249747495DE8349E480E4789B12D366D /* SnapKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SnapKit.modulemap; sourceTree = ""; }; - 24AD5C352BC2F291B84A39565644DE44 /* RDEPUBPaginator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPaginator.swift; sourceTree = ""; }; - 252896D6D97D0F06D5937D72DD041E65 /* RDEPUBWebView+FixedLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+FixedLayout.swift"; sourceTree = ""; }; + 24B33931EE71CDEAAB6870A8DD00F441 /* RDEPUBPaginationModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPaginationModels.swift; sourceTree = ""; }; + 2514DF9190BDB37F2BE5FB4389C84579 /* RDReaderView.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = RDReaderView.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 25F52D9C765ED2FA87834D4310203E0B /* DTFoundation-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DTFoundation-prefix.pch"; sourceTree = ""; }; - 26CF54683B145156F178EF99B0C7046A /* RDEPUBTextLayoutFrame.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextLayoutFrame.swift; sourceTree = ""; }; + 268AF3AC14AC70E5C08F5EF60DBFC4F8 /* RDEPUBWebView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebView.swift; sourceTree = ""; }; 2702E05AA7A266B7D8D0F881FD4E09DA /* DTCompatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCompatibility.h; path = Core/Source/DTCompatibility.h; sourceTree = ""; }; 27205D159B39090908CA0E9C3997B784 /* NSDictionary+DTCoreText.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+DTCoreText.h"; path = "Core/Source/NSDictionary+DTCoreText.h"; sourceTree = ""; }; 27B7D3C6F0C845E3D42BBA613FFB0465 /* FileManager+ZIP.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "FileManager+ZIP.swift"; path = "Sources/ZIPFoundation/FileManager+ZIP.swift"; sourceTree = ""; }; 28043173CA3B3787DC401ACEB5398108 /* DTCoreTextLayoutFrame+Cursor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "DTCoreTextLayoutFrame+Cursor.h"; path = "Core/Source/DTCoreTextLayoutFrame+Cursor.h"; sourceTree = ""; }; 28D62D3200AC3D4E42C81CDFF7E884E8 /* ConstraintItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintItem.swift; path = Sources/ConstraintItem.swift; sourceTree = ""; }; + 291992715B24D4645FF6E45D0881635F /* RDReaderContentCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderContentCell.swift; sourceTree = ""; }; + 2AEE00544B5F5C69A1636474C8E76690 /* RDEPUBFontNormalizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBFontNormalizer.swift; sourceTree = ""; }; 2BA19BD582C1BD54AF673014C04848B6 /* DTFoundation-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "DTFoundation-Info.plist"; sourceTree = ""; }; - 2C26AFBE041DB9C0B1BFC9B71CFCA31F /* RDReaderView-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "RDReaderView-Info.plist"; sourceTree = ""; }; + 2BCD9B2E62051692362E404278683A18 /* RDEPUBReaderChromeCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderChromeCoordinator.swift; sourceTree = ""; }; 2C50CAB4241C141EF6EC9ED22AB3A7E8 /* NSDictionary+DTCoreText.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+DTCoreText.m"; path = "Core/Source/NSDictionary+DTCoreText.m"; sourceTree = ""; }; 2CD63BBDA1CB02B15A54D2CDD8A3ACD7 /* SnapKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SnapKit-prefix.pch"; sourceTree = ""; }; 2D36F7CDCD7D5788CDCB3A1421DB242B /* NSURL+DTAppLinks.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURL+DTAppLinks.m"; path = "Core/Source/iOS/NSURL+DTAppLinks.m"; sourceTree = ""; }; 2D3D7C8444F58D9B3FCB4F3006D2CFBA /* NSAttributedString+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+HTML.h"; path = "Core/Source/NSAttributedString+HTML.h"; sourceTree = ""; }; - 2D5AA1752B9A4DA12829506699ACB35F /* RDEPUBParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBParser.swift; sourceTree = ""; }; 2DE4FA5133891593CB45AF36B8EBF60C /* SSAlertSwift-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SSAlertSwift-dummy.m"; sourceTree = ""; }; - 2F8C4EB13F9C6442DAD004816726B09A /* RDEPUBTextSearchEngine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextSearchEngine.swift; sourceTree = ""; }; + 2F910EB56A7325328DCD94F082E99648 /* RDEPUBTextAnnotationOverlay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextAnnotationOverlay.swift; sourceTree = ""; }; + 2FBA8F4FD78EAB6A6E4011D92577EA42 /* wxread-dark.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-dark.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-dark.css"; sourceTree = ""; }; + 303F36CC1C0432C0F498EAED0D82A567 /* RDEPUBTextRendererSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextRendererSupport.swift; sourceTree = ""; }; 306477706BBB1E784F55BCF8EB1AD737 /* Pods-ReadViewDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ReadViewDemo.debug.xcconfig"; sourceTree = ""; }; - 3119842637EAE995A0DAEBEB1170E363 /* RDEPUBTextPerformanceSampler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPerformanceSampler.swift; sourceTree = ""; }; + 31407369BDB36B40D253244CCF1F954A /* RDReaderViewProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderViewProtocols.swift; sourceTree = ""; }; + 3156D6DC256E20EF49691134C4342473 /* RDEPUBReaderSearchCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderSearchCoordinator.swift; sourceTree = ""; }; 31BD7533867336DEA9B835F471767178 /* NSString+DTURLEncoding.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+DTURLEncoding.h"; path = "Core/Source/NSString+DTURLEncoding.h"; sourceTree = ""; }; - 31DEEEBC962A7BAB4EDB772B3A2279B2 /* RDEPUBSelectionOverlayView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSelectionOverlayView.swift; sourceTree = ""; }; 31F3F91D5624C451E5D44D85B1DDE7C5 /* NSMutableString+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSMutableString+HTML.m"; path = "Core/Source/NSMutableString+HTML.m"; sourceTree = ""; }; - 32769058982DA3B3ADB0051377B41916 /* RDEPUBResourceURLSchemeHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBResourceURLSchemeHandler.swift; sourceTree = ""; }; - 32A3B34DE993508CAEBD18287344623E /* RDEPUBWebViewDebug.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebViewDebug.swift; sourceTree = ""; }; 32A5ABB8F3656A110B955B0306E9A8C3 /* ConstraintInsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintInsetTarget.swift; path = Sources/ConstraintInsetTarget.swift; sourceTree = ""; }; 32A8BED62113FF47E3925378065FA3BE /* SSAlertSwift-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SSAlertSwift-prefix.pch"; sourceTree = ""; }; - 32D260607CE02388D7741193F901AF75 /* wxread-default.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-default.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-default.css"; sourceTree = ""; }; 32FB2B1B0498DCBE7B79BDF49E963E3D /* DTHTMLParser.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHTMLParser.m; path = Core/Source/DTHTMLParser/DTHTMLParser.m; sourceTree = ""; }; - 33D8E2EC0A0BDF6FCA33CA829E2C6FCF /* epub-bridge.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "epub-bridge.js"; path = "Sources/RDReaderView/EPUBCore/Resources/epub-bridge.js"; sourceTree = ""; }; 33DB57DF8C5AE334B1BBE9F989352F1D /* DTCustomColoredAccessory.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCustomColoredAccessory.h; path = Core/Source/iOS/DTCustomColoredAccessory.h; sourceTree = ""; }; - 353AB6D63236BF3C98B630EAB53AC742 /* epub-fixed-layout.html */ = {isa = PBXFileReference; includeInIndex = 1; name = "epub-fixed-layout.html"; path = "Sources/RDReaderView/EPUBCore/Resources/epub-fixed-layout.html"; sourceTree = ""; }; + 34D4FDB3A066F397D1FF9727C43A0777 /* RDEPUBReaderController+RuntimeBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+RuntimeBridge.swift"; sourceTree = ""; }; 354C5DD0ACC742762200A640ADA859C6 /* NSCoder+DTCompatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSCoder+DTCompatibility.h"; path = "Core/Source/NSCoder+DTCompatibility.h"; sourceTree = ""; }; - 35FA9133889FF0F7999C456AC029966C /* RDEPUBTextPositionConverter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPositionConverter.swift; sourceTree = ""; }; 365FB4C3E0FF364C076EE4E9E47C32EA /* DTCoreText.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = DTCoreText.modulemap; sourceTree = ""; }; 36FCB869246FE45FD1C9609A34EB4E74 /* DTCoreTextGlyphRun.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextGlyphRun.m; path = Core/Source/DTCoreTextGlyphRun.m; sourceTree = ""; }; 375179DD3A4576CEF947E1760BBDC69B /* DTBlockFunctions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTBlockFunctions.m; path = Core/Source/DTBlockFunctions.m; sourceTree = ""; }; - 37C1467D2571E3541EC221A49C90B110 /* RDEPUBStyleSheetBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBStyleSheetBuilder.swift; sourceTree = ""; }; 392A75EAF280876545D14AA00517BA4C /* ConstraintMakerEditable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerEditable.swift; path = Sources/ConstraintMakerEditable.swift; sourceTree = ""; }; - 39A422EF35AF8F3EC0DCD8BCAF01A32E /* RDEPUBSearchModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSearchModels.swift; sourceTree = ""; }; + 3A525BB5B1708520D409C1A30CF1CD2D /* RDReaderView+PageCurl.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDReaderView+PageCurl.swift"; sourceTree = ""; }; 3B610E558C4917C4257DC198EAFEE2F6 /* DTCSSStylesheet.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCSSStylesheet.h; path = Core/Source/DTCSSStylesheet.h; sourceTree = ""; }; 3B931AB189BDDE2CCE8578B50BD60AF0 /* DTIframeTextAttachment.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTIframeTextAttachment.h; path = Core/Source/DTIframeTextAttachment.h; sourceTree = ""; }; - 3C67B49D37D02FC71C317CCA00B61EDB /* RDEPUBReadingSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReadingSession.swift; sourceTree = ""; }; + 3D246461688CA3E0592B5536C95A624A /* RDEPUBReaderPaginationCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderPaginationCoordinator.swift; sourceTree = ""; }; 3DC78D0B0F37BF9AD1058CACA9AAF9BE /* Data+Serialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Data+Serialization.swift"; path = "Sources/ZIPFoundation/Data+Serialization.swift"; sourceTree = ""; }; - 40156267853001A18D40A4515A03C5A7 /* RDEPUBReaderSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderSettingsViewController.swift; sourceTree = ""; }; + 3EADCA56DD969174A20085845D93C409 /* RDEPUBTypesettingPipeline.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTypesettingPipeline.swift; sourceTree = ""; }; + 3F274F6DE3095EAF4042CC6BF0F3E566 /* RDEPUBReaderBottomToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderBottomToolView.swift; sourceTree = ""; }; + 413B561A626E7BC1C78EFEFA131B92B0 /* RDEPUBReaderRuntime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderRuntime.swift; sourceTree = ""; }; 4219E7C32019899890CAB66E5FBDF524 /* UIView+DTFoundation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+DTFoundation.m"; path = "Core/Source/iOS/UIView+DTFoundation.m"; sourceTree = ""; }; 424F12BEB0EDAAF1C719B5103D30C653 /* DTAnchorHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAnchorHTMLElement.h; path = Core/Source/DTAnchorHTMLElement.h; sourceTree = ""; }; 426D442C01742E4361F5BC34D43152A6 /* DTImageTextAttachment.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTImageTextAttachment.h; path = Core/Source/DTImageTextAttachment.h; sourceTree = ""; }; + 42B4304B138A25135B0946C6A7A3F4A8 /* RDEPUBViewportTypes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBViewportTypes.swift; sourceTree = ""; }; 43CFADE0F46014990052AD67F17C86FF /* DTAccessibilityViewProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAccessibilityViewProxy.m; path = Core/Source/DTAccessibilityViewProxy.m; sourceTree = ""; }; 4428633C8EC3F6289CCF376F588CE0C4 /* NSString+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+HTML.m"; path = "Core/Source/NSString+HTML.m"; sourceTree = ""; }; 4472755F83C121DE5E1E505D145F6DBE /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CoreText.framework; sourceTree = DEVELOPER_DIR; }; + 45989DEA616D69D14306213B39E79FD6 /* RDReaderGestureController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderGestureController.swift; sourceTree = ""; }; 45C7129757B54DFD9E445D0A17D73A83 /* ConstraintPriority.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintPriority.swift; path = Sources/ConstraintPriority.swift; sourceTree = ""; }; - 45D36E42A61555CAE286BFC8FC062999 /* ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; sourceTree = ""; }; 4600E9C0EF3B23864C9D7E5E90155F9D /* SSAlertSwift.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SSAlertSwift.modulemap; sourceTree = ""; }; 4684B406B325B35F39322D75B59B6479 /* ConstraintLayoutGuideDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutGuideDSL.swift; path = Sources/ConstraintLayoutGuideDSL.swift; sourceTree = ""; }; + 476A874AD38352C8F072379DDDEBE8CD /* RDEPUBTextPositionConverter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPositionConverter.swift; sourceTree = ""; }; 477A0FC2A1F0B7F7CCF72D201ECEBAB0 /* DTDictationPlaceholderTextAttachment.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTDictationPlaceholderTextAttachment.m; path = Core/Source/DTDictationPlaceholderTextAttachment.m; sourceTree = ""; }; 48A4A6EDC08C8AC933D0DCD507E9D173 /* ZIPFoundation */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ZIPFoundation; path = ZIPFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 49439BEDC4669BE8E3E9D3315C568645 /* RDEPUBReadingModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReadingModels.swift; sourceTree = ""; }; - 49553EE889D9BB1C29A6AA69A857A8EB /* rangy-core.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "rangy-core.js"; path = "Sources/RDReaderView/EPUBCore/Resources/rangy-core.js"; sourceTree = ""; }; 49A21AE1A0AB5383F895B34EA7D6973D /* DTCoreTextGlyphRun.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextGlyphRun.h; path = Core/Source/DTCoreTextGlyphRun.h; sourceTree = ""; }; + 49F47726654DBD3FAFF21FFA121D6EE7 /* RDEPUBReaderSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderSettingsViewController.swift; sourceTree = ""; }; + 49F7945AF94DCC77CB3E7F3F711BD4AB /* RDEPUBTextBookModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBookModels.swift; sourceTree = ""; }; 4A06A36ADBE678DB44158D32E751FAFF /* ZIPFoundation-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "ZIPFoundation-dummy.m"; sourceTree = ""; }; 4A2659DB0685E16ABAE0382F726DA8C8 /* Archive+Helpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+Helpers.swift"; path = "Sources/ZIPFoundation/Archive+Helpers.swift"; sourceTree = ""; }; 4AF6156128A1A0CA24D9D06E346B9445 /* DTVersion.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTVersion.m; path = Core/Source/DTVersion.m; sourceTree = ""; }; + 4B142DE3191F790898FB028F3E6C91BA /* RDEPUBParser+Package.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Package.swift"; sourceTree = ""; }; + 4B789EEAB8791D76E537C6186EA1AC1D /* RDEPUBTextContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextContentView.swift; sourceTree = ""; }; 4BEC60CB3EDA1C189D3B062E29217440 /* ZIPFoundation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ZIPFoundation.debug.xcconfig; sourceTree = ""; }; + 4C2412CF0CE63E83F2F0EC492D3256F9 /* RDEPUBTextPaginationInterfaces.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPaginationInterfaces.swift; sourceTree = ""; }; 4C2B218B5EAA5831A036BBF7E1EFC1B9 /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/MediaPlayer.framework; sourceTree = DEVELOPER_DIR; }; - 4CA430D9A4A6EAE818564BB80309781D /* RDEPUBTextAnchor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextAnchor.swift; sourceTree = ""; }; + 4C94F70B03585FF87A0DEF959FD7E5A6 /* RDEPUBReaderLoadCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderLoadCoordinator.swift; sourceTree = ""; }; 4D641809A99849F80FDCB02F8CE8D6A0 /* Data+CompressionDeprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Data+CompressionDeprecated.swift"; path = "Sources/ZIPFoundation/Data+CompressionDeprecated.swift"; sourceTree = ""; }; + 4D9B8DA4629A86EE848CD980111120C3 /* cssInjector.js */ = {isa = PBXFileReference; includeInIndex = 1; name = cssInjector.js; path = Sources/RDReaderView/EPUBCore/Resources/cssInjector.js; sourceTree = ""; }; 4EEFAE80193E1F0DA53400C9AD641410 /* DTActivityTitleView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTActivityTitleView.m; path = Core/Source/iOS/DTActivityTitleView.m; sourceTree = ""; }; 4F0486D3B46DC92AB254D6EE9CC233BF /* DTAccessibilityElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAccessibilityElement.m; path = Core/Source/DTAccessibilityElement.m; sourceTree = ""; }; 50297AAB8278F2BB6FCFE82B20C7A049 /* ConstraintLayoutSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutSupport.swift; path = Sources/ConstraintLayoutSupport.swift; sourceTree = ""; }; 5155CCDD61E030184249A020FE5D74B0 /* DTWeakSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTWeakSupport.h; path = Core/Source/DTWeakSupport.h; sourceTree = ""; }; + 51F09F20128209EC507BBD2E73457DCD /* RDEPUBReaderTableOfContentsItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTableOfContentsItem.swift; sourceTree = ""; }; + 521E0E2A04F1C8B2E1AB5D30420A010B /* RDEPUBNavigatorLayoutContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBNavigatorLayoutContext.swift; sourceTree = ""; }; 5245010774F8F1C4857763CC7B96E7B4 /* ConstraintMakerExtendable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerExtendable.swift; path = Sources/ConstraintMakerExtendable.swift; sourceTree = ""; }; - 52BAAD948F35A4788F3441FA024D937B /* RDEPUBReaderTopToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTopToolView.swift; sourceTree = ""; }; 533CFFF04BE7BC4BDEAE11DF675A84CA /* DTBreakHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTBreakHTMLElement.h; path = Core/Source/DTBreakHTMLElement.h; sourceTree = ""; }; + 5341C765E36D0B172F2F8D4812829521 /* RDPlainTextBookBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDPlainTextBookBuilder.swift; sourceTree = ""; }; 53499F40E1790A6CBE8AFEB3AC5BEFC8 /* NSMutableArray+DTMoving.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSMutableArray+DTMoving.m"; path = "Core/Source/NSMutableArray+DTMoving.m"; sourceTree = ""; }; 537BB06317FA188DF7A8A960152520E8 /* URL+ZIP.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URL+ZIP.swift"; path = "Sources/ZIPFoundation/URL+ZIP.swift"; sourceTree = ""; }; 538572A5C098252D629F2185F1B1C9E8 /* DTExtendedFileAttributes.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTExtendedFileAttributes.h; path = Core/Source/DTExtendedFileAttributes.h; sourceTree = ""; }; 53D2AB2348A9B9C582FDC2CDB2E434FC /* SnapKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SnapKit.release.xcconfig; sourceTree = ""; }; 53D473C6F07982D3D25781A343EC5A99 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Sources/PrivacyInfo.xcprivacy; sourceTree = ""; }; 53F98F0F060B066CAFBCD3C85C26B2B4 /* DTCoreText.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreText.h; path = Core/Source/DTCoreText.h; sourceTree = ""; }; - 551DE8194BF851EE58EA61CBBA1710B1 /* RDEPUBParser+Package.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Package.swift"; sourceTree = ""; }; 557DA029E15EC99631B647AE8922007F /* NSURL+DTAppLinks.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSURL+DTAppLinks.h"; path = "Core/Source/iOS/NSURL+DTAppLinks.h"; sourceTree = ""; }; + 5612A9760A2FEC2682E417DF75F4C869 /* RDEPUBHTMLNormalizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBHTMLNormalizer.swift; sourceTree = ""; }; 56489D9BA834497E48438A4B4E1CCCFF /* DTCoreTextFontDescriptor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextFontDescriptor.h; path = Core/Source/DTCoreTextFontDescriptor.h; sourceTree = ""; }; 5679D281C282896E6FC352C48E0AF439 /* DTAttributedLabel.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAttributedLabel.h; path = Core/Source/DTAttributedLabel.h; sourceTree = ""; }; - 57DE6F8B9CE9F488188CFE16DDF7929B /* wxread-dark.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-dark.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-dark.css"; sourceTree = ""; }; - 580328EB7B35E7A9C48515F737D984ED /* RDReaderView.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = RDReaderView.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 591B0B382F10A52D84F90D5C34893CB8 /* RDEPUBTextSearchEngine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextSearchEngine.swift; sourceTree = ""; }; + 59444DC57CE3171F4EF0CD6481FACF30 /* RDReaderView-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "RDReaderView-umbrella.h"; sourceTree = ""; }; 5964AF1336E2F0DA0E2C32991E76C78C /* Data+Compression.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Data+Compression.swift"; path = "Sources/ZIPFoundation/Data+Compression.swift"; sourceTree = ""; }; 5977986EDFE76D5EF96A6D95483B12F7 /* Pods-ReadViewDemo-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-ReadViewDemo-frameworks.sh"; sourceTree = ""; }; - 59FAF02EDC6AC0DB9FA24A64E7B870CB /* RDEPUBRenderRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBRenderRequest.swift; sourceTree = ""; }; 5A9A82CB094636AFB53D6D130D6219BA /* ConstraintLayoutGuide+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintLayoutGuide+Extensions.swift"; path = "Sources/ConstraintLayoutGuide+Extensions.swift"; sourceTree = ""; }; 5B007AEEACE9EA271DD73AFD80A74D83 /* DTLog.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTLog.m; path = Core/Source/DTLog.m; sourceTree = ""; }; + 5B55540809AED42C4B61333680CEC4CB /* RDEPUBWebView+Configuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Configuration.swift"; sourceTree = ""; }; 5BA251BAC8B430B3A4D4C4321046B990 /* Archive+Deprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+Deprecated.swift"; path = "Sources/ZIPFoundation/Archive+Deprecated.swift"; sourceTree = ""; }; 5D62C5531667F364F14DB67D33FFA554 /* DTCoreTextParagraphStyle.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextParagraphStyle.h; path = Core/Source/DTCoreTextParagraphStyle.h; sourceTree = ""; }; + 5D8DA3839256D5A245E96F2C67A5E232 /* wxread-default.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-default.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-default.css"; sourceTree = ""; }; 5DA1E8494B86FFD79C639DCB8C21E1BB /* DTPieProgressIndicator.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTPieProgressIndicator.m; path = Core/Source/iOS/DTPieProgressIndicator.m; sourceTree = ""; }; + 5E030AC563D5D21F6792CA5056667F3F /* RDEPUBTextPageRenderView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPageRenderView.swift; sourceTree = ""; }; 5E791886E59DC0FAB02C6BBAB5BE5B6B /* DTCoreTextFontCollection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextFontCollection.m; path = Core/Source/DTCoreTextFontCollection.m; sourceTree = ""; }; - 5EDE24320E72FF6E77D1E402600E9753 /* RDEPUBReaderChapterListController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderChapterListController.swift; sourceTree = ""; }; + 5EFD8EDA8C70B31844B0A06CBCD0B403 /* RDEPUBReaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderDelegate.swift; sourceTree = ""; }; 5F760D62F6C6F083374702146337F2CB /* DTCoreTextParagraphStyle.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextParagraphStyle.m; path = Core/Source/DTCoreTextParagraphStyle.m; sourceTree = ""; }; 5F991240340FA026C71B68EB25B9F9C8 /* Entry+Serialization.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Entry+Serialization.swift"; path = "Sources/ZIPFoundation/Entry+Serialization.swift"; sourceTree = ""; }; 5FAB3C7F19A511C2B1B7DFBA3CA308C4 /* DTFoundation-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DTFoundation-umbrella.h"; sourceTree = ""; }; - 6002874EC82D09DCF64076F537D57EE4 /* RDEPUBResourceResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBResourceResolver.swift; sourceTree = ""; }; - 6009A6FAECBFC4615F869A0D90D8A460 /* wxread-replace.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-replace.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-replace.css"; sourceTree = ""; }; 6014DA7213E4687224F2BF921E30002C /* DTFoundation.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DTFoundation.debug.xcconfig; sourceTree = ""; }; 60657D08CCDB38BFED30BD40F4720CE4 /* ConstraintMaker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMaker.swift; path = Sources/ConstraintMaker.swift; sourceTree = ""; }; - 609F81945B589AC45E4E531C01F6EE3E /* RDReaderContentCell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderContentCell.swift; sourceTree = ""; }; + 615F9666D462FC95654FDC2732F700A6 /* RDEPUBResourceResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBResourceResolver.swift; sourceTree = ""; }; 61E36C4FE448DDDA03450847FCB785B6 /* Archive+BackingConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+BackingConfiguration.swift"; path = "Sources/ZIPFoundation/Archive+BackingConfiguration.swift"; sourceTree = ""; }; + 6369702FFDE8929ED1931FC77B6B02A1 /* RDReaderPageChildViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderPageChildViewController.swift; sourceTree = ""; }; 63A6EDF10DA5A051C6CDCBE5D97AF11E /* DTVideoTextAttachment.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTVideoTextAttachment.m; path = Core/Source/DTVideoTextAttachment.m; sourceTree = ""; }; - 63C6DCCE49AF60A4CD752939A15DB118 /* RDEPUBReaderHighlightsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderHighlightsViewController.swift; sourceTree = ""; }; + 64644C3A8F17BE731AE00482BDEE9D86 /* RDEPUBResourceURLSchemeHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBResourceURLSchemeHandler.swift; sourceTree = ""; }; 647FB0C5AAF84C71A7784DD277E48910 /* UIFont+DTCoreText.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIFont+DTCoreText.h"; path = "Core/Source/UIFont+DTCoreText.h"; sourceTree = ""; }; 64868ADD0419A5D112863F638932DA34 /* ZIPFoundation-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ZIPFoundation-Info.plist"; sourceTree = ""; }; 64AB89EAB10148C39B726B26BE04F4F8 /* DTCoreTextLayoutLine.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextLayoutLine.h; path = Core/Source/DTCoreTextLayoutLine.h; sourceTree = ""; }; 6578DFAB51436E8DF266FD087700E26B /* ConstraintDescription.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDescription.swift; path = Sources/ConstraintDescription.swift; sourceTree = ""; }; - 659E6B7AFC5374BF3A56A306238C3E96 /* RDEPUBReaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderDelegate.swift; sourceTree = ""; }; - 660901C91F4E1D354689FF2F8B6E1679 /* RDEPUBFixedLayoutTemplate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBFixedLayoutTemplate.swift; sourceTree = ""; }; - 6861909964EF2CEDB7ED884A3B410A7D /* RDEPUBWebView+Reflowable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Reflowable.swift"; sourceTree = ""; }; + 667442978548AAC9B24B14B6F2597536 /* RDEPUBParser+ReadingProfile.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+ReadingProfile.swift"; sourceTree = ""; }; + 6703BD7A2507028A11319843D0D7945F /* RDEPUBReaderChapterListController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderChapterListController.swift; sourceTree = ""; }; + 694DC251BF921B37330A35A6ACD9DF62 /* RDEPUBChapterPageCounter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBChapterPageCounter.swift; sourceTree = ""; }; + 69F083BF98854C4CB74C7C21DA34399A /* RDEPUBTextAnchor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextAnchor.swift; sourceTree = ""; }; 6A1EBB9497F4846C84CA0BF5E4F84FC7 /* DTLog.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTLog.h; path = Core/Source/DTLog.h; sourceTree = ""; }; - 6A800873D793225974FA75561B0810DF /* RDEPUBNavigatorState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBNavigatorState.swift; sourceTree = ""; }; + 6A2067EA81E19AA5F8E66DE4300DAB4A /* RDEPUBModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBModels.swift; sourceTree = ""; }; 6B96B0998B49D8E149C995474DA5FE5F /* DTHTMLWriter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLWriter.h; path = Core/Source/DTHTMLWriter.h; sourceTree = ""; }; - 6BAB3488CD710ECC37CE9B0F10A10545 /* RDEPUBTextIndexTable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextIndexTable.swift; sourceTree = ""; }; 6C647A2EB2720BCBF88B160D124B57BF /* UILayoutSupport+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UILayoutSupport+Extensions.swift"; path = "Sources/UILayoutSupport+Extensions.swift"; sourceTree = ""; }; + 6CB839342BEE5213E1F9D59970EEC467 /* epub-fixed-layout.html */ = {isa = PBXFileReference; includeInIndex = 1; name = "epub-fixed-layout.html"; path = "Sources/RDReaderView/EPUBCore/Resources/epub-fixed-layout.html"; sourceTree = ""; }; 6D2EC0B845131A88247902AEA26F40AE /* SSAlertAnimation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertAnimation.swift; path = Sources/SSAlertSwift/SSAlertAnimation.swift; sourceTree = ""; }; - 6D4134DC0BA1A654610741FB33CA9BA7 /* RDReaderFlowLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderFlowLayout.swift; sourceTree = ""; }; 6D86C7063D1128EAEB3A30EBEF214B3F /* NSString+DTFormatNumbers.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+DTFormatNumbers.h"; path = "Core/Source/NSString+DTFormatNumbers.h"; sourceTree = ""; }; - 70AA5021FC2962BB31E28C8BFDA797CD /* RDReaderView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderView.swift; sourceTree = ""; }; 70F5D03B9B3B7F182C8F5C1F5D4FA7E7 /* DTTiledLayerWithoutFade.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTTiledLayerWithoutFade.m; path = Core/Source/iOS/DTTiledLayerWithoutFade.m; sourceTree = ""; }; + 71171943FCD1D8271266E2F6347D503B /* RDEPUBWebView+FixedLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+FixedLayout.swift"; sourceTree = ""; }; 713C9FD0354D16AFCCFA630A97F2DA68 /* NSCharacterSet+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSCharacterSet+HTML.m"; path = "Core/Source/NSCharacterSet+HTML.m"; sourceTree = ""; }; 7176B677A350927FC670368313DC2CA1 /* ConstraintConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintConfig.swift; path = Sources/ConstraintConfig.swift; sourceTree = ""; }; 71E2D85A099C99CD82FD1842EE7C52D2 /* NSAttributedString+DTDebug.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+DTDebug.h"; path = "Core/Source/NSAttributedString+DTDebug.h"; sourceTree = ""; }; + 72E8D63E9D57DA89DEE2774A89A61152 /* RDEPUBReaderAssemblyCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderAssemblyCoordinator.swift; sourceTree = ""; }; 731C0C61E3AD227D691FBA1C2C17BF7B /* NSURL+DTComparing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSURL+DTComparing.h"; path = "Core/Source/NSURL+DTComparing.h"; sourceTree = ""; }; 7345CE45F696634CEA692A86D16F684C /* Entry+ZIP64.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Entry+ZIP64.swift"; path = "Sources/ZIPFoundation/Entry+ZIP64.swift"; sourceTree = ""; }; 73940D215D644B6815A09DA802CFBDDF /* NSArray+DTError.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSArray+DTError.m"; path = "Core/Source/NSArray+DTError.m"; sourceTree = ""; }; 73A99927E52B24583A6A2CF7E6F23848 /* DTVideoTextAttachment.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTVideoTextAttachment.h; path = Core/Source/DTVideoTextAttachment.h; sourceTree = ""; }; - 749B6320F95F8F75FDF6B8ACC3E43D57 /* RDEPUBTextRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextRenderer.swift; sourceTree = ""; }; + 7429300C02F7E8453A429E08C9BD10EA /* RDEPUBReadingSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReadingSession.swift; sourceTree = ""; }; + 74F9C4D562F4650166F650997ADA132B /* RDEPUBReaderController+RenderSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+RenderSupport.swift"; sourceTree = ""; }; + 75838C8BB89B6B0B142EC3F6922D925B /* epub-bridge.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "epub-bridge.js"; path = "Sources/RDReaderView/EPUBCore/Resources/epub-bridge.js"; sourceTree = ""; }; 75C5986EF2D63801A8A30B9D77A7FDD0 /* ResourceBundle-ZIPFoundation_Privacy-ZIPFoundation-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-ZIPFoundation_Privacy-ZIPFoundation-Info.plist"; sourceTree = ""; }; 75FA8E843D9225BD65EB9D17825EC0F4 /* NSCharacterSet+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSCharacterSet+HTML.h"; path = "Core/Source/NSCharacterSet+HTML.h"; sourceTree = ""; }; - 76374229228450DD3ADBF2DB618329F5 /* RDEPUBReaderTableOfContentsItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTableOfContentsItem.swift; sourceTree = ""; }; - 763D7B1705C9113C0A37E0775C4F9FEC /* RDEPUBTextBookBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBookBuilder.swift; sourceTree = ""; }; 76F99C8130EE83A22CD223C7F5E6E66E /* NSAttributedString+SmallCaps.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+SmallCaps.m"; path = "Core/Source/NSAttributedString+SmallCaps.m"; sourceTree = ""; }; 77213203802F44012346181FED2856F9 /* UIApplication+DTNetworkActivity.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIApplication+DTNetworkActivity.m"; path = "Core/Source/iOS/UIApplication+DTNetworkActivity.m"; sourceTree = ""; }; 77A613841EB90827AF321DD50B21DB5D /* DTAttributedTextView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAttributedTextView.m; path = Core/Source/DTAttributedTextView.m; sourceTree = ""; }; - 7863FD91C5BD0B2EE358458E09C81194 /* RDEPUBPageLayoutSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPageLayoutSnapshot.swift; sourceTree = ""; }; 78873FAFC0AD9E930D6050A7F976714B /* DTCoreText.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DTCoreText.debug.xcconfig; sourceTree = ""; }; 78EA0DF405F8465A86BFD3DDFD2EC295 /* Pods-ReadViewDemo */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-ReadViewDemo"; path = Pods_ReadViewDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79A77584AB3B92B2508AE0BCFCB8FD87 /* UIView+DTFoundation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+DTFoundation.h"; path = "Core/Source/iOS/UIView+DTFoundation.h"; sourceTree = ""; }; 7AB9016408894675D11069CDAACEDE94 /* DTImage+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "DTImage+HTML.h"; path = "Core/Source/DTImage+HTML.h"; sourceTree = ""; }; - 7AD993F901F35B7F58E0A7AC2F8962BA /* RDEPUBParser+Archive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Archive.swift"; sourceTree = ""; }; - 7B89D235A3ECE3B096FBB5E7A2626C9C /* RDEPUBTextPaginationSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPaginationSupport.swift; sourceTree = ""; }; 7C496AB6A73C2D0BD0FF340A6A2960D5 /* DTHTMLAttributedStringBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHTMLAttributedStringBuilder.m; path = Core/Source/DTHTMLAttributedStringBuilder.m; sourceTree = ""; }; - 7D078643E7C9EA66EB922D04504AC413 /* RDPlainTextBookBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDPlainTextBookBuilder.swift; sourceTree = ""; }; 7D63AE1801C7AB149219317BE7529B26 /* DTAnchorHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAnchorHTMLElement.m; path = Core/Source/DTAnchorHTMLElement.m; sourceTree = ""; }; + 7E2F4C746E0B7479BE677A20358CB93B /* RDEPUBSemanticMarkerInjector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSemanticMarkerInjector.swift; sourceTree = ""; }; + 7ED26E7B899A0B29A202F0CB011D5F09 /* RDEPUBTextBookBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBookBuilder.swift; sourceTree = ""; }; + 7F04C6EEE7B8C63B04236F35AE34B289 /* wxread-replace.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-replace.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-replace.css"; sourceTree = ""; }; 7F051D813D50D20D53205D5FDCAA877C /* UIScreen+DTFoundation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIScreen+DTFoundation.m"; path = "Core/Source/iOS/UIScreen+DTFoundation.m"; sourceTree = ""; }; + 8067F377315BFA008F9F0795CCB68D5F /* RDEPUBWebView+Reflowable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Reflowable.swift"; sourceTree = ""; }; 80B3D15ED0D19D020663EF055386291A /* DTColor+Compatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "DTColor+Compatibility.h"; path = "Core/Source/DTColor+Compatibility.h"; sourceTree = ""; }; - 8173B8E972A645B3A420B3C5C0DCDF6A /* RDEPUBPreferences.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPreferences.swift; sourceTree = ""; }; 81B8F42440B802E6EC2B1FF7E3022F3A /* SSAlertDefaultAnmation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertDefaultAnmation.swift; path = Sources/SSAlertSwift/SSAlertDefaultAnmation.swift; sourceTree = ""; }; 8272D0B59ABD6652D0CAE3DFCC2C5EE1 /* DTActivityTitleView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTActivityTitleView.h; path = Core/Source/iOS/DTActivityTitleView.h; sourceTree = ""; }; 83410CC9CF2ABE63B90A92F2F988BF65 /* RDReaderView-RDReaderViewAssets */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "RDReaderView-RDReaderViewAssets"; path = RDReaderViewAssets.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 83E37DC74BB53F90A5AD54842858A62F /* NSString+Paragraphs.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+Paragraphs.h"; path = "Core/Source/NSString+Paragraphs.h"; sourceTree = ""; }; - 8432F1648F7FDA0F1A736F8A53739308 /* RDReaderView-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "RDReaderView-dummy.m"; sourceTree = ""; }; + 851EF2369FB225D981E94F4BB2E9F65F /* RDEPUBReaderDependencies.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderDependencies.swift; sourceTree = ""; }; 8552BE1A1F6A6D2879763DE3B2B8A2DD /* NSNumber+RomanNumerals.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSNumber+RomanNumerals.h"; path = "Core/Source/NSNumber+RomanNumerals.h"; sourceTree = ""; }; + 8607E8F20399521FFA80047941709E8C /* RDEPUBTextPerformanceSampler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPerformanceSampler.swift; sourceTree = ""; }; + 862E061063601D25DB09C808CA390C18 /* RDEPUBReaderTheme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTheme.swift; sourceTree = ""; }; 86A0361390AA0C29BC1A5487083A12A7 /* DTColorFunctions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTColorFunctions.h; path = Core/Source/DTColorFunctions.h; sourceTree = ""; }; - 875F2A69FD6767E29074114D638B2934 /* RDReaderView-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "RDReaderView-umbrella.h"; sourceTree = ""; }; - 87AAB03A10169416E66E50562D9A57A0 /* RDEPUBParser+TOC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+TOC.swift"; sourceTree = ""; }; 882FA707ED3513D263A1E0656AAD2D33 /* ConstraintPriorityTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintPriorityTarget.swift; path = Sources/ConstraintPriorityTarget.swift; sourceTree = ""; }; + 8830746779660E908FADB2DEAD879AA2 /* RDEPUBSearchModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBSearchModels.swift; sourceTree = ""; }; 885653999BD237A574B67DB279DE7E87 /* ConstraintRelatableTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintRelatableTarget.swift; path = Sources/ConstraintRelatableTarget.swift; sourceTree = ""; }; - 89ED8313799E7A012F5A06BCEACF09B8 /* RDEPUBReaderConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderConfiguration.swift; sourceTree = ""; }; - 89FBCDE54D10D205F325580161FB9FC3 /* RDEPUBWebView+Configuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Configuration.swift"; sourceTree = ""; }; 8ABDEFEA94DC6408D92AC66DA3625DE5 /* NSString+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+HTML.h"; path = "Core/Source/NSString+HTML.h"; sourceTree = ""; }; 8B35D0622F339A5B8A864FAA0C9EAE47 /* Pods-ReadViewDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ReadViewDemo-acknowledgements.plist"; sourceTree = ""; }; + 8B57C7D26C0F345A62E14DAB51521E69 /* RDReaderPreloadController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderPreloadController.swift; sourceTree = ""; }; 8B59C9A1B265393E4BA8383B308A5917 /* DTHTMLAttributedStringBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLAttributedStringBuilder.h; path = Core/Source/DTHTMLAttributedStringBuilder.h; sourceTree = ""; }; + 8B8F6026114BC156F5EAC3906C3DAD24 /* RDEPUBReaderController+DataSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBReaderController+DataSource.swift"; sourceTree = ""; }; 8BD568D38C209BA1CC0EBFA809A35204 /* DTFoundationConstants.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTFoundationConstants.m; path = Core/Source/DTFoundationConstants.m; sourceTree = ""; }; 8BD9F2023FF826F67E69E3407A70A8A6 /* DTVersion.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTVersion.h; path = Core/Source/DTVersion.h; sourceTree = ""; }; + 8CEAB7724C9E71FFC8A368EECE6AADF9 /* RDEPUBChapterData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBChapterData.swift; sourceTree = ""; }; 8CFEE298DD52246B46E6FA62B16B38DF /* DTTextHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTTextHTMLElement.m; path = Core/Source/DTTextHTMLElement.m; sourceTree = ""; }; - 8E2529CE2E852CB74565B24AC7B88965 /* RDEPUBPageInteractionController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPageInteractionController.swift; sourceTree = ""; }; 8E3E834CCC952717089550F50A2DE59C /* SSAlertSwift-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SSAlertSwift-Info.plist"; sourceTree = ""; }; 8E5637603691B9B35C2A886B686D3CE7 /* ConstraintViewDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintViewDSL.swift; path = Sources/ConstraintViewDSL.swift; sourceTree = ""; }; 8F2D44B82815CA3397BE2C9F64819B88 /* DTCoreTextFunctions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextFunctions.m; path = Core/Source/DTCoreTextFunctions.m; sourceTree = ""; }; 8F950CEF31AC0065C0BEAB26BA2CEFA1 /* ConstraintOffsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintOffsetTarget.swift; path = Sources/ConstraintOffsetTarget.swift; sourceTree = ""; }; + 8FD217FE3F87580A8BA3BD7D8992D4EB /* RDEPUBReaderViewportMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderViewportMonitor.swift; sourceTree = ""; }; 91144FC099DD458E61625CD939577DAA /* ConstraintDirectionalInsets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDirectionalInsets.swift; path = Sources/ConstraintDirectionalInsets.swift; sourceTree = ""; }; 9129D37C7F89AD8FD530663BB4503FC0 /* NSCoder+DTCompatibility.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSCoder+DTCompatibility.m"; path = "Core/Source/NSCoder+DTCompatibility.m"; sourceTree = ""; }; - 9197422E9FAD3EDDC5C5844350D497F7 /* RDEPUBNavigatorLayoutContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBNavigatorLayoutContext.swift; sourceTree = ""; }; 92460D196C5300656C9649784EBD3C58 /* NSAttributedString+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+HTML.m"; path = "Core/Source/NSAttributedString+HTML.m"; sourceTree = ""; }; - 929106407362D571E93A73D87EAB8107 /* RDEPUBWebView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebView.swift; sourceTree = ""; }; 932FD688DE493323BA6B691BC2CB5094 /* LayoutConstraint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LayoutConstraint.swift; path = Sources/LayoutConstraint.swift; sourceTree = ""; }; + 933F9E11CFE9DD8A63C31D119B381969 /* UIColor+RDEPUBHex.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+RDEPUBHex.swift"; sourceTree = ""; }; 934F9BB2B3A313294F121484C3A97A14 /* Archive+ReadingDeprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+ReadingDeprecated.swift"; path = "Sources/ZIPFoundation/Archive+ReadingDeprecated.swift"; sourceTree = ""; }; 937CEFA81DA4227008DBFB114783A08B /* SSAlertSwift-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SSAlertSwift-umbrella.h"; sourceTree = ""; }; - 93B0E9F83D59A57EF00599DBE41952BF /* RDEPUBReaderPersistence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderPersistence.swift; sourceTree = ""; }; + 9392F7CFCA8C63DD163AAB74B900D729 /* RDReaderView+ToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDReaderView+ToolView.swift"; sourceTree = ""; }; 945D4549E0B9168D7DDB910DE874B874 /* Typealiases.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Typealiases.swift; path = Sources/Typealiases.swift; sourceTree = ""; }; 957DB4675D5CCBD3CB1762F08619F3F7 /* UIApplication+DTNetworkActivity.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIApplication+DTNetworkActivity.h"; path = "Core/Source/iOS/UIApplication+DTNetworkActivity.h"; sourceTree = ""; }; - 9654CB2B436E0FB30970E9FCF3870820 /* RDEPUBTextRendererSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextRendererSupport.swift; sourceTree = ""; }; - 967506B2C9A33F6E902BDA98940C603F /* RDEPUBChapterData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBChapterData.swift; sourceTree = ""; }; 96EEB6B022E22A4ABC86D87469D4815B /* DTSmartPagingScrollView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTSmartPagingScrollView.m; path = Core/Source/iOS/DTSmartPagingScrollView.m; sourceTree = ""; }; 96FA99AA7D4AD0D4246C8DAC1CC0C24E /* DTStylesheetHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTStylesheetHTMLElement.h; path = Core/Source/DTStylesheetHTMLElement.h; sourceTree = ""; }; 979486118B3E90C08386079D57962701 /* SnapKit */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SnapKit; path = SnapKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 97995C73297871C353364E685939DED9 /* SSAlertAnimationController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertAnimationController.swift; path = Sources/SSAlertSwift/SSAlertAnimationController.swift; sourceTree = ""; }; - 97FCA163E8AE5E977B12F9060F220252 /* RDEPUBReaderBottomToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderBottomToolView.swift; sourceTree = ""; }; 982391EC8620A617B8788C8A6101C96A /* DTDictationPlaceholderView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTDictationPlaceholderView.h; path = Core/Source/DTDictationPlaceholderView.h; sourceTree = ""; }; 98658510AA525496EBF5792E5D9884ED /* DTListItemHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTListItemHTMLElement.h; path = Core/Source/DTListItemHTMLElement.h; sourceTree = ""; }; + 98BFF67943C8CABB6171A6EDC7639F82 /* RDEPUBAssetRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBAssetRepository.swift; sourceTree = ""; }; 98C7359A7C3FC7675FED220200A43A17 /* Archive+WritingDeprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+WritingDeprecated.swift"; path = "Sources/ZIPFoundation/Archive+WritingDeprecated.swift"; sourceTree = ""; }; 99299A9FD6EF739D18352AC5B9325CB0 /* DTAccessibilityElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAccessibilityElement.h; path = Core/Source/DTAccessibilityElement.h; sourceTree = ""; }; - 992AFC205F9539B89BF279A5EE6CB266 /* RDEPUBParser+ReadingProfile.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+ReadingProfile.swift"; sourceTree = ""; }; 9A1B75050E71BDD96FC19EC6BA129E6A /* DTLinkButton.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTLinkButton.m; path = Core/Source/DTLinkButton.m; sourceTree = ""; }; + 9AE2A1B83DCC99894642D65CE5EADFD4 /* RDEPUBNavigatorState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBNavigatorState.swift; sourceTree = ""; }; + 9B1FF16A8E3839BF599427F23328492C /* RDEPUBPreferences.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPreferences.swift; sourceTree = ""; }; + 9BBE8424D2FE92DCAA8D2B1CA7D4D029 /* RDEPUBReaderConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderConfiguration.swift; sourceTree = ""; }; 9BFCAC7C2AEA867E67C63A852576774F /* NSURL+DTComparing.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURL+DTComparing.m"; path = "Core/Source/NSURL+DTComparing.m"; sourceTree = ""; }; - 9C2DE084B4CF376178F1AFB96BD84562 /* RDEPUBModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBModels.swift; sourceTree = ""; }; 9C6293E97E128E98559FB74D9A342EAD /* NSMutableString+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSMutableString+HTML.h"; path = "Core/Source/NSMutableString+HTML.h"; sourceTree = ""; }; - 9CBC86AC7AC17CD094469EC7DCE21724 /* RDEPUBDTCoreTextRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBDTCoreTextRenderer.swift; sourceTree = ""; }; 9CE078B465FDEF40EC0469626A2C2469 /* SnapKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SnapKit-dummy.m"; sourceTree = ""; }; 9D6C48939A8498A09DCFC7F41ECB8A25 /* NSString+CSS.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+CSS.h"; path = "Core/Source/NSString+CSS.h"; sourceTree = ""; }; 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9DB5F45FEE54BD8D3F49BD345EDAB5D9 /* RDEPUBTextPageDecorationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPageDecorationView.swift; sourceTree = ""; }; 9E6BA0ECCB0ACF8580C3B5192CA9F11B /* ConstraintLayoutGuide.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutGuide.swift; path = Sources/ConstraintLayoutGuide.swift; sourceTree = ""; }; 9F8F40493DF0BBB3EA5FC1F759A883C4 /* ResourceBundle-Resources-DTCoreText-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-Resources-DTCoreText-Info.plist"; sourceTree = ""; }; A02EA0374C693F881AB36202FB276DCD /* ConstraintView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintView.swift; path = Sources/ConstraintView.swift; sourceTree = ""; }; A0927A875CA1F11DBCE60DFC6F0CA0B6 /* NSScanner+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSScanner+HTML.h"; path = "Core/Source/NSScanner+HTML.h"; sourceTree = ""; }; A0BD48DAF5AE9DB087F2552E99069709 /* DTBlockFunctions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTBlockFunctions.h; path = Core/Source/DTBlockFunctions.h; sourceTree = ""; }; A0EBAC7714A5F73B75AAB47D24B08B71 /* Constraint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constraint.swift; path = Sources/Constraint.swift; sourceTree = ""; }; + A16842E6F1C3E8526D28C7DABA1D6898 /* RDEPUBStyleSheetComposer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBStyleSheetComposer.swift; sourceTree = ""; }; A1B031E2B903C2F177195C1C3D1D1EDB /* DTCoreTextFunctions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextFunctions.h; path = Core/Source/DTCoreTextFunctions.h; sourceTree = ""; }; A276DE925253460239BFAEC7663F60D3 /* DTAttributedTextView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAttributedTextView.h; path = Core/Source/DTAttributedTextView.h; sourceTree = ""; }; + A2BD2727B338CE5C2C3BC6E8B8A23DE9 /* rangy-core.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "rangy-core.js"; path = "Sources/RDReaderView/EPUBCore/Resources/rangy-core.js"; sourceTree = ""; }; A2C299CFB47AA6ECF86F7348E1EB751F /* NSMutableAttributedString+HTML.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSMutableAttributedString+HTML.h"; path = "Core/Source/NSMutableAttributedString+HTML.h"; sourceTree = ""; }; A3565FEE19F19CB43A43409AFFF2BD2C /* NSString+DTPaths.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+DTPaths.m"; path = "Core/Source/NSString+DTPaths.m"; sourceTree = ""; }; A3765B289CB7E6C5DF6160D8EBC83C3C /* UIScreen+DTFoundation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIScreen+DTFoundation.h"; path = "Core/Source/iOS/UIScreen+DTFoundation.h"; sourceTree = ""; }; + A3A5FB560E84A3FE29C291466EAE9E22 /* RDEPUBDTCoreTextRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBDTCoreTextRenderer.swift; sourceTree = ""; }; + A42EA2D403B179C805701DD3638696B0 /* RDEPUBTextSelectionController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextSelectionController.swift; sourceTree = ""; }; A4E43DCDE0C8751557E7E65FEEB1C865 /* DTLazyImageView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTLazyImageView.m; path = Core/Source/DTLazyImageView.m; sourceTree = ""; }; - A4EE1E0ACE64032C6C8CB26A91235993 /* RDEPUBParser+Resources.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Resources.swift"; sourceTree = ""; }; A651CEE8062C84A597DBC44ECA3DBCC1 /* ConstraintDirectionalInsetTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintDirectionalInsetTarget.swift; path = Sources/ConstraintDirectionalInsetTarget.swift; sourceTree = ""; }; A68D72764140BC3DF67A2B9E52CEC7A4 /* DTHorizontalRuleHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTHorizontalRuleHTMLElement.m; path = Core/Source/DTHorizontalRuleHTMLElement.m; sourceTree = ""; }; + A6B5316985734E51970FA0FC168498A6 /* RDReaderView.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = RDReaderView.release.xcconfig; sourceTree = ""; }; A8DFDCAB69B471EB5667C964865C26CF /* DTCoreGraphicsUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreGraphicsUtils.m; path = Core/Source/DTCoreGraphicsUtils.m; sourceTree = ""; }; A9FBC69C7DA29A7283920FDA9EBBCE8A /* DTAnimatedGIF.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAnimatedGIF.h; path = Core/Source/iOS/DTAnimatedGIF/DTAnimatedGIF.h; sourceTree = ""; }; AA0811310099A4BCB3D52683B70DB3D1 /* Pods-ReadViewDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-ReadViewDemo-dummy.m"; sourceTree = ""; }; AA3CFD777AD76F97EBA05B3920F5786A /* DTIframeTextAttachment.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTIframeTextAttachment.m; path = Core/Source/DTIframeTextAttachment.m; sourceTree = ""; }; + AA479EAA368CC8B528282E49B6C96BC8 /* RDEPUBPaginator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPaginator.swift; sourceTree = ""; }; AD987C2FA7C8FBC96FD024E421DA0E68 /* Archive+Progress.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+Progress.swift"; path = "Sources/ZIPFoundation/Archive+Progress.swift"; sourceTree = ""; }; ADC5AC8FD4EC45E9BA5654A38715BB93 /* DTTextBlock.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTTextBlock.m; path = Core/Source/DTTextBlock.m; sourceTree = ""; }; - AE1A53CCB5BC34FB44E35A4A29AA5591 /* RDEPUBWebContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebContentView.swift; sourceTree = ""; }; + AE4740D05411538E3715704ED63C6E76 /* RDEPUBParser+Resources.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Resources.swift"; sourceTree = ""; }; AFC4C9F69BF2D0CAEBAA83896C139D2C /* DTColorFunctions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTColorFunctions.m; path = Core/Source/DTColorFunctions.m; sourceTree = ""; }; AFE0B56638AFB77F4455784A7ED59876 /* DTDictationPlaceholderView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTDictationPlaceholderView.m; path = Core/Source/DTDictationPlaceholderView.m; sourceTree = ""; }; B0500CA69405E23B09291359EB28331E /* NSMutableAttributedString+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSMutableAttributedString+HTML.m"; path = "Core/Source/NSMutableAttributedString+HTML.m"; sourceTree = ""; }; B0AC307F12521444C973EA0DE38B4608 /* NSMutableArray+DTMoving.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSMutableArray+DTMoving.h"; path = "Core/Source/NSMutableArray+DTMoving.h"; sourceTree = ""; }; B0B32EA4B7A492EEF26CE260E9AEFA0B /* ConstraintMakerFinalizable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerFinalizable.swift; path = Sources/ConstraintMakerFinalizable.swift; sourceTree = ""; }; + B1DD2563775FC83493319BFC4E953F00 /* RDEPUBWebView+Search.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Search.swift"; sourceTree = ""; }; B245F7E143F3091652C64F6593E33F7A /* DTFolderMonitor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTFolderMonitor.m; path = Core/Source/DTFolderMonitor.m; sourceTree = ""; }; - B2682E7883D110ECC2F46C6270375D24 /* RDReaderGestureController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderGestureController.swift; sourceTree = ""; }; B2B3D0CB47B0A8E9EC5AF48B7F2DD6B0 /* DTCoreTextConstants.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextConstants.m; path = Core/Source/DTCoreTextConstants.m; sourceTree = ""; }; + B3346B9CF2882D4D658CE30CDCCC93CA /* RDReaderFlowLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderFlowLayout.swift; sourceTree = ""; }; B50104DE2E35D820ECA22CDB3E6F96EA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; B509CD4B498635D4536245E43D802ED9 /* DTCSSListStyle.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCSSListStyle.m; path = Core/Source/DTCSSListStyle.m; sourceTree = ""; }; + B58EB90E2A4610839F356FE9B2A97486 /* RDEPUBFragmentMarkerInjector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBFragmentMarkerInjector.swift; sourceTree = ""; }; B5FCE04EBB553439D1C2BA1001936293 /* DTCompatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCompatibility.h; path = Core/Source/DTCompatibility.h; sourceTree = ""; }; B6425B2235BA00FE865AF58EF44F1144 /* DTCoreGraphicsUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreGraphicsUtils.h; path = Core/Source/DTCoreGraphicsUtils.h; sourceTree = ""; }; - B64B372E68C6C74223A76117BFA80B15 /* RDEPUBReaderController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderController.swift; sourceTree = ""; }; - B6F64F7254BE291374ECDC3E59BDB065 /* RDEPUBWebDecorationOverlayView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebDecorationOverlayView.swift; sourceTree = ""; }; + B69E0662B2604A49919B5DDA504A39C4 /* RDEPUBWebContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebContentView.swift; sourceTree = ""; }; B7672A26922DBD934FA1AC60D49EB322 /* NSNumber+RomanNumerals.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSNumber+RomanNumerals.m"; path = "Core/Source/NSNumber+RomanNumerals.m"; sourceTree = ""; }; B8E40E302B85493BDE0FE91F829EC01E /* DTBase64Coding.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTBase64Coding.h; path = Core/Source/DTBase64Coding.h; sourceTree = ""; }; - B8FAF5A1A2890703F132819BD138C1A9 /* RDReaderView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = RDReaderView.debug.xcconfig; sourceTree = ""; }; + B92A6483AF878B266B8FE776A9A6F4B3 /* RDReaderView-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "RDReaderView-dummy.m"; sourceTree = ""; }; B9DCB5EC0B1CDADD221717CADDF62359 /* SnapKit-SnapKit_Privacy */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "SnapKit-SnapKit_Privacy"; path = SnapKit_Privacy.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; B9E40164920C9AC1B1413C51835CD370 /* DTCoreText-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "DTCoreText-Info.plist"; sourceTree = ""; }; BC332335B380BC51C4F05C5CCDFB9189 /* DTAnimatedGIF.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAnimatedGIF.m; path = Core/Source/iOS/DTAnimatedGIF/DTAnimatedGIF.m; sourceTree = ""; }; BCCA590DB72AA93275262A414F16578A /* DTCoreTextFontDescriptor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextFontDescriptor.m; path = Core/Source/DTCoreTextFontDescriptor.m; sourceTree = ""; }; + BD245FC216DD480CF148530003BAA4BC /* RDEPUBReaderHighlightsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderHighlightsViewController.swift; sourceTree = ""; }; BD768C27E392C934C06DFB4AC401037C /* ConstraintMakerRelatable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintMakerRelatable.swift; path = Sources/ConstraintMakerRelatable.swift; sourceTree = ""; }; BDD081272B47A3B73CE693D21DC26827 /* Entry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Entry.swift; path = Sources/ZIPFoundation/Entry.swift; sourceTree = ""; }; + BE13C3BA99810308E47EC53C68F6169A /* RDEPUBTextBookCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextBookCache.swift; sourceTree = ""; }; BE547E464E5D34F9FFB24BAC81D1E4CC /* NSString+DTUtilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+DTUtilities.h"; path = "Core/Source/NSString+DTUtilities.h"; sourceTree = ""; }; + BE9D5BE3BC1B9E7C17553D6377DAA5D3 /* RDEPUBReaderTopToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTopToolView.swift; sourceTree = ""; }; BF4F3EC137168B92A5DA85AF50CD239F /* NSAttributedString+DTDebug.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+DTDebug.m"; path = "Core/Source/NSAttributedString+DTDebug.m"; sourceTree = ""; }; C123C127CC539ACCB510F665DB774899 /* DTAttributedTextContentView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAttributedTextContentView.h; path = Core/Source/DTAttributedTextContentView.h; sourceTree = ""; }; C18480913E002315E3F5EC6945371697 /* NSDictionary+DTError.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+DTError.m"; path = "Core/Source/NSDictionary+DTError.m"; sourceTree = ""; }; C241AD9CEC9D6732CFBA6A55B49E64C9 /* ConstraintInsets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintInsets.swift; path = Sources/ConstraintInsets.swift; sourceTree = ""; }; + C26BE3D7337938B1DB1B7610001CD169 /* RDEPUBReaderLocationCoordinator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderLocationCoordinator.swift; sourceTree = ""; }; C26DBE1150BC9A30B69839783E000358 /* DTCoreTextLayouter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextLayouter.h; path = Core/Source/DTCoreTextLayouter.h; sourceTree = ""; }; + C274719939F1335BCB8FA97636A9D2DD /* RDEPUBAnnotationModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBAnnotationModels.swift; sourceTree = ""; }; C2840BE6D91479CCBC2E908FBD9DA183 /* DTCoreText.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DTCoreText.release.xcconfig; sourceTree = ""; }; C298199F5DC5390E8B6666F638B34C10 /* DTTextAttachmentHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTTextAttachmentHTMLElement.h; path = Core/Source/DTTextAttachmentHTMLElement.h; sourceTree = ""; }; C30D143134EEC5D4BA99459104B76A2E /* DTHTMLParserTextNode.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLParserTextNode.h; path = Core/Source/DTHTMLParserTextNode.h; sourceTree = ""; }; C422BCADBB48E28B549495663705A0FB /* DTWeakSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTWeakSupport.h; path = Core/Source/DTWeakSupport.h; sourceTree = ""; }; - C5C4BF1170DCF38B8CC1E4979A0334FA /* RDEPUBAssetRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBAssetRepository.swift; sourceTree = ""; }; - C65067E704BE5EEEDE5FC6FF04C9C0AA /* wxread-replace-latin.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-replace-latin.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-replace-latin.css"; sourceTree = ""; }; - C6A6391BB8EAA2BA440A903CFBD508ED /* RDEPUBReaderToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderToolView.swift; sourceTree = ""; }; + C55B9A91FBFB85980E2E199DC0011868 /* RDEPUBReaderSettings.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderSettings.swift; sourceTree = ""; }; + C637C7A440920FA0074A7ECF39D9C6F8 /* RDReaderTapRegionHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderTapRegionHandler.swift; sourceTree = ""; }; + C6A32D83CDE7A6494252691BE7E99638 /* RDEPUBTextLayouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextLayouter.swift; sourceTree = ""; }; C6DB34ACB132E465CDF8191F7DB71EF6 /* ConstraintView+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintView+Extensions.swift"; path = "Sources/ConstraintView+Extensions.swift"; sourceTree = ""; }; - C6F199830BC382B6C9AEF29F421EE0C9 /* cssInjector.js */ = {isa = PBXFileReference; includeInIndex = 1; name = cssInjector.js; path = Sources/RDReaderView/EPUBCore/Resources/cssInjector.js; sourceTree = ""; }; C71D83D6AEF198FA680B8A80BF2D9605 /* DTCoreTextLayoutLine.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextLayoutLine.m; path = Core/Source/DTCoreTextLayoutLine.m; sourceTree = ""; }; - C73611E91719890E1DD889DEBBA1DDF2 /* RDReaderView.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = RDReaderView.release.xcconfig; sourceTree = ""; }; C80065DBA5634DAA71129404975D7349 /* NSURL+DTUnshorten.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURL+DTUnshorten.m"; path = "Core/Source/NSURL+DTUnshorten.m"; sourceTree = ""; }; C8343CDCF82729A8EBCCA37A6650E34F /* DTHTMLParserNode.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLParserNode.h; path = Core/Source/DTHTMLParserNode.h; sourceTree = ""; }; C8C20D9A71E6605DEE80342E7122B683 /* DTCoreTextConstants.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextConstants.h; path = Core/Source/DTCoreTextConstants.h; sourceTree = ""; }; C8E2FC7BE14F90878CE3FB3E317996ED /* ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist"; sourceTree = ""; }; + C9087CF398AFBBC8FFB5C60E6389EAB1 /* RDEPUBParser+Archive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBParser+Archive.swift"; sourceTree = ""; }; C9593E7B37569219F8D9A504B47707DC /* ConstraintAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintAttributes.swift; path = Sources/ConstraintAttributes.swift; sourceTree = ""; }; C960B7AA0B9B8CC473FACFEC64F9906D /* SnapKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SnapKit.debug.xcconfig; sourceTree = ""; }; + C996E23A573AA37FB9B35A09725C1D1D /* RDEPUBCoreTextPageFrameFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBCoreTextPageFrameFactory.swift; sourceTree = ""; }; + CA31C1F2D2AF87B527C261FF3EF62C58 /* RDEPUBPageInteractionController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPageInteractionController.swift; sourceTree = ""; }; CA4A114775B98CEA4A566C18123B145F /* ZIPFoundation-ZIPFoundation_Privacy */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "ZIPFoundation-ZIPFoundation_Privacy"; path = ZIPFoundation_Privacy.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; CA538A48DD3691701EC6B5021D7B7381 /* NSScanner+HTML.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSScanner+HTML.m"; path = "Core/Source/NSScanner+HTML.m"; sourceTree = ""; }; CB7F3535507F56824BA21509302CF8B3 /* NSString+DTURLEncoding.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+DTURLEncoding.m"; path = "Core/Source/NSString+DTURLEncoding.m"; sourceTree = ""; }; - CE2C63A2462C2CA8751808A396C796F2 /* RDEPUBReaderSettings.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderSettings.swift; sourceTree = ""; }; - CE2D421FC5D3B9864967FD50215C0A4B /* RDReaderView.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = RDReaderView.modulemap; sourceTree = ""; }; CE6907AC04870132CCE3177E894607DD /* DTCoreTextLayoutFrame.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextLayoutFrame.h; path = Core/Source/DTCoreTextLayoutFrame.h; sourceTree = ""; }; CE7768E097F8BDE9FBE3752B98CD0101 /* DTCoreText-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DTCoreText-umbrella.h"; sourceTree = ""; }; CEA93EDA6ED54D374265AA5D6B9EBBA9 /* NSArray+DTError.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSArray+DTError.h"; path = "Core/Source/NSArray+DTError.h"; sourceTree = ""; }; + D0311E22DAAE8E4ECEE12DFDB8A092A9 /* RDEPUBReadingLocationModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReadingLocationModels.swift; sourceTree = ""; }; D1599698B5A62671C488F13E07272B09 /* DTCoreTextLayoutFrameAccessibilityElementGenerator.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTCoreTextLayoutFrameAccessibilityElementGenerator.m; path = Core/Source/DTCoreTextLayoutFrameAccessibilityElementGenerator.m; sourceTree = ""; }; D1E89A31867FF1FB44B4B999988F1B82 /* DTFoundation */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = DTFoundation; path = DTFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D24FE8634A7AD2562DB8ADB9D4BBB36A /* CTLineUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = CTLineUtils.m; path = Core/Source/CTLineUtils.m; sourceTree = ""; }; D2BA6D5DBD4C6A9285C4592721790A26 /* DTSmartPagingScrollView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTSmartPagingScrollView.h; path = Core/Source/iOS/DTSmartPagingScrollView.h; sourceTree = ""; }; D3593ADF561D422F73C8EABAD4A4F440 /* ConstraintRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintRelation.swift; path = Sources/ConstraintRelation.swift; sourceTree = ""; }; + D366C7AF049977D42FB1B20524EB0B15 /* RDEPUBTextPaginationSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextPaginationSupport.swift; sourceTree = ""; }; D36ED8209C907E3E9B074DDB399F54FF /* DTTextAttachment.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTTextAttachment.h; path = Core/Source/DTTextAttachment.h; sourceTree = ""; }; D4093D30B372960168C1349100DF7DE0 /* DTAttributedTextCell.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAttributedTextCell.h; path = Core/Source/DTAttributedTextCell.h; sourceTree = ""; }; + D479BB941D318A247DDD0A5C55B859C4 /* RDEPUBReaderController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderController.swift; sourceTree = ""; }; D495E2AB1223BD4D4909F8FF1FF47DAF /* Debugging.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Debugging.swift; path = Sources/Debugging.swift; sourceTree = ""; }; D497C3CA364E37DAAFF2C3B90CC7DBE8 /* DTTextBlock.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTTextBlock.h; path = Core/Source/DTTextBlock.h; sourceTree = ""; }; - D73C71810144AB20A3D87FADF45109EF /* RDEPUBTextLayouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextLayouter.swift; sourceTree = ""; }; + D49AD091BED2546F36EC238A5E811D8D /* RDReaderView.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = RDReaderView.modulemap; sourceTree = ""; }; + D6422B9C1E04A7B5F008E4D4ED496CE6 /* RDReaderSpreadResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderSpreadResolver.swift; sourceTree = ""; }; D7B5E61166782ED8D585D4FCEE2074DB /* DTLinkButton.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTLinkButton.h; path = Core/Source/DTLinkButton.h; sourceTree = ""; }; D80CD20CB452B31CCCB14AF8567524D3 /* DTCoreText-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DTCoreText-prefix.pch"; sourceTree = ""; }; D85A85D891A320165843EF9F489178F7 /* UIImage+DTFoundation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+DTFoundation.m"; path = "Core/Source/iOS/UIImage+DTFoundation.m"; sourceTree = ""; }; D863AE4F659A0F71B22DC9FF334DDF4E /* DTCoreTextLayoutFrameAccessibilityElementGenerator.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCoreTextLayoutFrameAccessibilityElementGenerator.h; path = Core/Source/DTCoreTextLayoutFrameAccessibilityElementGenerator.h; sourceTree = ""; }; - D8BCB7DFA6D42E5D07629E5E2F44FD0D /* RDURLReaderController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDURLReaderController.swift; sourceTree = ""; }; D8C351514678D41C58869BD057D5483F /* UIViewFrameExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UIViewFrameExtension.swift; path = Sources/SSAlertSwift/UIViewFrameExtension.swift; sourceTree = ""; }; + D8E247404E492652921952ECE5BA420E /* RDEPUBFixedLayoutTemplate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBFixedLayoutTemplate.swift; sourceTree = ""; }; D927F128EBF26AD2C61FD0E6B6F6F5ED /* ZIPFoundation-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ZIPFoundation-umbrella.h"; sourceTree = ""; }; D9AFC4B66F872436A059FE53780D216F /* SSAlertSwift */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SSAlertSwift; path = SSAlertSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9D07EA99A4878E9F8A8839F19056654 /* RDEPUBBuildDiagnosticsReporter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBBuildDiagnosticsReporter.swift; sourceTree = ""; }; DA25968B7C9A40031DA45BB328EA8A2E /* DTStylesheetHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTStylesheetHTMLElement.m; path = Core/Source/DTStylesheetHTMLElement.m; sourceTree = ""; }; DA9E9711A4073440ECC06C5BD6D8702F /* SnapKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SnapKit-Info.plist"; sourceTree = ""; }; DBC9280BA00935565E9658775DD7404D /* NSAttributedString+DTCoreText.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+DTCoreText.m"; path = "Core/Source/NSAttributedString+DTCoreText.m"; sourceTree = ""; }; @@ -851,6 +942,7 @@ DD189DA6054AAE2242F6644A2623B50E /* NSAttributedString+SmallCaps.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+SmallCaps.h"; path = "Core/Source/NSAttributedString+SmallCaps.h"; sourceTree = ""; }; DD58A1908F3C532210EA65ACE410A44D /* CTLineUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = CTLineUtils.h; path = Core/Source/CTLineUtils.h; sourceTree = ""; }; DE4821B98DDE6C71411EC139EC6D2762 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CoreGraphics.framework; sourceTree = DEVELOPER_DIR; }; + DE9F8F33299CEDB042A85FFA3C4DDA5E /* RDReaderView-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "RDReaderView-prefix.pch"; sourceTree = ""; }; DF5EDCA3B21964D0462BE0982E2F0BF8 /* DTTiledLayerWithoutFade.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTTiledLayerWithoutFade.h; path = Core/Source/iOS/DTTiledLayerWithoutFade.h; sourceTree = ""; }; DF670EC438F05ACC1F1C7B725DBDAA18 /* DTFoundation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DTFoundation.release.xcconfig; sourceTree = ""; }; DF988440049800DB7824A94B81D20254 /* Archive+Writing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+Writing.swift"; path = "Sources/ZIPFoundation/Archive+Writing.swift"; sourceTree = ""; }; @@ -865,19 +957,24 @@ E5F69A1E1260BAD7737BEB533F643A15 /* DTLazyImageView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTLazyImageView.h; path = Core/Source/DTLazyImageView.h; sourceTree = ""; }; E62C5E593037EF8A45E7FAA847D0277C /* DTCoreTextLayoutFrame+Cursor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "DTCoreTextLayoutFrame+Cursor.m"; path = "Core/Source/DTCoreTextLayoutFrame+Cursor.m"; sourceTree = ""; }; E6F2E79CC33E0823C4E719ECDDFBBE08 /* NSAttributedStringRunDelegates.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = NSAttributedStringRunDelegates.m; path = Core/Source/NSAttributedStringRunDelegates.m; sourceTree = ""; }; - EA685920BF70A411A454D84901312521 /* RDEPUBReaderTheme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderTheme.swift; sourceTree = ""; }; - EB29D001CA71EAB0F6E4C7A8B0F04DA0 /* RDEPUBWebView+Search.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+Search.swift"; sourceTree = ""; }; + E8E7E1D7707960ECBBBA26D5E04CE1A8 /* RDReaderView+ContentAccess.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDReaderView+ContentAccess.swift"; sourceTree = ""; }; EBA0272559B0F7FAD8AC670AC7431188 /* Pods-ReadViewDemo-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-ReadViewDemo-umbrella.h"; sourceTree = ""; }; + EC9B5052FF6F4941CE226FF90ED457DE /* RDEPUBParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBParser.swift; sourceTree = ""; }; + ECBA4C3DE9806BC9E5E66E0EE07FA019 /* RDReaderView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = RDReaderView.debug.xcconfig; sourceTree = ""; }; + ECDEDEE1D78BA473378CB3D26C486E5B /* WeReadApi.js */ = {isa = PBXFileReference; includeInIndex = 1; name = WeReadApi.js; path = Sources/RDReaderView/EPUBCore/Resources/WeReadApi.js; sourceTree = ""; }; ECFE19508E12BF8B0B52E8CB4F724217 /* ConstraintMakerRelatable+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ConstraintMakerRelatable+Extensions.swift"; path = "Sources/ConstraintMakerRelatable+Extensions.swift"; sourceTree = ""; }; ED5693BD171B879406EA14C97F4AD8D8 /* Archive+ZIP64.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Archive+ZIP64.swift"; path = "Sources/ZIPFoundation/Archive+ZIP64.swift"; sourceTree = ""; }; EDA6D5D7D0A04878EE78FAAE94F65068 /* DTCoreText */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = DTCoreText; path = DTCoreText.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EE03724A9B7893A98793AE13319755A1 /* RDEPUBReaderToolView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBReaderToolView.swift; sourceTree = ""; }; EE4EE750340347ED584C5A2C1164331A /* SSAlertCommonView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertCommonView.swift; path = Sources/SSAlertSwift/SSAlertCommonView.swift; sourceTree = ""; }; EE9D4A455723797A27F04A15C4E47823 /* DTHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTHTMLElement.h; path = Core/Source/DTHTMLElement.h; sourceTree = ""; }; + EEB1B8F778B5A8442BF121C13E5E460A /* RDEPUBTextIndexTable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextIndexTable.swift; sourceTree = ""; }; EF27E643619E369E8A9C1FE6EFE18C78 /* DTFoundation.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = DTFoundation.modulemap; sourceTree = ""; }; F038FB491B9A4A53447EF0DC333301AE /* DTObjectTextAttachment.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTObjectTextAttachment.m; path = Core/Source/DTObjectTextAttachment.m; sourceTree = ""; }; F0548F684CA2D8C445086389A2BD5265 /* ZIPFoundation-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ZIPFoundation-prefix.pch"; sourceTree = ""; }; F0D72C5680EBE6CA716EE77281AA4436 /* DTAttributedTextContentView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTAttributedTextContentView.m; path = Core/Source/DTAttributedTextContentView.m; sourceTree = ""; }; - F1C425CE28DE9FA9CDD148A2986752D1 /* RDEPUBJavaScriptBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBJavaScriptBridge.swift; sourceTree = ""; }; + F132DA04F32D0155954A84A4467C95CC /* RDEPUBAttachmentNormalizer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBAttachmentNormalizer.swift; sourceTree = ""; }; + F159D5AE51CF38BA79429767DAA95194 /* wxread-replace-latin.css */ = {isa = PBXFileReference; includeInIndex = 1; name = "wxread-replace-latin.css"; path = "Sources/RDReaderView/EPUBCore/Resources/wxread-replace-latin.css"; sourceTree = ""; }; F35039A8D0AA1F7BAFC4E5E016057E15 /* DTTextAttachment.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTTextAttachment.m; path = Core/Source/DTTextAttachment.m; sourceTree = ""; }; F37563A56C180CE18EEFADE56FA3C17F /* DTAccessibilityViewProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTAccessibilityViewProxy.h; path = Core/Source/DTAccessibilityViewProxy.h; sourceTree = ""; }; F410ED25E4E84C3D5166261CDF4B58F2 /* SSAlertView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SSAlertView.swift; path = Sources/SSAlertSwift/SSAlertView.swift; sourceTree = ""; }; @@ -885,19 +982,24 @@ F5389D4E96A4AFE175691B4D3D4524E7 /* DTExtendedFileAttributes.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTExtendedFileAttributes.m; path = Core/Source/DTExtendedFileAttributes.m; sourceTree = ""; }; F53CCB60DE5D06A43E4A52EDB274E7D2 /* NSString+Paragraphs.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+Paragraphs.m"; path = "Core/Source/NSString+Paragraphs.m"; sourceTree = ""; }; F5B34EE2160C27947D1EEFD42184CF5D /* ConstraintLayoutSupportDSL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConstraintLayoutSupportDSL.swift; path = Sources/ConstraintLayoutSupportDSL.swift; sourceTree = ""; }; - F5DF77100B1D534940D2F5CEFCBB9F01 /* UIColor+RDEPUBHex.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "UIColor+RDEPUBHex.swift"; sourceTree = ""; }; + F5F17B78960BAA0E4F63CBDDEAD953FE /* RDReaderPagingController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderPagingController.swift; sourceTree = ""; }; + F616DAE29591FE9EC5FF94F4A922B115 /* RDEPUBWebDecorationOverlayView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBWebDecorationOverlayView.swift; sourceTree = ""; }; + F62A82E787C2CF458A244942F96F35A3 /* RDEPUBPublication.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBPublication.swift; sourceTree = ""; }; + F6910514D1BAFDF69B99E120E4872FF9 /* ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; sourceTree = ""; }; F699881ABDBEB419AEBA661AD5A21F35 /* ZIPFoundation.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ZIPFoundation.release.xcconfig; sourceTree = ""; }; F734214E067BDBDB581911E1A5CDAADB /* DTBreakHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTBreakHTMLElement.m; path = Core/Source/DTBreakHTMLElement.m; sourceTree = ""; }; F76A97683FB5E8E8B86EA4AD71F6F4B4 /* Date+ZIP.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Date+ZIP.swift"; path = "Sources/ZIPFoundation/Date+ZIP.swift"; sourceTree = ""; }; F7B0087DE7C394A677F5883541AB4FB7 /* DTListItemHTMLElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = DTListItemHTMLElement.m; path = Core/Source/DTListItemHTMLElement.m; sourceTree = ""; }; - F815D5A48F693355A35C89FC9E51D336 /* RDEPUBTextContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBTextContentView.swift; sourceTree = ""; }; + F8BD060A6BD280BDB1107988B230DB1A /* RDEPUBRenderRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBRenderRequest.swift; sourceTree = ""; }; F9A2565B302334B662AB51DCF8E65EEE /* DTDictationPlaceholderTextAttachment.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTDictationPlaceholderTextAttachment.h; path = Core/Source/DTDictationPlaceholderTextAttachment.h; sourceTree = ""; }; - FA510A2ECD919C8E8C8EAA7770D37E49 /* WeReadApi.js */ = {isa = PBXFileReference; includeInIndex = 1; name = WeReadApi.js; path = Sources/RDReaderView/EPUBCore/Resources/WeReadApi.js; sourceTree = ""; }; + F9FC3B2914C05EFDDFF68ABE5C7A83F0 /* RDEPUBWebView+JavaScriptBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDEPUBWebView+JavaScriptBridge.swift"; sourceTree = ""; }; + FB0484A107F418C2B5E2944F8BA3C1E6 /* RDEPUBRenderDiagnosticsCollector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDEPUBRenderDiagnosticsCollector.swift; sourceTree = ""; }; FB2ABB1D297442D91AA42F1407DB4A58 /* DTTextHTMLElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTTextHTMLElement.h; path = Core/Source/DTTextHTMLElement.h; sourceTree = ""; }; - FC307B372D2CF490DBFE40A0328E8B0A /* RDReaderPageChildViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RDReaderPageChildViewController.swift; sourceTree = ""; }; FC5EC7C3D1808C9CF53354D7A3D69EE7 /* NSString+DTPaths.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+DTPaths.h"; path = "Core/Source/NSString+DTPaths.h"; sourceTree = ""; }; FCC718115E5C31C88A680522AE36B089 /* DTFoundation-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "DTFoundation-dummy.m"; sourceTree = ""; }; FD03567681A79FFB6E81B3105503065D /* DTFoundationConstants.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTFoundationConstants.h; path = Core/Source/DTFoundationConstants.h; sourceTree = ""; }; + FD2D305B938F31ABC9EC9369E230A62B /* rangy-serializer.js */ = {isa = PBXFileReference; includeInIndex = 1; name = "rangy-serializer.js"; path = "Sources/RDReaderView/EPUBCore/Resources/rangy-serializer.js"; sourceTree = ""; }; + FD7D36A8CADACEF369077B844EA911D0 /* RDReaderView+CollectionView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RDReaderView+CollectionView.swift"; sourceTree = ""; }; FDD333B409D528E5006FA48F4D70A6F5 /* DTCSSListStyle.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = DTCSSListStyle.h; path = Core/Source/DTCSSListStyle.h; sourceTree = ""; }; FE2991699D0391172DEADF2F4613E320 /* SSAlertSwift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SSAlertSwift.release.xcconfig; sourceTree = ""; }; FEA9F91758013B23B0EEA71DA22E7475 /* Pods-ReadViewDemo-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ReadViewDemo-Info.plist"; sourceTree = ""; }; @@ -907,11 +1009,17 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 4D5098EB3DCA949E688884A176837775 /* Frameworks */ = { + 414777EE6814256DD02CD768EC83A8F9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 504AA8F5440FC78EE91E2B72C8A465E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C41084FA13EB4E085E82258526EC3565 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -923,10 +1031,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 65C91618FE0CB90E6BB0ABEB407880D7 /* Frameworks */ = { + 6ACDB35CBA9857418F5EA7E98BD92808 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B883A5F1131F3F9776604F3F9452AC65 /* Foundation.framework in Frameworks */, + 6D96D665BAD918861C54F9682459BE3E /* ImageIO.framework in Frameworks */, + 3C5EDCAB9E3A36C8F43A339B24ADCAB3 /* QuartzCore.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -943,58 +1054,49 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 79526559ADAB3441AA19B5D001BBED4B /* Frameworks */ = { + 85B8A5489B0E61482745C19DC7F2037E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 64122D8C357DCDCBF0BD1BA6080A46FE /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9B1811D8A0826DD2CCA0B0A05A3E4486 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 768BBF03C64346E98F23D30DABEE3CAB /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B1079564B4F05764E0390AB29473621E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0CD6D54912E8E2787802A5EEE4773070 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5F6669E35F9EDE5C335ECB6992286D4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - A8ADF33814FF0B23DBED6ED0B87125B5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A667E54F636BD55EC03EACCB937AF1D9 /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B1998251DB29113903A1C1519FCDA773 /* Frameworks */ = { + BD50AEAFDACDDF335896EACA81D8ADEB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - B7AB287FB809BC6EE97D2CE702B7D742 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ADD70654B7F6B32B2A687B2A7866027E /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D8A394261F2CD1CE7076622CB7CBEA95 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 52A961050B64F5F0985064C3BF7EBDBB /* Foundation.framework in Frameworks */, - F9C5409DC727419DBFA6AD6701108F13 /* ImageIO.framework in Frameworks */, - 0D5E63AF74BC876DEE822780271F8773 /* QuartzCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - FAB45208492061B3E5832F229C3ECE28 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 47AA74DAF06F2F7A71F70FE9F6CC6CAE /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - FF20913263E229616A291202A88A5304 /* Frameworks */ = { + D45D6255F3CEDAD6DEA0624E59C43096 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 58EB34A71BF81209B8649B5D44C5458F /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1073,35 +1175,6 @@ path = "Target Support Files/Pods-ReadViewDemo"; sourceTree = ""; }; - 1171313905A14B172C82D274D7BCCC5F /* EPUBUI */ = { - isa = PBXGroup; - children = ( - 8E2529CE2E852CB74565B24AC7B88965 /* RDEPUBPageInteractionController.swift */, - 7863FD91C5BD0B2EE358458E09C81194 /* RDEPUBPageLayoutSnapshot.swift */, - 97FCA163E8AE5E977B12F9060F220252 /* RDEPUBReaderBottomToolView.swift */, - 5EDE24320E72FF6E77D1E402600E9753 /* RDEPUBReaderChapterListController.swift */, - 89ED8313799E7A012F5A06BCEACF09B8 /* RDEPUBReaderConfiguration.swift */, - B64B372E68C6C74223A76117BFA80B15 /* RDEPUBReaderController.swift */, - 659E6B7AFC5374BF3A56A306238C3E96 /* RDEPUBReaderDelegate.swift */, - 63C6DCCE49AF60A4CD752939A15DB118 /* RDEPUBReaderHighlightsViewController.swift */, - 93B0E9F83D59A57EF00599DBE41952BF /* RDEPUBReaderPersistence.swift */, - CE2C63A2462C2CA8751808A396C796F2 /* RDEPUBReaderSettings.swift */, - 40156267853001A18D40A4515A03C5A7 /* RDEPUBReaderSettingsViewController.swift */, - 76374229228450DD3ADBF2DB618329F5 /* RDEPUBReaderTableOfContentsItem.swift */, - EA685920BF70A411A454D84901312521 /* RDEPUBReaderTheme.swift */, - C6A6391BB8EAA2BA440A903CFBD508ED /* RDEPUBReaderToolView.swift */, - 52BAAD948F35A4788F3441FA024D937B /* RDEPUBReaderTopToolView.swift */, - 31DEEEBC962A7BAB4EDB772B3A2279B2 /* RDEPUBSelectionOverlayView.swift */, - F815D5A48F693355A35C89FC9E51D336 /* RDEPUBTextContentView.swift */, - AE1A53CCB5BC34FB44E35A4A29AA5591 /* RDEPUBWebContentView.swift */, - B6F64F7254BE291374ECDC3E59BDB065 /* RDEPUBWebDecorationOverlayView.swift */, - D8BCB7DFA6D42E5D07629E5E2F44FD0D /* RDURLReaderController.swift */, - F5DF77100B1D534940D2F5CEFCBB9F01 /* UIColor+RDEPUBHex.swift */, - ); - name = EPUBUI; - path = Sources/RDReaderView/EPUBUI; - sourceTree = ""; - }; 1B7515007EDD63EDCCE4BAD15D6CA9BA /* Support Files */ = { isa = PBXGroup; children = ( @@ -1117,22 +1190,61 @@ path = "../Target Support Files/DTFoundation"; sourceTree = ""; }; - 25B0D89DFD7C438AC069E3F7994A0054 /* Support Files */ = { + 1C426635171E4D71C172024364A0CCF7 /* Support Files */ = { isa = PBXGroup; children = ( - CE2D421FC5D3B9864967FD50215C0A4B /* RDReaderView.modulemap */, - 8432F1648F7FDA0F1A736F8A53739308 /* RDReaderView-dummy.m */, - 2C26AFBE041DB9C0B1BFC9B71CFCA31F /* RDReaderView-Info.plist */, - 1C17D914B39D21C0A4943F60349040BA /* RDReaderView-prefix.pch */, - 875F2A69FD6767E29074114D638B2934 /* RDReaderView-umbrella.h */, - B8FAF5A1A2890703F132819BD138C1A9 /* RDReaderView.debug.xcconfig */, - C73611E91719890E1DD889DEBBA1DDF2 /* RDReaderView.release.xcconfig */, - 45D36E42A61555CAE286BFC8FC062999 /* ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist */, + D49AD091BED2546F36EC238A5E811D8D /* RDReaderView.modulemap */, + B92A6483AF878B266B8FE776A9A6F4B3 /* RDReaderView-dummy.m */, + 136BC2B3FC110EA496A8803FF547F72D /* RDReaderView-Info.plist */, + DE9F8F33299CEDB042A85FFA3C4DDA5E /* RDReaderView-prefix.pch */, + 59444DC57CE3171F4EF0CD6481FACF30 /* RDReaderView-umbrella.h */, + ECBA4C3DE9806BC9E5E66E0EE07FA019 /* RDReaderView.debug.xcconfig */, + A6B5316985734E51970FA0FC168498A6 /* RDReaderView.release.xcconfig */, + F6910514D1BAFDF69B99E120E4872FF9 /* ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist */, ); name = "Support Files"; path = "ReadViewDemo/Pods/Target Support Files/RDReaderView"; sourceTree = ""; }; + 26C8D2BAAE04A16BA4FFF4C03445849A /* EPUBUI */ = { + isa = PBXGroup; + children = ( + 3F274F6DE3095EAF4042CC6BF0F3E566 /* RDEPUBReaderBottomToolView.swift */, + 6703BD7A2507028A11319843D0D7945F /* RDEPUBReaderChapterListController.swift */, + D479BB941D318A247DDD0A5C55B859C4 /* RDEPUBReaderController.swift */, + 127FB4B3B74B022CBE4054BA33CB24A0 /* RDEPUBReaderController+ContentDelegates.swift */, + 8B8F6026114BC156F5EAC3906C3DAD24 /* RDEPUBReaderController+DataSource.swift */, + 1BD33493F588BFD65E47EC9B8773C74C /* RDEPUBReaderController+PublicAPI.swift */, + 74F9C4D562F4650166F650997ADA132B /* RDEPUBReaderController+RenderSupport.swift */, + 34D4FDB3A066F397D1FF9727C43A0777 /* RDEPUBReaderController+RuntimeBridge.swift */, + 173FEADB0A73EA7EB5B3B7CADB74C3A4 /* RDEPUBReaderController+TableOfContents.swift */, + 5EFD8EDA8C70B31844B0A06CBCD0B403 /* RDEPUBReaderDelegate.swift */, + BD245FC216DD480CF148530003BAA4BC /* RDEPUBReaderHighlightsViewController.swift */, + 1B8132548C8745F3AE7F3CBC78E1DDA9 /* RDEPUBReaderPersistence.swift */, + 51F09F20128209EC507BBD2E73457DCD /* RDEPUBReaderTableOfContentsItem.swift */, + EE03724A9B7893A98793AE13319755A1 /* RDEPUBReaderToolView.swift */, + BE9D5BE3BC1B9E7C17553D6377DAA5D3 /* RDEPUBReaderTopToolView.swift */, + 42B4304B138A25135B0946C6A7A3F4A8 /* RDEPUBViewportTypes.swift */, + B69E0662B2604A49919B5DDA504A39C4 /* RDEPUBWebContentView.swift */, + F616DAE29591FE9EC5FF94F4A922B115 /* RDEPUBWebDecorationOverlayView.swift */, + 0ECB9576AFBC841E4282C8091B9D1244 /* RDURLReaderController.swift */, + 933F9E11CFE9DD8A63C31D119B381969 /* UIColor+RDEPUBHex.swift */, + E921D3C17874C2A9BF3C204AC3A7C43E /* ReaderController */, + 93807EF10CD76F202828F0798B353B35 /* Settings */, + 87F9D48B334C3113CB9F2C32C7153527 /* TextPage */, + ); + name = EPUBUI; + path = Sources/RDReaderView/EPUBUI; + sourceTree = ""; + }; + 28720FBBB275C39E9BBB8E26AEECE661 /* Pod */ = { + isa = PBXGroup; + children = ( + 2514DF9190BDB37F2BE5FB4389C84579 /* RDReaderView.podspec */, + ); + name = Pod; + sourceTree = ""; + }; 29A410FBD4C19138B1D732EC94D8EFB2 /* DTFoundation */ = { isa = PBXGroup; children = ( @@ -1146,6 +1258,23 @@ path = DTFoundation; sourceTree = ""; }; + 2B8E00F93C74E92445A9818148634890 /* Typesetter */ = { + isa = PBXGroup; + children = ( + F132DA04F32D0155954A84A4467C95CC /* RDEPUBAttachmentNormalizer.swift */, + 2AEE00544B5F5C69A1636474C8E76690 /* RDEPUBFontNormalizer.swift */, + B58EB90E2A4610839F356FE9B2A97486 /* RDEPUBFragmentMarkerInjector.swift */, + 5612A9760A2FEC2682E417DF75F4C869 /* RDEPUBHTMLNormalizer.swift */, + FB0484A107F418C2B5E2944F8BA3C1E6 /* RDEPUBRenderDiagnosticsCollector.swift */, + 7E2F4C746E0B7479BE677A20358CB93B /* RDEPUBSemanticMarkerInjector.swift */, + A16842E6F1C3E8526D28C7DABA1D6898 /* RDEPUBStyleSheetComposer.swift */, + 303F36CC1C0432C0F498EAED0D82A567 /* RDEPUBTextRendererSupport.swift */, + 3EADCA56DD969174A20085845D93C409 /* RDEPUBTypesettingPipeline.swift */, + ); + name = Typesetter; + path = Typesetter; + sourceTree = ""; + }; 3595ED200AA07F0B71CB42506574798F /* Support Files */ = { isa = PBXGroup; children = ( @@ -1162,6 +1291,17 @@ path = "../Target Support Files/SnapKit"; sourceTree = ""; }; + 3ACDEB98F13FA1C726293C9FE5C43FC0 /* Models */ = { + isa = PBXGroup; + children = ( + C274719939F1335BCB8FA97636A9D2DD /* RDEPUBAnnotationModels.swift */, + 24B33931EE71CDEAAB6870A8DD00F441 /* RDEPUBPaginationModels.swift */, + D0311E22DAAE8E4ECEE12DFDB8A092A9 /* RDEPUBReadingLocationModels.swift */, + ); + name = Models; + path = Models; + sourceTree = ""; + }; 3C15BAF0B39DC004AB2E6FA38175E25D /* DTAnimatedGIF */ = { isa = PBXGroup; children = ( @@ -1235,45 +1375,6 @@ path = "../Target Support Files/DTCoreText"; sourceTree = ""; }; - 5DE502ECE610FE31567AEF3D1AEAC2B0 /* EPUBCore */ = { - isa = PBXGroup; - children = ( - C5C4BF1170DCF38B8CC1E4979A0334FA /* RDEPUBAssetRepository.swift */, - 660901C91F4E1D354689FF2F8B6E1679 /* RDEPUBFixedLayoutTemplate.swift */, - F1C425CE28DE9FA9CDD148A2986752D1 /* RDEPUBJavaScriptBridge.swift */, - 9C2DE084B4CF376178F1AFB96BD84562 /* RDEPUBModels.swift */, - 9197422E9FAD3EDDC5C5844350D497F7 /* RDEPUBNavigatorLayoutContext.swift */, - 6A800873D793225974FA75561B0810DF /* RDEPUBNavigatorState.swift */, - 24AD5C352BC2F291B84A39565644DE44 /* RDEPUBPaginator.swift */, - 2D5AA1752B9A4DA12829506699ACB35F /* RDEPUBParser.swift */, - 7AD993F901F35B7F58E0A7AC2F8962BA /* RDEPUBParser+Archive.swift */, - 551DE8194BF851EE58EA61CBBA1710B1 /* RDEPUBParser+Package.swift */, - 992AFC205F9539B89BF279A5EE6CB266 /* RDEPUBParser+ReadingProfile.swift */, - A4EE1E0ACE64032C6C8CB26A91235993 /* RDEPUBParser+Resources.swift */, - 87AAB03A10169416E66E50562D9A57A0 /* RDEPUBParser+TOC.swift */, - 8173B8E972A645B3A420B3C5C0DCDF6A /* RDEPUBPreferences.swift */, - 1CF085139C084B6CB83EB511352CC385 /* RDEPUBPublication.swift */, - 49439BEDC4669BE8E3E9D3315C568645 /* RDEPUBReadingModels.swift */, - 3C67B49D37D02FC71C317CCA00B61EDB /* RDEPUBReadingSession.swift */, - 59FAF02EDC6AC0DB9FA24A64E7B870CB /* RDEPUBRenderRequest.swift */, - 6002874EC82D09DCF64076F537D57EE4 /* RDEPUBResourceResolver.swift */, - 32769058982DA3B3ADB0051377B41916 /* RDEPUBResourceURLSchemeHandler.swift */, - 16973940E1B38B29D620DAB562AB7396 /* RDEPUBSearchEngine.swift */, - 39A422EF35AF8F3EC0DCD8BCAF01A32E /* RDEPUBSearchModels.swift */, - 37C1467D2571E3541EC221A49C90B110 /* RDEPUBStyleSheetBuilder.swift */, - 4CA430D9A4A6EAE818564BB80309781D /* RDEPUBTextAnchor.swift */, - 929106407362D571E93A73D87EAB8107 /* RDEPUBWebView.swift */, - 89FBCDE54D10D205F325580161FB9FC3 /* RDEPUBWebView+Configuration.swift */, - 252896D6D97D0F06D5937D72DD041E65 /* RDEPUBWebView+FixedLayout.swift */, - 0B1A759F6D90A955E7DF86A14FEE1737 /* RDEPUBWebView+JavaScriptBridge.swift */, - 6861909964EF2CEDB7ED884A3B410A7D /* RDEPUBWebView+Reflowable.swift */, - EB29D001CA71EAB0F6E4C7A8B0F04DA0 /* RDEPUBWebView+Search.swift */, - 32A3B34DE993508CAEBD18287344623E /* RDEPUBWebViewDebug.swift */, - ); - name = EPUBCore; - path = Sources/RDReaderView/EPUBCore; - sourceTree = ""; - }; 62598D566A90C2374701CEB798B77521 /* Support Files */ = { isa = PBXGroup; children = ( @@ -1320,14 +1421,6 @@ path = ZIPFoundation; sourceTree = ""; }; - 6436F80D3F77FC2FDE2075BCCFD6B16A /* Pod */ = { - isa = PBXGroup; - children = ( - 580328EB7B35E7A9C48515F737D984ED /* RDReaderView.podspec */, - ); - name = Pod; - sourceTree = ""; - }; 64771901367D11DEDD868C197369EA56 /* DTHTMLParser */ = { isa = PBXGroup; children = ( @@ -1337,6 +1430,24 @@ name = DTHTMLParser; sourceTree = ""; }; + 7975B11608EFC0BE3054608D14E34E8E /* EPUBTextRendering */ = { + isa = PBXGroup; + children = ( + 8CEAB7724C9E71FFC8A368EECE6AADF9 /* RDEPUBChapterData.swift */, + A3A5FB560E84A3FE29C291466EAE9E22 /* RDEPUBDTCoreTextRenderer.swift */, + EEB1B8F778B5A8442BF121C13E5E460A /* RDEPUBTextIndexTable.swift */, + 476A874AD38352C8F072379DDDEBE8CD /* RDEPUBTextPositionConverter.swift */, + 01073FE593A6AE17D4376AF909753A8F /* RDEPUBTextRenderer.swift */, + 591B0B382F10A52D84F90D5C34893CB8 /* RDEPUBTextSearchEngine.swift */, + 5341C765E36D0B172F2F8D4812829521 /* RDPlainTextBookBuilder.swift */, + E22D5E335BCECB300822469235AD868D /* BuildPipeline */, + C826EF057BCF3A109AB5A1D470AF72FF /* Pagination */, + 2B8E00F93C74E92445A9818148634890 /* Typesetter */, + ); + name = EPUBTextRendering; + path = Sources/RDReaderView/EPUBTextRendering; + sourceTree = ""; + }; 81FAF4124052D4A411C0A8C21B48174C /* Core */ = { isa = PBXGroup; children = ( @@ -1344,6 +1455,35 @@ name = Core; sourceTree = ""; }; + 87F9D48B334C3113CB9F2C32C7153527 /* TextPage */ = { + isa = PBXGroup; + children = ( + CA31C1F2D2AF87B527C261FF3EF62C58 /* RDEPUBPageInteractionController.swift */, + 19F9801AD3675102B14F64AFAFB99D89 /* RDEPUBPageLayoutSnapshot.swift */, + 0A65FCB87389CF471A9A4BD659D58346 /* RDEPUBSelectableTextView.swift */, + 185CD1A4E30E72715AF3129924F063E6 /* RDEPUBSelectionOverlayView.swift */, + 2F910EB56A7325328DCD94F082E99648 /* RDEPUBTextAnnotationOverlay.swift */, + 4B789EEAB8791D76E537C6186EA1AC1D /* RDEPUBTextContentView.swift */, + 9DB5F45FEE54BD8D3F49BD345EDAB5D9 /* RDEPUBTextPageDecorationView.swift */, + 5E030AC563D5D21F6792CA5056667F3F /* RDEPUBTextPageRenderView.swift */, + A42EA2D403B179C805701DD3638696B0 /* RDEPUBTextSelectionController.swift */, + ); + name = TextPage; + path = TextPage; + sourceTree = ""; + }; + 93807EF10CD76F202828F0798B353B35 /* Settings */ = { + isa = PBXGroup; + children = ( + 9BBE8424D2FE92DCAA8D2B1CA7D4D029 /* RDEPUBReaderConfiguration.swift */, + C55B9A91FBFB85980E2E199DC0011868 /* RDEPUBReaderSettings.swift */, + 49F47726654DBD3FAFF21FFA121D6EE7 /* RDEPUBReaderSettingsViewController.swift */, + 862E061063601D25DB09C808CA390C18 /* RDEPUBReaderTheme.swift */, + ); + name = Settings; + path = Settings; + sourceTree = ""; + }; 994C39A644C0722C7A9E97AB46DB8965 /* Targets Support Files */ = { isa = PBXGroup; children = ( @@ -1352,6 +1492,37 @@ name = "Targets Support Files"; sourceTree = ""; }; + 9BAE9E3C4CCFBCB6D66C7E0B219AD7D6 /* ReaderView */ = { + isa = PBXGroup; + children = ( + 291992715B24D4645FF6E45D0881635F /* RDReaderContentCell.swift */, + B3346B9CF2882D4D658CE30CDCCC93CA /* RDReaderFlowLayout.swift */, + 45989DEA616D69D14306213B39E79FD6 /* RDReaderGestureController.swift */, + 6369702FFDE8929ED1931FC77B6B02A1 /* RDReaderPageChildViewController.swift */, + 0A294C65CC6C154EF2122380953FEFE3 /* RDReaderView.swift */, + FD7D36A8CADACEF369077B844EA911D0 /* RDReaderView+CollectionView.swift */, + E8E7E1D7707960ECBBBA26D5E04CE1A8 /* RDReaderView+ContentAccess.swift */, + 3A525BB5B1708520D409C1A30CF1CD2D /* RDReaderView+PageCurl.swift */, + 9392F7CFCA8C63DD163AAB74B900D729 /* RDReaderView+ToolView.swift */, + 31407369BDB36B40D253244CCF1F954A /* RDReaderViewProtocols.swift */, + A243F0443A0A5FBBAD729A69DBDA15CE /* Paging */, + ); + name = ReaderView; + path = Sources/RDReaderView/ReaderView; + sourceTree = ""; + }; + A243F0443A0A5FBBAD729A69DBDA15CE /* Paging */ = { + isa = PBXGroup; + children = ( + F5F17B78960BAA0E4F63CBDDEAD953FE /* RDReaderPagingController.swift */, + 8B57C7D26C0F345A62E14DAB51521E69 /* RDReaderPreloadController.swift */, + D6422B9C1E04A7B5F008E4D4ED496CE6 /* RDReaderSpreadResolver.swift */, + C637C7A440920FA0074A7ECF39D9C6F8 /* RDReaderTapRegionHandler.swift */, + ); + name = Paging; + path = Paging; + sourceTree = ""; + }; A5E1FCBC8B49DE230DF2EACDA19A840B /* DTCoreText */ = { isa = PBXGroup; children = ( @@ -1489,69 +1660,49 @@ path = DTCoreText; sourceTree = ""; }; - A618C18152B0CC69EBA32288A9A260E9 /* EPUBTextRendering */ = { + A8C5EB6578A4DB2C5736D37C10EC378E /* EPUBCore */ = { isa = PBXGroup; children = ( - 967506B2C9A33F6E902BDA98940C603F /* RDEPUBChapterData.swift */, - 9CBC86AC7AC17CD094469EC7DCE21724 /* RDEPUBDTCoreTextRenderer.swift */, - 763D7B1705C9113C0A37E0775C4F9FEC /* RDEPUBTextBookBuilder.swift */, - 16D52166703BA10418F79BCD397B07DB /* RDEPUBTextBookCache.swift */, - 6BAB3488CD710ECC37CE9B0F10A10545 /* RDEPUBTextIndexTable.swift */, - D73C71810144AB20A3D87FADF45109EF /* RDEPUBTextLayouter.swift */, - 26CF54683B145156F178EF99B0C7046A /* RDEPUBTextLayoutFrame.swift */, - 7B89D235A3ECE3B096FBB5E7A2626C9C /* RDEPUBTextPaginationSupport.swift */, - 3119842637EAE995A0DAEBEB1170E363 /* RDEPUBTextPerformanceSampler.swift */, - 35FA9133889FF0F7999C456AC029966C /* RDEPUBTextPositionConverter.swift */, - 749B6320F95F8F75FDF6B8ACC3E43D57 /* RDEPUBTextRenderer.swift */, - 9654CB2B436E0FB30970E9FCF3870820 /* RDEPUBTextRendererSupport.swift */, - 2F8C4EB13F9C6442DAD004816726B09A /* RDEPUBTextSearchEngine.swift */, - 7D078643E7C9EA66EB922D04504AC413 /* RDPlainTextBookBuilder.swift */, + 98BFF67943C8CABB6171A6EDC7639F82 /* RDEPUBAssetRepository.swift */, + D8E247404E492652921952ECE5BA420E /* RDEPUBFixedLayoutTemplate.swift */, + 04C13EA061DD9866E60F683AD49EE945 /* RDEPUBJavaScriptBridge.swift */, + 6A2067EA81E19AA5F8E66DE4300DAB4A /* RDEPUBModels.swift */, + 521E0E2A04F1C8B2E1AB5D30420A010B /* RDEPUBNavigatorLayoutContext.swift */, + 9AE2A1B83DCC99894642D65CE5EADFD4 /* RDEPUBNavigatorState.swift */, + AA479EAA368CC8B528282E49B6C96BC8 /* RDEPUBPaginator.swift */, + EC9B5052FF6F4941CE226FF90ED457DE /* RDEPUBParser.swift */, + C9087CF398AFBBC8FFB5C60E6389EAB1 /* RDEPUBParser+Archive.swift */, + 4B142DE3191F790898FB028F3E6C91BA /* RDEPUBParser+Package.swift */, + 667442978548AAC9B24B14B6F2597536 /* RDEPUBParser+ReadingProfile.swift */, + AE4740D05411538E3715704ED63C6E76 /* RDEPUBParser+Resources.swift */, + 137B4BC4EAE7AE0D9567E53EBBBA687E /* RDEPUBParser+TOC.swift */, + 9B1FF16A8E3839BF599427F23328492C /* RDEPUBPreferences.swift */, + F62A82E787C2CF458A244942F96F35A3 /* RDEPUBPublication.swift */, + 7429300C02F7E8453A429E08C9BD10EA /* RDEPUBReadingSession.swift */, + F8BD060A6BD280BDB1107988B230DB1A /* RDEPUBRenderRequest.swift */, + 615F9666D462FC95654FDC2732F700A6 /* RDEPUBResourceResolver.swift */, + 64644C3A8F17BE731AE00482BDEE9D86 /* RDEPUBResourceURLSchemeHandler.swift */, + 16C3C684271C3DE0964041A2D8701E58 /* RDEPUBSearchEngine.swift */, + 8830746779660E908FADB2DEAD879AA2 /* RDEPUBSearchModels.swift */, + 164CFAED45E63A7592580A342D44A43E /* RDEPUBStyleSheetBuilder.swift */, + 69F083BF98854C4CB74C7C21DA34399A /* RDEPUBTextAnchor.swift */, + 268AF3AC14AC70E5C08F5EF60DBFC4F8 /* RDEPUBWebView.swift */, + 5B55540809AED42C4B61333680CEC4CB /* RDEPUBWebView+Configuration.swift */, + 71171943FCD1D8271266E2F6347D503B /* RDEPUBWebView+FixedLayout.swift */, + F9FC3B2914C05EFDDFF68ABE5C7A83F0 /* RDEPUBWebView+JavaScriptBridge.swift */, + 8067F377315BFA008F9F0795CCB68D5F /* RDEPUBWebView+Reflowable.swift */, + B1DD2563775FC83493319BFC4E953F00 /* RDEPUBWebView+Search.swift */, + 1D01284125120D95F9E41743D2CE9F81 /* RDEPUBWebViewDebug.swift */, + 3ACDEB98F13FA1C726293C9FE5C43FC0 /* Models */, ); - name = EPUBTextRendering; - path = Sources/RDReaderView/EPUBTextRendering; - sourceTree = ""; - }; - AE4B5910B2AE28EB710635EA73985005 /* RDReaderView */ = { - isa = PBXGroup; - children = ( - C6F199830BC382B6C9AEF29F421EE0C9 /* cssInjector.js */, - 33D8E2EC0A0BDF6FCA33CA829E2C6FCF /* epub-bridge.js */, - 353AB6D63236BF3C98B630EAB53AC742 /* epub-fixed-layout.html */, - 49553EE889D9BB1C29A6AA69A857A8EB /* rangy-core.js */, - 129DD2A43FC15DC9844C508DFE51D24A /* rangy-serializer.js */, - FA510A2ECD919C8E8C8EAA7770D37E49 /* WeReadApi.js */, - 57DE6F8B9CE9F488188CFE16DDF7929B /* wxread-dark.css */, - 32D260607CE02388D7741193F901AF75 /* wxread-default.css */, - 6009A6FAECBFC4615F869A0D90D8A460 /* wxread-replace.css */, - C65067E704BE5EEEDE5FC6FF04C9C0AA /* wxread-replace-latin.css */, - 5DE502ECE610FE31567AEF3D1AEAC2B0 /* EPUBCore */, - A618C18152B0CC69EBA32288A9A260E9 /* EPUBTextRendering */, - 1171313905A14B172C82D274D7BCCC5F /* EPUBUI */, - 6436F80D3F77FC2FDE2075BCCFD6B16A /* Pod */, - B56A5C449EDE2FBDC257BA766361A6D1 /* ReaderView */, - 25B0D89DFD7C438AC069E3F7994A0054 /* Support Files */, - ); - name = RDReaderView; - path = ../..; - sourceTree = ""; - }; - B56A5C449EDE2FBDC257BA766361A6D1 /* ReaderView */ = { - isa = PBXGroup; - children = ( - 609F81945B589AC45E4E531C01F6EE3E /* RDReaderContentCell.swift */, - 6D4134DC0BA1A654610741FB33CA9BA7 /* RDReaderFlowLayout.swift */, - B2682E7883D110ECC2F46C6270375D24 /* RDReaderGestureController.swift */, - FC307B372D2CF490DBFE40A0328E8B0A /* RDReaderPageChildViewController.swift */, - 70AA5021FC2962BB31E28C8BFDA797CD /* RDReaderView.swift */, - ); - name = ReaderView; - path = Sources/RDReaderView/ReaderView; + name = EPUBCore; + path = Sources/RDReaderView/EPUBCore; sourceTree = ""; }; B9CF9F63ECCD4435AA8F5FEDC37E6258 /* Development Pods */ = { isa = PBXGroup; children = ( - AE4B5910B2AE28EB710635EA73985005 /* RDReaderView */, + E15A4B07B2D069E67E96FC5DB659AD8E /* RDReaderView */, ); name = "Development Pods"; sourceTree = ""; @@ -1564,6 +1715,21 @@ name = Resources; sourceTree = ""; }; + C826EF057BCF3A109AB5A1D470AF72FF /* Pagination */ = { + isa = PBXGroup; + children = ( + 694DC251BF921B37330A35A6ACD9DF62 /* RDEPUBChapterPageCounter.swift */, + C996E23A573AA37FB9B35A09725C1D1D /* RDEPUBCoreTextPageFrameFactory.swift */, + 117036F7DB9A22578522D0B397C28F26 /* RDEPUBPageBreakPolicy.swift */, + C6A32D83CDE7A6494252691BE7E99638 /* RDEPUBTextLayouter.swift */, + 141325B9AB28E266F40552E4F82F952D /* RDEPUBTextLayoutFrame.swift */, + 4C2412CF0CE63E83F2F0EC492D3256F9 /* RDEPUBTextPaginationInterfaces.swift */, + D366C7AF049977D42FB1B20524EB0B15 /* RDEPUBTextPaginationSupport.swift */, + ); + name = Pagination; + path = Pagination; + sourceTree = ""; + }; CA6142FEF8E51F79243E3C87378F50AF /* UIKit */ = { isa = PBXGroup; children = ( @@ -1619,6 +1785,46 @@ name = Resources; sourceTree = ""; }; + E15A4B07B2D069E67E96FC5DB659AD8E /* RDReaderView */ = { + isa = PBXGroup; + children = ( + 4D9B8DA4629A86EE848CD980111120C3 /* cssInjector.js */, + 75838C8BB89B6B0B142EC3F6922D925B /* epub-bridge.js */, + 6CB839342BEE5213E1F9D59970EEC467 /* epub-fixed-layout.html */, + A2BD2727B338CE5C2C3BC6E8B8A23DE9 /* rangy-core.js */, + FD2D305B938F31ABC9EC9369E230A62B /* rangy-serializer.js */, + ECDEDEE1D78BA473378CB3D26C486E5B /* WeReadApi.js */, + 2FBA8F4FD78EAB6A6E4011D92577EA42 /* wxread-dark.css */, + 5D8DA3839256D5A245E96F2C67A5E232 /* wxread-default.css */, + 7F04C6EEE7B8C63B04236F35AE34B289 /* wxread-replace.css */, + F159D5AE51CF38BA79429767DAA95194 /* wxread-replace-latin.css */, + A8C5EB6578A4DB2C5736D37C10EC378E /* EPUBCore */, + 7975B11608EFC0BE3054608D14E34E8E /* EPUBTextRendering */, + 26C8D2BAAE04A16BA4FFF4C03445849A /* EPUBUI */, + 28720FBBB275C39E9BBB8E26AEECE661 /* Pod */, + 9BAE9E3C4CCFBCB6D66C7E0B219AD7D6 /* ReaderView */, + 1C426635171E4D71C172024364A0CCF7 /* Support Files */, + ); + name = RDReaderView; + path = ../..; + sourceTree = ""; + }; + E22D5E335BCECB300822469235AD868D /* BuildPipeline */ = { + isa = PBXGroup; + children = ( + D9D07EA99A4878E9F8A8839F19056654 /* RDEPUBBuildDiagnosticsReporter.swift */, + 0B832537E4E508747319F7DF89BE256A /* RDEPUBChapterTailNormalizer.swift */, + 20AA41D781113C9A30CD0E9EDD6835D5 /* RDEPUBPaginationCacheCoordinator.swift */, + 7ED26E7B899A0B29A202F0CB011D5F09 /* RDEPUBTextBookBuilder.swift */, + BE13C3BA99810308E47EC53C68F6169A /* RDEPUBTextBookCache.swift */, + 49F7945AF94DCC77CB3E7F3F711BD4AB /* RDEPUBTextBookModels.swift */, + 2055F08A4A307EA51828460481FE8276 /* RDEPUBTextBuildPipelineInterfaces.swift */, + 8607E8F20399521FFA80047941709E8C /* RDEPUBTextPerformanceSampler.swift */, + ); + name = BuildPipeline; + path = BuildPipeline; + sourceTree = ""; + }; E2CD122CF123DD3681DAEE021EA37359 /* Core */ = { isa = PBXGroup; children = ( @@ -1678,6 +1884,25 @@ name = Pods; sourceTree = ""; }; + E921D3C17874C2A9BF3C204AC3A7C43E /* ReaderController */ = { + isa = PBXGroup; + children = ( + 0C7333A1BFCB9D7512A152B2B0EC6350 /* RDEPUBReaderAnnotationCoordinator.swift */, + 72E8D63E9D57DA89DEE2774A89A61152 /* RDEPUBReaderAssemblyCoordinator.swift */, + 2BCD9B2E62051692362E404278683A18 /* RDEPUBReaderChromeCoordinator.swift */, + 1CA5A4277907D54250DB4BF7B95CE1A9 /* RDEPUBReaderContext.swift */, + 851EF2369FB225D981E94F4BB2E9F65F /* RDEPUBReaderDependencies.swift */, + 4C94F70B03585FF87A0DEF959FD7E5A6 /* RDEPUBReaderLoadCoordinator.swift */, + C26BE3D7337938B1DB1B7610001CD169 /* RDEPUBReaderLocationCoordinator.swift */, + 3D246461688CA3E0592B5536C95A624A /* RDEPUBReaderPaginationCoordinator.swift */, + 413B561A626E7BC1C78EFEFA131B92B0 /* RDEPUBReaderRuntime.swift */, + 3156D6DC256E20EF49691134C4342473 /* RDEPUBReaderSearchCoordinator.swift */, + 8FD217FE3F87580A8BA3BD7D8992D4EB /* RDEPUBReaderViewportMonitor.swift */, + ); + name = ReaderController; + path = ReaderController; + sourceTree = ""; + }; F3D99F452BF74190AE8A6D307FDF149B /* Support Files */ = { isa = PBXGroup; children = ( @@ -1697,14 +1922,6 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 24479202F4DAEA9BF631CC781DC01B4A /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - A1FE48E415D345B47864748E6852C4D2 /* RDReaderView-umbrella.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 29CB77F5EC62298B8E19FEFFD791AEAD /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1778,52 +1995,68 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4C9C4A053FEC3DEEEA6841DDB13465DD /* Headers */ = { + 34A6CF459BA72007E08489B574B5F8E6 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 8AA746B78ADA7C80D5BC28F8B53D8723 /* DTActivityTitleView.h in Headers */, - B533B190B349CC90CA4814887EA7185F /* DTAnimatedGIF.h in Headers */, - FD036CA8E474138A8DAC47527185B5FA /* DTBase64Coding.h in Headers */, - CAD07B5D2FADB9B4A8F807CB4EB4F92A /* DTBlockFunctions.h in Headers */, - 239D541A0D14415051D9B84A6FAC4396 /* DTCompatibility.h in Headers */, - EFDDBC3AC88A2F9843346E3A0A5389F0 /* DTCoreGraphicsUtils.h in Headers */, - 701E081379C13353FC056435950394DE /* DTCustomColoredAccessory.h in Headers */, - 77DEFBEDEE73BB372A60ECB671AEE41E /* DTExtendedFileAttributes.h in Headers */, - 6C50D4014739F5FF607E4E9731191A16 /* DTFolderMonitor.h in Headers */, - 982E91D1CAADCF6953B81B68AB159CEB /* DTFoundation-umbrella.h in Headers */, - 96FB53A9630F4A8B5A772DDC36E52801 /* DTFoundationConstants.h in Headers */, - 3D57FD29F6030DC9F461D1344BD78986 /* DTHTMLParser.h in Headers */, - E61C54D454FC27FD101B58B9BC395466 /* DTLog.h in Headers */, - 5155756A1E960CE0786993DB461B11E9 /* DTPieProgressIndicator.h in Headers */, - 6AC9ABB942082B71200798D8E9E0AAEE /* DTSmartPagingScrollView.h in Headers */, - 989258CEB05005E2A8BEA35661CA8591 /* DTTiledLayerWithoutFade.h in Headers */, - EDE07C2D89DEC86AF5B3BDB830DFDFDB /* DTVersion.h in Headers */, - CCC981AE077E9F3E0BCFD8914E7D6F0B /* DTWeakSupport.h in Headers */, - 8B831C5DE899A4C6E6A9756FF359E6F3 /* NSArray+DTError.h in Headers */, - 11C954E3357C1EEB7CC99D689FF2EF01 /* NSData+DTCrypto.h in Headers */, - 69B3DF49F4AB7646FD5A4584F9DCEF98 /* NSDictionary+DTError.h in Headers */, - E94DC7964A5276288537BA9B36377647 /* NSFileWrapper+DTCopying.h in Headers */, - D38A633998460910E98BF5E93841DAD6 /* NSMutableArray+DTMoving.h in Headers */, - 7F1244DE6C0ED5875B9149BDD300C1BC /* NSString+DTFormatNumbers.h in Headers */, - 8CA81801E1B75ED092543CB44D5C29EC /* NSString+DTPaths.h in Headers */, - 660A86A14D4B4568FA3DFABC53BA83ED /* NSString+DTURLEncoding.h in Headers */, - 249683EC17AB72DA0F517756C7657A79 /* NSString+DTUtilities.h in Headers */, - F588926C35B92A750FB13835C0111BB1 /* NSURL+DTAppLinks.h in Headers */, - FA5321384E837B212BA8DBD3FBE3FB04 /* NSURL+DTComparing.h in Headers */, - F25AD7EBA6F5DF35A82207F6D7E7A25C /* NSURL+DTUnshorten.h in Headers */, - 9D8CB20F75B0767AF187FAE3C595D790 /* UIApplication+DTNetworkActivity.h in Headers */, - 00C49E16B834531525EA78BB63A6E504 /* UIImage+DTFoundation.h in Headers */, - E1A1B8B02C3EF2EF0008C7DAF6608192 /* UIScreen+DTFoundation.h in Headers */, - 9DF3F596DF4C655F29378648A7229D0A /* UIView+DTFoundation.h in Headers */, + 7B03A8945E4D03692C21B116D97DC74C /* SSAlertSwift-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; - 894BFED04DDC330581AD10C7E6D852BB /* Headers */ = { + 4171195ED1E9280FD3E4BA5F78CC2311 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 453D3B15ED40279A37CA0D74DBE4FFF2 /* SSAlertSwift-umbrella.h in Headers */, + 28BF24C2F4CD401C4C24E1DE0516746E /* DTActivityTitleView.h in Headers */, + D578954B277D170B2054CBA6081BEC4D /* DTAnimatedGIF.h in Headers */, + 2E26468A7766E7F228328A1B13FC92A8 /* DTBase64Coding.h in Headers */, + 19BA94EB41DF632F761221B74DAD526A /* DTBlockFunctions.h in Headers */, + 58A61B581F5E418C86458C824089E6DC /* DTCompatibility.h in Headers */, + 58F5FFB8103C4733FF2F207057E60CE5 /* DTCoreGraphicsUtils.h in Headers */, + 90BC61DF679C4367DAD09A5D02E0AB02 /* DTCustomColoredAccessory.h in Headers */, + AB9713F8457EB1C14E53C76F0E442D82 /* DTExtendedFileAttributes.h in Headers */, + 27B96826B55ACC5EBD0442CE693D0B49 /* DTFolderMonitor.h in Headers */, + AD103B6A562A487B79DDB6617889D429 /* DTFoundation-umbrella.h in Headers */, + C0ADCA6F77E50FEDD72E2EF8FFA5B9A1 /* DTFoundationConstants.h in Headers */, + 6AB30A0403FF95366915988914FB7D3C /* DTHTMLParser.h in Headers */, + D02DF9F9FB5549E3D0A75B88BF9A30A8 /* DTLog.h in Headers */, + AD4B914F1D725723EF1927B09FD6CC64 /* DTPieProgressIndicator.h in Headers */, + 8115CAF95FA9A1ED63213498A1CA421E /* DTSmartPagingScrollView.h in Headers */, + 13E76CEF04DCA3C012D6BE385BD843F9 /* DTTiledLayerWithoutFade.h in Headers */, + 7104D4CFBCF8831BEFD9EC0DD9AB678F /* DTVersion.h in Headers */, + 2617C9F41307C3F9C95A8D3A226B4F9F /* DTWeakSupport.h in Headers */, + 6FF9A524B248BAACCF8E3D4A00BF4CF1 /* NSArray+DTError.h in Headers */, + 38D8FC875F9F62E1F50B427EE02B2DA8 /* NSData+DTCrypto.h in Headers */, + 27C72FB405DFBC4E9922AD382A2C35EB /* NSDictionary+DTError.h in Headers */, + 53D7A11DAB7412522214EBA6CCB2ABE3 /* NSFileWrapper+DTCopying.h in Headers */, + BBF0C5F3F558E5C82C28F9F2885428B2 /* NSMutableArray+DTMoving.h in Headers */, + 431305719D350F47AB85C2E09B3FB6D4 /* NSString+DTFormatNumbers.h in Headers */, + 708B7BF70311E17A3B27C8AF36937ED9 /* NSString+DTPaths.h in Headers */, + E2EE910F549C5D233EB236BB480CC09D /* NSString+DTURLEncoding.h in Headers */, + E5AB0AA1CD3062159407E53FD7365145 /* NSString+DTUtilities.h in Headers */, + A6A66002BC974BBD05385DC49E9A4CD6 /* NSURL+DTAppLinks.h in Headers */, + 0F7F1426A2C194B170E82DE115A66DB1 /* NSURL+DTComparing.h in Headers */, + 5B36BCAC2EE5770917EE46A25443744F /* NSURL+DTUnshorten.h in Headers */, + DBF7631687BF37FCD33C20E23D6C291C /* UIApplication+DTNetworkActivity.h in Headers */, + CCF3600DBACB7361D2130AAFD7A6A83D /* UIImage+DTFoundation.h in Headers */, + 809E658A89FF98AF699C66D2429B29FD /* UIScreen+DTFoundation.h in Headers */, + 584FC22D5B5F0120FCF9ACE0E570D137 /* UIView+DTFoundation.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5D0FD1429CBDECE5A0750FDD9C003FFD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1AF5422FD56B541040966046B479CE53 /* ZIPFoundation-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84AB3C42692480DA8A73D2EDF29029AA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 48BE5A69C2B044534A947BC810B1DED4 /* RDReaderView-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1835,19 +2068,11 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE7522D7BD4EDC854C005F9E164F7422 /* Headers */ = { + CD9C4BFCB8CA7DF04B716BF3CF57DDCA /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 555E422A804DAB73D29F1E68F33BA9BD /* ZIPFoundation-umbrella.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - FDF930B0AC649D7D0C762D73710CB9EE /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - C346D582EB4C894FB4D2FFBC5FDD2D28 /* SnapKit-umbrella.h in Headers */, + BB782BBE8587EDB13C294A81E7F36225 /* SnapKit-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1856,11 +2081,11 @@ /* Begin PBXNativeTarget section */ 0C24CB0E87A728A11AA1124CB360D6A1 /* DTCoreText-Resources */ = { isa = PBXNativeTarget; - buildConfigurationList = 1DE4C138F942A2A0F6A14D11F475D90E /* Build configuration list for PBXNativeTarget "DTCoreText-Resources" */; + buildConfigurationList = F45A5207FBF041DEA1FF688C5CC78E8B /* Build configuration list for PBXNativeTarget "DTCoreText-Resources" */; buildPhases = ( - FC050EDAFDDEA3B65238F3C4C214D150 /* Sources */, - 65C91618FE0CB90E6BB0ABEB407880D7 /* Frameworks */, - 567034FDEEAB950DD14247A5BD72A079 /* Resources */, + 0C2FD0281726C9DFE0D55D11BA576B8C /* Sources */, + 414777EE6814256DD02CD768EC83A8F9 /* Frameworks */, + 9AC7661DA4E67875FBDF24E284ABD99E /* Resources */, ); buildRules = ( ); @@ -1873,17 +2098,17 @@ }; 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */ = { isa = PBXNativeTarget; - buildConfigurationList = 71C725761AC2E4DC24695B87CC437F3F /* Build configuration list for PBXNativeTarget "SnapKit" */; + buildConfigurationList = 9E4196CFA8812083973C756D405439CE /* Build configuration list for PBXNativeTarget "SnapKit" */; buildPhases = ( - FDF930B0AC649D7D0C762D73710CB9EE /* Headers */, - 31E7C626BF4CBF6351C306F9BED25765 /* Sources */, - A8ADF33814FF0B23DBED6ED0B87125B5 /* Frameworks */, - D4AAF1FB7417F68DA0B421E556AB25AD /* Resources */, + CD9C4BFCB8CA7DF04B716BF3CF57DDCA /* Headers */, + C3B8C9C3E670F432088AFC01765CDA60 /* Sources */, + B1079564B4F05764E0390AB29473621E /* Frameworks */, + 54C1A5A54C4B5CCE4F5B8565BB1FBBAE /* Resources */, ); buildRules = ( ); dependencies = ( - F2B0F6EE052F080271AEAFFE90B26584 /* PBXTargetDependency */, + 79C767A041874113D95C87515FD8FFBB /* PBXTargetDependency */, ); name = SnapKit; productName = SnapKit; @@ -1892,12 +2117,12 @@ }; 8619D5ADECF2B26CFD9A9826D61D289A /* SSAlertSwift */ = { isa = PBXNativeTarget; - buildConfigurationList = 4FF3470542147C289B711C20EF27792C /* Build configuration list for PBXNativeTarget "SSAlertSwift" */; + buildConfigurationList = D216B4C9C1EB4572666F55DEAC1E9CA1 /* Build configuration list for PBXNativeTarget "SSAlertSwift" */; buildPhases = ( - 894BFED04DDC330581AD10C7E6D852BB /* Headers */, - F8DC34B39FD5C083F90D263320E44F0E /* Sources */, - FAB45208492061B3E5832F229C3ECE28 /* Frameworks */, - 976D116B6B0E0AD17D3D6C5A142AD725 /* Resources */, + 34A6CF459BA72007E08489B574B5F8E6 /* Headers */, + 136703804E83202294E6BC7CCC007F8C /* Sources */, + D45D6255F3CEDAD6DEA0624E59C43096 /* Frameworks */, + 92D5CBA64CED2E01EC09E6047F2BAE24 /* Resources */, ); buildRules = ( ); @@ -1920,12 +2145,12 @@ buildRules = ( ); dependencies = ( - 020F0A0B69436E99808F13904976F379 /* PBXTargetDependency */, - 715597FB3F1E0E95F8A3F4B3271288A7 /* PBXTargetDependency */, - 2AC1DEEE4B5E32E1FA841E25B3D2FDD4 /* PBXTargetDependency */, - 4F9D9C658D922102865227C060F26475 /* PBXTargetDependency */, - 2771AC7F4AA9CF4C86A3D614D77F33CD /* PBXTargetDependency */, - 95AAA3C6A552559016545CAD5B50333C /* PBXTargetDependency */, + 72E48F15033CCF95398C27EBDD634478 /* PBXTargetDependency */, + E2AA5E04C0D28740D6B40D3F0CFD237B /* PBXTargetDependency */, + 1027BF5D9BE9ECB1B97EDFFEBDDF4B46 /* PBXTargetDependency */, + 5308D4674A93AF4C1261B8B667E85038 /* PBXTargetDependency */, + C3E83C94AC96087258AD38AFA3A7F7D6 /* PBXTargetDependency */, + B49B8B5B0940A82855A49049BD54424B /* PBXTargetDependency */, ); name = "Pods-ReadViewDemo"; productName = Pods_ReadViewDemo; @@ -1934,11 +2159,11 @@ }; 8A8DB685241263AFDF5E6B20FE67B93A /* SnapKit-SnapKit_Privacy */ = { isa = PBXNativeTarget; - buildConfigurationList = F05CB5E657DCFC1458E4475B7909467D /* Build configuration list for PBXNativeTarget "SnapKit-SnapKit_Privacy" */; + buildConfigurationList = 91423BFD48D44DAFE716B4DA47D303CA /* Build configuration list for PBXNativeTarget "SnapKit-SnapKit_Privacy" */; buildPhases = ( - BD23672C3C7E57623DDEEBBDF147F41C /* Sources */, - 79526559ADAB3441AA19B5D001BBED4B /* Frameworks */, - B2D977C0921F825C2D1AA2F600EE603E /* Resources */, + 9518653016D8243678367F2922A236EC /* Sources */, + B5F6669E35F9EDE5C335ECB6992286D4 /* Frameworks */, + B261D0E343DA2B67FFE08CD0DC80A8A4 /* Resources */, ); buildRules = ( ); @@ -1951,12 +2176,12 @@ }; 8F6E5A5BF72D62CDFD25F91A7CFA3309 /* DTFoundation */ = { isa = PBXNativeTarget; - buildConfigurationList = 5BD9E11EEAC07E694B8A86BF10ECDE00 /* Build configuration list for PBXNativeTarget "DTFoundation" */; + buildConfigurationList = 693BA49EA4876BA40D36F688DAD8D7BF /* Build configuration list for PBXNativeTarget "DTFoundation" */; buildPhases = ( - 4C9C4A053FEC3DEEEA6841DDB13465DD /* Headers */, - AC980E6A0BED2BC61EBDA417E64924C2 /* Sources */, - D8A394261F2CD1CE7076622CB7CBEA95 /* Frameworks */, - 1E351406FB9FFE0E85F762D25B8BC155 /* Resources */, + 4171195ED1E9280FD3E4BA5F78CC2311 /* Headers */, + A522E405BDB282804B667EBF296C09E7 /* Sources */, + 6ACDB35CBA9857418F5EA7E98BD92808 /* Frameworks */, + F07CE27A09EC4341775D7D7AA24D00B3 /* Resources */, ); buildRules = ( ); @@ -1969,17 +2194,17 @@ }; AA15C8469D67684160CC2A7098EB841C /* ZIPFoundation */ = { isa = PBXNativeTarget; - buildConfigurationList = 30520D73A57B5DC231BF4D593A3E1FC0 /* Build configuration list for PBXNativeTarget "ZIPFoundation" */; + buildConfigurationList = 5DA67BEF5F46ECF4C32ACCFE78FCEF84 /* Build configuration list for PBXNativeTarget "ZIPFoundation" */; buildPhases = ( - DE7522D7BD4EDC854C005F9E164F7422 /* Headers */, - 8E8E3D911D9107530D85E2A57EA135DE /* Sources */, - 4D5098EB3DCA949E688884A176837775 /* Frameworks */, - 0DE75912EA2B3E3D57F61F203610A360 /* Resources */, + 5D0FD1429CBDECE5A0750FDD9C003FFD /* Headers */, + FE207FF2A3BE8F6502C76C63584668D5 /* Sources */, + 9B1811D8A0826DD2CCA0B0A05A3E4486 /* Frameworks */, + C5436E06F170164ADB98DD88938C2651 /* Resources */, ); buildRules = ( ); dependencies = ( - 2116DF18700ED27036D60C0892EDD986 /* PBXTargetDependency */, + 5ABBCDAACF3D91809C05806C33121BBC /* PBXTargetDependency */, ); name = ZIPFoundation; productName = ZIPFoundation; @@ -1988,21 +2213,21 @@ }; AA2E57587AA8EECA63C4BE08EA3CB6D2 /* RDReaderView */ = { isa = PBXNativeTarget; - buildConfigurationList = DC8655349C54538232C4CB81B137860C /* Build configuration list for PBXNativeTarget "RDReaderView" */; + buildConfigurationList = 73B0667287C7357BD7E64D54FC10E7CA /* Build configuration list for PBXNativeTarget "RDReaderView" */; buildPhases = ( - 24479202F4DAEA9BF631CC781DC01B4A /* Headers */, - ABB1B32F91B263FB2E62673C0B647044 /* Sources */, - B7AB287FB809BC6EE97D2CE702B7D742 /* Frameworks */, - 4A5F464DEADCA4865A6F0EC153A2E8FE /* Resources */, + 84AB3C42692480DA8A73D2EDF29029AA /* Headers */, + 5A715F992DB5A4CBF5EA14986468652B /* Sources */, + 85B8A5489B0E61482745C19DC7F2037E /* Frameworks */, + 1F7B3183D3A473148A98C90378860F91 /* Resources */, ); buildRules = ( ); dependencies = ( - 5E87F0E1B9AF330712D0384388AFA603 /* PBXTargetDependency */, - 3C71D3E681036CA8EEF99BD3B1B33169 /* PBXTargetDependency */, - 0FD34BEAFBBC108E029BD172F1A001AC /* PBXTargetDependency */, - 1C87262B71402CF8076F136C711B3B87 /* PBXTargetDependency */, - 48FD93EF0BB78DB9428C4189B580A86D /* PBXTargetDependency */, + 9561148C38876D85CE97DA5473FE5CD1 /* PBXTargetDependency */, + BDB2E3ACF2EDD090E479F5F13928DEFD /* PBXTargetDependency */, + 18B19C677B889F6727DBDA0F865BF1AB /* PBXTargetDependency */, + 17938577AC1DECCDD4044365CD98BEC2 /* PBXTargetDependency */, + 6087769AB1CAFBD7015BB5E5FB531C1C /* PBXTargetDependency */, ); name = RDReaderView; productName = RDReaderView; @@ -2011,11 +2236,11 @@ }; AE7F393FB7805DE2664AB4111873F907 /* RDReaderView-RDReaderViewAssets */ = { isa = PBXNativeTarget; - buildConfigurationList = 1CA7C9D5DFD7044C7A36338BE2AC73C5 /* Build configuration list for PBXNativeTarget "RDReaderView-RDReaderViewAssets" */; + buildConfigurationList = 84B6961CC1F169E2D6D5FC7861A8582E /* Build configuration list for PBXNativeTarget "RDReaderView-RDReaderViewAssets" */; buildPhases = ( - 4BA48F20A933ADB499C30AFEF782AAF0 /* Sources */, - B1998251DB29113903A1C1519FCDA773 /* Frameworks */, - 3C6E2B57EC3F11B635842B368CE854EF /* Resources */, + 89A30FE57CA1ED2B9D8B021F0F206ED4 /* Sources */, + BD50AEAFDACDDF335896EACA81D8ADEB /* Frameworks */, + 5A5FE7130469EBCA4D546CBC42EAA4BB /* Resources */, ); buildRules = ( ); @@ -2038,8 +2263,8 @@ buildRules = ( ); dependencies = ( - 43C7FA9296E0A69B39E9CEDD513DB5E3 /* PBXTargetDependency */, - BAEA1865D980379B2B1067919B5DC4F9 /* PBXTargetDependency */, + 463CDA137195F14051A84236470C93F2 /* PBXTargetDependency */, + FD3219BCD8F392FD503D4E3C397FFF1B /* PBXTargetDependency */, ); name = DTCoreText; productName = DTCoreText; @@ -2048,11 +2273,11 @@ }; C7A8D82E407CD3FDC3BA55CEE519B252 /* ZIPFoundation-ZIPFoundation_Privacy */ = { isa = PBXNativeTarget; - buildConfigurationList = 470D80E3313FE7E739AE9D34641F2DB4 /* Build configuration list for PBXNativeTarget "ZIPFoundation-ZIPFoundation_Privacy" */; + buildConfigurationList = D280590029F64DDED598A210724CF31E /* Build configuration list for PBXNativeTarget "ZIPFoundation-ZIPFoundation_Privacy" */; buildPhases = ( - F96FFD5113D092836D4AD41A4F201A88 /* Sources */, - FF20913263E229616A291202A88A5304 /* Frameworks */, - CFE61AF525405B7385CFFF1FE43E0DA6 /* Resources */, + FAFBEB5C156B9C7A1B4A93303133A682 /* Sources */, + 504AA8F5440FC78EE91E2B72C8A465E3 /* Frameworks */, + 0DF1717C86492B32F0404A3DB682D4DB /* Resources */, ); buildRules = ( ); @@ -2103,18 +2328,19 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 0DE75912EA2B3E3D57F61F203610A360 /* Resources */ = { + 0DF1717C86492B32F0404A3DB682D4DB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - F2376D242202DBE8F1E8ED36A43FB5D3 /* ZIPFoundation-ZIPFoundation_Privacy in Resources */, + 636106852946C6686CE248B0722AE689 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 1E351406FB9FFE0E85F762D25B8BC155 /* Resources */ = { + 1F7B3183D3A473148A98C90378860F91 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B96925D97DBA16A527D4FB87673AFF4E /* RDReaderView-RDReaderViewAssets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2126,36 +2352,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 3C6E2B57EC3F11B635842B368CE854EF /* Resources */ = { + 54C1A5A54C4B5CCE4F5B8565BB1FBBAE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 90F6FF9B3DA53C4B6C2D2FC588AA385E /* cssInjector.js in Resources */, - D7159CF29AB1F8821F9D68A1412B0BD7 /* epub-bridge.js in Resources */, - CDA8EE4B287129954949825C50EBD3EB /* epub-fixed-layout.html in Resources */, - BE268016D8D21C191A2E82ED75A1F686 /* rangy-core.js in Resources */, - D275D1EAFB36E5273169E7A4417E1219 /* rangy-serializer.js in Resources */, - F9D7FD379D25CE2B854236C1051D4B1C /* WeReadApi.js in Resources */, - 1DB7CE877F9241539B365C9FA1D26217 /* wxread-dark.css in Resources */, - 84BEA3D5956D75FED777C15736EE787A /* wxread-default.css in Resources */, - 072E9480C779CFF4764E08D82F4B67CF /* wxread-replace.css in Resources */, - 8B81B840ED3E5B4E15887391DD7E5B87 /* wxread-replace-latin.css in Resources */, + AA2ADBCF2640D8FF5BD8D071271284E2 /* SnapKit-SnapKit_Privacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 4A5F464DEADCA4865A6F0EC153A2E8FE /* Resources */ = { + 5A5FE7130469EBCA4D546CBC42EAA4BB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2D271CB0ADE6190C0835F90BC684CF0F /* RDReaderView-RDReaderViewAssets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 567034FDEEAB950DD14247A5BD72A079 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - AAFA34024F80407B268712A30687AE65 /* default.css in Resources */, + 3CCC8FC7FF95E7A988B0F0B0468C12BF /* cssInjector.js in Resources */, + FD6F8A543C3241B08E21FBBD47911F6D /* epub-bridge.js in Resources */, + 38F8914C748138F46D406222CA4E85AC /* epub-fixed-layout.html in Resources */, + F677C12701262AFF0F6FCCE4C2150CF9 /* rangy-core.js in Resources */, + F5142E93B76DB1917D507EECB36E5DED /* rangy-serializer.js in Resources */, + 19F07715D355FB1869128B129D015C90 /* WeReadApi.js in Resources */, + A118C8C3DDFA7F01BD8CD0BD57C74098 /* wxread-dark.css in Resources */, + 1163FCAE7A2F60099BF3A9675E47E8E2 /* wxread-default.css in Resources */, + 4FFAECEE5100543DF0EE4113D9973AEC /* wxread-replace.css in Resources */, + 8250FB5F79016BDCDD60AD731F0BD350 /* wxread-replace-latin.css in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2166,89 +2384,197 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 976D116B6B0E0AD17D3D6C5A142AD725 /* Resources */ = { + 92D5CBA64CED2E01EC09E6047F2BAE24 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - B2D977C0921F825C2D1AA2F600EE603E /* Resources */ = { + 9AC7661DA4E67875FBDF24E284ABD99E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 6E390B8259A9728AAA7EA8B864179418 /* PrivacyInfo.xcprivacy in Resources */, + 9504C9FFDCBBF6F619C349CB657093A1 /* default.css in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - CFE61AF525405B7385CFFF1FE43E0DA6 /* Resources */ = { + B261D0E343DA2B67FFE08CD0DC80A8A4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - AE25127DC32DB2C11CF9136EC7E484E8 /* PrivacyInfo.xcprivacy in Resources */, + 59272D5476F27119EF257930FE3D4722 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - D4AAF1FB7417F68DA0B421E556AB25AD /* Resources */ = { + C5436E06F170164ADB98DD88938C2651 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD5647950937A07ACA022C75387CF527 /* ZIPFoundation-ZIPFoundation_Privacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F07CE27A09EC4341775D7D7AA24D00B3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78000F5A9639B1D9577CDD05682454A5 /* SnapKit-SnapKit_Privacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 31E7C626BF4CBF6351C306F9BED25765 /* Sources */ = { + 0C2FD0281726C9DFE0D55D11BA576B8C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4FF142B3F09AA12AF220CB09D92F37D3 /* Constraint.swift in Sources */, - DD75D7D90DE6DB0FC4722C6A267B173F /* ConstraintAttributes.swift in Sources */, - 6D8994F32576C73ECE67B2A7BC0DD084 /* ConstraintConfig.swift in Sources */, - 8E9C2D9C827EA566D22F5E1131F794A9 /* ConstraintConstantTarget.swift in Sources */, - 8BE4E5EF4D6B2F597605AE93CD3452D4 /* ConstraintDescription.swift in Sources */, - 8B35034FEA4599E7AC24215B10A2CC54 /* ConstraintDirectionalInsets.swift in Sources */, - F7B3249FC969AB2C684558D696807F42 /* ConstraintDirectionalInsetTarget.swift in Sources */, - 3E10312523D443840CFBA72845ACCED9 /* ConstraintDSL.swift in Sources */, - 3F8EDE1AE8937FC335704E3F15C7C0B5 /* ConstraintInsets.swift in Sources */, - C341EF847BE944F959495C8145AE915D /* ConstraintInsetTarget.swift in Sources */, - 50526E23A8AC98FB5502D6E22758A56D /* ConstraintItem.swift in Sources */, - A3FF25BFBC5DBC01FB99578E25B361E7 /* ConstraintLayoutGuide.swift in Sources */, - 23C510B1C699288E3E15795A6C0835DB /* ConstraintLayoutGuide+Extensions.swift in Sources */, - AAAC44275D726EADF484E946691E09EF /* ConstraintLayoutGuideDSL.swift in Sources */, - 0279DC0CB56DFD659743755DF5932E28 /* ConstraintLayoutSupport.swift in Sources */, - 9E8DCCE70D492157FC50318D42742D5F /* ConstraintLayoutSupportDSL.swift in Sources */, - 3FA49154704A5A890D4CF70CA5C57665 /* ConstraintMaker.swift in Sources */, - 7C046A2B1C3E09420711F10C0B5FED3D /* ConstraintMakerEditable.swift in Sources */, - 10891A3745ED879C322F4EAFB6A24F36 /* ConstraintMakerExtendable.swift in Sources */, - 273EEB3E0A49E67E561DF3C5EE3E67C2 /* ConstraintMakerFinalizable.swift in Sources */, - 36C2DB50F6A31A36A860540B4EF32E0F /* ConstraintMakerPrioritizable.swift in Sources */, - 94A20C3CAA5381176EFE5636D8ECFFD3 /* ConstraintMakerRelatable.swift in Sources */, - F1E8826F0590E0CC1F5A0128D99D7AA3 /* ConstraintMakerRelatable+Extensions.swift in Sources */, - BFF4E73F7207C5C28837D4A6684055B1 /* ConstraintMultiplierTarget.swift in Sources */, - 68E20F956433EA0CEDAC3948339C74E1 /* ConstraintOffsetTarget.swift in Sources */, - 3E8AFCDE8242393816FB4CFCA5048FCA /* ConstraintPriority.swift in Sources */, - 3B7E31D804C0208ECAEF8630D8CE6E8C /* ConstraintPriorityTarget.swift in Sources */, - AA10BB78D6DCE8229EB4E146BCC9105A /* ConstraintRelatableTarget.swift in Sources */, - 3821BB7D496697C9F3CA8E00DD53115A /* ConstraintRelation.swift in Sources */, - 04AC4C71A95C08C9A1158AEA6B5B04B5 /* ConstraintView.swift in Sources */, - F675FAE700CEDACDC117FD78CBC91B9E /* ConstraintView+Extensions.swift in Sources */, - 8EC243629BE685783F8B3E64FA924738 /* ConstraintViewDSL.swift in Sources */, - DD4857F182195748AB22953A3AD5CFBC /* Debugging.swift in Sources */, - 6B22BC159183EB1F45E0EA101C69982A /* LayoutConstraint.swift in Sources */, - 52C2803D19934E29EC31F21B0808F3BC /* LayoutConstraintItem.swift in Sources */, - 3139FB597E4CA2C87278EAB2944CFD31 /* SnapKit-dummy.m in Sources */, - 2E477BA16E25771C05B852318212C523 /* Typealiases.swift in Sources */, - F045CE2C2618CE50C24CD7337B7C69A3 /* UILayoutSupport+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 4BA48F20A933ADB499C30AFEF782AAF0 /* Sources */ = { + 136703804E83202294E6BC7CCC007F8C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 68B185C109A9E448C95B2153715DBF41 /* SSAlertAnimation.swift in Sources */, + 571B611A9DE148C57AF2C7EA53857C76 /* SSAlertAnimationController.swift in Sources */, + F367BD50B50B680A2DA23005EAC2A7BD /* SSAlertCommonView.swift in Sources */, + 2C4050D8A87BF13B3029E82DF0D712C5 /* SSAlertDefaultAnmation.swift in Sources */, + 6F6EE18D7F839AB238A8B47527737580 /* SSAlertPresentAnimation.swift in Sources */, + A7F239422F3768F1B4B3A696DD485D3B /* SSAlertSwift-dummy.m in Sources */, + F6E6CAC7A3C273A7C4227BD6F0E36EE1 /* SSAlertView.swift in Sources */, + AFCBCBAE8B0C6A6EF6A6832A7213DC0D /* SSAlertViewExtention.swift in Sources */, + B18755975A9D04D3DB2732447D98CCB4 /* UIViewFrameExtension.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5A715F992DB5A4CBF5EA14986468652B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F638D941A6B83EE3D0FA3116BFE9CE52 /* RDEPUBAnnotationModels.swift in Sources */, + C75B12810764BCA70B81F9A3C4A562C1 /* RDEPUBAssetRepository.swift in Sources */, + B4F34BAC839565D3D79B44FD9930560F /* RDEPUBAttachmentNormalizer.swift in Sources */, + BC7BDDC881DED4124CD0C95A9B170A30 /* RDEPUBBuildDiagnosticsReporter.swift in Sources */, + 8E1EFA605CED65BF0D98CD48AD0DB08B /* RDEPUBChapterData.swift in Sources */, + C078EB1EF2AD0AB675C64A3B920CDE8D /* RDEPUBChapterPageCounter.swift in Sources */, + 3AF7CE34BE9312F7027DC4763B33C4EB /* RDEPUBChapterTailNormalizer.swift in Sources */, + 5894AF8B6F6022A84E3FB6F22494A364 /* RDEPUBCoreTextPageFrameFactory.swift in Sources */, + CA0C55A7CB67DBA313171CF5AF49A349 /* RDEPUBDTCoreTextRenderer.swift in Sources */, + F8D652BDA6DF2BCEECB48009D34C6834 /* RDEPUBFixedLayoutTemplate.swift in Sources */, + 973D5F77AA6D4F77CEF20353736DA403 /* RDEPUBFontNormalizer.swift in Sources */, + D5F33E813692FC371616921D7C4AD1FF /* RDEPUBFragmentMarkerInjector.swift in Sources */, + 416D5AD659DCF761E28F76F7E23B9F7F /* RDEPUBHTMLNormalizer.swift in Sources */, + AC787FA868EAD10F9929927B2AEBD247 /* RDEPUBJavaScriptBridge.swift in Sources */, + 53D8E4E9570BCD825E00CDD8A881054E /* RDEPUBModels.swift in Sources */, + 012337AC75CC2DE92285DA91F07B1CC5 /* RDEPUBNavigatorLayoutContext.swift in Sources */, + A8BDFF208A28D6E52EDD9138290A3708 /* RDEPUBNavigatorState.swift in Sources */, + C37E74CF4A213BB3E7AE31B676812BD9 /* RDEPUBPageBreakPolicy.swift in Sources */, + 96AB626B9C20C3A6512835EC71A4F9FD /* RDEPUBPageInteractionController.swift in Sources */, + DF39E502984126B7FFA216FC647FDB02 /* RDEPUBPageLayoutSnapshot.swift in Sources */, + 18EE808287AF9003259B828F3C4A6B3F /* RDEPUBPaginationCacheCoordinator.swift in Sources */, + 58B093EF9A7EA0373AD770DC0939D925 /* RDEPUBPaginationModels.swift in Sources */, + D1B22560078A0D3A65D0427B3A0A38DF /* RDEPUBPaginator.swift in Sources */, + ABB8DE00DCF34B2ED79C27585E97D5FB /* RDEPUBParser.swift in Sources */, + EA42097748117159D15207C9323ABDED /* RDEPUBParser+Archive.swift in Sources */, + 7A7F0DE40D8B77129FC1E7FB54D79F7C /* RDEPUBParser+Package.swift in Sources */, + DD098AE376D237EBCD0602DC493FD49E /* RDEPUBParser+ReadingProfile.swift in Sources */, + 0220B94FD97D36708E40E33DB89C76C8 /* RDEPUBParser+Resources.swift in Sources */, + D26C59ED1011B3D0A364E09AD764683C /* RDEPUBParser+TOC.swift in Sources */, + 7CB2AB8578F4A18DAEC3D738EC46F13A /* RDEPUBPreferences.swift in Sources */, + 73C9F968338A5A64F77CC8E763D05AB5 /* RDEPUBPublication.swift in Sources */, + 4432641D03459CFA21386E96E69D470A /* RDEPUBReaderAnnotationCoordinator.swift in Sources */, + 53E8737EF21A600E0BFCD6F5DE467C5F /* RDEPUBReaderAssemblyCoordinator.swift in Sources */, + 1A35329A88FF592894E7396831A4C88B /* RDEPUBReaderBottomToolView.swift in Sources */, + 88A854FEBA21BB5C77868E663A7C4346 /* RDEPUBReaderChapterListController.swift in Sources */, + 54B9DDFA2E91F83BB5C9E2F5722884EE /* RDEPUBReaderChromeCoordinator.swift in Sources */, + 2A8AC1184CB8CD9ECA5115B5DB902490 /* RDEPUBReaderConfiguration.swift in Sources */, + 90C2D02582043D4CF91BDAD5FB0BC344 /* RDEPUBReaderContext.swift in Sources */, + 767F49D5DE536CA0332B88DAD52D3A5F /* RDEPUBReaderController.swift in Sources */, + E8C0D5929CEA330C30F63AA5502723DD /* RDEPUBReaderController+ContentDelegates.swift in Sources */, + A8F6AFC87576314D31AC08581890E4CD /* RDEPUBReaderController+DataSource.swift in Sources */, + 466AB83B99254DDBDF7ED5730D665FEC /* RDEPUBReaderController+PublicAPI.swift in Sources */, + C107081396894ED1035195055E1D03D4 /* RDEPUBReaderController+RenderSupport.swift in Sources */, + 0B834BEE7479E0F78E3A7D8235A2EC95 /* RDEPUBReaderController+RuntimeBridge.swift in Sources */, + 197770F407C11D14237B98158E119C55 /* RDEPUBReaderController+TableOfContents.swift in Sources */, + 63A8D8CEE40C56246D84EB59FCC78AF6 /* RDEPUBReaderDelegate.swift in Sources */, + EA53228A41C6502A614D69F15A3B408A /* RDEPUBReaderDependencies.swift in Sources */, + A115BC82073566E4B9E4901AD77705B4 /* RDEPUBReaderHighlightsViewController.swift in Sources */, + A0C7E2ACBEA4D3103EB06870D860E2BE /* RDEPUBReaderLoadCoordinator.swift in Sources */, + 699FFBEC77DD1CC3A54701AFC65FB2D0 /* RDEPUBReaderLocationCoordinator.swift in Sources */, + AE2EA185BE47AA7636E4F927C1B220C7 /* RDEPUBReaderPaginationCoordinator.swift in Sources */, + 7877D94D10C10C9B7AC335B0A5864575 /* RDEPUBReaderPersistence.swift in Sources */, + EBDF8A9891119113AA5CA45851DBD1B7 /* RDEPUBReaderRuntime.swift in Sources */, + BCB700C605AF5689D526F24D02B97BFA /* RDEPUBReaderSearchCoordinator.swift in Sources */, + 07C1A54BF3FAE709C8CD683DC9199B17 /* RDEPUBReaderSettings.swift in Sources */, + 4626D775FF8050FC1B85AED719C6CCD3 /* RDEPUBReaderSettingsViewController.swift in Sources */, + 22AEC3422EFA234293F90AB3738CE694 /* RDEPUBReaderTableOfContentsItem.swift in Sources */, + B2065C3E8DA14CE92675506BBC1A71F2 /* RDEPUBReaderTheme.swift in Sources */, + B686B485ACD5C39FEA878C32F4741AD9 /* RDEPUBReaderToolView.swift in Sources */, + 30566C35A1D4F14A6A128B570E061108 /* RDEPUBReaderTopToolView.swift in Sources */, + D39C3766E7B664581FAD2688E295BAB3 /* RDEPUBReaderViewportMonitor.swift in Sources */, + E005DCCFE4D00062C0593F16CB0FB7C9 /* RDEPUBReadingLocationModels.swift in Sources */, + 9476BAD56597A02E230B9E4F8F6D306E /* RDEPUBReadingSession.swift in Sources */, + A6C184213CB62062887604E95FCEC014 /* RDEPUBRenderDiagnosticsCollector.swift in Sources */, + 35A8A9292DCF25CF8E47462B12D246EA /* RDEPUBRenderRequest.swift in Sources */, + 441269EE3B2065102DE8D9F0D7EB461B /* RDEPUBResourceResolver.swift in Sources */, + 7C026FA594F06339EAAEB774A776A030 /* RDEPUBResourceURLSchemeHandler.swift in Sources */, + 4D00F4DD1B8D720FFA715756C7D70D40 /* RDEPUBSearchEngine.swift in Sources */, + 53240F8A0E98A48B59C6B2FA88270310 /* RDEPUBSearchModels.swift in Sources */, + 22FD1F39B4B94E9E33FDDF0C6880AA53 /* RDEPUBSelectableTextView.swift in Sources */, + 44B918236776349CDE1D85C70EDD7328 /* RDEPUBSelectionOverlayView.swift in Sources */, + F77B604D4BCCE6CBE8520E588343F0A1 /* RDEPUBSemanticMarkerInjector.swift in Sources */, + 0A37B6C1B26290A715DB47A6D750293D /* RDEPUBStyleSheetBuilder.swift in Sources */, + DC5C1FECCEB131BAD26561FEBE22745C /* RDEPUBStyleSheetComposer.swift in Sources */, + BE67CF1248E014FD0397C32C8EC6D868 /* RDEPUBTextAnchor.swift in Sources */, + 92594DF73D4E65DE3A257494085A7FA4 /* RDEPUBTextAnnotationOverlay.swift in Sources */, + 597A2991F0147098A1A5F3555E7A4433 /* RDEPUBTextBookBuilder.swift in Sources */, + 8483273E622F869454330F71D3824098 /* RDEPUBTextBookCache.swift in Sources */, + 5DCA3B311B4E46F3E96BF3D73119EB86 /* RDEPUBTextBookModels.swift in Sources */, + 21A7DDFC2A430E9105419DF3DC2D4AA5 /* RDEPUBTextBuildPipelineInterfaces.swift in Sources */, + 3AD8BF0401CF9D849805A1E3A57C5399 /* RDEPUBTextContentView.swift in Sources */, + 67FC2E8BF2E51B5AEBE73C81BE0375C6 /* RDEPUBTextIndexTable.swift in Sources */, + EA3DDFFEE504AA55AD089305ACA3D763 /* RDEPUBTextLayouter.swift in Sources */, + C10B9F0291FFE41EBE7AF0CE975B3EB8 /* RDEPUBTextLayoutFrame.swift in Sources */, + 4B520B6C488D585307F8B6A48DC788B6 /* RDEPUBTextPageDecorationView.swift in Sources */, + 48A82D1D9C23DC22512E4005270631D8 /* RDEPUBTextPageRenderView.swift in Sources */, + A43A87703C16488B17C5871E7398371B /* RDEPUBTextPaginationInterfaces.swift in Sources */, + A9BDCA1E1104F38F9823FF2F9D06FE7E /* RDEPUBTextPaginationSupport.swift in Sources */, + 59FB4EE13B3168CADAAF65DC27802EFA /* RDEPUBTextPerformanceSampler.swift in Sources */, + A3045667F74EFACBD3F29F46BFB4EBC0 /* RDEPUBTextPositionConverter.swift in Sources */, + 308F8ADD0B8367CBD99AA8781FF1B580 /* RDEPUBTextRenderer.swift in Sources */, + 3F9097BE7A6CA36DA12224F99D0D37BF /* RDEPUBTextRendererSupport.swift in Sources */, + 912F0C755DC31C3010FB5700F94193DD /* RDEPUBTextSearchEngine.swift in Sources */, + CFCACF1D9A6846A81C7F50C8E6A23F70 /* RDEPUBTextSelectionController.swift in Sources */, + E0A64565947C7016CD15A87A29C06FE1 /* RDEPUBTypesettingPipeline.swift in Sources */, + 12E1D9F1AA8C4E17DC54886C3CE410C2 /* RDEPUBViewportTypes.swift in Sources */, + 36B1D51E18199A4B80157CED6D97C104 /* RDEPUBWebContentView.swift in Sources */, + D77ADB50A40D2C35FAA905E24838929B /* RDEPUBWebDecorationOverlayView.swift in Sources */, + 3238A1DC9C7C19F477F62081CEB1CDF6 /* RDEPUBWebView.swift in Sources */, + 69A2C721B126BE26E4BBAC1DF63858BB /* RDEPUBWebView+Configuration.swift in Sources */, + 4D1B1254D155186FEE0796901E729627 /* RDEPUBWebView+FixedLayout.swift in Sources */, + 077A7464B5BCC77F710DC721FF09B515 /* RDEPUBWebView+JavaScriptBridge.swift in Sources */, + D61D512B689DBF87FCF457C01594871A /* RDEPUBWebView+Reflowable.swift in Sources */, + EF09C17C1A9226B1D3377A4186237715 /* RDEPUBWebView+Search.swift in Sources */, + 023CD43B36E7FC2AEEB3E546121AD89F /* RDEPUBWebViewDebug.swift in Sources */, + B85FE04FC7958BAB89406AD289D1D018 /* RDPlainTextBookBuilder.swift in Sources */, + D031693A1CC970AC82EF0835E9DAD65D /* RDReaderContentCell.swift in Sources */, + CE5C346EF640EA27FE463DBC8D1E1C88 /* RDReaderFlowLayout.swift in Sources */, + 600D8BCA0F833306A821AE9963A2705F /* RDReaderGestureController.swift in Sources */, + 855020CBFB65BB877255A61BAFFF7245 /* RDReaderPageChildViewController.swift in Sources */, + 8F8C9BEEF983EFFB9D38B5A7C8BA5518 /* RDReaderPagingController.swift in Sources */, + 0DC397CC2F255374FD7DD584D742622E /* RDReaderPreloadController.swift in Sources */, + 3349AC669F17F9DE48E0CBEF213C71F7 /* RDReaderSpreadResolver.swift in Sources */, + 552B25799A8AAD599BFAB1C6C3B4D454 /* RDReaderTapRegionHandler.swift in Sources */, + 48B2080D6CD3A7C757A1F69635D1446E /* RDReaderView.swift in Sources */, + AD3F24E834B5215C632C984EFD959D10 /* RDReaderView+CollectionView.swift in Sources */, + 31EFA2212FA286AB73DE8ADE65F32224 /* RDReaderView+ContentAccess.swift in Sources */, + B678CD12611B4C83E3AF88FF80B8D092 /* RDReaderView+PageCurl.swift in Sources */, + 5C08CE24A3901A3E0F8B090F71A5871B /* RDReaderView+ToolView.swift in Sources */, + 44688B5127920D2151DD116BB8709AE0 /* RDReaderView-dummy.m in Sources */, + 755E82D4E33E3D24EBA3F426A58961D9 /* RDReaderViewProtocols.swift in Sources */, + 488A6649575C6F4DB55809630C3218B1 /* RDURLReaderController.swift in Sources */, + 7224AF6EDE2F24361DFAC2B54ADA9FE4 /* UIColor+RDEPUBHex.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2260,157 +2586,101 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8E8E3D911D9107530D85E2A57EA135DE /* Sources */ = { + 89A30FE57CA1ED2B9D8B021F0F206ED4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 25E47E6CE8DF5BE732774ACEAE47BBF8 /* Archive.swift in Sources */, - 2A875BEC32568EFA0FE0F3E58F0AA37C /* Archive+BackingConfiguration.swift in Sources */, - DCD33DEBC59EAAB25D3B3F34EA206133 /* Archive+Deprecated.swift in Sources */, - 5E7FDA93607BDB800512A93BE294B12B /* Archive+Helpers.swift in Sources */, - 06675C3F0CB6A5662061DB48B55B7DF3 /* Archive+MemoryFile.swift in Sources */, - 2E8C5F61C3B6A6D116BE6612C62C7370 /* Archive+Progress.swift in Sources */, - 6268A3E8B23E586AE603A5F40F40F277 /* Archive+Reading.swift in Sources */, - 23EA3FF5A4302C09BE16C4F0EFEBF0D3 /* Archive+ReadingDeprecated.swift in Sources */, - 3257F2C96D06EA0B53E9CA9610A9137A /* Archive+Writing.swift in Sources */, - FE80BC2F78769407CABD426D8569A855 /* Archive+WritingDeprecated.swift in Sources */, - A0C9C636444B262AFF4C7AC91A9FF591 /* Archive+ZIP64.swift in Sources */, - 789D40DEE2CC4E0CBDFC873B9E30FE3F /* Data+Compression.swift in Sources */, - F0561A69C416F2368A40D6BAF663B2E1 /* Data+CompressionDeprecated.swift in Sources */, - 8444C850D0A40CD0A8F32EF44197A6FF /* Data+Serialization.swift in Sources */, - 72599431CE239793C684C877B4D23347 /* Date+ZIP.swift in Sources */, - 9959EBA37505B2AACF77057A32B8F7F0 /* Entry.swift in Sources */, - 3CF8124B3C1C0C80E1784E221E564CC9 /* Entry+Serialization.swift in Sources */, - B9F9B1F8E6FE6D22AAD7C2D9B8E660BB /* Entry+ZIP64.swift in Sources */, - 81086493EE5742CF823461235F60AFD2 /* FileManager+ZIP.swift in Sources */, - 018208E7B5B1C22398872119C18BD05B /* FileManager+ZIPDeprecated.swift in Sources */, - 03B6C4C7DA368F0B8C15FAFD93358E87 /* URL+ZIP.swift in Sources */, - 4EEA1AF224E7966AC2BD3AD70903681A /* ZIPFoundation-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - ABB1B32F91B263FB2E62673C0B647044 /* Sources */ = { + 9518653016D8243678367F2922A236EC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9A57BBF6B1A6FEC692F444E92C508997 /* RDEPUBAssetRepository.swift in Sources */, - D47B1A18F9830B10F0BE5D3E7B13C13D /* RDEPUBChapterData.swift in Sources */, - 9FC9FAE8041266F3F85ACD5B0E1E7B17 /* RDEPUBDTCoreTextRenderer.swift in Sources */, - DD55868C1A308AE1C5D56E5E3FA0AE44 /* RDEPUBFixedLayoutTemplate.swift in Sources */, - 9BB5822506549A94B4A5A0B5F40ADC9A /* RDEPUBJavaScriptBridge.swift in Sources */, - AC6220500850D0DB8142B44396FCA6BC /* RDEPUBModels.swift in Sources */, - 456926E08F81E14D4462225F8219D7B6 /* RDEPUBNavigatorLayoutContext.swift in Sources */, - E0021B223A2E2D11206F395CCFA31A02 /* RDEPUBNavigatorState.swift in Sources */, - 7A8961D4D3F7C2D7141C3AF9B2E60F0E /* RDEPUBPageInteractionController.swift in Sources */, - 1AD6564F00D7B59D818A6AF35FCFDB3C /* RDEPUBPageLayoutSnapshot.swift in Sources */, - 372E05B50EF2FD40BBF07BB08DA32E4A /* RDEPUBPaginator.swift in Sources */, - 317C3FF6D87E75F29CD74E44342734B3 /* RDEPUBParser.swift in Sources */, - BBE7C12DD486B57DBAA98F86AC80994A /* RDEPUBParser+Archive.swift in Sources */, - E5926D0035C1FE12AD81A4441276CB8C /* RDEPUBParser+Package.swift in Sources */, - 41C4267B8D6E2C867C47F26F46116693 /* RDEPUBParser+ReadingProfile.swift in Sources */, - F6511F62871012278880F32E36DC1B20 /* RDEPUBParser+Resources.swift in Sources */, - 69F197DAC284EBCD47A972FB1D112707 /* RDEPUBParser+TOC.swift in Sources */, - B05D1543F8BF9AC00B000026C03067B9 /* RDEPUBPreferences.swift in Sources */, - B0BDB773F63288118EB1DE8DB294E53C /* RDEPUBPublication.swift in Sources */, - 8093FDC6F3C2D5F6250492A548EA73A2 /* RDEPUBReaderBottomToolView.swift in Sources */, - 8AB361C3413C66575600A33ED1EC699C /* RDEPUBReaderChapterListController.swift in Sources */, - D68CDB4A9AFFBBDE8383EB3B4D27D7C2 /* RDEPUBReaderConfiguration.swift in Sources */, - 12840AD82C5E66B9DAABE907647D0961 /* RDEPUBReaderController.swift in Sources */, - 6439A2CC45B255D86F1BBF560BE64F7B /* RDEPUBReaderDelegate.swift in Sources */, - F9F8A1BE8710CBCA9E2DEA97D35B17C0 /* RDEPUBReaderHighlightsViewController.swift in Sources */, - A33EB2CB8F875B027925A5CC9EEF8117 /* RDEPUBReaderPersistence.swift in Sources */, - 8548C2C333CE758AF37B85D03AE4CFB7 /* RDEPUBReaderSettings.swift in Sources */, - 713D20A253E4AA77903E15DA2A9BA37F /* RDEPUBReaderSettingsViewController.swift in Sources */, - 2EABBFC2F99A82EDEE15171F3F02126A /* RDEPUBReaderTableOfContentsItem.swift in Sources */, - 78B7944A0F9E1A14F2787D72C0133A91 /* RDEPUBReaderTheme.swift in Sources */, - 7BBEA8E422292F56BFCE3A04391EBE01 /* RDEPUBReaderToolView.swift in Sources */, - FD0BD9D4FCDB12FAC03FC94901DB81B6 /* RDEPUBReaderTopToolView.swift in Sources */, - 42B5874545A810ADB3F168784542C3D4 /* RDEPUBReadingModels.swift in Sources */, - 3EB94B3FE3C93EF534899C25C60E25EB /* RDEPUBReadingSession.swift in Sources */, - 782BAE4F5C0FA3E6EB6EF30D83DB8653 /* RDEPUBRenderRequest.swift in Sources */, - AF2E9051FD1821C3DC9C3AFBE674E0CF /* RDEPUBResourceResolver.swift in Sources */, - 07512D0A383FA3C65725DB0BE97A97A6 /* RDEPUBResourceURLSchemeHandler.swift in Sources */, - 4D981359F53E36D187F92A2EC9F23BEE /* RDEPUBSearchEngine.swift in Sources */, - 8999804F6563D2105144FA9D821F3166 /* RDEPUBSearchModels.swift in Sources */, - A323CECAFCDE8E4009A1E598D839BBD1 /* RDEPUBSelectionOverlayView.swift in Sources */, - F27B7E0ECDB515E97953535C268DFC0E /* RDEPUBStyleSheetBuilder.swift in Sources */, - C0CDCF140488BC1D63E83DBA74D30C2C /* RDEPUBTextAnchor.swift in Sources */, - C78E9E8C277379E5FA7E49DD9C3A730B /* RDEPUBTextBookBuilder.swift in Sources */, - CAA01BF56A0A87A5C9260A864755863F /* RDEPUBTextBookCache.swift in Sources */, - D2FC00885DA4EA4587ABF3384C82062B /* RDEPUBTextContentView.swift in Sources */, - FCDDEA00D578F661AEFF4B75CFF80B60 /* RDEPUBTextIndexTable.swift in Sources */, - 0BFC402A79CB21B8CF1A99D7258961F7 /* RDEPUBTextLayouter.swift in Sources */, - 1509118FA9E9AD657EAC9BEE5C49BE6E /* RDEPUBTextLayoutFrame.swift in Sources */, - BA714218310D9BC815D508D7EBA90089 /* RDEPUBTextPaginationSupport.swift in Sources */, - D8435365E01789A7EACBA963C1C92C6D /* RDEPUBTextPerformanceSampler.swift in Sources */, - 4AEC4012771A907108FD2D0C2237BCBD /* RDEPUBTextPositionConverter.swift in Sources */, - 83D0422FB78BED08AE09435CCB354F4D /* RDEPUBTextRenderer.swift in Sources */, - 96C233ED331A3F6A71D589160B7668F3 /* RDEPUBTextRendererSupport.swift in Sources */, - D4EAEB6CFB1CB5C3B2456D475B2A460F /* RDEPUBTextSearchEngine.swift in Sources */, - 7CAA2C10A02EE7F4E6AF65DCB7F3772B /* RDEPUBWebContentView.swift in Sources */, - 3CDE626DA9038F9143A6004836F6284E /* RDEPUBWebDecorationOverlayView.swift in Sources */, - 7CCE1F11430F490242DCFFC47DD434A2 /* RDEPUBWebView.swift in Sources */, - 5271068D32AFE88CAC78B59ADED07EDB /* RDEPUBWebView+Configuration.swift in Sources */, - 7C899BA1C93C579E0071B1C4AFF75521 /* RDEPUBWebView+FixedLayout.swift in Sources */, - 32A7AD3C6E3D5990746B47165F582A1D /* RDEPUBWebView+JavaScriptBridge.swift in Sources */, - D8F2872A4205075B286FE7718FC696CE /* RDEPUBWebView+Reflowable.swift in Sources */, - A92FACEFAAAFBCEF79747B917B280B85 /* RDEPUBWebView+Search.swift in Sources */, - 6214D10FFC4107B61C0A7D2124BE832D /* RDEPUBWebViewDebug.swift in Sources */, - 6C6BA0FF3D91F5121393BBF59DA93352 /* RDPlainTextBookBuilder.swift in Sources */, - 1CAB795ED271F58E3A9321F3562CFFD0 /* RDReaderContentCell.swift in Sources */, - 14429631792B0E8330AB420662AFE7D6 /* RDReaderFlowLayout.swift in Sources */, - E0DA92D28A9D84B483111D6C0D35A118 /* RDReaderGestureController.swift in Sources */, - 31BE851901D14096E7AA66B5CDEE4D55 /* RDReaderPageChildViewController.swift in Sources */, - AC190D36F6593D186EF5CA6E01C3016D /* RDReaderView.swift in Sources */, - D275929B8625E9F1E4A8306279B8B916 /* RDReaderView-dummy.m in Sources */, - 45648E6F07341B6DC91D6E22FD4E253C /* RDURLReaderController.swift in Sources */, - D0DEF92032CC5203F7CA51CCE03979FD /* UIColor+RDEPUBHex.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - AC980E6A0BED2BC61EBDA417E64924C2 /* Sources */ = { + A522E405BDB282804B667EBF296C09E7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C7B4B692AC1E9B6F6A844F171DE1BEC4 /* DTActivityTitleView.m in Sources */, - 0622935FFD2ADD637FBDC5278F8D5E92 /* DTAnimatedGIF.m in Sources */, - 086B6FE0EFE46CBAA9CA4A74720B6C52 /* DTBase64Coding.m in Sources */, - 02BD5B00D86A7A544629B90103ACB148 /* DTBlockFunctions.m in Sources */, - B189C3640D0AACE5C0B2513E932166F6 /* DTCoreGraphicsUtils.m in Sources */, - 09F91906617C975ABCF9D9C11BDA8C4F /* DTCustomColoredAccessory.m in Sources */, - 2952D6997F5580919F4825DCAA1A4EFA /* DTExtendedFileAttributes.m in Sources */, - 8AD34CC035AC1D89AB68C1327FAE3C62 /* DTFolderMonitor.m in Sources */, - E3726D55A7B60C3DC86EC4D90370426A /* DTFoundation-dummy.m in Sources */, - E8AB2FEE8443436B10DC3BD74A8226FD /* DTFoundationConstants.m in Sources */, - 34BBABDE700AB81718A4752E7D1E0140 /* DTHTMLParser.m in Sources */, - A2F28D0E1EE3C7B2135DF059CC5B8FBA /* DTLog.m in Sources */, - 9144EF4B8243782D693D9C3F18E4D968 /* DTPieProgressIndicator.m in Sources */, - 5D58B68AE7FBF43CF6F7B5AD1285C803 /* DTSmartPagingScrollView.m in Sources */, - 6E90373CEFA7629B5FF48F610799E606 /* DTTiledLayerWithoutFade.m in Sources */, - 9752BDBC8B255FC16E8DC9E10F407147 /* DTVersion.m in Sources */, - 0A64A09E852BBF52677A7BC5CC7691C0 /* NSArray+DTError.m in Sources */, - 18E05C9D1D63A2AD105D6CC0E0C64C57 /* NSData+DTCrypto.m in Sources */, - 5486FFE1398EF5F6BBE4EE0871A21C39 /* NSDictionary+DTError.m in Sources */, - BA174999E3413F5A45786FF54383240D /* NSFileWrapper+DTCopying.m in Sources */, - D7972A2540E20B28E2C9D299AAE8AF46 /* NSMutableArray+DTMoving.m in Sources */, - 2F39C838FD072D7CA4D3978C874DCF86 /* NSString+DTFormatNumbers.m in Sources */, - 7A6320A72DFF2C533B1CA8A927A9D516 /* NSString+DTPaths.m in Sources */, - EABC41A8E3D4525FEEC80C2F6CF0C108 /* NSString+DTURLEncoding.m in Sources */, - F65F1F57FE114E39EDF8BD657EF8565B /* NSString+DTUtilities.m in Sources */, - 7B6DFB4412FF0ACDAD4320AA4EEA8E63 /* NSURL+DTAppLinks.m in Sources */, - FF6E330BB1C384D79EA36415F5415511 /* NSURL+DTComparing.m in Sources */, - D1792FAE89B67B4379A9B4157E22290F /* NSURL+DTUnshorten.m in Sources */, - 94FF947022AF6FE1F04D484EB69A2048 /* UIApplication+DTNetworkActivity.m in Sources */, - 18A69B8D310DB6279273A14E9DB1A1DF /* UIImage+DTFoundation.m in Sources */, - D47F4852A393C8731EA395FE7CB059F4 /* UIScreen+DTFoundation.m in Sources */, - 08E897E7340B3D5708F3520FE146F124 /* UIView+DTFoundation.m in Sources */, + D9B2103AEF9770DFFED080731A236489 /* DTActivityTitleView.m in Sources */, + 43E3E9B7575ADB1B90A26C2879C6BD75 /* DTAnimatedGIF.m in Sources */, + A0A8F3BD0C99B4713511CC6D1691976E /* DTBase64Coding.m in Sources */, + C100C86A98BE2D71D4DCC0C14BC9C933 /* DTBlockFunctions.m in Sources */, + CC52D4A2D391BFE5D9F6F39DB6913757 /* DTCoreGraphicsUtils.m in Sources */, + 096932721786FDD80CE0F45A097E7162 /* DTCustomColoredAccessory.m in Sources */, + 0F79C2851ACB90CAE1412D895C7010D7 /* DTExtendedFileAttributes.m in Sources */, + 7B06E0624E131F4B4AE9CBA7C182DDD3 /* DTFolderMonitor.m in Sources */, + 6AAD95B33C144808A58590F8EEC1812B /* DTFoundation-dummy.m in Sources */, + E454A390799FA2F7E988A23592713FCE /* DTFoundationConstants.m in Sources */, + C252D4D3D440861EA6122DF66EB33500 /* DTHTMLParser.m in Sources */, + 9BC6C98935EEE83035986E164F76BC47 /* DTLog.m in Sources */, + 89A4857FB58483273A0954DA4935A558 /* DTPieProgressIndicator.m in Sources */, + 5720E398FFB4ECEDDE02A66A43FB901D /* DTSmartPagingScrollView.m in Sources */, + 5D8A0C9FDE40C0ECD438718CAE0080B4 /* DTTiledLayerWithoutFade.m in Sources */, + C86AF629D40E46FD947662786FAEA40C /* DTVersion.m in Sources */, + EDAD4A9457EBB9E65A2381991AB738F7 /* NSArray+DTError.m in Sources */, + 30FFF4A91F83509371C5B0CC18D5B919 /* NSData+DTCrypto.m in Sources */, + D2EC76E48BBF742006A8DE10F31F1A25 /* NSDictionary+DTError.m in Sources */, + 154E19088C6CC946DD47CB238C4C9BCC /* NSFileWrapper+DTCopying.m in Sources */, + 2DDC5E420C2953CB9DB36BCEB59C1F29 /* NSMutableArray+DTMoving.m in Sources */, + FC24AD03BC7583FF043A6723AC086232 /* NSString+DTFormatNumbers.m in Sources */, + 4B184BEB24237C483AE54DD52C6C107A /* NSString+DTPaths.m in Sources */, + 90C189E1159D4DE5725F33E1E8ABF5D3 /* NSString+DTURLEncoding.m in Sources */, + D886581273ADE0CC6CA73210AA2A8740 /* NSString+DTUtilities.m in Sources */, + 93CDFBD7BE5E4EB5ED06820EB9A89794 /* NSURL+DTAppLinks.m in Sources */, + 60974BD9B17F54CEB69CA6B80BB25730 /* NSURL+DTComparing.m in Sources */, + F68D834D874BA6D351EA0BAE720DB1A6 /* NSURL+DTUnshorten.m in Sources */, + 38A65ED943B0C81B4EA40C50BBC63E50 /* UIApplication+DTNetworkActivity.m in Sources */, + 20BA812F7892018A2905DFB5E9195B33 /* UIImage+DTFoundation.m in Sources */, + B7A73A26B8B88977C3DDC07D559EAAEC /* UIScreen+DTFoundation.m in Sources */, + DF7B0A62AD5A1F39463B55A890F32E50 /* UIView+DTFoundation.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - BD23672C3C7E57623DDEEBBDF147F41C /* Sources */ = { + C3B8C9C3E670F432088AFC01765CDA60 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 16F7D312910FD9A6F8CCE8C7128390EB /* Constraint.swift in Sources */, + 99BEFA2791CD385F40B63F36FDE54020 /* ConstraintAttributes.swift in Sources */, + 48C129F6559E80BA658FE6824B8BA94F /* ConstraintConfig.swift in Sources */, + 7E2ACD5F2A6DB7B183F56A4B3F356AD0 /* ConstraintConstantTarget.swift in Sources */, + E55D6001C2F52C1C47E541F84B4C5A8A /* ConstraintDescription.swift in Sources */, + 5498E8AC301713ED5BEBDDA779B37AC9 /* ConstraintDirectionalInsets.swift in Sources */, + 3A6BC96AC902E04E85517D39B48F1FCD /* ConstraintDirectionalInsetTarget.swift in Sources */, + 2B81BEF86D6B38359592B4576EB1851A /* ConstraintDSL.swift in Sources */, + FDAAF8F1F0F13A657B57B92F75B5F2A0 /* ConstraintInsets.swift in Sources */, + A7AFFAFDE55A53049223F920254CAF86 /* ConstraintInsetTarget.swift in Sources */, + 9B739D53E63B8810FC6F307C7997A363 /* ConstraintItem.swift in Sources */, + 714C19C6756608FBFDD1DDBDBCBC294D /* ConstraintLayoutGuide.swift in Sources */, + FB2BFDE8FC140377D92952992080A8DB /* ConstraintLayoutGuide+Extensions.swift in Sources */, + F4A25EE18B07DECDA92E9D539D70A76B /* ConstraintLayoutGuideDSL.swift in Sources */, + 89068EC008D095EA391E422D25604753 /* ConstraintLayoutSupport.swift in Sources */, + 30618822C1BD51F652D2F1A6ABA7E871 /* ConstraintLayoutSupportDSL.swift in Sources */, + C1730B2336E26354E273E454C18E48A5 /* ConstraintMaker.swift in Sources */, + 03AA3739E62E025785FFB6EB6828B7C0 /* ConstraintMakerEditable.swift in Sources */, + AF261667F646799AC6B2D780044128B8 /* ConstraintMakerExtendable.swift in Sources */, + A15106734ACDCD5AA02C4F67C8F0F2B2 /* ConstraintMakerFinalizable.swift in Sources */, + D6104499A1F639473F78E573D24672F1 /* ConstraintMakerPrioritizable.swift in Sources */, + DA009A1B7AF4A6BB397EF6A0D54199EB /* ConstraintMakerRelatable.swift in Sources */, + 7AF352A0D8851E7032D631349459BADB /* ConstraintMakerRelatable+Extensions.swift in Sources */, + 7A31B9816036902B3A32A5C00CF641AC /* ConstraintMultiplierTarget.swift in Sources */, + 04138A4A20D11E47461B6CE8605C8080 /* ConstraintOffsetTarget.swift in Sources */, + DDFCE8D603027FA6C70FCED85A181CA8 /* ConstraintPriority.swift in Sources */, + 0F9CC95163443852A6C43FB306CBFCCA /* ConstraintPriorityTarget.swift in Sources */, + 32B51B6FF5F874A18A0C55F9D785CB24 /* ConstraintRelatableTarget.swift in Sources */, + 415F0FDFD64679F7847A8E1E92277D68 /* ConstraintRelation.swift in Sources */, + 20CBAD9874EBC0BECD8003634E76CC12 /* ConstraintView.swift in Sources */, + B00DB012B30976FE2972CC22585AE9DE /* ConstraintView+Extensions.swift in Sources */, + 7E68A2D73CBE9B63C25ED43B6D33D494 /* ConstraintViewDSL.swift in Sources */, + A2F0139EB87218A9361BB1F85800192A /* Debugging.swift in Sources */, + D0F53E98858850D4E7537E7BD40E8796 /* LayoutConstraint.swift in Sources */, + 2ACD02E15A8ABD8CD8D51AE3A6C29683 /* LayoutConstraintItem.swift in Sources */, + B16CC6DF8233C4E37B30A83420E10597 /* SnapKit-dummy.m in Sources */, + 113D2805F027DF665F9FEE905F6F9D0F /* Typealiases.swift in Sources */, + D5F9B30BDC204B4148E3BA2D6D69B90B /* UILayoutSupport+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2483,149 +2753,176 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F8DC34B39FD5C083F90D263320E44F0E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 62AD6F599831925D68B03800823FFE73 /* SSAlertAnimation.swift in Sources */, - E2731FCFCF96868342E633F7962FD9F5 /* SSAlertAnimationController.swift in Sources */, - 213651A172BE115A3EB6597D4EF16EF9 /* SSAlertCommonView.swift in Sources */, - 95596341B01BC4C7E8E25D13DF885DE6 /* SSAlertDefaultAnmation.swift in Sources */, - 512C2FE338700D1FD6DFCFF2D1C96903 /* SSAlertPresentAnimation.swift in Sources */, - AF9AE1A497A51A6C99E09D260EE8939F /* SSAlertSwift-dummy.m in Sources */, - 3CFE245716B8EAC81022BB5543A419B4 /* SSAlertView.swift in Sources */, - 9A35B17A98BF4BA386549F582B9C8118 /* SSAlertViewExtention.swift in Sources */, - DF8D260A10033EBCFE8C5413EFF1535F /* UIViewFrameExtension.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F96FFD5113D092836D4AD41A4F201A88 /* Sources */ = { + FAFBEB5C156B9C7A1B4A93303133A682 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - FC050EDAFDDEA3B65238F3C4C214D150 /* Sources */ = { + FE207FF2A3BE8F6502C76C63584668D5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4195E8AE080906ABE5E4F962B53AF658 /* Archive.swift in Sources */, + 562A2DC2B45B453B2FD5C3F5F6EBE405 /* Archive+BackingConfiguration.swift in Sources */, + 43C745CDBA3B1C734F9F93E63D48FF51 /* Archive+Deprecated.swift in Sources */, + 1CB67E5F08E9C847E38ED61A79A1F656 /* Archive+Helpers.swift in Sources */, + 453AA8CE0133392DB836780F707A41C1 /* Archive+MemoryFile.swift in Sources */, + E6C30E9127E9DCDE49BDD8BBB7F81507 /* Archive+Progress.swift in Sources */, + 2D03767663B1C3251B80EDAB4F33D3CE /* Archive+Reading.swift in Sources */, + F981518F2DA0E5B53C2560759D14986B /* Archive+ReadingDeprecated.swift in Sources */, + 71624302D6B9222103A160FCC949D7B3 /* Archive+Writing.swift in Sources */, + 54634310A0DDCA6986A95D3A66CEECE0 /* Archive+WritingDeprecated.swift in Sources */, + 1693DD61EFC198586623CFC4CFCAD1DB /* Archive+ZIP64.swift in Sources */, + 359B41BE1F797CD1708D6E6473542222 /* Data+Compression.swift in Sources */, + 106FD56F563F6B4531E0C35C1680358E /* Data+CompressionDeprecated.swift in Sources */, + A12A29C8B64DAB94E6013711400452B1 /* Data+Serialization.swift in Sources */, + EE18BE5AFEFAA1893D26EFBA4AFA7193 /* Date+ZIP.swift in Sources */, + C48E39E0973F3D272FF1B3A23A43A4A0 /* Entry.swift in Sources */, + FD4B6F54758FB3894484A092B0A1B2AC /* Entry+Serialization.swift in Sources */, + 29DA9E401CE134D89FFFD1B7C2641DDA /* Entry+ZIP64.swift in Sources */, + 5BD388950720A80033201DCCC5B396B1 /* FileManager+ZIP.swift in Sources */, + A37500E37BE9E26DA5EE55916224DD7E /* FileManager+ZIPDeprecated.swift in Sources */, + E3B9A9854AB1AB2154E1CA2C6B9E61E2 /* URL+ZIP.swift in Sources */, + F2BAD19B5E842273FA3B4FB464F24EBA /* ZIPFoundation-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 020F0A0B69436E99808F13904976F379 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = DTCoreText; - target = B88F4EA0695B6B3165C64594850D72C7 /* DTCoreText */; - targetProxy = 87730E6F16E8FFBF6898B9EBD0AD14C5 /* PBXContainerItemProxy */; - }; - 0FD34BEAFBBC108E029BD172F1A001AC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SSAlertSwift; - target = 8619D5ADECF2B26CFD9A9826D61D289A /* SSAlertSwift */; - targetProxy = 57FCFD9D0AF20D20A8404315B4634FEB /* PBXContainerItemProxy */; - }; - 1C87262B71402CF8076F136C711B3B87 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SnapKit; - target = 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */; - targetProxy = B1F14D48BA2B3361AB7CF6CFB4248E92 /* PBXContainerItemProxy */; - }; - 2116DF18700ED27036D60C0892EDD986 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "ZIPFoundation-ZIPFoundation_Privacy"; - target = C7A8D82E407CD3FDC3BA55CEE519B252 /* ZIPFoundation-ZIPFoundation_Privacy */; - targetProxy = 54FE0EFC4BD35EB51504F9D17F65C630 /* PBXContainerItemProxy */; - }; - 2771AC7F4AA9CF4C86A3D614D77F33CD /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = SnapKit; - target = 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */; - targetProxy = F1618E89E8894E616222D6212EEB9BE2 /* PBXContainerItemProxy */; - }; - 2AC1DEEE4B5E32E1FA841E25B3D2FDD4 /* PBXTargetDependency */ = { + 1027BF5D9BE9ECB1B97EDFFEBDDF4B46 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = RDReaderView; target = AA2E57587AA8EECA63C4BE08EA3CB6D2 /* RDReaderView */; - targetProxy = 37F6C824B43C269AA070A9970F478E23 /* PBXContainerItemProxy */; + targetProxy = EA5C5FB405F5479FFA8A607CD33F3ECF /* PBXContainerItemProxy */; }; - 3C71D3E681036CA8EEF99BD3B1B33169 /* PBXTargetDependency */ = { + 17938577AC1DECCDD4044365CD98BEC2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = "RDReaderView-RDReaderViewAssets"; - target = AE7F393FB7805DE2664AB4111873F907 /* RDReaderView-RDReaderViewAssets */; - targetProxy = E02332EF5B2CCE697593D8FC99DCEBB2 /* PBXContainerItemProxy */; + name = SnapKit; + target = 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */; + targetProxy = A7A366C3CFD62C32F5B8E5EB9A8310BB /* PBXContainerItemProxy */; }; - 43C7FA9296E0A69B39E9CEDD513DB5E3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "DTCoreText-Resources"; - target = 0C24CB0E87A728A11AA1124CB360D6A1 /* DTCoreText-Resources */; - targetProxy = BDB86097405AC17908E1D824C764DF29 /* PBXContainerItemProxy */; - }; - 48FD93EF0BB78DB9428C4189B580A86D /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = ZIPFoundation; - target = AA15C8469D67684160CC2A7098EB841C /* ZIPFoundation */; - targetProxy = AD170DC0B755D9C13E44D4703F93ECAC /* PBXContainerItemProxy */; - }; - 4F9D9C658D922102865227C060F26475 /* PBXTargetDependency */ = { + 18B19C677B889F6727DBDA0F865BF1AB /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = SSAlertSwift; target = 8619D5ADECF2B26CFD9A9826D61D289A /* SSAlertSwift */; - targetProxy = 6FEB3EC13F3780DE28D9E3A7027F5EF4 /* PBXContainerItemProxy */; + targetProxy = A0BFC7B494DC59D52F43761C722DAB2D /* PBXContainerItemProxy */; }; - 5E87F0E1B9AF330712D0384388AFA603 /* PBXTargetDependency */ = { + 463CDA137195F14051A84236470C93F2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = DTCoreText; - target = B88F4EA0695B6B3165C64594850D72C7 /* DTCoreText */; - targetProxy = 5A688446F3834F916B644BF13086903B /* PBXContainerItemProxy */; + name = "DTCoreText-Resources"; + target = 0C24CB0E87A728A11AA1124CB360D6A1 /* DTCoreText-Resources */; + targetProxy = 2B7231DD02DE6D67ED0FF347C2D2831A /* PBXContainerItemProxy */; }; - 715597FB3F1E0E95F8A3F4B3271288A7 /* PBXTargetDependency */ = { + 5308D4674A93AF4C1261B8B667E85038 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = DTFoundation; - target = 8F6E5A5BF72D62CDFD25F91A7CFA3309 /* DTFoundation */; - targetProxy = 2DEC0E6E6DD76FFDDCA8167EC51975D3 /* PBXContainerItemProxy */; + name = SSAlertSwift; + target = 8619D5ADECF2B26CFD9A9826D61D289A /* SSAlertSwift */; + targetProxy = 7C28D11939EC5ABB1D98898772B52AED /* PBXContainerItemProxy */; }; - 95AAA3C6A552559016545CAD5B50333C /* PBXTargetDependency */ = { + 5ABBCDAACF3D91809C05806C33121BBC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "ZIPFoundation-ZIPFoundation_Privacy"; + target = C7A8D82E407CD3FDC3BA55CEE519B252 /* ZIPFoundation-ZIPFoundation_Privacy */; + targetProxy = 41C358828795BDBE26E60DEDDB6F5A31 /* PBXContainerItemProxy */; + }; + 6087769AB1CAFBD7015BB5E5FB531C1C /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = ZIPFoundation; target = AA15C8469D67684160CC2A7098EB841C /* ZIPFoundation */; - targetProxy = B27CFAAF383D62C8A184778C6B4667E7 /* PBXContainerItemProxy */; + targetProxy = 962A98D094FBC182A4454FA956B2E681 /* PBXContainerItemProxy */; }; - BAEA1865D980379B2B1067919B5DC4F9 /* PBXTargetDependency */ = { + 72E48F15033CCF95398C27EBDD634478 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - name = DTFoundation; - target = 8F6E5A5BF72D62CDFD25F91A7CFA3309 /* DTFoundation */; - targetProxy = CAB04B4C082D733C23D698A32EF0BD3C /* PBXContainerItemProxy */; + name = DTCoreText; + target = B88F4EA0695B6B3165C64594850D72C7 /* DTCoreText */; + targetProxy = A0CA6E77551A041C81844E5901F24EFC /* PBXContainerItemProxy */; }; - F2B0F6EE052F080271AEAFFE90B26584 /* PBXTargetDependency */ = { + 79C767A041874113D95C87515FD8FFBB /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "SnapKit-SnapKit_Privacy"; target = 8A8DB685241263AFDF5E6B20FE67B93A /* SnapKit-SnapKit_Privacy */; - targetProxy = 5C2DF152EE7F8EAB792D96A02ED313A6 /* PBXContainerItemProxy */; + targetProxy = B1D7453B0F93836CC086060595CB753A /* PBXContainerItemProxy */; + }; + 9561148C38876D85CE97DA5473FE5CD1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DTCoreText; + target = B88F4EA0695B6B3165C64594850D72C7 /* DTCoreText */; + targetProxy = 5BA4345CC5FD80F403AD69F002484017 /* PBXContainerItemProxy */; + }; + B49B8B5B0940A82855A49049BD54424B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = ZIPFoundation; + target = AA15C8469D67684160CC2A7098EB841C /* ZIPFoundation */; + targetProxy = 8EC7B58761479718591F7FDFB9BC34E2 /* PBXContainerItemProxy */; + }; + BDB2E3ACF2EDD090E479F5F13928DEFD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "RDReaderView-RDReaderViewAssets"; + target = AE7F393FB7805DE2664AB4111873F907 /* RDReaderView-RDReaderViewAssets */; + targetProxy = 51F9BB05FB5362314AABDBF12FD0F619 /* PBXContainerItemProxy */; + }; + C3E83C94AC96087258AD38AFA3A7F7D6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SnapKit; + target = 19622742EBA51E823D6DAE3F8CDBFAD4 /* SnapKit */; + targetProxy = 24BA72D0D912AE5A45EAC25AC27B6B7F /* PBXContainerItemProxy */; + }; + E2AA5E04C0D28740D6B40D3F0CFD237B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DTFoundation; + target = 8F6E5A5BF72D62CDFD25F91A7CFA3309 /* DTFoundation */; + targetProxy = F24E133D0B4B0D43780A5C9975FA4EF9 /* PBXContainerItemProxy */; + }; + FD3219BCD8F392FD503D4E3C397FFF1B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DTFoundation; + target = 8F6E5A5BF72D62CDFD25F91A7CFA3309 /* DTFoundation */; + targetProxy = CE5F9B32E7622F85FC1F741CE5013575 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 04DEDD89C96E9D39D24C0E818528ACCA /* Release */ = { + 0509BF697B2586AC7FD23EAB586720C2 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F699881ABDBEB419AEBA661AD5A21F35 /* ZIPFoundation.release.xcconfig */; + baseConfigurationReference = ECBA4C3DE9806BC9E5E66E0EE07FA019 /* RDReaderView.debug.xcconfig */; buildSettings = { - CODE_SIGNING_ALLOWED = NO; - CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/ZIPFoundation"; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - IBSC_MODULE = ZIPFoundation; - INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ResourceBundle-ZIPFoundation_Privacy-ZIPFoundation-Info.plist"; + GCC_PREFIX_HEADER = "Target Support Files/RDReaderView/RDReaderView-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/RDReaderView/RDReaderView-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; - PRODUCT_NAME = ZIPFoundation_Privacy; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/RDReaderView/RDReaderView.modulemap"; + PRODUCT_MODULE_NAME = RDReaderView; + PRODUCT_NAME = RDReaderView; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.10; TARGETED_DEVICE_FAMILY = "1,2"; - WRAPPER_EXTENSION = bundle; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; }; - name = Release; + name = Debug; }; 0FAC9B086846BEC73DD79CD3B21F22ED /* Release */ = { isa = XCBuildConfiguration; @@ -2667,9 +2964,9 @@ }; name = Release; }; - 199ABCEB03829A86D129CB2B64059604 /* Debug */ = { + 14E902315F96C3E06466914BDEF2E4DE /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B8FAF5A1A2890703F132819BD138C1A9 /* RDReaderView.debug.xcconfig */; + baseConfigurationReference = F699881ABDBEB419AEBA661AD5A21F35 /* ZIPFoundation.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -2682,9 +2979,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/RDReaderView/RDReaderView-prefix.pch"; + GCC_PREFIX_HEADER = "Target Support Files/ZIPFoundation/ZIPFoundation-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/RDReaderView/RDReaderView-Info.plist"; + INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( @@ -2692,31 +2989,89 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/RDReaderView/RDReaderView.modulemap"; - PRODUCT_MODULE_NAME = RDReaderView; - PRODUCT_NAME = RDReaderView; + MODULEMAP_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation.modulemap"; + PRODUCT_MODULE_NAME = ZIPFoundation; + PRODUCT_NAME = ZIPFoundation; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.10; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 1E23F8CF35DBDFA6A4493BEBC89A2DD9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F699881ABDBEB419AEBA661AD5A21F35 /* ZIPFoundation.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/ZIPFoundation"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IBSC_MODULE = ZIPFoundation; + INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ResourceBundle-ZIPFoundation_Privacy-ZIPFoundation-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + PRODUCT_NAME = ZIPFoundation_Privacy; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; + 2A0DB7A5A82BC920C14BAAD0FEC0A0B0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4BEC60CB3EDA1C189D3B062E29217440 /* ZIPFoundation.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/ZIPFoundation/ZIPFoundation-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation.modulemap"; + PRODUCT_MODULE_NAME = ZIPFoundation; + PRODUCT_NAME = ZIPFoundation; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; - 3DF0A71B4EF8D878F0159B87589F7B3A /* Debug */ = { + 2D10C67881789378F732CC76CCAE6837 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B8FAF5A1A2890703F132819BD138C1A9 /* RDReaderView.debug.xcconfig */; + baseConfigurationReference = C960B7AA0B9B8CC473FACFEC64F9906D /* SnapKit.debug.xcconfig */; buildSettings = { CODE_SIGNING_ALLOWED = NO; - CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/RDReaderView"; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SnapKit"; ENABLE_USER_SCRIPT_SANDBOXING = NO; - IBSC_MODULE = RDReaderView; - INFOPLIST_FILE = "Target Support Files/RDReaderView/ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; + IBSC_MODULE = SnapKit; + INFOPLIST_FILE = "Target Support Files/SnapKit/ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; - PRODUCT_NAME = RDReaderViewAssets; + PRODUCT_NAME = SnapKit_Privacy; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2787,9 +3142,47 @@ }; name = Release; }; - 53A2283E0892EB077409247A1CCA0CF0 /* Release */ = { + 5494BEAD922982C0DE945CEFF977EEEF /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F699881ABDBEB419AEBA661AD5A21F35 /* ZIPFoundation.release.xcconfig */; + baseConfigurationReference = 18387159515E0523AFB4204B1AF940ED /* SSAlertSwift.debug.xcconfig */; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/SSAlertSwift/SSAlertSwift-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift.modulemap"; + PRODUCT_MODULE_NAME = SSAlertSwift; + PRODUCT_NAME = SSAlertSwift; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 580453B8C583E8C0040C721E84F93EC9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DF670EC438F05ACC1F1C7B725DBDAA18 /* DTFoundation.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -2802,9 +3195,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/ZIPFoundation/ZIPFoundation-prefix.pch"; + GCC_PREFIX_HEADER = "Target Support Files/DTFoundation/DTFoundation-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation-Info.plist"; + INFOPLIST_FILE = "Target Support Files/DTFoundation/DTFoundation-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( @@ -2812,9 +3205,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation.modulemap"; - PRODUCT_MODULE_NAME = ZIPFoundation; - PRODUCT_NAME = ZIPFoundation; + MODULEMAP_FILE = "Target Support Files/DTFoundation/DTFoundation.modulemap"; + PRODUCT_MODULE_NAME = DTFoundation; + PRODUCT_NAME = DTFoundation; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; @@ -2827,11 +3220,10 @@ }; name = Release; }; - 63E9CCD27034A1EBCB978ECA99F55EB8 /* Debug */ = { + 69CF396177D4FBBD3D4F9FC3FABCEB0F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C960B7AA0B9B8CC473FACFEC64F9906D /* SnapKit.debug.xcconfig */; + baseConfigurationReference = FE2991699D0391172DEADF2F4613E320 /* SSAlertSwift.release.xcconfig */; buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; @@ -2842,9 +3234,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/SnapKit/SnapKit-prefix.pch"; + GCC_PREFIX_HEADER = "Target Support Files/SSAlertSwift/SSAlertSwift-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/SnapKit/SnapKit-Info.plist"; + INFOPLIST_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( @@ -2852,23 +3244,24 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/SnapKit/SnapKit.modulemap"; - PRODUCT_MODULE_NAME = SnapKit; - PRODUCT_NAME = SnapKit; + MODULEMAP_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift.modulemap"; + PRODUCT_MODULE_NAME = SSAlertSwift; + PRODUCT_NAME = SSAlertSwift; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; - name = Debug; + name = Release; }; - 6D650492638AE82D03F063433932545C /* Debug */ = { + 73B63AD8CE8CC4CDED4CF1429F2905C4 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 78873FAFC0AD9E930D6050A7F976714B /* DTCoreText.debug.xcconfig */; + baseConfigurationReference = C2840BE6D91479CCBC2E908FBD9DA183 /* DTCoreText.release.xcconfig */; buildSettings = { CODE_SIGNING_ALLOWED = NO; CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/DTCoreText"; @@ -2882,7 +3275,7 @@ TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = bundle; }; - name = Debug; + name = Release; }; 73BBB08AD96AC1A6A095E15ADC5FCEE1 /* Debug */ = { isa = XCBuildConfiguration; @@ -2923,28 +3316,11 @@ }; name = Debug; }; - 84907734FE753401B60117961CB771E4 /* Release */ = { + 7E52461F2289583CD17270976F26947F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 53D2AB2348A9B9C582FDC2CDB2E434FC /* SnapKit.release.xcconfig */; - buildSettings = { - CODE_SIGNING_ALLOWED = NO; - CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SnapKit"; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - IBSC_MODULE = SnapKit; - INFOPLIST_FILE = "Target Support Files/SnapKit/ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - PRODUCT_NAME = SnapKit_Privacy; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - WRAPPER_EXTENSION = bundle; - }; - name = Release; - }; - 8F6E6E8D8D3FE8229120F940ACC10E69 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 18387159515E0523AFB4204B1AF940ED /* SSAlertSwift.debug.xcconfig */; + baseConfigurationReference = 6014DA7213E4687224F2BF921E30002C /* DTFoundation.debug.xcconfig */; buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; @@ -2955,9 +3331,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/SSAlertSwift/SSAlertSwift-prefix.pch"; + GCC_PREFIX_HEADER = "Target Support Files/DTFoundation/DTFoundation-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift-Info.plist"; + INFOPLIST_FILE = "Target Support Files/DTFoundation/DTFoundation-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( @@ -2965,9 +3341,9 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift.modulemap"; - PRODUCT_MODULE_NAME = SSAlertSwift; - PRODUCT_NAME = SSAlertSwift; + MODULEMAP_FILE = "Target Support Files/DTFoundation/DTFoundation.modulemap"; + PRODUCT_MODULE_NAME = DTFoundation; + PRODUCT_NAME = DTFoundation; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; @@ -2979,6 +3355,24 @@ }; name = Debug; }; + 9AECF78B85E25BE28C3B863489170F98 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = ECBA4C3DE9806BC9E5E66E0EE07FA019 /* RDReaderView.debug.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/RDReaderView"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IBSC_MODULE = RDReaderView; + INFOPLIST_FILE = "Target Support Files/RDReaderView/ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + PRODUCT_NAME = RDReaderViewAssets; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; A0DC1C2C814704734F83EAB35BB20B21 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3046,6 +3440,24 @@ }; name = Debug; }; + A13E928C9D069741C4FD709D8B157643 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A6B5316985734E51970FA0FC168498A6 /* RDReaderView.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/RDReaderView"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IBSC_MODULE = RDReaderView; + INFOPLIST_FILE = "Target Support Files/RDReaderView/ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + PRODUCT_NAME = RDReaderViewAssets; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; A1F5B44E8E7C59A962189D14989BCAFF /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C2840BE6D91479CCBC2E908FBD9DA183 /* DTCoreText.release.xcconfig */; @@ -3086,46 +3498,6 @@ }; name = Release; }; - A3ADDFBF740E9EF025733DF5A86606A9 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DF670EC438F05ACC1F1C7B725DBDAA18 /* DTFoundation.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/DTFoundation/DTFoundation-prefix.pch"; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/DTFoundation/DTFoundation-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/DTFoundation/DTFoundation.modulemap"; - PRODUCT_MODULE_NAME = DTFoundation; - PRODUCT_NAME = DTFoundation; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; A843E21C1B95C92BE904EB3F8115DD30 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 78873FAFC0AD9E930D6050A7F976714B /* DTCoreText.debug.xcconfig */; @@ -3165,83 +3537,7 @@ }; name = Debug; }; - ABCA9F44E70D3EE1081789983480BA88 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C73611E91719890E1DD889DEBBA1DDF2 /* RDReaderView.release.xcconfig */; - buildSettings = { - CODE_SIGNING_ALLOWED = NO; - CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/RDReaderView"; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - IBSC_MODULE = RDReaderView; - INFOPLIST_FILE = "Target Support Files/RDReaderView/ResourceBundle-RDReaderViewAssets-RDReaderView-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - PRODUCT_NAME = RDReaderViewAssets; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - WRAPPER_EXTENSION = bundle; - }; - name = Release; - }; - AFD738A5A56DD586CDE6EED5E9D055DD /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C73611E91719890E1DD889DEBBA1DDF2 /* RDReaderView.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/RDReaderView/RDReaderView-prefix.pch"; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/RDReaderView/RDReaderView-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/RDReaderView/RDReaderView.modulemap"; - PRODUCT_MODULE_NAME = RDReaderView; - PRODUCT_NAME = RDReaderView; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.10; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - B30EB3155DF64947C68870EA04BB6BC3 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C960B7AA0B9B8CC473FACFEC64F9906D /* SnapKit.debug.xcconfig */; - buildSettings = { - CODE_SIGNING_ALLOWED = NO; - CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SnapKit"; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - IBSC_MODULE = SnapKit; - INFOPLIST_FILE = "Target Support Files/SnapKit/ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - PRODUCT_NAME = SnapKit_Privacy; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - TARGETED_DEVICE_FAMILY = "1,2"; - WRAPPER_EXTENSION = bundle; - }; - name = Debug; - }; - CB5CC2DCDF7737CE4E8DB57DE721E159 /* Debug */ = { + C2D179142F0FE1E8B60F6EC6DFDA73E9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4BEC60CB3EDA1C189D3B062E29217440 /* ZIPFoundation.debug.xcconfig */; buildSettings = { @@ -3259,7 +3555,7 @@ }; name = Debug; }; - DB6496470B753FBB57ED2D213D8C5E8E /* Release */ = { + D709D1D91FCABD64C95632AEF46E83A2 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 53D2AB2348A9B9C582FDC2CDB2E434FC /* SnapKit.release.xcconfig */; buildSettings = { @@ -3299,9 +3595,48 @@ }; name = Release; }; - E7EB20FBCA7F7CF4F3AB0D6B4D088BBC /* Release */ = { + DCF7FA9FEC7849EB35AD1E621B454F4E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C2840BE6D91479CCBC2E908FBD9DA183 /* DTCoreText.release.xcconfig */; + baseConfigurationReference = C960B7AA0B9B8CC473FACFEC64F9906D /* SnapKit.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/SnapKit/SnapKit-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/SnapKit/SnapKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SnapKit/SnapKit.modulemap"; + PRODUCT_MODULE_NAME = SnapKit; + PRODUCT_NAME = SnapKit; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + EEBB94326521D5AE916353F8BB9D139A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 78873FAFC0AD9E930D6050A7F976714B /* DTCoreText.debug.xcconfig */; buildSettings = { CODE_SIGNING_ALLOWED = NO; CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/DTCoreText"; @@ -3315,11 +3650,11 @@ TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = bundle; }; - name = Release; + name = Debug; }; - F1B7FFA17A844069CBE9D0CC155A0DF8 /* Debug */ = { + F281040EA5CFC27B696ACB295764A9B6 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4BEC60CB3EDA1C189D3B062E29217440 /* ZIPFoundation.debug.xcconfig */; + baseConfigurationReference = A6B5316985734E51970FA0FC168498A6 /* RDReaderView.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; @@ -3332,9 +3667,9 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/ZIPFoundation/ZIPFoundation-prefix.pch"; + GCC_PREFIX_HEADER = "Target Support Files/RDReaderView/RDReaderView-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation-Info.plist"; + INFOPLIST_FILE = "Target Support Files/RDReaderView/RDReaderView-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( @@ -3342,91 +3677,14 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MODULEMAP_FILE = "Target Support Files/ZIPFoundation/ZIPFoundation.modulemap"; - PRODUCT_MODULE_NAME = ZIPFoundation; - PRODUCT_NAME = ZIPFoundation; + MODULEMAP_FILE = "Target Support Files/RDReaderView/RDReaderView.modulemap"; + PRODUCT_MODULE_NAME = RDReaderView; + PRODUCT_NAME = RDReaderView; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - FA83B89AEE2BB71FF01D300E5297250A /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6014DA7213E4687224F2BF921E30002C /* DTFoundation.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/DTFoundation/DTFoundation-prefix.pch"; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/DTFoundation/DTFoundation-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/DTFoundation/DTFoundation.modulemap"; - PRODUCT_MODULE_NAME = DTFoundation; - PRODUCT_NAME = DTFoundation; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - FAFF47CBF775C9FB76C9E18B0DC93D60 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = FE2991699D0391172DEADF2F4613E320 /* SSAlertSwift.release.xcconfig */; - buildSettings = { - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = NO; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_PREFIX_HEADER = "Target Support Files/SSAlertSwift/SSAlertSwift-prefix.pch"; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/SSAlertSwift/SSAlertSwift.modulemap"; - PRODUCT_MODULE_NAME = SSAlertSwift; - PRODUCT_NAME = SSAlertSwift; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_INSTALL_OBJC_HEADER = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 5.10; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -3434,6 +3692,24 @@ }; name = Release; }; + F4D4FF0CE7108A328E6383C37F51C2BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 53D2AB2348A9B9C582FDC2CDB2E434FC /* SnapKit.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SnapKit"; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + IBSC_MODULE = SnapKit; + INFOPLIST_FILE = "Target Support Files/SnapKit/ResourceBundle-SnapKit_Privacy-SnapKit-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + PRODUCT_NAME = SnapKit_Privacy; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3446,42 +3722,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 1CA7C9D5DFD7044C7A36338BE2AC73C5 /* Build configuration list for PBXNativeTarget "RDReaderView-RDReaderViewAssets" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 3DF0A71B4EF8D878F0159B87589F7B3A /* Debug */, - ABCA9F44E70D3EE1081789983480BA88 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 1DE4C138F942A2A0F6A14D11F475D90E /* Build configuration list for PBXNativeTarget "DTCoreText-Resources" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 6D650492638AE82D03F063433932545C /* Debug */, - E7EB20FBCA7F7CF4F3AB0D6B4D088BBC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 30520D73A57B5DC231BF4D593A3E1FC0 /* Build configuration list for PBXNativeTarget "ZIPFoundation" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F1B7FFA17A844069CBE9D0CC155A0DF8 /* Debug */, - 53A2283E0892EB077409247A1CCA0CF0 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 470D80E3313FE7E739AE9D34641F2DB4 /* Build configuration list for PBXNativeTarget "ZIPFoundation-ZIPFoundation_Privacy" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CB5CC2DCDF7737CE4E8DB57DE721E159 /* Debug */, - 04DEDD89C96E9D39D24C0E818528ACCA /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -3491,29 +3731,56 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4FF3470542147C289B711C20EF27792C /* Build configuration list for PBXNativeTarget "SSAlertSwift" */ = { + 5DA67BEF5F46ECF4C32ACCFE78FCEF84 /* Build configuration list for PBXNativeTarget "ZIPFoundation" */ = { isa = XCConfigurationList; buildConfigurations = ( - 8F6E6E8D8D3FE8229120F940ACC10E69 /* Debug */, - FAFF47CBF775C9FB76C9E18B0DC93D60 /* Release */, + 2A0DB7A5A82BC920C14BAAD0FEC0A0B0 /* Debug */, + 14E902315F96C3E06466914BDEF2E4DE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 5BD9E11EEAC07E694B8A86BF10ECDE00 /* Build configuration list for PBXNativeTarget "DTFoundation" */ = { + 693BA49EA4876BA40D36F688DAD8D7BF /* Build configuration list for PBXNativeTarget "DTFoundation" */ = { isa = XCConfigurationList; buildConfigurations = ( - FA83B89AEE2BB71FF01D300E5297250A /* Debug */, - A3ADDFBF740E9EF025733DF5A86606A9 /* Release */, + 7E52461F2289583CD17270976F26947F /* Debug */, + 580453B8C583E8C0040C721E84F93EC9 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 71C725761AC2E4DC24695B87CC437F3F /* Build configuration list for PBXNativeTarget "SnapKit" */ = { + 73B0667287C7357BD7E64D54FC10E7CA /* Build configuration list for PBXNativeTarget "RDReaderView" */ = { isa = XCConfigurationList; buildConfigurations = ( - 63E9CCD27034A1EBCB978ECA99F55EB8 /* Debug */, - DB6496470B753FBB57ED2D213D8C5E8E /* Release */, + 0509BF697B2586AC7FD23EAB586720C2 /* Debug */, + F281040EA5CFC27B696ACB295764A9B6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84B6961CC1F169E2D6D5FC7861A8582E /* Build configuration list for PBXNativeTarget "RDReaderView-RDReaderViewAssets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9AECF78B85E25BE28C3B863489170F98 /* Debug */, + A13E928C9D069741C4FD709D8B157643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 91423BFD48D44DAFE716B4DA47D303CA /* Build configuration list for PBXNativeTarget "SnapKit-SnapKit_Privacy" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2D10C67881789378F732CC76CCAE6837 /* Debug */, + F4D4FF0CE7108A328E6383C37F51C2BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9E4196CFA8812083973C756D405439CE /* Build configuration list for PBXNativeTarget "SnapKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DCF7FA9FEC7849EB35AD1E621B454F4E /* Debug */, + D709D1D91FCABD64C95632AEF46E83A2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3527,20 +3794,29 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DC8655349C54538232C4CB81B137860C /* Build configuration list for PBXNativeTarget "RDReaderView" */ = { + D216B4C9C1EB4572666F55DEAC1E9CA1 /* Build configuration list for PBXNativeTarget "SSAlertSwift" */ = { isa = XCConfigurationList; buildConfigurations = ( - 199ABCEB03829A86D129CB2B64059604 /* Debug */, - AFD738A5A56DD586CDE6EED5E9D055DD /* Release */, + 5494BEAD922982C0DE945CEFF977EEEF /* Debug */, + 69CF396177D4FBBD3D4F9FC3FABCEB0F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F05CB5E657DCFC1458E4475B7909467D /* Build configuration list for PBXNativeTarget "SnapKit-SnapKit_Privacy" */ = { + D280590029F64DDED598A210724CF31E /* Build configuration list for PBXNativeTarget "ZIPFoundation-ZIPFoundation_Privacy" */ = { isa = XCConfigurationList; buildConfigurations = ( - B30EB3155DF64947C68870EA04BB6BC3 /* Debug */, - 84907734FE753401B60117961CB771E4 /* Release */, + C2D179142F0FE1E8B60F6EC6DFDA73E9 /* Debug */, + 1E23F8CF35DBDFA6A4493BEBC89A2DD9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F45A5207FBF041DEA1FF688C5CC78E8B /* Build configuration list for PBXNativeTarget "DTCoreText-Resources" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EEBB94326521D5AE916353F8BB9D139A /* Debug */, + 73B63AD8CE8CC4CDED4CF1429F2905C4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj index 1a1aebb..0d517cc 100644 --- a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj +++ b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj @@ -185,14 +185,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n"; diff --git a/Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift b/Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift new file mode 100644 index 0000000..afa1721 --- /dev/null +++ b/Sources/RDReaderView/EPUBCore/Models/RDEPUBAnnotationModels.swift @@ -0,0 +1,350 @@ +import Foundation + +public struct RDEPUBSelection: Codable, Equatable { + /// 书籍标识符 + public var bookIdentifier: String? + /// 选择发生的位置 + public var location: RDEPUBLocation + /// 选中的文本内容 + public var text: String + /// DOM Range 信息(JSON 序列化字符串,用于精确重建选区) + public var rangeInfo: String? + /// 选择创建时间 + public var createdAt: Date + + public init( + bookIdentifier: String? = nil, + location: RDEPUBLocation, + text: String, + rangeInfo: String? = nil, + createdAt: Date = Date() + ) { + self.bookIdentifier = bookIdentifier + self.location = location + self.text = text + self.rangeInfo = rangeInfo?.nilIfEmpty + self.createdAt = createdAt + } + + /// 判断是否为空选择(无文本或无范围信息) + public var isEmpty: Bool { + text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || rangeInfo?.isEmpty != false + } +} + +/// 高亮样式类型 +/// - highlight: 背景色高亮 +/// - underline: 下划线标记 +public enum RDEPUBHighlightStyle: String, Codable { + case highlight + case underline +} + +/// 统一标注类型,对标 WXRead 的 WRBookmark 语义分层。 +public enum RDEPUBAnnotationKind: String, Codable, Equatable { + case bookmark + case highlight + case underline +} + +/// 标注菜单操作类型,用于用户长按选中文本后的操作选项 +public enum RDEPUBAnnotationMenuAction: Equatable { + case copy + case highlight + case annotate +} + +/// 文本偏移范围信息,用于文本 EPUB 的高亮精确定位 +/// 使用字符偏移量而非 DOM Range,适用于 DTCoreText 渲染的纯文本内容 +public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable { + /// 类型标识符,固定为 "text-offset" + public var kind: String + /// 资源路径 + public var href: String + /// 起始字符偏移量 + public var start: Int + /// 结束字符偏移量 + public var end: Int + + /// 初始化文本偏移范围 + /// - Parameters: + /// - href: 资源路径 + /// - start: 起始字符偏移量 + /// - end: 结束字符偏移量 + public init(href: String, start: Int, end: Int) { + self.kind = "text-offset" + self.href = href + self.start = start + self.end = end + } + + /// 转换为 NSRange(用于 NSAttributedString 操作) + public var nsRange: NSRange? { + guard end > start else { return nil } + return NSRange(location: start, length: end - start) + } + + /// 序列化为 JSON 字符串 + public func jsonString() -> String? { + guard let data = try? JSONEncoder().encode(self) else { return nil } + return String(data: data, encoding: .utf8) + } + + /// 从 JSON 字符串反序列化,校验 kind 和范围有效性 + public static func decode(from string: String?) -> RDEPUBTextOffsetRangeInfo? { + guard let string, + let data = string.data(using: .utf8), + let rangeInfo = try? JSONDecoder().decode(RDEPUBTextOffsetRangeInfo.self, from: data), + rangeInfo.kind == "text-offset", + rangeInfo.end > rangeInfo.start else { + return nil + } + return rangeInfo + } +} + +/// 文本分页截断原因,描述为什么在当前位置分页 +public struct RDEPUBHighlight: Codable, Equatable { + /// 高亮唯一标识符 + public var id: String + /// 书籍标识符 + public var bookIdentifier: String? + /// 高亮发生的位置 + public var location: RDEPUBLocation + /// 高亮的文本内容 + public var text: String + /// 范围信息(Web EPUB 为 DOM Range JSON,文本 EPUB 为字符偏移 JSON) + public var rangeInfo: String? + /// 高亮样式(背景色高亮或下划线) + public var style: RDEPUBHighlightStyle + /// 高亮颜色(CSS 颜色值,如 "#F8E16C") + public var color: String + /// 用户备注 + public var note: String? + /// 创建时间 + public var createdAt: Date + + public init( + id: String = UUID().uuidString, + bookIdentifier: String? = nil, + location: RDEPUBLocation, + text: String, + rangeInfo: String? = nil, + style: RDEPUBHighlightStyle = .highlight, + color: String = "#F8E16C", + note: String? = nil, + createdAt: Date = Date() + ) { + self.id = id + self.bookIdentifier = bookIdentifier + self.location = location + self.text = text + self.rangeInfo = rangeInfo?.nilIfEmpty + self.style = style + self.color = color + self.note = note?.nilIfEmpty + self.createdAt = createdAt + } + + /// 是否包含用户备注 + public var hasNote: Bool { + note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + } + + /// 编码键,用于 Codable 序列化/反序列化 + private enum CodingKeys: String, CodingKey { + case id + case bookIdentifier + case location + case text + case rangeInfo + case style + case color + case note + case createdAt + } + + /// 自定义解码器,兼容旧版本数据格式(缺失字段使用默认值) + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + bookIdentifier = try container.decodeIfPresent(String.self, forKey: .bookIdentifier) + location = try container.decode(RDEPUBLocation.self, forKey: .location) + text = try container.decode(String.self, forKey: .text) + rangeInfo = try container.decodeIfPresent(String.self, forKey: .rangeInfo)?.nilIfEmpty + if let rawStyle = try container.decodeIfPresent(String.self, forKey: .style), + let decodedStyle = RDEPUBHighlightStyle(rawValue: rawStyle) { + style = decodedStyle + } else { + style = .highlight + } + color = try container.decodeIfPresent(String.self, forKey: .color) ?? "#F8E16C" + note = try container.decodeIfPresent(String.self, forKey: .note)?.nilIfEmpty + createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date() + } + + /// 自定义编码器 + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encodeIfPresent(bookIdentifier, forKey: .bookIdentifier) + try container.encode(location, forKey: .location) + try container.encode(text, forKey: .text) + try container.encodeIfPresent(rangeInfo, forKey: .rangeInfo) + try container.encode(style, forKey: .style) + try container.encode(color, forKey: .color) + try container.encodeIfPresent(note, forKey: .note) + try container.encode(createdAt, forKey: .createdAt) + } + + public var annotation: RDEPUBAnnotation { + RDEPUBAnnotation(highlight: self) + } +} + +/// 书签模型,记录用户标记的阅读位置 +public struct RDEPUBBookmark: Codable, Equatable { + /// 书签唯一标识符 + public var id: String + /// 书籍标识符 + public var bookIdentifier: String? + /// 书签位置 + public var location: RDEPUBLocation + /// 章节标题(用于书签列表展示) + public var chapterTitle: String? + /// 用户备注 + public var note: String? + /// 创建时间 + public var createdAt: Date + + public init( + id: String = UUID().uuidString, + bookIdentifier: String? = nil, + location: RDEPUBLocation, + chapterTitle: String? = nil, + note: String? = nil, + createdAt: Date = Date() + ) { + self.id = id + self.bookIdentifier = bookIdentifier + self.location = location + self.chapterTitle = chapterTitle?.nilIfEmpty + self.note = note?.nilIfEmpty + self.createdAt = createdAt + } + + /// 是否包含用户备注 + public var hasNote: Bool { + note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false + } + + public var annotation: RDEPUBAnnotation { + RDEPUBAnnotation(bookmark: self) + } +} + +/// 统一标注模型,对标 WXRead 的 WRBookmark。 +public struct RDEPUBAnnotation: Codable, Equatable { + public var id: String + public var bookIdentifier: String? + public var kind: RDEPUBAnnotationKind + public var location: RDEPUBLocation + public var text: String? + public var rangeInfo: String? + public var color: String? + public var chapterTitle: String? + public var note: String? + public var createdAt: Date + + public init( + id: String = UUID().uuidString, + bookIdentifier: String? = nil, + kind: RDEPUBAnnotationKind, + location: RDEPUBLocation, + text: String? = nil, + rangeInfo: String? = nil, + color: String? = nil, + chapterTitle: String? = nil, + note: String? = nil, + createdAt: Date = Date() + ) { + self.id = id + self.bookIdentifier = bookIdentifier + self.kind = kind + self.location = location + self.text = text?.nilIfEmpty + self.rangeInfo = rangeInfo?.nilIfEmpty + self.color = color?.nilIfEmpty + self.chapterTitle = chapterTitle?.nilIfEmpty + self.note = note?.nilIfEmpty + self.createdAt = createdAt + } + + public init(highlight: RDEPUBHighlight) { + self.init( + id: highlight.id, + bookIdentifier: highlight.bookIdentifier, + kind: highlight.style == .underline ? .underline : .highlight, + location: highlight.location, + text: highlight.text, + rangeInfo: highlight.rangeInfo, + color: highlight.color, + chapterTitle: nil, + note: highlight.note, + createdAt: highlight.createdAt + ) + } + + public init(bookmark: RDEPUBBookmark) { + self.init( + id: bookmark.id, + bookIdentifier: bookmark.bookIdentifier, + kind: .bookmark, + location: bookmark.location, + text: nil, + rangeInfo: nil, + color: nil, + chapterTitle: bookmark.chapterTitle, + note: bookmark.note, + createdAt: bookmark.createdAt + ) + } + + public var bookmark: RDEPUBBookmark? { + guard kind == .bookmark else { return nil } + return RDEPUBBookmark( + id: id, + bookIdentifier: bookIdentifier, + location: location, + chapterTitle: chapterTitle, + note: note, + createdAt: createdAt + ) + } + + public var highlight: RDEPUBHighlight? { + guard kind == .highlight || kind == .underline, + let text else { + return nil + } + return RDEPUBHighlight( + id: id, + bookIdentifier: bookIdentifier, + location: location, + text: text, + rangeInfo: rangeInfo, + style: kind == .underline ? .underline : .highlight, + color: color ?? "#F8E16C", + note: note, + createdAt: createdAt + ) + } +} + + +private extension String { + var nilIfEmpty: String? { + let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } +} diff --git a/Sources/RDReaderView/EPUBCore/Models/RDEPUBPaginationModels.swift b/Sources/RDReaderView/EPUBCore/Models/RDEPUBPaginationModels.swift new file mode 100644 index 0000000..843c001 --- /dev/null +++ b/Sources/RDReaderView/EPUBCore/Models/RDEPUBPaginationModels.swift @@ -0,0 +1,229 @@ +import Foundation + +public enum RDEPUBTextPageBreakReason: String, Codable, Equatable { + /// 章节自然结束 + case chapterEnd + /// 达到帧容量限制 + case frameLimit + /// 块级元素边界(如段落、标题) + case blockBoundary + /// 附件(图片等)边界 + case attachmentBoundary + /// 语义边界(如列表、引用块) + case semanticBoundary +} + +/// 文本附件类型 +public enum RDEPUBTextAttachmentKind: String, Codable, Equatable { + /// 图片附件 + case image + /// 通用附件 + case generic +} + +/// 文本页元数据,记录分页时的上下文信息 +/// 用于调试分页问题和优化分页质量 +public struct RDEPUBTextPageMetadata: Codable, Equatable { + /// 分页截断原因 + public var breakReason: RDEPUBTextPageBreakReason + /// 本页覆盖的块级元素范围 + public var blockRange: NSRange? + /// 本页包含的附件范围列表 + public var attachmentRanges: [NSRange] + /// 附件类型列表 + public var attachmentKinds: [RDEPUBTextAttachmentKind] + /// 块级元素类型列表 + public var blockKinds: [RDEPUBTextBlockKind] + /// 语义提示信息 + public var semanticHints: [RDEPUBTextSemanticHint] + /// 附件布局位置信息 + public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] + /// 本页末尾的 fragment ID(用于恢复位置) + public var trailingFragmentID: String? + /// 诊断信息列表(调试用) + public var diagnostics: [String] + + public init( + breakReason: RDEPUBTextPageBreakReason, + blockRange: NSRange? = nil, + attachmentRanges: [NSRange] = [], + attachmentKinds: [RDEPUBTextAttachmentKind] = [], + blockKinds: [RDEPUBTextBlockKind] = [], + semanticHints: [RDEPUBTextSemanticHint] = [], + attachmentPlacements: [RDEPUBTextAttachmentPlacement] = [], + trailingFragmentID: String? = nil, + diagnostics: [String] = [] + ) { + self.breakReason = breakReason + self.blockRange = blockRange + self.attachmentRanges = attachmentRanges + self.attachmentKinds = attachmentKinds + self.blockKinds = blockKinds + self.semanticHints = semanticHints + self.attachmentPlacements = attachmentPlacements + self.trailingFragmentID = trailingFragmentID + self.diagnostics = diagnostics + } +} + +/// 高亮模型,表示用户在阅读中标记的文本片段 +public struct EPUBChapterInfo: Codable, Equatable { + /// 对应的 spine 索引 + public var spineIndex: Int + /// 章节标题 + public var title: String + /// 该章节的总页数 + public var pageCount: Int + + public init(spineIndex: Int, title: String, pageCount: Int) { + self.spineIndex = spineIndex + self.title = title + self.pageCount = pageCount + } +} + +/// 页面模型,描述单页的完整信息 +/// 由 RDEPUBReadingSession 根据分页结果构建,是翻页容器层的数据单元 +public struct EPUBPage: Codable, Equatable { + /// 对应的 spine 索引 + public var spineIndex: Int + /// 章节索引(从 0 开始的章节序号) + public var chapterIndex: Int + /// 页在章节中的索引(从 0 开始) + public var pageIndexInChapter: Int + /// 该章节的总页数 + public var totalPagesInChapter: Int + /// 章节标题 + public var chapterTitle: String + /// 固定版式 Spread 数据(仅 fixed layout 书籍有值) + public var fixedSpread: EPUBFixedSpread? + + public init( + spineIndex: Int, + chapterIndex: Int, + pageIndexInChapter: Int, + totalPagesInChapter: Int, + chapterTitle: String, + fixedSpread: EPUBFixedSpread? + ) { + self.spineIndex = spineIndex + self.chapterIndex = chapterIndex + self.pageIndexInChapter = pageIndexInChapter + self.totalPagesInChapter = totalPagesInChapter + self.chapterTitle = chapterTitle + self.fixedSpread = fixedSpread + } +} + +/// 固定版式 Spread 中的单个资源 +public struct EPUBFixedSpreadResource: Codable, Equatable { + /// 对应的 spine 索引 + public var spineIndex: Int + /// 资源路径 + public var href: String + /// 资源标题 + public var title: String + /// 在 spread 中的位置偏好 + public var pageSpread: RDEPUBPageSpread? + + public init(spineIndex: Int, href: String, title: String, pageSpread: RDEPUBPageSpread? = nil) { + self.spineIndex = spineIndex + self.href = href + self.title = title + self.pageSpread = pageSpread + } +} + +/// 固定版式 Spread(双页展开)模型 +/// 将 1-2 个 spine 资源组合为一个显示单元 +public struct EPUBFixedSpread: Codable, Equatable { + /// 该 spread 包含的资源列表(通常 1-2 个) + public var resources: [EPUBFixedSpreadResource] + + public init(resources: [EPUBFixedSpreadResource]) { + self.resources = resources + } + + /// 主资源(列表中的第一个),用于标识和定位 + public var primaryResource: EPUBFixedSpreadResource { + resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "") + } + + /// 判断指定 href 的资源是否属于此 spread + /// - Parameters: + /// - normalizedHref: 标准化后的 href + /// - normalizer: href 标准化函数 + /// - Returns: 是否包含该资源 + public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool { + resources.contains { resource in + normalizer(resource.href) == normalizedHref + } + } + + /// 根据 spine 列表和 spread 设置生成 Spread 数组 + /// 根据 pageSpread 属性(left/right/center)决定资源配对 + /// - Parameters: + /// - spine: spine 项列表 + /// - spreadEnabled: 是否启用 spread 模式 + /// - Returns: spread 数组 + public static func makeSpreads(spine: [RDEPUBSpineItem], spreadEnabled: Bool) -> [EPUBFixedSpread] { + let resources = spine.enumerated().map { index, item in + EPUBFixedSpreadResource( + spineIndex: index, + href: item.href, + title: item.title, + pageSpread: item.pageSpread + ) + } + + guard spreadEnabled, resources.count > 1 else { + return resources.map { EPUBFixedSpread(resources: [$0]) } + } + + var spreads: [EPUBFixedSpread] = [] + var cursor = 0 + + while cursor < resources.count { + let current = resources[cursor] + + guard cursor + 1 < resources.count else { + spreads.append(EPUBFixedSpread(resources: [current])) + break + } + + let next = resources[cursor + 1] + if shouldPair(current: current, next: next) { + spreads.append(EPUBFixedSpread(resources: [current, next])) + cursor += 2 + } else { + spreads.append(EPUBFixedSpread(resources: [current])) + cursor += 1 + } + } + + return spreads + } + + /// 便捷方法:从 parser 构建 spread + public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] { + makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled) + } + + /// 判断两个资源是否应该配对为一个 spread + /// center 类型不参与配对,left+right 或 right+left 配对 + private static func shouldPair(current: EPUBFixedSpreadResource, next: EPUBFixedSpreadResource) -> Bool { + if current.pageSpread == .center || next.pageSpread == .center { + return false + } + + switch (current.pageSpread, next.pageSpread) { + case (.left, .right), (.right, .left): + return true + case (.left, .left), (.right, .right): + return false + default: + return true + } + } +} + diff --git a/Sources/RDReaderView/EPUBCore/Models/RDEPUBReadingLocationModels.swift b/Sources/RDReaderView/EPUBCore/Models/RDEPUBReadingLocationModels.swift new file mode 100644 index 0000000..8f1f92d --- /dev/null +++ b/Sources/RDReaderView/EPUBCore/Models/RDEPUBReadingLocationModels.swift @@ -0,0 +1,135 @@ +import Foundation + +public struct RDEPUBLocation: Codable, Equatable { + /// 书籍唯一标识符,用于隔离不同书的阅读进度 + public var bookIdentifier: String? + /// OPF 相对路径,对应 spine 中的资源文件 + public var href: String + /// 视口起始位置在章节中的相对进度 [0, 1] + public var progression: Double + /// 视口末尾位置在章节中的相对进度 [0, 1](可选) + public var lastProgression: Double? + /// 锚点标识符(如 #section1),用于精确跳转 + public var fragment: String? + /// 文本范围锚点(用于文本 EPUB 的高亮定位) + public var rangeAnchor: RDEPUBTextRangeAnchor? + + public init( + bookIdentifier: String? = nil, + href: String, + progression: Double, + lastProgression: Double? = nil, + fragment: String? = nil, + rangeAnchor: RDEPUBTextRangeAnchor? = nil + ) { + self.bookIdentifier = bookIdentifier + self.href = href + self.progression = Self.clamp(progression) + self.lastProgression = lastProgression.map(Self.clamp) + self.fragment = fragment?.nilIfEmpty + self.rangeAnchor = rangeAnchor + } + + /// 导航用的中间进度值,取 progression 和 lastProgression 的中点 + /// 用于在字号/横竖屏变化后估算最接近的页号 + public var navigationProgression: Double { + let end = lastProgression ?? progression + if end.isNaN || end.isInfinite { + return progression + } + return Self.clamp((progression + end) / 2.0) + } + + /// 将进度值限制在 [0, 1] 范围内,防止越界 + private static func clamp(_ value: Double) -> Double { + guard value.isFinite else { return 0 } + return min(1, max(0, value)) + } +} + +/// 视口中的单个资源信息,表示当前屏幕可见区域覆盖的某个 spine 资源 +public struct RDEPUBViewportResource: Codable, Equatable { + /// 资源路径(相对于 OPF 目录) + public var href: String + /// 对应的 spine 索引 + public var spineIndex: Int + /// 该资源在视口中的起始进度 [0, 1] + public var progression: Double + /// 该资源在视口中的结束进度 [0, 1] + public var lastProgression: Double? + /// 锚点标识符 + public var fragment: String? + + public init( + href: String, + spineIndex: Int, + progression: Double, + lastProgression: Double? = nil, + fragment: String? = nil + ) { + self.href = href + self.spineIndex = spineIndex + self.progression = progression + self.lastProgression = lastProgression + self.fragment = fragment + } +} + +/// 视口模型,描述当前屏幕可见区域的完整状态 +/// 可能跨越多个 spine 资源(如分栏布局时) +public struct RDEPUBViewport: Codable, Equatable { + /// 当前视口覆盖的资源列表(可能有多个,如分栏或跨页场景) + public var resources: [RDEPUBViewportResource] + /// 当前可见的页码 + public var visiblePageNumber: Int + /// 当前章节索引 + public var chapterIndex: Int? + /// 是否为固定版式内容 + public var isFixedLayout: Bool + + public init( + resources: [RDEPUBViewportResource], + visiblePageNumber: Int, + chapterIndex: Int? = nil, + isFixedLayout: Bool + ) { + self.resources = resources + self.visiblePageNumber = visiblePageNumber + self.chapterIndex = chapterIndex + self.isFixedLayout = isFixedLayout + } +} + +/// 阅读上下文,聚合了位置、视口和页码等完整阅读状态 +/// 由 RDEPUBReadingSession 维护,是翻页容器层获取当前阅读状态的主要数据源 +public struct RDEPUBReadingContext: Codable, Equatable { + /// 当前阅读位置(href + progression) + public var location: RDEPUBLocation + /// 当前视口状态 + public var viewport: RDEPUBViewport + /// 当前扁平页码(跨章节累计) + public var pageNumber: Int + /// 当前章节索引 + public var chapterIndex: Int? + + public init( + location: RDEPUBLocation, + viewport: RDEPUBViewport, + pageNumber: Int, + chapterIndex: Int? = nil + ) { + self.location = location + self.viewport = viewport + self.pageNumber = pageNumber + self.chapterIndex = chapterIndex + } +} + +/// 文本选择模型,由 JS 桥接 ssReaderSelectionChanged 消息触发创建 + +private extension String { + var nilIfEmpty: String? { + let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } +} diff --git a/Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift b/Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift deleted file mode 100644 index fa6a943..0000000 --- a/Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift +++ /dev/null @@ -1,719 +0,0 @@ -// -// RDEPUBReadingModels.swift -// RDReaderView -// -// EPUBCore 层阅读会话相关数据模型定义文件。 -// 包含阅读位置(Location)、视口(Viewport)、阅读上下文(ReadingContext)、 -// 文本选择(Selection)、高亮(Highlight)、书签(Bookmark)、 -// 章节信息(ChapterInfo)、页面模型(Page)、固定版式 Spread 模型等。 -// 这些模型由 RDEPUBReadingSession 管理,贯穿整个阅读生命周期。 -// - -import Foundation - -/// 阅读位置模型,EPUB 是可重排内容,使用 href + progression 而非页号来定位 -/// progression 取值 [0, 1],表示当前视口在章节中的相对位置 -public struct RDEPUBLocation: Codable, Equatable { - /// 书籍唯一标识符,用于隔离不同书的阅读进度 - public var bookIdentifier: String? - /// OPF 相对路径,对应 spine 中的资源文件 - public var href: String - /// 视口起始位置在章节中的相对进度 [0, 1] - public var progression: Double - /// 视口末尾位置在章节中的相对进度 [0, 1](可选) - public var lastProgression: Double? - /// 锚点标识符(如 #section1),用于精确跳转 - public var fragment: String? - /// 文本范围锚点(用于文本 EPUB 的高亮定位) - public var rangeAnchor: RDEPUBTextRangeAnchor? - - public init( - bookIdentifier: String? = nil, - href: String, - progression: Double, - lastProgression: Double? = nil, - fragment: String? = nil, - rangeAnchor: RDEPUBTextRangeAnchor? = nil - ) { - self.bookIdentifier = bookIdentifier - self.href = href - self.progression = Self.clamp(progression) - self.lastProgression = lastProgression.map(Self.clamp) - self.fragment = fragment?.nilIfEmpty - self.rangeAnchor = rangeAnchor - } - - /// 导航用的中间进度值,取 progression 和 lastProgression 的中点 - /// 用于在字号/横竖屏变化后估算最接近的页号 - public var navigationProgression: Double { - let end = lastProgression ?? progression - if end.isNaN || end.isInfinite { - return progression - } - return Self.clamp((progression + end) / 2.0) - } - - /// 将进度值限制在 [0, 1] 范围内,防止越界 - private static func clamp(_ value: Double) -> Double { - guard value.isFinite else { return 0 } - return min(1, max(0, value)) - } -} - -/// 视口中的单个资源信息,表示当前屏幕可见区域覆盖的某个 spine 资源 -public struct RDEPUBViewportResource: Codable, Equatable { - /// 资源路径(相对于 OPF 目录) - public var href: String - /// 对应的 spine 索引 - public var spineIndex: Int - /// 该资源在视口中的起始进度 [0, 1] - public var progression: Double - /// 该资源在视口中的结束进度 [0, 1] - public var lastProgression: Double? - /// 锚点标识符 - public var fragment: String? - - public init( - href: String, - spineIndex: Int, - progression: Double, - lastProgression: Double? = nil, - fragment: String? = nil - ) { - self.href = href - self.spineIndex = spineIndex - self.progression = progression - self.lastProgression = lastProgression - self.fragment = fragment - } -} - -/// 视口模型,描述当前屏幕可见区域的完整状态 -/// 可能跨越多个 spine 资源(如分栏布局时) -public struct RDEPUBViewport: Codable, Equatable { - /// 当前视口覆盖的资源列表(可能有多个,如分栏或跨页场景) - public var resources: [RDEPUBViewportResource] - /// 当前可见的页码 - public var visiblePageNumber: Int - /// 当前章节索引 - public var chapterIndex: Int? - /// 是否为固定版式内容 - public var isFixedLayout: Bool - - public init( - resources: [RDEPUBViewportResource], - visiblePageNumber: Int, - chapterIndex: Int? = nil, - isFixedLayout: Bool - ) { - self.resources = resources - self.visiblePageNumber = visiblePageNumber - self.chapterIndex = chapterIndex - self.isFixedLayout = isFixedLayout - } -} - -/// 阅读上下文,聚合了位置、视口和页码等完整阅读状态 -/// 由 RDEPUBReadingSession 维护,是翻页容器层获取当前阅读状态的主要数据源 -public struct RDEPUBReadingContext: Codable, Equatable { - /// 当前阅读位置(href + progression) - public var location: RDEPUBLocation - /// 当前视口状态 - public var viewport: RDEPUBViewport - /// 当前扁平页码(跨章节累计) - public var pageNumber: Int - /// 当前章节索引 - public var chapterIndex: Int? - - public init( - location: RDEPUBLocation, - viewport: RDEPUBViewport, - pageNumber: Int, - chapterIndex: Int? = nil - ) { - self.location = location - self.viewport = viewport - self.pageNumber = pageNumber - self.chapterIndex = chapterIndex - } -} - -/// 文本选择模型,由 JS 桥接 ssReaderSelectionChanged 消息触发创建 -public struct RDEPUBSelection: Codable, Equatable { - /// 书籍标识符 - public var bookIdentifier: String? - /// 选择发生的位置 - public var location: RDEPUBLocation - /// 选中的文本内容 - public var text: String - /// DOM Range 信息(JSON 序列化字符串,用于精确重建选区) - public var rangeInfo: String? - /// 选择创建时间 - public var createdAt: Date - - public init( - bookIdentifier: String? = nil, - location: RDEPUBLocation, - text: String, - rangeInfo: String? = nil, - createdAt: Date = Date() - ) { - self.bookIdentifier = bookIdentifier - self.location = location - self.text = text - self.rangeInfo = rangeInfo?.nilIfEmpty - self.createdAt = createdAt - } - - /// 判断是否为空选择(无文本或无范围信息) - public var isEmpty: Bool { - text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || rangeInfo?.isEmpty != false - } -} - -/// 高亮样式类型 -/// - highlight: 背景色高亮 -/// - underline: 下划线标记 -public enum RDEPUBHighlightStyle: String, Codable { - case highlight - case underline -} - -/// 统一标注类型,对标 WXRead 的 WRBookmark 语义分层。 -public enum RDEPUBAnnotationKind: String, Codable, Equatable { - case bookmark - case highlight - case underline -} - -/// 标注菜单操作类型,用于用户长按选中文本后的操作选项 -public enum RDEPUBAnnotationMenuAction: Equatable { - case copy - case highlight - case annotate -} - -/// 文本偏移范围信息,用于文本 EPUB 的高亮精确定位 -/// 使用字符偏移量而非 DOM Range,适用于 DTCoreText 渲染的纯文本内容 -public struct RDEPUBTextOffsetRangeInfo: Codable, Equatable { - /// 类型标识符,固定为 "text-offset" - public var kind: String - /// 资源路径 - public var href: String - /// 起始字符偏移量 - public var start: Int - /// 结束字符偏移量 - public var end: Int - - /// 初始化文本偏移范围 - /// - Parameters: - /// - href: 资源路径 - /// - start: 起始字符偏移量 - /// - end: 结束字符偏移量 - public init(href: String, start: Int, end: Int) { - self.kind = "text-offset" - self.href = href - self.start = start - self.end = end - } - - /// 转换为 NSRange(用于 NSAttributedString 操作) - public var nsRange: NSRange? { - guard end > start else { return nil } - return NSRange(location: start, length: end - start) - } - - /// 序列化为 JSON 字符串 - public func jsonString() -> String? { - guard let data = try? JSONEncoder().encode(self) else { return nil } - return String(data: data, encoding: .utf8) - } - - /// 从 JSON 字符串反序列化,校验 kind 和范围有效性 - public static func decode(from string: String?) -> RDEPUBTextOffsetRangeInfo? { - guard let string, - let data = string.data(using: .utf8), - let rangeInfo = try? JSONDecoder().decode(RDEPUBTextOffsetRangeInfo.self, from: data), - rangeInfo.kind == "text-offset", - rangeInfo.end > rangeInfo.start else { - return nil - } - return rangeInfo - } -} - -/// 文本分页截断原因,描述为什么在当前位置分页 -public enum RDEPUBTextPageBreakReason: String, Codable, Equatable { - /// 章节自然结束 - case chapterEnd - /// 达到帧容量限制 - case frameLimit - /// 块级元素边界(如段落、标题) - case blockBoundary - /// 附件(图片等)边界 - case attachmentBoundary - /// 语义边界(如列表、引用块) - case semanticBoundary -} - -/// 文本附件类型 -public enum RDEPUBTextAttachmentKind: String, Codable, Equatable { - /// 图片附件 - case image - /// 通用附件 - case generic -} - -/// 文本页元数据,记录分页时的上下文信息 -/// 用于调试分页问题和优化分页质量 -public struct RDEPUBTextPageMetadata: Codable, Equatable { - /// 分页截断原因 - public var breakReason: RDEPUBTextPageBreakReason - /// 本页覆盖的块级元素范围 - public var blockRange: NSRange? - /// 本页包含的附件范围列表 - public var attachmentRanges: [NSRange] - /// 附件类型列表 - public var attachmentKinds: [RDEPUBTextAttachmentKind] - /// 块级元素类型列表 - public var blockKinds: [RDEPUBTextBlockKind] - /// 语义提示信息 - public var semanticHints: [RDEPUBTextSemanticHint] - /// 附件布局位置信息 - public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] - /// 本页末尾的 fragment ID(用于恢复位置) - public var trailingFragmentID: String? - /// 诊断信息列表(调试用) - public var diagnostics: [String] - - public init( - breakReason: RDEPUBTextPageBreakReason, - blockRange: NSRange? = nil, - attachmentRanges: [NSRange] = [], - attachmentKinds: [RDEPUBTextAttachmentKind] = [], - blockKinds: [RDEPUBTextBlockKind] = [], - semanticHints: [RDEPUBTextSemanticHint] = [], - attachmentPlacements: [RDEPUBTextAttachmentPlacement] = [], - trailingFragmentID: String? = nil, - diagnostics: [String] = [] - ) { - self.breakReason = breakReason - self.blockRange = blockRange - self.attachmentRanges = attachmentRanges - self.attachmentKinds = attachmentKinds - self.blockKinds = blockKinds - self.semanticHints = semanticHints - self.attachmentPlacements = attachmentPlacements - self.trailingFragmentID = trailingFragmentID - self.diagnostics = diagnostics - } -} - -/// 高亮模型,表示用户在阅读中标记的文本片段 -/// 支持 Web EPUB(DOM Range 定位)和文本 EPUB(字符偏移定位)两种模式 -public struct RDEPUBHighlight: Codable, Equatable { - /// 高亮唯一标识符 - public var id: String - /// 书籍标识符 - public var bookIdentifier: String? - /// 高亮发生的位置 - public var location: RDEPUBLocation - /// 高亮的文本内容 - public var text: String - /// 范围信息(Web EPUB 为 DOM Range JSON,文本 EPUB 为字符偏移 JSON) - public var rangeInfo: String? - /// 高亮样式(背景色高亮或下划线) - public var style: RDEPUBHighlightStyle - /// 高亮颜色(CSS 颜色值,如 "#F8E16C") - public var color: String - /// 用户备注 - public var note: String? - /// 创建时间 - public var createdAt: Date - - public init( - id: String = UUID().uuidString, - bookIdentifier: String? = nil, - location: RDEPUBLocation, - text: String, - rangeInfo: String? = nil, - style: RDEPUBHighlightStyle = .highlight, - color: String = "#F8E16C", - note: String? = nil, - createdAt: Date = Date() - ) { - self.id = id - self.bookIdentifier = bookIdentifier - self.location = location - self.text = text - self.rangeInfo = rangeInfo?.nilIfEmpty - self.style = style - self.color = color - self.note = note?.nilIfEmpty - self.createdAt = createdAt - } - - /// 是否包含用户备注 - public var hasNote: Bool { - note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false - } - - /// 编码键,用于 Codable 序列化/反序列化 - private enum CodingKeys: String, CodingKey { - case id - case bookIdentifier - case location - case text - case rangeInfo - case style - case color - case note - case createdAt - } - - /// 自定义解码器,兼容旧版本数据格式(缺失字段使用默认值) - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(String.self, forKey: .id) - bookIdentifier = try container.decodeIfPresent(String.self, forKey: .bookIdentifier) - location = try container.decode(RDEPUBLocation.self, forKey: .location) - text = try container.decode(String.self, forKey: .text) - rangeInfo = try container.decodeIfPresent(String.self, forKey: .rangeInfo)?.nilIfEmpty - if let rawStyle = try container.decodeIfPresent(String.self, forKey: .style), - let decodedStyle = RDEPUBHighlightStyle(rawValue: rawStyle) { - style = decodedStyle - } else { - style = .highlight - } - color = try container.decodeIfPresent(String.self, forKey: .color) ?? "#F8E16C" - note = try container.decodeIfPresent(String.self, forKey: .note)?.nilIfEmpty - createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) ?? Date() - } - - /// 自定义编码器 - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encodeIfPresent(bookIdentifier, forKey: .bookIdentifier) - try container.encode(location, forKey: .location) - try container.encode(text, forKey: .text) - try container.encodeIfPresent(rangeInfo, forKey: .rangeInfo) - try container.encode(style, forKey: .style) - try container.encode(color, forKey: .color) - try container.encodeIfPresent(note, forKey: .note) - try container.encode(createdAt, forKey: .createdAt) - } - - public var annotation: RDEPUBAnnotation { - RDEPUBAnnotation(highlight: self) - } -} - -/// 书签模型,记录用户标记的阅读位置 -public struct RDEPUBBookmark: Codable, Equatable { - /// 书签唯一标识符 - public var id: String - /// 书籍标识符 - public var bookIdentifier: String? - /// 书签位置 - public var location: RDEPUBLocation - /// 章节标题(用于书签列表展示) - public var chapterTitle: String? - /// 用户备注 - public var note: String? - /// 创建时间 - public var createdAt: Date - - public init( - id: String = UUID().uuidString, - bookIdentifier: String? = nil, - location: RDEPUBLocation, - chapterTitle: String? = nil, - note: String? = nil, - createdAt: Date = Date() - ) { - self.id = id - self.bookIdentifier = bookIdentifier - self.location = location - self.chapterTitle = chapterTitle?.nilIfEmpty - self.note = note?.nilIfEmpty - self.createdAt = createdAt - } - - /// 是否包含用户备注 - public var hasNote: Bool { - note?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false - } - - public var annotation: RDEPUBAnnotation { - RDEPUBAnnotation(bookmark: self) - } -} - -/// 统一标注模型,对标 WXRead 的 WRBookmark。 -public struct RDEPUBAnnotation: Codable, Equatable { - public var id: String - public var bookIdentifier: String? - public var kind: RDEPUBAnnotationKind - public var location: RDEPUBLocation - public var text: String? - public var rangeInfo: String? - public var color: String? - public var chapterTitle: String? - public var note: String? - public var createdAt: Date - - public init( - id: String = UUID().uuidString, - bookIdentifier: String? = nil, - kind: RDEPUBAnnotationKind, - location: RDEPUBLocation, - text: String? = nil, - rangeInfo: String? = nil, - color: String? = nil, - chapterTitle: String? = nil, - note: String? = nil, - createdAt: Date = Date() - ) { - self.id = id - self.bookIdentifier = bookIdentifier - self.kind = kind - self.location = location - self.text = text?.nilIfEmpty - self.rangeInfo = rangeInfo?.nilIfEmpty - self.color = color?.nilIfEmpty - self.chapterTitle = chapterTitle?.nilIfEmpty - self.note = note?.nilIfEmpty - self.createdAt = createdAt - } - - public init(highlight: RDEPUBHighlight) { - self.init( - id: highlight.id, - bookIdentifier: highlight.bookIdentifier, - kind: highlight.style == .underline ? .underline : .highlight, - location: highlight.location, - text: highlight.text, - rangeInfo: highlight.rangeInfo, - color: highlight.color, - chapterTitle: nil, - note: highlight.note, - createdAt: highlight.createdAt - ) - } - - public init(bookmark: RDEPUBBookmark) { - self.init( - id: bookmark.id, - bookIdentifier: bookmark.bookIdentifier, - kind: .bookmark, - location: bookmark.location, - text: nil, - rangeInfo: nil, - color: nil, - chapterTitle: bookmark.chapterTitle, - note: bookmark.note, - createdAt: bookmark.createdAt - ) - } - - public var bookmark: RDEPUBBookmark? { - guard kind == .bookmark else { return nil } - return RDEPUBBookmark( - id: id, - bookIdentifier: bookIdentifier, - location: location, - chapterTitle: chapterTitle, - note: note, - createdAt: createdAt - ) - } - - public var highlight: RDEPUBHighlight? { - guard kind == .highlight || kind == .underline, - let text else { - return nil - } - return RDEPUBHighlight( - id: id, - bookIdentifier: bookIdentifier, - location: location, - text: text, - rangeInfo: rangeInfo, - style: kind == .underline ? .underline : .highlight, - color: color ?? "#F8E16C", - note: note, - createdAt: createdAt - ) - } -} - -/// 章节信息模型,描述单个章节的元数据 -public struct EPUBChapterInfo: Codable, Equatable { - /// 对应的 spine 索引 - public var spineIndex: Int - /// 章节标题 - public var title: String - /// 该章节的总页数 - public var pageCount: Int - - public init(spineIndex: Int, title: String, pageCount: Int) { - self.spineIndex = spineIndex - self.title = title - self.pageCount = pageCount - } -} - -/// 页面模型,描述单页的完整信息 -/// 由 RDEPUBReadingSession 根据分页结果构建,是翻页容器层的数据单元 -public struct EPUBPage: Codable, Equatable { - /// 对应的 spine 索引 - public var spineIndex: Int - /// 章节索引(从 0 开始的章节序号) - public var chapterIndex: Int - /// 页在章节中的索引(从 0 开始) - public var pageIndexInChapter: Int - /// 该章节的总页数 - public var totalPagesInChapter: Int - /// 章节标题 - public var chapterTitle: String - /// 固定版式 Spread 数据(仅 fixed layout 书籍有值) - public var fixedSpread: EPUBFixedSpread? - - public init( - spineIndex: Int, - chapterIndex: Int, - pageIndexInChapter: Int, - totalPagesInChapter: Int, - chapterTitle: String, - fixedSpread: EPUBFixedSpread? - ) { - self.spineIndex = spineIndex - self.chapterIndex = chapterIndex - self.pageIndexInChapter = pageIndexInChapter - self.totalPagesInChapter = totalPagesInChapter - self.chapterTitle = chapterTitle - self.fixedSpread = fixedSpread - } -} - -/// 固定版式 Spread 中的单个资源 -public struct EPUBFixedSpreadResource: Codable, Equatable { - /// 对应的 spine 索引 - public var spineIndex: Int - /// 资源路径 - public var href: String - /// 资源标题 - public var title: String - /// 在 spread 中的位置偏好 - public var pageSpread: RDEPUBPageSpread? - - public init(spineIndex: Int, href: String, title: String, pageSpread: RDEPUBPageSpread? = nil) { - self.spineIndex = spineIndex - self.href = href - self.title = title - self.pageSpread = pageSpread - } -} - -/// 固定版式 Spread(双页展开)模型 -/// 将 1-2 个 spine 资源组合为一个显示单元 -public struct EPUBFixedSpread: Codable, Equatable { - /// 该 spread 包含的资源列表(通常 1-2 个) - public var resources: [EPUBFixedSpreadResource] - - public init(resources: [EPUBFixedSpreadResource]) { - self.resources = resources - } - - /// 主资源(列表中的第一个),用于标识和定位 - public var primaryResource: EPUBFixedSpreadResource { - resources.first ?? EPUBFixedSpreadResource(spineIndex: 0, href: "", title: "") - } - - /// 判断指定 href 的资源是否属于此 spread - /// - Parameters: - /// - normalizedHref: 标准化后的 href - /// - normalizer: href 标准化函数 - /// - Returns: 是否包含该资源 - public func contains(normalizedHref: String, normalizer: (String) -> String?) -> Bool { - resources.contains { resource in - normalizer(resource.href) == normalizedHref - } - } - - /// 根据 spine 列表和 spread 设置生成 Spread 数组 - /// 根据 pageSpread 属性(left/right/center)决定资源配对 - /// - Parameters: - /// - spine: spine 项列表 - /// - spreadEnabled: 是否启用 spread 模式 - /// - Returns: spread 数组 - public static func makeSpreads(spine: [RDEPUBSpineItem], spreadEnabled: Bool) -> [EPUBFixedSpread] { - let resources = spine.enumerated().map { index, item in - EPUBFixedSpreadResource( - spineIndex: index, - href: item.href, - title: item.title, - pageSpread: item.pageSpread - ) - } - - guard spreadEnabled, resources.count > 1 else { - return resources.map { EPUBFixedSpread(resources: [$0]) } - } - - var spreads: [EPUBFixedSpread] = [] - var cursor = 0 - - while cursor < resources.count { - let current = resources[cursor] - - guard cursor + 1 < resources.count else { - spreads.append(EPUBFixedSpread(resources: [current])) - break - } - - let next = resources[cursor + 1] - if shouldPair(current: current, next: next) { - spreads.append(EPUBFixedSpread(resources: [current, next])) - cursor += 2 - } else { - spreads.append(EPUBFixedSpread(resources: [current])) - cursor += 1 - } - } - - return spreads - } - - /// 便捷方法:从 parser 构建 spread - public static func makeSpreads(parser: RDEPUBParser, spreadEnabled: Bool) -> [EPUBFixedSpread] { - makeSpreads(spine: parser.spine, spreadEnabled: spreadEnabled) - } - - /// 判断两个资源是否应该配对为一个 spread - /// center 类型不参与配对,left+right 或 right+left 配对 - private static func shouldPair(current: EPUBFixedSpreadResource, next: EPUBFixedSpreadResource) -> Bool { - if current.pageSpread == .center || next.pageSpread == .center { - return false - } - - switch (current.pageSpread, next.pageSpread) { - case (.left, .right), (.right, .left): - return true - case (.left, .left), (.right, .right): - return false - default: - return true - } - } -} - -/// String 扩展:空字符串转为 nil,用于 Codable 容错处理 -private extension String { - /// 空白字符修剪后如果是空字符串则返回 nil - var nilIfEmpty: String? { - let trimmed = trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? nil : trimmed - } -} diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBBuildDiagnosticsReporter.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBBuildDiagnosticsReporter.swift new file mode 100644 index 0000000..6bbb0e4 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBBuildDiagnosticsReporter.swift @@ -0,0 +1,57 @@ +import Foundation + +/// Builds diagnostics and human-readable summaries for a text book build. +struct RDEPUBBuildDiagnosticsReporter { + func phase7SemanticSummary( + title: String?, + diagnostics: [RDEPUBTextChapterPaginationDiagnostic] + ) -> String? { + guard !diagnostics.isEmpty else { return nil } + + let blockKinds = uniqueValues(from: diagnostics.flatMap(\.blockKinds)) + let semanticHints = uniqueValues(from: diagnostics.flatMap(\.semanticHints)) + let attachmentPlacements = uniqueValues(from: diagnostics.flatMap(\.attachmentPlacements)) + let note = diagnostics + .flatMap(\.sampleNotes) + .first(where: { $0.contains("semantic") || $0.contains("attachment") || $0.contains("block kinds") }) + + var parts = [ + title, + "章节 \(diagnostics.count)", + blockKinds.isEmpty ? nil : "block kinds [\(blockKinds.map(\.rawValue).joined(separator: ","))]", + semanticHints.isEmpty ? nil : "hints [\(semanticHints.map(\.rawValue).joined(separator: ","))]", + attachmentPlacements.isEmpty ? nil : "placements [\(attachmentPlacements.map(\.rawValue).joined(separator: ","))]" + ].compactMap { $0 } + if let note { + parts.append(note) + } + return parts.joined(separator: " · ") + } + + func chapterDiagnostic( + href: String, + title: String, + pages: [RDEPUBTextPage] + ) -> RDEPUBTextChapterPaginationDiagnostic { + RDEPUBTextChapterPaginationDiagnostic( + href: href, + title: title, + pageCount: pages.count, + breakReasons: pages.map(\.metadata.breakReason), + attachmentPageCount: pages.filter { !$0.metadata.attachmentKinds.isEmpty }.count, + blockAdjustedPageCount: pages.filter { $0.metadata.breakReason == .blockBoundary || $0.metadata.breakReason == .attachmentBoundary }.count, + blockKinds: uniqueValues(from: pages.flatMap(\.metadata.blockKinds)), + semanticHints: uniqueValues(from: pages.flatMap(\.metadata.semanticHints)), + attachmentPlacements: uniqueValues(from: pages.flatMap(\.metadata.attachmentPlacements)), + sampleNotes: Array(pages.flatMap(\.metadata.diagnostics).prefix(4)) + ) + } + + private func uniqueValues(from values: [T]) -> [T] { + values.reduce(into: [T]()) { result, value in + if !result.contains(value) { + result.append(value) + } + } + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBChapterTailNormalizer.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBChapterTailNormalizer.swift new file mode 100644 index 0000000..0425413 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBChapterTailNormalizer.swift @@ -0,0 +1,153 @@ +import Foundation + +/// Normalizes suspicious trailing or whitespace-only page frames after pagination. +struct RDEPUBChapterTailNormalizer { + func normalize( + _ frames: [RDEPUBTextLayoutFrame], + content: NSAttributedString, + href: String + ) -> [RDEPUBTextLayoutFrame] { + guard frames.count > 1 else { return frames } + + var normalized = frames + + var compacted: [RDEPUBTextLayoutFrame] = [] + compacted.reserveCapacity(normalized.count) + for frame in normalized { + if shouldDropWhitespaceOnlyFrame(frame, in: content) { + let note = "normalized: dropped whitespace-only intermediate page \(NSStringFromRange(frame.contentRange))" + if var previous = compacted.popLast() { + previous.diagnostics.append(note) + compacted.append(previous) + } else { + print("[EPUB][Pagination] href=\(href) dropped leading/intermediate whitespace frame \(NSStringFromRange(frame.contentRange))") + } + continue + } + compacted.append(frame) + } + normalized = compacted + + while let lastFrame = normalized.last, + shouldDropWhitespaceOnlyFrame(lastFrame, in: content) { + normalized.removeLast() + let note = "normalized: dropped whitespace-only trailing page \(NSStringFromRange(lastFrame.contentRange))" + if var previousFrame = normalized.popLast() { + previousFrame.diagnostics.append(note) + normalized.append(previousFrame) + } else { + print("[EPUB][Pagination] href=\(href) dropped trailing frame \(NSStringFromRange(lastFrame.contentRange))") + } + } + + guard normalized.count > 1, + let lastFrame = normalized.last, + let previousFrame = normalized.dropLast().last, + shouldMergeShortTrailingFrame(lastFrame, previousFrame: previousFrame, in: content) else { + return normalized + } + + let mergedFrame = mergeTrailingFrame(previousFrame, with: lastFrame) + normalized.removeLast(2) + normalized.append(mergedFrame) + return normalized + } + + private func shouldDropWhitespaceOnlyFrame( + _ frame: RDEPUBTextLayoutFrame, + in content: NSAttributedString + ) -> Bool { + guard frame.contentRange.length > 0, + attachmentCount(in: content, range: frame.contentRange) == 0 else { + return false + } + return visibleCharacterCount(in: content, range: frame.contentRange) == 0 + } + + private func shouldMergeShortTrailingFrame( + _ trailingFrame: RDEPUBTextLayoutFrame, + previousFrame: RDEPUBTextLayoutFrame, + in content: NSAttributedString + ) -> Bool { + guard trailingFrame.contentRange.length > 0, + NSMaxRange(previousFrame.contentRange) == trailingFrame.contentRange.location else { + return false + } + + let visibleCount = visibleCharacterCount(in: content, range: trailingFrame.contentRange) + let trailingAttachmentCount = attachmentCount(in: content, range: trailingFrame.contentRange) + guard visibleCount <= 2, + trailingFrame.contentRange.length <= 2, + visibleCount > 0 || trailingAttachmentCount > 0 else { + return false + } + + let previousVisibleCount = visibleCharacterCount(in: content, range: previousFrame.contentRange) + return previousVisibleCount >= max(visibleCount * 8, 12) + } + + private func mergeTrailingFrame( + _ previousFrame: RDEPUBTextLayoutFrame, + with trailingFrame: RDEPUBTextLayoutFrame + ) -> RDEPUBTextLayoutFrame { + let mergedRange = NSRange( + location: previousFrame.contentRange.location, + length: NSMaxRange(trailingFrame.contentRange) - previousFrame.contentRange.location + ) + + return RDEPUBTextLayoutFrame( + contentRange: mergedRange, + breakReason: trailingFrame.breakReason, + blockRange: trailingFrame.blockRange ?? previousFrame.blockRange, + attachmentRanges: uniqueRanges(from: previousFrame.attachmentRanges + trailingFrame.attachmentRanges), + attachmentKinds: uniqueValues(from: previousFrame.attachmentKinds + trailingFrame.attachmentKinds), + blockKinds: uniqueValues(from: previousFrame.blockKinds + trailingFrame.blockKinds), + semanticHints: uniqueValues(from: previousFrame.semanticHints + trailingFrame.semanticHints), + attachmentPlacements: uniqueValues(from: previousFrame.attachmentPlacements + trailingFrame.attachmentPlacements), + trailingFragmentID: trailingFrame.trailingFragmentID ?? previousFrame.trailingFragmentID, + diagnostics: previousFrame.diagnostics + + trailingFrame.diagnostics + + ["normalized: merged short trailing page \(NSStringFromRange(trailingFrame.contentRange)) into previous page"] + ) + } + + private func visibleCharacterCount( + in content: NSAttributedString, + range: NSRange + ) -> Int { + guard range.length > 0 else { return 0 } + let string = content.attributedSubstring(from: range).string + let filteredScalars = string.unicodeScalars.filter { scalar in + !CharacterSet.whitespacesAndNewlines.contains(scalar) + && !CharacterSet.controlCharacters.contains(scalar) + } + return filteredScalars.count + } + + private func attachmentCount(in content: NSAttributedString, range: NSRange) -> Int { + guard content.length > 0, range.length > 0 else { return 0 } + var count = 0 + content.enumerateAttribute(.attachment, in: range) { value, _, _ in + if value != nil { + count += 1 + } + } + return count + } + + private func uniqueValues(from values: [T]) -> [T] { + values.reduce(into: [T]()) { result, value in + if !result.contains(value) { + result.append(value) + } + } + } + + private func uniqueRanges(from ranges: [NSRange]) -> [NSRange] { + ranges.reduce(into: [NSRange]()) { result, value in + if !result.contains(value) { + result.append(value) + } + } + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBPaginationCacheCoordinator.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBPaginationCacheCoordinator.swift new file mode 100644 index 0000000..44b49f8 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBPaginationCacheCoordinator.swift @@ -0,0 +1,45 @@ +import UIKit + +/// Keeps pagination cache key generation and cache IO in one BuildPipeline role. +struct RDEPUBPaginationCacheCoordinator { + private let cache: RDEPUBTextBookCache? + private let layoutConfig: RDEPUBTextLayoutConfig + + init(cache: RDEPUBTextBookCache?, layoutConfig: RDEPUBTextLayoutConfig) { + self.cache = cache + self.layoutConfig = layoutConfig + } + + func cacheKey( + bookID: String, + pageSize: CGSize, + style: RDEPUBTextRenderStyle + ) -> String? { + guard let cache else { return nil } + return cache.cacheKey( + bookID: bookID, + fontSize: style.font.pointSize, + lineHeightMultiple: style.lineSpacing, + contentInsets: layoutConfig.edgeInsets, + pageSize: pageSize, + layoutConfigSignature: layoutConfig.cacheSignature + ) + } + + func load(key: String?) -> [String: RDEPUBTextChapterPaginationCache]? { + key.flatMap { cache?.load(key: $0) } + } + + func save(chapters: [RDEPUBTextChapter], key: String?) { + guard let key else { return } + let paginationCache = chapters.map { chapter in + RDEPUBTextChapterPaginationCache( + href: chapter.href, + pageRanges: chapter.pages.map(\.contentRange), + breakReasons: chapter.pages.map(\.metadata.breakReason), + semanticHints: Array(Set(chapter.pages.flatMap(\.metadata.semanticHints))) + ) + } + cache?.save(paginationCache, key: key) + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift new file mode 100644 index 0000000..3b6be60 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift @@ -0,0 +1,395 @@ +import UIKit +public final class RDEPUBTextBookBuilder { + private let renderer: RDEPUBTextRenderer + private let cache: RDEPUBTextBookCache? + private let layoutConfig: RDEPUBTextLayoutConfig + private let sampler: RDEPUBTextPerformanceSampler + private let renderPipeline: RDEPUBChapterRenderPipeline + private let paginationPipeline: RDEPUBChapterPaginationPipeline + private let tailNormalizer: RDEPUBChapterTailNormalizer + private let cacheCoordinator: RDEPUBPaginationCacheCoordinator + private let diagnosticsReporter: RDEPUBBuildDiagnosticsReporter + + /// 最后一次构建的资源引用诊断(样式表、图片等) + public private(set) var lastBuildResourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic] = [] + /// 最后一次构建的分页诊断(每章一页的分页原因、附件统计等) + public private(set) var lastBuildPaginationDiagnostics: [RDEPUBTextChapterPaginationDiagnostic] = [] + /// 最后一次构建的性能采样数据 + public private(set) var lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample] = [] + /// 最后一次构建的缓存命中/未命中统计 + public private(set) var lastBuildCacheStats: (hits: Int, misses: Int) = (0, 0) + + public init( + renderer: RDEPUBTextRenderer, + cache: RDEPUBTextBookCache? = nil, + layoutConfig: RDEPUBTextLayoutConfig = .default + ) { + self.renderer = renderer + self.cache = cache + self.layoutConfig = layoutConfig + self.sampler = RDEPUBTextPerformanceSampler() + self.renderPipeline = RDEPUBChapterRenderPipeline(renderer: renderer) + self.paginationPipeline = RDEPUBChapterPaginationPipeline() + self.tailNormalizer = RDEPUBChapterTailNormalizer() + self.cacheCoordinator = RDEPUBPaginationCacheCoordinator(cache: cache, layoutConfig: layoutConfig) + self.diagnosticsReporter = RDEPUBBuildDiagnosticsReporter() + } + + /// 默认构造器,使用 DTCoreText 渲染器 + public convenience init() { + self.init(renderer: RDEPUBDTCoreTextRenderer()) + } + + private var isPaginationDebugEnabled: Bool { + ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug") + } + + /// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志 + public func phase7SemanticSummary(title: String? = nil) -> String? { + diagnosticsReporter.phase7SemanticSummary( + title: title, + diagnostics: lastBuildPaginationDiagnostics + ) + } + + /// 核心构建方法:从 EPUB publication 构建分页书籍。 + /// + /// 流程: + /// 1. 生成缓存键,尝试加载分页缓存 + /// 2. 遍历 spine 中的线性 HTML 章节 + /// 3. 渲染每章 HTML → NSAttributedString + /// 4. 跳过空白的封面/扉页章节 + /// 5. 分页:缓存命中则使用缓存的页范围,否则调用 CoreText 分页引擎 + /// 6. 尾页规范化:丢弃纯空白尾页、合并过短尾页 + /// 7. 构建 RDEPUBTextBook 并保存分页缓存 + public func build( + parser: RDEPUBParser, + publication: RDEPUBPublication, + pageSize: CGSize, + style: RDEPUBTextRenderStyle + ) throws -> RDEPUBTextBook { + if isPaginationDebugEnabled { + print("[PaginationDebug] build pageSize=\(NSCoder.string(for: pageSize)) layoutInsets=\(NSCoder.string(for: layoutConfig.edgeInsets))") + } + var chapters: [RDEPUBTextChapter] = [] + var flatPages: [RDEPUBTextPage] = [] + lastBuildResourceDiagnostics = [] + lastBuildPaginationDiagnostics = [] + lastBuildPerformanceSamples = [] + lastBuildCacheStats = (0, 0) + sampler.reset() + + let buildStart = CFAbsoluteTimeGetCurrent() + + // 缓存查询 — WXRead 模式:只加载每章的页范围,不缓存富文本 + let bookID = publication.metadata.identifier ?? publication.metadata.title + let cacheKey = cacheCoordinator.cacheKey(bookID: bookID, pageSize: pageSize, style: style) + let cachedPagination = cacheCoordinator.load(key: cacheKey) + + for (spineIndex, item) in publication.spine.enumerated() where item.linear { + guard item.mediaType.contains("html") || item.mediaType.contains("xhtml"), + let rawHTML = parser.htmlString(forRelativePath: item.href) else { + continue + } + + // 从目录表中解析章节标题 + let chapterTitle = resolvedChapterTitle(for: item, toc: publication.tableOfContents) + let request = RDEPUBTextTypesetterPipeline().makeRequest( + from: RDEPUBTypesettingInput( + href: item.href, + title: chapterTitle, + rawHTML: rawHTML, + baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(), + style: style, + resourceResolver: publication.resourceResolver, + contentLanguageCode: publication.metadata.language, + pageSize: pageSize, + layoutConfig: layoutConfig + ) + ).request + + // 渲染 HTML → NSAttributedString + let renderStart = CFAbsoluteTimeGetCurrent() + let rendered = try renderPipeline.render(request) + let renderDuration = CFAbsoluteTimeGetCurrent() - renderStart + + lastBuildResourceDiagnostics.append(contentsOf: rendered.resourceDiagnostics) + + let plainText = rendered.attributedString.string.trimmingCharacters(in: .whitespacesAndNewlines) + if item.href.lowercased().contains("cover") { + print("[EPUB][Cover] rendered href=\(item.href) textLength=\(plainText.count) attrLength=\(rendered.attributedString.length) attachments=\(attachmentCount(in: rendered.attributedString))") + } + // 跳过空白的封面/扉页章节 + if shouldSkipChapter(item: item, content: rendered.attributedString, text: plainText) { + if item.href.lowercased().contains("cover") { + print("[EPUB][Cover] skipped href=\(item.href)") + } + continue + } + + let chapterIndex = chapters.count + let content = NSMutableAttributedString(attributedString: rendered.attributedString) + + // 分页:封面图片章节走特殊路径,缓存命中则跳过分页引擎 + let paginateStart = CFAbsoluteTimeGetCurrent() + let layoutFrames: [RDEPUBTextLayoutFrame] + let isCacheHit: Bool + if isAttachmentOnlyCoverChapter(item: item, content: content, plainText: plainText) { + // 纯图片封面:整个内容作为一页 + layoutFrames = [ + RDEPUBTextLayoutFrame( + contentRange: NSRange(location: 0, length: content.length), + breakReason: .chapterEnd, + blockRange: nil, + attachmentRanges: attachmentRanges(in: content), + attachmentKinds: [], + blockKinds: [], + semanticHints: [], + attachmentPlacements: [], + trailingFragmentID: nil, + diagnostics: [ + "page break: chapterEnd", + "cover fallback: single attachment page", + "page range: \(NSStringFromRange(NSRange(location: 0, length: content.length)))" + ] + ) + ] + isCacheHit = false + } else if let cached = cachedPagination?[item.href] { + // 缓存命中:直接使用缓存的页范围,跳过 CoreText 分页 + layoutFrames = cached.pageRanges.enumerated().map { idx, range in + let breakReason = idx < cached.breakReasons.count ? cached.breakReasons[idx] : .frameLimit + return RDEPUBTextLayoutFrame( + contentRange: range, + breakReason: breakReason, + blockRange: nil, + attachmentRanges: [], + attachmentKinds: [], + blockKinds: [], + semanticHints: cached.semanticHints, + attachmentPlacements: [], + trailingFragmentID: nil, + diagnostics: ["page break: \(breakReason.rawValue)", "page range: \(NSStringFromRange(range))", "source: cache hit"] + ) + } + isCacheHit = true + } else { + // 缓存未命中:调用 CoreText 分页引擎 + layoutFrames = content.length > 0 + ? paginationPipeline.frames( + for: content, + pageSize: pageSize, + config: layoutConfig, + fragmentOffsets: rendered.fragmentOffsets + ) + : [] + isCacheHit = false + } + let paginateDuration = CFAbsoluteTimeGetCurrent() - paginateStart + + // 尾页规范化:丢弃纯空白尾页、合并过短尾页 + let normalizedFrames = tailNormalizer.normalize( + layoutFrames, + content: content, + href: item.href + ) + let effectiveFrames = normalizedFrames.isEmpty && content.length > 0 + ? [ + RDEPUBTextLayoutFrame( + contentRange: NSRange(location: 0, length: content.length), + breakReason: .chapterEnd, + blockRange: nil, + attachmentRanges: [], + attachmentKinds: [], + blockKinds: [], + semanticHints: [], + attachmentPlacements: [], + trailingFragmentID: nil, + diagnostics: [ + "page break: chapterEnd", + "page range: \(NSStringFromRange(NSRange(location: 0, length: content.length)))" + ] + ) + ] + : normalizedFrames + if item.href.lowercased().contains("cover") { + print("[EPUB][Cover] paginated href=\(item.href) pages=\(effectiveFrames.count) firstRange=\(effectiveFrames.first.map { NSStringFromRange($0.contentRange) } ?? "none")") + } + + // 记录性能采样 + sampler.record(RDEPUBTextPerformanceSample( + chapterHref: item.href, + renderDuration: renderDuration, + paginateDuration: paginateDuration, + pageCount: effectiveFrames.count, + attributedStringLength: content.length, + cacheHit: isCacheHit + )) + if isCacheHit { + lastBuildCacheStats.hits += 1 + } else { + lastBuildCacheStats.misses += 1 + } + + // 构建页面和章节模型 + let chapterAttributedContent = content.copy() as! NSAttributedString + let pages = effectiveFrames.enumerated().map { localPageIndex, frame in + let range = frame.contentRange + return RDEPUBTextPage( + absolutePageIndex: flatPages.count + localPageIndex, + chapterIndex: chapterIndex, + spineIndex: spineIndex, + href: item.href, + chapterTitle: chapterTitle, + pageIndexInChapter: localPageIndex, + totalPagesInChapter: effectiveFrames.count, + chapterContent: chapterAttributedContent, + content: content.attributedSubstring(from: range), + contentRange: range, + pageStartOffset: range.location, + pageEndOffset: range.location + max(range.length - 1, 0), + metadata: frame.metadata + ) + } + + if isPaginationDebugEnabled, + item.href.contains("Chapter_3.xhtml") { + print("[PaginationDebug] href=\(item.href) pages=\(pages.count)") + for page in pages { + let preview = debugPreview(for: page.content, limit: 36) + print("[PaginationDebug] absPage=\(page.absolutePageIndex + 1) localPage=\(page.pageIndexInChapter + 1) range=\(NSStringFromRange(page.contentRange)) break=\(page.metadata.breakReason.rawValue) preview=\(preview)") + for note in page.metadata.diagnostics.prefix(4) { + print("[PaginationDebug] note=\(note)") + } + } + } + + chapters.append( + RDEPUBTextChapter( + chapterIndex: chapterIndex, + spineIndex: spineIndex, + href: item.href, + title: chapterTitle, + attributedContent: chapterAttributedContent, + fragmentOffsets: rendered.fragmentOffsets, + pageBreakReasons: pages.map(\.metadata.breakReason), + pages: pages + ) + ) + lastBuildPaginationDiagnostics.append( + diagnosticsReporter.chapterDiagnostic( + href: item.href, + title: chapterTitle, + pages: pages + ) + ) + flatPages.append(contentsOf: pages) + } + + let book = RDEPUBTextBook(chapters: chapters, pages: flatPages) + + sampler.totalBuildDuration = CFAbsoluteTimeGetCurrent() - buildStart + + // 保存分页缓存(只缓存页范围和分页原因,不缓存富文本) + cacheCoordinator.save(chapters: chapters, key: cacheKey) + + print(sampler.summary()) + lastBuildPerformanceSamples = sampler.samples + + return book + } + + // MARK: - 章节标题解析 + + /// 从目录表中查找章节标题,找不到则回退到 spine item 的 title 或 href + private func resolvedChapterTitle(for item: RDEPUBSpineItem, toc: [EPUBTableOfContentsItem]) -> String { + if let title = flattenedTOCItems(from: toc).first(where: { tocItem in + tocItem.href.components(separatedBy: "#").first == item.href + })?.title.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty { + return title + } + let trimmedTitle = item.title.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedTitle.isEmpty ? item.href : trimmedTitle + } + + /// 递归展开嵌套目录为扁平列表 + private func flattenedTOCItems(from items: [EPUBTableOfContentsItem]) -> [EPUBTableOfContentsItem] { + items.flatMap { item in + [item] + flattenedTOCItems(from: item.children) + } + } + + // MARK: - 章节过滤 + + /// 判断是否应跳过该章节(空白的封面/扉页,无文本且无附件) + private func shouldSkipChapter(item: RDEPUBSpineItem, content: NSAttributedString, text: String) -> Bool { + let lowercasedHref = item.href.lowercased() + var hasAttachment = false + if content.length > 0 { + content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in + guard value != nil else { return } + hasAttachment = true + stop.pointee = true + } + } + if text.isEmpty && !hasAttachment && (lowercasedHref.contains("cover") || lowercasedHref.contains("title")) { + return true + } + return false + } + + // MARK: - 附件统计 + + /// 统计富文本中的附件数量 + private func attachmentCount(in content: NSAttributedString) -> Int { + guard content.length > 0 else { return 0 } + var count = 0 + content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, _ in + if value != nil { + count += 1 + } + } + return count + } + + /// 获取富文本中所有附件的 NSRange 列表 + private func attachmentRanges(in content: NSAttributedString) -> [NSRange] { + guard content.length > 0 else { return [] } + var ranges: [NSRange] = [] + content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, range, _ in + if value != nil { + ranges.append(range) + } + } + return ranges + } + + // MARK: - 封面章节检测 + + /// 判断是否为纯图片封面章节(href 包含 cover 且有附件但几乎无文本) + private func isAttachmentOnlyCoverChapter( + item: RDEPUBSpineItem, + content: NSAttributedString, + plainText: String + ) -> Bool { + let lowercasedHref = item.href.lowercased() + guard lowercasedHref.contains("cover") else { return false } + let trimmed = plainText.trimmingCharacters(in: .whitespacesAndNewlines) + return attachmentCount(in: content) > 0 && trimmed.count <= 1 + } + + private func debugPreview(for content: NSAttributedString, limit: Int) -> String { + let collapsed = content.string + .replacingOccurrences(of: "\n", with: " ") + .replacingOccurrences(of: "\r", with: " ") + .replacingOccurrences(of: "\t", with: " ") + .trimmingCharacters(in: .whitespacesAndNewlines) + guard !collapsed.isEmpty else { return "" } + if collapsed.count <= limit { + return collapsed + } + let head = collapsed.prefix(limit) + return "\(head)…" + } + +} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift similarity index 100% rename from Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift rename to Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookCache.swift diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swift new file mode 100644 index 0000000..b3c9ed5 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookModels.swift @@ -0,0 +1,193 @@ +import UIKit + +// MARK: - 章节分页诊断数据 + +/// 单个章节的分页诊断信息,用于调试和质量检测。 +/// 记录页数、分页原因、附件/块级元素统计等。 +public struct RDEPUBTextChapterPaginationDiagnostic: Equatable { + public var href: String + public var title: String + public var pageCount: Int + /// 每页的分页原因列表 + public var breakReasons: [RDEPUBTextPageBreakReason] + /// 包含附件的页数 + public var attachmentPageCount: Int + /// 因块级/附件边界调整而分页的页数 + public var blockAdjustedPageCount: Int + public var blockKinds: [RDEPUBTextBlockKind] + public var semanticHints: [RDEPUBTextSemanticHint] + public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] + /// 采样诊断日志(最多 4 条) + public var sampleNotes: [String] +} + +// MARK: - 页面数据模型 + +/// 分页后的单页数据,包含全书绝对页码、所属章节、内容范围等。 +/// +/// 每个 `RDEPUBTextPage` 由 `RDEPUBTextLayoutFrame` 生成, +/// `contentRange` 标记了该页在章节富文本中的字符范围。 +public struct RDEPUBTextPage: Equatable { + /// 全书绝对页码(从 0 开始) + public var absolutePageIndex: Int + public var chapterIndex: Int + public var spineIndex: Int + public var href: String + public var chapterTitle: String + /// 该页在所属章节中的相对页码(从 0 开始) + public var pageIndexInChapter: Int + public var totalPagesInChapter: Int + /// 所属章节的完整富文本内容(用于跨页查询) + public var chapterContent: NSAttributedString + /// 该页的富文本片段 + public var content: NSAttributedString + /// 该页在章节富文本中的范围 + public var contentRange: NSRange + public var pageStartOffset: Int + public var pageEndOffset: Int + /// 页的元数据(分页原因、附件、语义标记等) + public var metadata: RDEPUBTextPageMetadata +} + +// MARK: - 章节数据模型 + +/// 渲染并分页后的单个章节,包含完整富文本和分页结果。 +public struct RDEPUBTextChapter: Equatable { + public var chapterIndex: Int + public var spineIndex: Int + public var href: String + public var title: String + /// 章节完整富文本内容 + public var attributedContent: NSAttributedString + /// fragment ID → 字符偏移量映射(用于锚点定位) + public var fragmentOffsets: [String: Int] + public var pageBreakReasons: [RDEPUBTextPageBreakReason] + public var pages: [RDEPUBTextPage] +} + +// MARK: - 分页书籍模型 + +/// 整本 EPUB 的分页书籍模型,包含所有章节、页面和全局索引表。 +/// +/// 这是 EPUBTextRendering 层的最终产物,由 `RDEPUBTextBookBuilder.build()` 生成。 +/// 通过 `chapterData(for:)` 或 `chapterData(atChapterIndex:)` 获取 `RDEPUBChapterData` 进行查询。 +public struct RDEPUBTextBook { + public var chapters: [RDEPUBTextChapter] + 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 + self.pages = pages + self.indexTable = RDEPUBTextIndexTable(chapters: chapters) + } + + public static func == (lhs: RDEPUBTextBook, rhs: RDEPUBTextBook) -> Bool { + lhs.chapters == rhs.chapters && lhs.pages == rhs.pages + } + + /// 按 href 获取章节的数据访问层(包含索引表) + public func chapterData(for href: String) -> RDEPUBChapterData? { + guard let chapter = chapters.first(where: { $0.href == href }) else { return nil } + 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 { + return nil + } + return pages[pageNumber - 1] + } + + /// 根据持久化位置计算对应页码(从 1 开始)。 + /// + /// 解析优先级: + /// 1. rangeAnchor 锚点定位 + /// 2. fragment 片段 ID + /// 3. navigationProgression 进度百分比回退 + public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? { + guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier), + let chapterData = chapterData(for: normalizedLocation.href) else { + return nil + } + + if let anchor = normalizedLocation.rangeAnchor?.start, + let page = positionConverter.pageNumber(for: anchor) { + return page + } + + if let anchor = indexTable.anchor(for: normalizedLocation), + let page = positionConverter.pageNumber(for: anchor) { + return page + } + + return chapterData.pageNumber(for: normalizedLocation) + } + + /// 根据页码生成持久化位置(RDEPUBLocation),包含起止锚点 + public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? { + guard let chapterData = chapterData(forPageNumber: pageNumber), + let page = page(at: pageNumber) else { + return nil + } + return chapterData.location(forPage: page, bookIdentifier: bookIdentifier) + } +} + +// MARK: - 分页书籍构建器 + +/// 从 EPUB publication 构建分页书籍模型的核心构建器。 +/// +/// 渲染链路:遍历 spine → 渲染每章 HTML → 分页 → 合并/规范化尾页 → 构建 RDEPUBTextBook +/// +/// 特性: +/// - 支持分页缓存(WXRead 模式:只缓存页范围,不缓存富文本) +/// - 性能采样(记录每章渲染/分页耗时) +/// - 尾页规范化(丢弃纯空白尾页、合并过短尾页) diff --git a/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBuildPipelineInterfaces.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBuildPipelineInterfaces.swift new file mode 100644 index 0000000..285cb25 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBuildPipelineInterfaces.swift @@ -0,0 +1,46 @@ +import UIKit + +protocol RDEPUBTextBookBuilding { + func build( + parser: RDEPUBParser, + publication: RDEPUBPublication, + pageSize: CGSize, + style: RDEPUBTextRenderStyle + ) throws -> RDEPUBTextBook +} + +struct RDEPUBChapterRenderPipeline { + private let renderer: RDEPUBTextRenderer + + init(renderer: RDEPUBTextRenderer) { + self.renderer = renderer + } + + func render(_ request: RDEPUBTextChapterRenderRequest) throws -> RDEPUBRenderedChapterContent { + try renderer.renderChapter(request: request) + } +} + +struct RDEPUBChapterPaginationPipeline { + private let frameFactory: RDEPUBPageFrameBuilding + + init(frameFactory: RDEPUBPageFrameBuilding = RDEPUBCoreTextPageFrameFactory()) { + self.frameFactory = frameFactory + } + + func frames( + for content: NSAttributedString, + pageSize: CGSize, + config: RDEPUBTextLayoutConfig, + fragmentOffsets: [String: Int] + ) -> [RDEPUBTextLayoutFrame] { + frameFactory.makeFrames( + attributedString: content, + pageSize: pageSize, + config: config, + fragmentOffsets: fragmentOffsets + ) + } +} + +extension RDEPUBTextBookBuilder: RDEPUBTextBookBuilding {} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift b/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextPerformanceSampler.swift similarity index 100% rename from Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPerformanceSampler.swift rename to Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextPerformanceSampler.swift diff --git a/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift new file mode 100644 index 0000000..07aa242 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift @@ -0,0 +1,270 @@ +import CoreText +import UIKit + +#if canImport(DTCoreText) +import DTCoreText +#endif + +/// 章节分页计数器:顶层编排循环,将富文本按页面尺寸拆分为多帧。 +/// +/// 分页策略优先级(从高到低): +/// 1. avoidPageBreakInside — 不在保护块内分页(WXRead 行级回退扫描) +/// 2. keepWithNext — 标题等元素需与下一段同页 +/// 3. 语义边界 — pageBreakBefore/After 等显式分页标记 +/// 4. pageRelate — 微信读书式的跨页关联元素 +/// 5. 附件边界 — 块级附件应整体移到下一页 +/// 6. 帧限制 — 默认按 CoreText 可视范围分页 +struct RDEPUBChapterPageCounter { + private let factory: RDEPUBCoreTextPageFrameFactory + private let attributedString: NSAttributedString + private let pageSize: CGSize + private let config: RDEPUBTextLayoutConfig + private let pageBreakPolicy: RDEPUBPageBreakPolicy + + /// CoreText 帧设置器 + private let framesetter: CTFramesetter + /// DTCoreText 路径可直接消费的单矩形布局区域 + private let dtLayoutRect: CGRect + + init(factory: RDEPUBCoreTextPageFrameFactory) { + self.factory = factory + self.attributedString = factory.attributedString + self.pageSize = factory.pageSize + self.config = factory.config + self.pageBreakPolicy = RDEPUBPageBreakPolicy(attributedString: factory.attributedString) + self.framesetter = CTFramesetterCreateWithAttributedString(factory.attributedString) + self.dtLayoutRect = factory.config.contentRect(fallback: factory.pageSize) + } + + /// 执行分页,返回布局帧列表(每帧对应一页)。 + func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] { + guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else { + return [] + } + +#if canImport(DTCoreText) + return layoutFramesUsingDTCoreText(fragmentOffsets: fragmentOffsets) +#else + return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) +#endif + } + + // MARK: - CoreText 分页路径(回退方案) + + private func layoutFramesUsingCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] { + guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else { + return [] + } + + var frames: [RDEPUBTextLayoutFrame] = [] + var location = 0 + let resolvedSize = config.resolvedFrameSize(fallback: pageSize) + let usableWidth = resolvedSize.width - config.edgeInsets.left - config.edgeInsets.right + let usableHeight = resolvedSize.height - config.edgeInsets.top - config.edgeInsets.bottom + + guard usableWidth > 0, usableHeight > 0 else { + return [] + } + + while location < attributedString.length { + let framePath = CGMutablePath() + let pageRect = CGRect( + x: config.edgeInsets.left, + y: config.edgeInsets.bottom, + width: usableWidth, + height: usableHeight + ) + framePath.addRect(pageRect) + let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), framePath, nil) + let proposedRange = proposedRangeUsingWXReadPageCount( + from: frame, + start: location, + usableHeight: usableHeight, + totalLength: attributedString.length + ) + guard proposedRange.length > 0 else { + break + } + + let avoidAdjusted = factory.trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange) + let lineRanges = RDEPUBCoreTextPageFrameFactory.lineRanges(from: frame) + + let adjusted = pageBreakPolicy.adjustedRange( + from: avoidAdjusted, + totalLength: attributedString.length, + lineRanges: lineRanges, + factory: factory + ) + let trailingFragmentID = factory.nearestTrailingFragmentID( + endingAt: adjusted.range.location + adjusted.range.length, + fragmentOffsets: fragmentOffsets + ) + frames.append( + RDEPUBTextLayoutFrame( + contentRange: adjusted.range, + breakReason: adjusted.breakReason, + blockRange: adjusted.blockRange, + attachmentRanges: adjusted.attachmentRanges, + attachmentKinds: adjusted.attachmentKinds, + blockKinds: adjusted.blockKinds, + semanticHints: adjusted.semanticHints, + attachmentPlacements: adjusted.attachmentPlacements, + trailingFragmentID: trailingFragmentID, + diagnostics: adjusted.diagnostics + ) + ) + + let nextLocation = adjusted.range.location + adjusted.range.length + guard nextLocation > location else { + location += max(proposedRange.length, 1) + continue + } + location = nextLocation + } + + return frames + } + + // MARK: - DTCoreText 分页路径(首选方案) + +#if canImport(DTCoreText) + private func layoutFramesUsingDTCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] { + guard config.numberOfColumns == 1 else { + return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) + } + + guard let layouter = DTCoreTextLayouter(attributedString: attributedString) else { + return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) + } + + layouter.shouldCacheLayoutFrames = false + + var frames: [RDEPUBTextLayoutFrame] = [] + var location = 0 + let pageRect = dtLayoutRect + + while location < attributedString.length { + guard let layoutFrame = layouter.layoutFrame(with: pageRect, range: NSRange(location: location, length: 0)) else { + break + } + + let visibleRange = layoutFrame.visibleStringRange() + guard visibleRange.length > 0 else { + break + } + + let proposedRange = NSRange(location: location, length: visibleRange.length) + let avoidAdjusted = factory.trimmedRangeForAvoidPageBreakInside(from: layoutFrame, proposed: proposedRange) + let lineAdjusted = factory.trimmedRangeForKeepWithNext(from: layoutFrame, proposed: avoidAdjusted) + let lineRanges = RDEPUBCoreTextPageFrameFactory.lineRanges(from: layoutFrame) + let adjusted = pageBreakPolicy.adjustedRange( + from: lineAdjusted, + totalLength: attributedString.length, + lineRanges: lineRanges, + factory: factory + ) + let verifiedRange: NSRange + if adjusted.breakReason == .attachmentBoundary { + verifiedRange = verifiedDisplayRange(for: adjusted.range) + } else { + verifiedRange = adjusted.range + } + let trailingFragmentID = factory.nearestTrailingFragmentID( + endingAt: verifiedRange.location + verifiedRange.length, + fragmentOffsets: fragmentOffsets + ) + let diagnostics = verifiedRange == adjusted.range + ? adjusted.diagnostics + : adjusted.diagnostics + ["verified-display-range \(NSStringFromRange(adjusted.range)) -> \(NSStringFromRange(verifiedRange))"] + + frames.append( + RDEPUBTextLayoutFrame( + contentRange: verifiedRange, + breakReason: adjusted.breakReason, + blockRange: factory.blockRange(at: max(verifiedRange.location, verifiedRange.location + verifiedRange.length - 1)), + attachmentRanges: factory.attachmentRanges(in: verifiedRange), + attachmentKinds: factory.attachmentKinds(in: verifiedRange), + blockKinds: factory.blockKinds(in: verifiedRange), + semanticHints: factory.semanticHints(in: verifiedRange), + attachmentPlacements: factory.attachmentPlacements(in: verifiedRange), + trailingFragmentID: trailingFragmentID, + diagnostics: diagnostics + ) + ) + + let nextLocation = verifiedRange.location + verifiedRange.length + guard nextLocation > location else { + location += max(visibleRange.length, 1) + continue + } + location = nextLocation + } + + return frames + } + + private func verifiedDisplayRange(for range: NSRange) -> NSRange { + guard let clampedRange = factory.clampedRange(range), + clampedRange.length > 0, + !factory.attachmentRanges(in: clampedRange).isEmpty else { + return range + } + + let pageContent = attributedString.attributedSubstring(from: clampedRange) + guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else { + return clampedRange + } + + layouter.shouldCacheLayoutFrames = false + guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else { + return clampedRange + } + + let visibleRange = layoutFrame.visibleStringRange() + guard visibleRange.length > 0, visibleRange.length < clampedRange.length else { + return clampedRange + } + + return NSRange(location: clampedRange.location, length: visibleRange.length) + } +#endif + + // MARK: - WXRead 分页对齐 + + /// 逐句对齐 WXRead `WRChapterPageCount.recalculatePageRangesForAttributedString` 的分页循环。 + private func proposedRangeUsingWXReadPageCount( + from frame: CTFrame, + start location: Int, + usableHeight: CGFloat, + totalLength: Int + ) -> NSRange { + let lines = CTFrameGetLines(frame) as! [CTLine] + guard !lines.isEmpty else { + return NSRange(location: location, length: 0) + } + + var origins = [CGPoint](repeating: .zero, count: lines.count) + CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) + + var pageCharCount = 0 + for (index, line) in lines.enumerated() { + let lineRange = CTLineGetStringRange(line) + let lineY = origins[index].y + var ascent: CGFloat = 0 + var descent: CGFloat = 0 + CTLineGetTypographicBounds(line, &ascent, &descent, nil) + + if lineY - ascent > usableHeight { + break + } + + pageCharCount += lineRange.length + } + + if pageCharCount == 0 { + pageCharCount = 1 + } + + return NSRange(location: location, length: min(pageCharCount, totalLength - location)) + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift new file mode 100644 index 0000000..7ba8244 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift @@ -0,0 +1,389 @@ +import CoreText +import UIKit + +#if canImport(DTCoreText) +import DTCoreText +#endif + +/// CoreText 帧工厂:负责帧创建、行级裁剪、属性查询和诊断构建。 +/// +/// 叶节点组件,被 RDEPUBChapterPageCounter 和 RDEPUBPageBreakPolicy 调用。 +struct RDEPUBCoreTextPageFrameFactory: RDEPUBPageFrameBuilding { + let attributedString: NSAttributedString + let pageSize: CGSize + let config: RDEPUBTextLayoutConfig + private let pageBreakPolicy: RDEPUBPageBreakPolicy + + init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) { + self.attributedString = attributedString + self.pageSize = pageSize + self.config = config + self.pageBreakPolicy = RDEPUBPageBreakPolicy(attributedString: attributedString) + } + + /// 协议要求的便捷初始化(使用默认配置) + init() { + self.init(attributedString: NSAttributedString(), pageSize: .zero, config: .default) + } + + // MARK: - RDEPUBPageFrameBuilding 协议 + + func makeFrames( + attributedString: NSAttributedString, + pageSize: CGSize, + config: RDEPUBTextLayoutConfig, + fragmentOffsets: [String: Int] + ) -> [RDEPUBTextLayoutFrame] { + let factory = RDEPUBCoreTextPageFrameFactory(attributedString: attributedString, pageSize: pageSize, config: config) + let counter = RDEPUBChapterPageCounter(factory: factory) + return counter.layoutFrames(fragmentOffsets: fragmentOffsets) + } + + // MARK: - 帧构建 + + /// 从列矩形构建 CGPath(用于 CTFramesetterCreateFrame)。 + static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath { + let columnRects = config.columnRects(fallback: pageSize) + guard columnRects.count > 1 else { + return CGPath(rect: columnRects.first ?? CGRect(origin: .zero, size: pageSize), transform: nil) + } + + let path = CGMutablePath() + for rect in columnRects { + path.addRect(rect) + } + return path + } + + // MARK: - 行级裁剪 + + /// 从 CTFrame 最后一行向前扫描,移除落在 avoidPageBreakInside 保护块内的尾部行。 + func trimmedRangeForAvoidPageBreakInside( + from frame: CTFrame, + proposed: NSRange + ) -> NSRange { + guard config.avoidPageBreakInsideEnabled else { return proposed } + + let lines = CTFrameGetLines(frame) as! [CTLine] + guard !lines.isEmpty else { return proposed } + + let lineRanges = lines.map { + let range = CTLineGetStringRange($0) + return NSRange(location: range.location, length: range.length) + } + return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges) + } + + /// CoreText 路径的 keepWithNext 处理:从最后行向前扫描 + func trimmedRangeForKeepWithNext( + from frame: CTFrame, + proposed: NSRange + ) -> NSRange { + let lines = CTFrameGetLines(frame) as! [CTLine] + let lineRanges = lines.map { + let range = CTLineGetStringRange($0) + return NSRange(location: range.location, length: range.length) + } + return trimmedRangeForKeepWithNext(proposed: proposed, lineRanges: lineRanges) + } + +#if canImport(DTCoreText) + /// DTCoreText 路径的 avoidPageBreakInside 处理 + func trimmedRangeForAvoidPageBreakInside( + from layoutFrame: DTCoreTextLayoutFrame, + proposed: NSRange + ) -> NSRange { + guard config.avoidPageBreakInsideEnabled else { return proposed } + + guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine], !lines.isEmpty else { + return proposed + } + let lineRanges = lines.map { $0.stringRange() } + return trimmedRangeForAvoidPageBreakInside(proposed: proposed, lineRanges: lineRanges) + } + + /// DTCoreText 路径的 keepWithNext 处理 + func trimmedRangeForKeepWithNext( + from layoutFrame: DTCoreTextLayoutFrame, + proposed: NSRange + ) -> NSRange { + guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine], !lines.isEmpty else { + return proposed + } + let lineRanges = lines.map { $0.stringRange() } + return trimmedRangeForKeepWithNext(proposed: proposed, lineRanges: lineRanges) + } +#endif + + /// 从最后行向前扫描,移除落在 avoidPageBreakInside 保护块内的尾部行(通用路径)。 + func trimmedRangeForAvoidPageBreakInside( + proposed: NSRange, + lineRanges: [NSRange] + ) -> NSRange { + guard config.avoidPageBreakInsideEnabled, !lineRanges.isEmpty else { return proposed } + + let kMaxLinesToRemove = 3 + var linesToRemove = 0 + + for lineRange in lineRanges.reversed() { + if pageBreakPolicy.lineIsInAvoidPageBreakInsideBlock(lineRange) { + linesToRemove += 1 + if linesToRemove >= kMaxLinesToRemove { + linesToRemove = kMaxLinesToRemove + break + } + } else { + break + } + } + + guard linesToRemove > 0 else { return proposed } + + let validLineCount = lineRanges.count - linesToRemove + guard validLineCount > 0 else { return proposed } + + let lastValidLine = lineRanges[validLineCount - 1] + let endLocation = lastValidLine.location + lastValidLine.length + let adjustedLength = endLocation - proposed.location + guard adjustedLength > 0 else { return proposed } + return NSRange(location: proposed.location, length: adjustedLength) + } + + /// 从最后行向前扫描,移除落在 keepWithNext 保护块内的尾部行。 + func trimmedRangeForKeepWithNext( + proposed: NSRange, + lineRanges: [NSRange] + ) -> NSRange { + guard !lineRanges.isEmpty else { return proposed } + + let kMaxLinesToRemove = 3 + var linesToRemove = 0 + + for lineRange in lineRanges.reversed() { + if pageBreakPolicy.lineIsInKeepWithNextBlock(lineRange) { + linesToRemove += 1 + if linesToRemove >= kMaxLinesToRemove { + linesToRemove = kMaxLinesToRemove + break + } + } else { + break + } + } + + guard linesToRemove > 0 else { return proposed } + + let validLineCount = lineRanges.count - linesToRemove + guard validLineCount > 0 else { return proposed } + + let lastValidLine = lineRanges[validLineCount - 1] + let endLocation = lastValidLine.location + lastValidLine.length + let adjustedLength = endLocation - proposed.location + guard adjustedLength > 0 else { return proposed } + return NSRange(location: proposed.location, length: adjustedLength) + } + + // MARK: - 行信息提取 + + /// 获取 CTFrame 中所有行的字符范围 + static func lineRanges(from frame: CTFrame) -> [NSRange] { + let lines = CTFrameGetLines(frame) as! [CTLine] + return lines.map { + let lineRange = CTLineGetStringRange($0) + return NSRange(location: lineRange.location, length: lineRange.length) + } + } + +#if canImport(DTCoreText) + /// 获取 DTCoreTextLayoutFrame 中所有行的字符范围 + static func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] { + guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else { + return [] + } + return lines.map { $0.stringRange() } + } +#endif + + // MARK: - 属性查询 + + /// 获取指定位置的块级元素范围 + func blockRange(at location: Int) -> NSRange? { + guard location >= 0, location < attributedString.length else { return nil } + let attributes = attributedString.attributes(at: location, effectiveRange: nil) + if let encodedRange = attributes[.rdPageBlockRange] as? String { + return NSRangeFromString(encodedRange) + } + return nil + } + + /// 获取指定位置的块级元素类型 + func blockKind(at location: Int) -> RDEPUBTextBlockKind? { + guard location >= 0, location < attributedString.length else { return nil } + let attributes = attributedString.attributes(at: location, effectiveRange: nil) + guard let rawValue = attributes[.rdPageBlockKind] as? String else { return nil } + return RDEPUBTextBlockKind(rawValue: rawValue) + } + + /// 获取指定位置的附件布局方式 + func attachmentPlacement(at location: Int) -> RDEPUBTextAttachmentPlacement? { + guard location >= 0, location < attributedString.length else { return nil } + let attributes = attributedString.attributes(at: location, effectiveRange: nil) + guard let rawValue = attributes[.rdPageAttachmentPlacement] as? String else { return nil } + return RDEPUBTextAttachmentPlacement(rawValue: rawValue) + } + + /// 获取包含指定位置的段落范围 + func paragraphRange(containing location: Int) -> NSRange { + let source = attributedString.string as NSString + guard source.length > 0 else { return NSRange(location: 0, length: 0) } + let safeLocation = min(max(location, 0), max(source.length - 1, 0)) + return source.paragraphRange(for: NSRange(location: safeLocation, length: 0)) + } + + /// 获取指定范围内的所有附件字符范围 + func attachmentRanges(in range: NSRange) -> [NSRange] { + guard let safeRange = clampedRange(range), safeRange.length > 0 else { + return [] + } + var results: [NSRange] = [] + attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, _ in + guard value != nil else { return } + results.append(attributeRange) + } + return results + } + + /// 获取指定位置的语义提示列表 + func semanticHints(at location: Int) -> [RDEPUBTextSemanticHint] { + guard location >= 0, location < attributedString.length else { return [] } + let attributes = attributedString.attributes(at: location, effectiveRange: nil) + guard let rawValue = attributes[.rdPageSemanticHints] as? String else { return [] } + return rawValue + .split(separator: ",") + .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } + } + + /// 获取指定范围内的附件类型列表(去重) + func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] { + guard let safeRange = clampedRange(range), safeRange.length > 0 else { + return [] + } + var kinds: [RDEPUBTextAttachmentKind] = [] + attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, _, _ in + guard let rawValue = value as? String, + let kind = RDEPUBTextAttachmentKind(rawValue: rawValue), + !kinds.contains(kind) else { + return + } + kinds.append(kind) + } + return kinds + } + + /// 获取指定范围内的块级元素类型列表(去重) + func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] { + guard let safeRange = clampedRange(range), safeRange.length > 0 else { + return [] + } + var kinds: [RDEPUBTextBlockKind] = [] + attributedString.enumerateAttribute(.rdPageBlockKind, in: safeRange) { value, _, _ in + guard let rawValue = value as? String, + let kind = RDEPUBTextBlockKind(rawValue: rawValue), + !kinds.contains(kind) else { + return + } + kinds.append(kind) + } + return kinds + } + + /// 获取指定范围内的语义提示列表(去重) + func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] { + guard let safeRange = clampedRange(range), safeRange.length > 0 else { + return [] + } + var hints: [RDEPUBTextSemanticHint] = [] + attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, _, _ in + guard let rawValue = value as? String else { return } + for hint in rawValue.split(separator: ",").compactMap({ RDEPUBTextSemanticHint(rawValue: String($0)) }) where !hints.contains(hint) { + hints.append(hint) + } + } + return hints + } + + /// 获取指定范围内的附件布局方式列表(去重) + func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] { + guard let safeRange = clampedRange(range), safeRange.length > 0 else { + return [] + } + var placements: [RDEPUBTextAttachmentPlacement] = [] + attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: safeRange) { value, _, _ in + guard let rawValue = value as? String, + let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue), + !placements.contains(placement) else { + return + } + placements.append(placement) + } + return placements + } + + /// 将范围裁剪到 attributedString 的合法边界内。 + 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)) + } + + /// 查找指定位置之前最近的 fragment ID(用于阅读位置恢复) + func nearestTrailingFragmentID( + endingAt location: Int, + fragmentOffsets: [String: Int] + ) -> String? { + fragmentOffsets + .filter { $0.value <= location } + .max { lhs, rhs in lhs.value < rhs.value }? + .key + } + + // MARK: - 诊断日志 + + /// 生成分页诊断日志 + func diagnostics( + reason: RDEPUBTextPageBreakReason, + range: NSRange, + attachmentRanges: [NSRange], + blockRange: NSRange?, + blockKinds: [RDEPUBTextBlockKind], + semanticHints: [RDEPUBTextSemanticHint], + attachmentPlacements: [RDEPUBTextAttachmentPlacement], + trigger: String? = nil + ) -> [String] { + var items = ["page break: \(reason.rawValue)", "page range: \(NSStringFromRange(range))"] + if let blockRange { + items.append("block range: \(NSStringFromRange(blockRange))") + } + if !attachmentRanges.isEmpty { + items.append("attachment ranges: \(attachmentRanges.map(NSStringFromRange).joined(separator: ","))") + } + if !blockKinds.isEmpty { + items.append("block kinds: \(blockKinds.map(\.rawValue).joined(separator: ","))") + } + if !semanticHints.isEmpty { + items.append("semantic hints: \(semanticHints.map(\.rawValue).joined(separator: ","))") + } + if !attachmentPlacements.isEmpty { + items.append("attachment placements: \(attachmentPlacements.map(\.rawValue).joined(separator: ","))") + } + if let trigger, !trigger.isEmpty { + items.append("semantic trigger: \(trigger)") + } + return items + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift new file mode 100644 index 0000000..77130c0 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift @@ -0,0 +1,308 @@ +import Foundation +import UIKit + +/// 分页规则策略:语义边界搜索、行级保护检查、范围调整调度器。 +/// +/// 不构建 CoreText frame,只基于 attributed string 属性做规则判定。 +struct RDEPUBPageBreakPolicy { + private let attributedString: NSAttributedString + + init(attributedString: NSAttributedString) { + self.attributedString = attributedString + } + + // MARK: - 行级保护检查 + + /// 检查指定行范围是否落在 avoidPageBreakInside 保护块内。 + 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 + } + + /// 检查指定行范围是否落在 keepWithNext 保护块内。 + 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 + } + + // MARK: - 范围调整调度器 + + /// 对 CoreText/DTCoreText 提出的分页范围进行语义边界调整。 + /// + /// 调整优先级: + /// 1. 若已达章节末尾,直接返回 chapterEnd + /// 2. pageRelate 跨页关联边界 + /// 3. 以上都不满足时,使用原始帧限制分页 + 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 + + // 对齐 WXRead:默认按 CTFrame 已经容纳的行数分页,仅保留 pageRelate 这种 + // 微信读书特有的跨页关联规则。 + 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 + ) + ) + } + + // MARK: - 语义边界查找 + + /// 在指定范围内查找最优的语义分页点(pageBreakBefore / pageBreakAfter)。 + func preferredSemanticBoundary( + in range: NSRange, + minimumEnd: Int, + factory: RDEPUBCoreTextPageFrameFactory + ) -> (location: Int, trigger: String)? { + guard let safeRange = factory.clampedRange(range), safeRange.length > 0 else { + return nil + } + var boundary: (location: Int, trigger: String)? + attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, attributeRange, stop in + guard let rawValue = value as? String else { return } + let hints = rawValue + .split(separator: ",") + .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } + guard !hints.isEmpty else { return } + + if hints.contains(.pageBreakBefore), + attributeRange.location > safeRange.location, + attributeRange.location >= minimumEnd { + boundary = (attributeRange.location, RDEPUBTextSemanticHint.pageBreakBefore.rawValue) + stop.pointee = true + return + } + + let attributeEnd = attributeRange.location + attributeRange.length + if hints.contains(.pageBreakAfter), + attributeEnd > minimumEnd, + attributeEnd < safeRange.location + safeRange.length { + boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue) + stop.pointee = true + return + } + } + return boundary + } + + /// 查找附件边界:只有块级附件才触发分页。 + func preferredAttachmentBoundary( + in range: NSRange, + minimumEnd: Int, + factory: RDEPUBCoreTextPageFrameFactory + ) -> Int? { + guard let safeRange = factory.clampedRange(range), safeRange.length > 0 else { + return nil + } + var boundary: Int? + attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, stop in + guard value != nil else { return } + + let location = attributeRange.location + let placement = factory.attachmentPlacement(at: location) + let blockKind = factory.blockKind(at: location) + + let isBlockLevelAttachment: Bool + switch placement { + case .centered: + isBlockLevelAttachment = true + case .inline, .baseline: + isBlockLevelAttachment = false + case nil: + isBlockLevelAttachment = blockKind == .attachment + } + guard isBlockLevelAttachment else { return } + + let boundaryRange = factory.blockRange(at: location) ?? factory.paragraphRange(containing: location) + if boundaryRange.location > safeRange.location, boundaryRange.location >= minimumEnd { + boundary = boundaryRange.location + stop.pointee = true + } + } + return boundary + } + + /// 查找 pageRelate 跨页关联边界。 + 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 + } + + // MARK: - 内部工具 + + 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:)) + + if blockKind == .attachment, 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)) + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayoutFrame.swift similarity index 100% rename from Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift rename to Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayoutFrame.swift diff --git a/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayouter.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayouter.swift new file mode 100644 index 0000000..bdce5a2 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextLayouter.swift @@ -0,0 +1,26 @@ +import CoreText +import UIKit + +#if canImport(DTCoreText) +import DTCoreText +#endif + +/// CoreText 分页引擎 Facade:将富文本按页面尺寸拆分为多帧(每帧对应一页)。 +/// +/// 内部委托给三个组件: +/// - RDEPUBCoreTextPageFrameFactory — 帧创建、属性查询、诊断 +/// - RDEPUBPageBreakPolicy — 分页规则(语义保护、keepWithNext) +/// - RDEPUBChapterPageCounter — 顶层分页循环编排 +struct RDEPUBTextLayouter { + private let counter: RDEPUBChapterPageCounter + + init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) { + let factory = RDEPUBCoreTextPageFrameFactory(attributedString: attributedString, pageSize: pageSize, config: config) + self.counter = RDEPUBChapterPageCounter(factory: factory) + } + + /// 执行分页,返回布局帧列表(每帧对应一页)。 + func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] { + counter.layoutFrames(fragmentOffsets: fragmentOffsets) + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationInterfaces.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationInterfaces.swift new file mode 100644 index 0000000..d33e3f0 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationInterfaces.swift @@ -0,0 +1,28 @@ +import Foundation +import UIKit + +// MARK: - 分页接口定义 + +struct RDEPUBPageBreakDecision { + var range: NSRange + var reason: RDEPUBTextPageBreakReason + var diagnostics: [String] +} + +protocol RDEPUBChapterPageCounting { + func pageRanges( + for attributedString: NSAttributedString, + pageSize: CGSize, + config: RDEPUBTextLayoutConfig, + fragmentOffsets: [String: Int] + ) -> [RDEPUBPageBreakDecision] +} + +protocol RDEPUBPageFrameBuilding { + func makeFrames( + attributedString: NSAttributedString, + pageSize: CGSize, + config: RDEPUBTextLayoutConfig, + fragmentOffsets: [String: Int] + ) -> [RDEPUBTextLayoutFrame] +} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationSupport.swift similarity index 86% rename from Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift rename to Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationSupport.swift index 1b304e2..39c7d20 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift +++ b/Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBTextPaginationSupport.swift @@ -18,8 +18,9 @@ extension NSAttributedString { fragmentOffsets: [String: Int] = [:], config: RDEPUBTextLayoutConfig = .default ) -> [RDEPUBTextLayoutFrame] { - RDEPUBTextLayouter(attributedString: self, pageSize: size, config: config) - .layoutFrames(fragmentOffsets: fragmentOffsets) + let factory = RDEPUBCoreTextPageFrameFactory(attributedString: self, pageSize: size, config: config) + let counter = RDEPUBChapterPageCounter(factory: factory) + return counter.layoutFrames(fragmentOffsets: fragmentOffsets) } /// 简化版分页:只返回每页的 NSRange 列表(不含语义元数据) diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift index 35cecb5..67c905d 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift @@ -25,13 +25,6 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { } /// 渲染单个章节:HTML → NSAttributedString,同时提取 fragment 和语义标记。 - /// - /// 渲染流程: - /// 1. 将 HTML 字符串编码为 Data - /// 2. 通过 DTCoreText 解析为富文本(失败则回退到纯文本) - /// 3. 注入分页语义标记(${rd-sem-start/end} → 属性字典) - /// 4. 提取 fragment 偏移量映射表 - /// 5. 规范化阅读属性(字体、行距、颜色统一) public func renderChapter( request: RDEPUBTextChapterRenderRequest ) throws -> RDEPUBRenderedChapterContent { @@ -46,8 +39,8 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { } let attributedString = NSMutableAttributedString(attributedString: rendered) - RDEPUBTextRendererSupport.applyPaginationSemantics(in: attributedString) - let fragmentOffsets = RDEPUBTextRendererSupport.extractFragmentOffsets(from: attributedString) + RDEPUBSemanticMarkerInjector.applyPaginationSemantics(in: attributedString) + let fragmentOffsets = RDEPUBFragmentMarkerInjector.extractFragmentOffsets(from: attributedString) RDEPUBTextRendererSupport.normalizeReadingAttributes(in: attributedString, style: request.style) return RDEPUBRenderedChapterContent( attributedString: attributedString, @@ -65,22 +58,24 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { baseURL: URL?, style: RDEPUBTextRenderStyle ) throws -> RDEPUBRenderedChapterContent { - let request = RDEPUBTextRendererSupport.makeChapterRenderRequest( - href: "", - title: "", - rawHTML: html, - baseURL: baseURL, - style: style, - resourceResolver: nil - ) + let request = RDEPUBTextTypesetterPipeline().makeRequest( + from: RDEPUBTypesettingInput( + href: "", + title: "", + rawHTML: html, + baseURL: baseURL, + style: style, + resourceResolver: nil + ) + ).request return try renderChapter(request: request) } /// 回退渲染:当 DTCoreText 不可用时,将 HTML 源码当作纯文本处理 private func fallbackRenderedContent(request: RDEPUBTextChapterRenderRequest) -> RDEPUBRenderedChapterContent { let attributedString = RDEPUBTextRendererSupport.fallbackAttributedString(for: request.context.html, style: request.style) - RDEPUBTextRendererSupport.applyPaginationSemantics(in: attributedString) - let fragmentOffsets = RDEPUBTextRendererSupport.extractFragmentOffsets(from: attributedString) + RDEPUBSemanticMarkerInjector.applyPaginationSemantics(in: attributedString) + let fragmentOffsets = RDEPUBFragmentMarkerInjector.extractFragmentOffsets(from: attributedString) RDEPUBTextRendererSupport.normalizeReadingAttributes(in: attributedString, style: request.style) return RDEPUBRenderedChapterContent( attributedString: attributedString, @@ -91,9 +86,6 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { #if canImport(DTCoreText) /// 使用 DTCoreText 将 HTML Data 解析为富文本。 - /// - /// 通过 `willFlushCallback` 回调,在每个 DOM 元素最终写入富文本前, - /// 对图片附件做尺寸规范化、判断是否为脚注/封面等特殊元素。 private func makeAttributedString(from data: Data, request: RDEPUBTextChapterRenderRequest) -> NSAttributedString? { let builder = DTHTMLAttributedStringBuilder( html: data, @@ -102,7 +94,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { ) builder?.willFlushCallback = { element in guard let element else { return } - RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering( + RDEPUBAttachmentNormalizer.prepareHTMLElementForReaderRendering( element, style: request.style, maxImageSize: resolvedMaxImageSize(for: request) @@ -112,9 +104,6 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { } /// 构建 DTCoreText 的解析选项字典,包括字体、行高、图片尺寸限制等。 - /// - /// - 注意:行高倍率计算公式为 `(字体行高 + 行间距) / 字体行高`, - /// 确保最终行距与用户设置的 style.lineSpacing 一致。 private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] { let style = request.style let maxImageSize = resolvedMaxImageSize(for: request) @@ -139,16 +128,12 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { } private func resolvedMaxImageSize(for request: RDEPUBTextChapterRenderRequest) -> CGSize { - if let pageSize = request.pageSize { - let layoutConfig = request.layoutConfig ?? .default - let contentRect = layoutConfig.contentRect(fallback: pageSize) - let maxWidth = max(round(contentRect.width), 1) - let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1) - return CGSize(width: maxWidth, height: maxHeight) - } - - let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28) - return CGSize(width: max(round(screenBounds.width), 1), height: max(round(screenBounds.height * 0.85), 1)) + let layoutConfig = request.layoutConfig ?? .default + let fallbackPageSize = request.pageSize ?? layoutConfig.fallbackViewportSize + let contentRect = layoutConfig.contentRect(fallback: fallbackPageSize) + let maxWidth = max(round(contentRect.width), 1) + let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1) + return CGSize(width: maxWidth, height: maxHeight) } #endif } diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift deleted file mode 100644 index e22c841..0000000 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift +++ /dev/null @@ -1,798 +0,0 @@ -import UIKit - -// MARK: - 章节分页诊断数据 - -/// 单个章节的分页诊断信息,用于调试和质量检测。 -/// 记录页数、分页原因、附件/块级元素统计等。 -public struct RDEPUBTextChapterPaginationDiagnostic: Equatable { - public var href: String - public var title: String - public var pageCount: Int - /// 每页的分页原因列表 - public var breakReasons: [RDEPUBTextPageBreakReason] - /// 包含附件的页数 - public var attachmentPageCount: Int - /// 因块级/附件边界调整而分页的页数 - public var blockAdjustedPageCount: Int - public var blockKinds: [RDEPUBTextBlockKind] - public var semanticHints: [RDEPUBTextSemanticHint] - public var attachmentPlacements: [RDEPUBTextAttachmentPlacement] - /// 采样诊断日志(最多 4 条) - public var sampleNotes: [String] -} - -// MARK: - 页面数据模型 - -/// 分页后的单页数据,包含全书绝对页码、所属章节、内容范围等。 -/// -/// 每个 `RDEPUBTextPage` 由 `RDEPUBTextLayoutFrame` 生成, -/// `contentRange` 标记了该页在章节富文本中的字符范围。 -public struct RDEPUBTextPage: Equatable { - /// 全书绝对页码(从 0 开始) - public var absolutePageIndex: Int - public var chapterIndex: Int - public var spineIndex: Int - public var href: String - public var chapterTitle: String - /// 该页在所属章节中的相对页码(从 0 开始) - public var pageIndexInChapter: Int - public var totalPagesInChapter: Int - /// 所属章节的完整富文本内容(用于跨页查询) - public var chapterContent: NSAttributedString - /// 该页的富文本片段 - public var content: NSAttributedString - /// 该页在章节富文本中的范围 - public var contentRange: NSRange - public var pageStartOffset: Int - public var pageEndOffset: Int - /// 页的元数据(分页原因、附件、语义标记等) - public var metadata: RDEPUBTextPageMetadata -} - -// MARK: - 章节数据模型 - -/// 渲染并分页后的单个章节,包含完整富文本和分页结果。 -public struct RDEPUBTextChapter: Equatable { - public var chapterIndex: Int - public var spineIndex: Int - public var href: String - public var title: String - /// 章节完整富文本内容 - public var attributedContent: NSAttributedString - /// fragment ID → 字符偏移量映射(用于锚点定位) - public var fragmentOffsets: [String: Int] - public var pageBreakReasons: [RDEPUBTextPageBreakReason] - public var pages: [RDEPUBTextPage] -} - -// MARK: - 分页书籍模型 - -/// 整本 EPUB 的分页书籍模型,包含所有章节、页面和全局索引表。 -/// -/// 这是 EPUBTextRendering 层的最终产物,由 `RDEPUBTextBookBuilder.build()` 生成。 -/// 通过 `chapterData(for:)` 或 `chapterData(atChapterIndex:)` 获取 `RDEPUBChapterData` 进行查询。 -public struct RDEPUBTextBook { - public var chapters: [RDEPUBTextChapter] - 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 - self.pages = pages - self.indexTable = RDEPUBTextIndexTable(chapters: chapters) - } - - public static func == (lhs: RDEPUBTextBook, rhs: RDEPUBTextBook) -> Bool { - lhs.chapters == rhs.chapters && lhs.pages == rhs.pages - } - - /// 按 href 获取章节的数据访问层(包含索引表) - public func chapterData(for href: String) -> RDEPUBChapterData? { - guard let chapter = chapters.first(where: { $0.href == href }) else { return nil } - 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 { - return nil - } - return pages[pageNumber - 1] - } - - /// 根据持久化位置计算对应页码(从 1 开始)。 - /// - /// 解析优先级: - /// 1. rangeAnchor 锚点定位 - /// 2. fragment 片段 ID - /// 3. navigationProgression 进度百分比回退 - public func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? { - guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier), - let chapterData = chapterData(for: normalizedLocation.href) else { - return nil - } - - if let anchor = normalizedLocation.rangeAnchor?.start, - let page = positionConverter.pageNumber(for: anchor) { - return page - } - - if let anchor = indexTable.anchor(for: normalizedLocation), - let page = positionConverter.pageNumber(for: anchor) { - return page - } - - return chapterData.pageNumber(for: normalizedLocation) - } - - /// 根据页码生成持久化位置(RDEPUBLocation),包含起止锚点 - public func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? { - guard let chapterData = chapterData(forPageNumber: pageNumber), - let page = page(at: pageNumber) else { - return nil - } - return chapterData.location(forPage: page, bookIdentifier: bookIdentifier) - } -} - -// MARK: - 分页书籍构建器 - -/// 从 EPUB publication 构建分页书籍模型的核心构建器。 -/// -/// 渲染链路:遍历 spine → 渲染每章 HTML → 分页 → 合并/规范化尾页 → 构建 RDEPUBTextBook -/// -/// 特性: -/// - 支持分页缓存(WXRead 模式:只缓存页范围,不缓存富文本) -/// - 性能采样(记录每章渲染/分页耗时) -/// - 尾页规范化(丢弃纯空白尾页、合并过短尾页) -/// - 封面章节特殊处理 -public final class RDEPUBTextBookBuilder { - private let renderer: RDEPUBTextRenderer - private let cache: RDEPUBTextBookCache? - private let layoutConfig: RDEPUBTextLayoutConfig - private let sampler: RDEPUBTextPerformanceSampler - - /// 最后一次构建的资源引用诊断(样式表、图片等) - public private(set) var lastBuildResourceDiagnostics: [RDEPUBTextResourceReferenceDiagnostic] = [] - /// 最后一次构建的分页诊断(每章一页的分页原因、附件统计等) - public private(set) var lastBuildPaginationDiagnostics: [RDEPUBTextChapterPaginationDiagnostic] = [] - /// 最后一次构建的性能采样数据 - public private(set) var lastBuildPerformanceSamples: [RDEPUBTextPerformanceSample] = [] - /// 最后一次构建的缓存命中/未命中统计 - public private(set) var lastBuildCacheStats: (hits: Int, misses: Int) = (0, 0) - - public init( - renderer: RDEPUBTextRenderer, - cache: RDEPUBTextBookCache? = nil, - layoutConfig: RDEPUBTextLayoutConfig = .default - ) { - self.renderer = renderer - self.cache = cache - self.layoutConfig = layoutConfig - self.sampler = RDEPUBTextPerformanceSampler() - } - - /// 默认构造器,使用 DTCoreText 渲染器 - public convenience init() { - self.init(renderer: RDEPUBDTCoreTextRenderer()) - } - - private var isPaginationDebugEnabled: Bool { - ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug") - } - - /// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志 - public func phase7SemanticSummary(title: String? = nil) -> String? { - guard !lastBuildPaginationDiagnostics.isEmpty else { return nil } - - let blockKinds = uniqueValues(from: lastBuildPaginationDiagnostics.flatMap(\.blockKinds)) - let semanticHints = uniqueValues(from: lastBuildPaginationDiagnostics.flatMap(\.semanticHints)) - let attachmentPlacements = uniqueValues(from: lastBuildPaginationDiagnostics.flatMap(\.attachmentPlacements)) - let note = lastBuildPaginationDiagnostics - .flatMap(\.sampleNotes) - .first(where: { $0.contains("semantic") || $0.contains("attachment") || $0.contains("block kinds") }) - - var parts = [ - title, - "章节 \(lastBuildPaginationDiagnostics.count)", - blockKinds.isEmpty ? nil : "block kinds [\(blockKinds.map(\.rawValue).joined(separator: ","))]", - semanticHints.isEmpty ? nil : "hints [\(semanticHints.map(\.rawValue).joined(separator: ","))]", - attachmentPlacements.isEmpty ? nil : "placements [\(attachmentPlacements.map(\.rawValue).joined(separator: ","))]" - ].compactMap { $0 } - if let note { - parts.append(note) - } - return parts.joined(separator: " · ") - } - - /// 核心构建方法:从 EPUB publication 构建分页书籍。 - /// - /// 流程: - /// 1. 生成缓存键,尝试加载分页缓存 - /// 2. 遍历 spine 中的线性 HTML 章节 - /// 3. 渲染每章 HTML → NSAttributedString - /// 4. 跳过空白的封面/扉页章节 - /// 5. 分页:缓存命中则使用缓存的页范围,否则调用 CoreText 分页引擎 - /// 6. 尾页规范化:丢弃纯空白尾页、合并过短尾页 - /// 7. 构建 RDEPUBTextBook 并保存分页缓存 - public func build( - parser: RDEPUBParser, - publication: RDEPUBPublication, - pageSize: CGSize, - style: RDEPUBTextRenderStyle - ) throws -> RDEPUBTextBook { - if isPaginationDebugEnabled { - print("[PaginationDebug] build pageSize=\(NSCoder.string(for: pageSize)) layoutInsets=\(NSCoder.string(for: layoutConfig.edgeInsets))") - } - var chapters: [RDEPUBTextChapter] = [] - var flatPages: [RDEPUBTextPage] = [] - lastBuildResourceDiagnostics = [] - lastBuildPaginationDiagnostics = [] - lastBuildPerformanceSamples = [] - lastBuildCacheStats = (0, 0) - sampler.reset() - - let buildStart = CFAbsoluteTimeGetCurrent() - - // 缓存查询 — WXRead 模式:只加载每章的页范围,不缓存富文本 - let bookID = publication.metadata.identifier ?? publication.metadata.title - let cacheKey = makeCacheKey(bookID: bookID, pageSize: pageSize, style: style) - let cachedPagination = cacheKey.flatMap { cache?.load(key: $0) } - - for (spineIndex, item) in publication.spine.enumerated() where item.linear { - guard item.mediaType.contains("html") || item.mediaType.contains("xhtml"), - let rawHTML = parser.htmlString(forRelativePath: item.href) else { - continue - } - - // 从目录表中解析章节标题 - let chapterTitle = resolvedChapterTitle(for: item, toc: publication.tableOfContents) - let request = RDEPUBTextRendererSupport.makeChapterRenderRequest( - href: item.href, - title: chapterTitle, - rawHTML: rawHTML, - baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(), - style: style, - resourceResolver: publication.resourceResolver, - contentLanguageCode: publication.metadata.language, - pageSize: pageSize, - layoutConfig: layoutConfig - ) - - // 渲染 HTML → NSAttributedString - let renderStart = CFAbsoluteTimeGetCurrent() - let rendered = try renderer.renderChapter(request: request) - let renderDuration = CFAbsoluteTimeGetCurrent() - renderStart - - lastBuildResourceDiagnostics.append(contentsOf: rendered.resourceDiagnostics) - - let plainText = rendered.attributedString.string.trimmingCharacters(in: .whitespacesAndNewlines) - if item.href.lowercased().contains("cover") { - print("[EPUB][Cover] rendered href=\(item.href) textLength=\(plainText.count) attrLength=\(rendered.attributedString.length) attachments=\(attachmentCount(in: rendered.attributedString))") - } - // 跳过空白的封面/扉页章节 - if shouldSkipChapter(item: item, content: rendered.attributedString, text: plainText) { - if item.href.lowercased().contains("cover") { - print("[EPUB][Cover] skipped href=\(item.href)") - } - continue - } - - let chapterIndex = chapters.count - let content = NSMutableAttributedString(attributedString: rendered.attributedString) - - // 分页:封面图片章节走特殊路径,缓存命中则跳过分页引擎 - let paginateStart = CFAbsoluteTimeGetCurrent() - let layoutFrames: [RDEPUBTextLayoutFrame] - let isCacheHit: Bool - if isAttachmentOnlyCoverChapter(item: item, content: content, plainText: plainText) { - // 纯图片封面:整个内容作为一页 - layoutFrames = [ - RDEPUBTextLayoutFrame( - contentRange: NSRange(location: 0, length: content.length), - breakReason: .chapterEnd, - blockRange: nil, - attachmentRanges: attachmentRanges(in: content), - attachmentKinds: [], - blockKinds: [], - semanticHints: [], - attachmentPlacements: [], - trailingFragmentID: nil, - diagnostics: [ - "page break: chapterEnd", - "cover fallback: single attachment page", - "page range: \(NSStringFromRange(NSRange(location: 0, length: content.length)))" - ] - ) - ] - isCacheHit = false - } else if let cached = cachedPagination?[item.href] { - // 缓存命中:直接使用缓存的页范围,跳过 CoreText 分页 - layoutFrames = cached.pageRanges.enumerated().map { idx, range in - let breakReason = idx < cached.breakReasons.count ? cached.breakReasons[idx] : .frameLimit - return RDEPUBTextLayoutFrame( - contentRange: range, - breakReason: breakReason, - blockRange: nil, - attachmentRanges: [], - attachmentKinds: [], - blockKinds: [], - semanticHints: cached.semanticHints, - attachmentPlacements: [], - trailingFragmentID: nil, - diagnostics: ["page break: \(breakReason.rawValue)", "page range: \(NSStringFromRange(range))", "source: cache hit"] - ) - } - isCacheHit = true - } else { - // 缓存未命中:调用 CoreText 分页引擎 - layoutFrames = content.length > 0 - ? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets, config: layoutConfig) - : [] - isCacheHit = false - } - let paginateDuration = CFAbsoluteTimeGetCurrent() - paginateStart - - // 尾页规范化:丢弃纯空白尾页、合并过短尾页 - let normalizedFrames = normalizeTrailingFrames( - layoutFrames, - content: content, - href: item.href - ) - let effectiveFrames = normalizedFrames.isEmpty && content.length > 0 - ? [ - RDEPUBTextLayoutFrame( - contentRange: NSRange(location: 0, length: content.length), - breakReason: .chapterEnd, - blockRange: nil, - attachmentRanges: [], - attachmentKinds: [], - blockKinds: [], - semanticHints: [], - attachmentPlacements: [], - trailingFragmentID: nil, - diagnostics: [ - "page break: chapterEnd", - "page range: \(NSStringFromRange(NSRange(location: 0, length: content.length)))" - ] - ) - ] - : normalizedFrames - if item.href.lowercased().contains("cover") { - print("[EPUB][Cover] paginated href=\(item.href) pages=\(effectiveFrames.count) firstRange=\(effectiveFrames.first.map { NSStringFromRange($0.contentRange) } ?? "none")") - } - - // 记录性能采样 - sampler.record(RDEPUBTextPerformanceSample( - chapterHref: item.href, - renderDuration: renderDuration, - paginateDuration: paginateDuration, - pageCount: effectiveFrames.count, - attributedStringLength: content.length, - cacheHit: isCacheHit - )) - if isCacheHit { - lastBuildCacheStats.hits += 1 - } else { - lastBuildCacheStats.misses += 1 - } - - // 构建页面和章节模型 - let chapterAttributedContent = content.copy() as! NSAttributedString - let pages = effectiveFrames.enumerated().map { localPageIndex, frame in - let range = frame.contentRange - return RDEPUBTextPage( - absolutePageIndex: flatPages.count + localPageIndex, - chapterIndex: chapterIndex, - spineIndex: spineIndex, - href: item.href, - chapterTitle: chapterTitle, - pageIndexInChapter: localPageIndex, - totalPagesInChapter: effectiveFrames.count, - chapterContent: chapterAttributedContent, - content: content.attributedSubstring(from: range), - contentRange: range, - pageStartOffset: range.location, - pageEndOffset: range.location + max(range.length - 1, 0), - metadata: frame.metadata - ) - } - - if isPaginationDebugEnabled, - item.href.contains("Chapter_3.xhtml") { - print("[PaginationDebug] href=\(item.href) pages=\(pages.count)") - for page in pages { - let preview = debugPreview(for: page.content, limit: 36) - print("[PaginationDebug] absPage=\(page.absolutePageIndex + 1) localPage=\(page.pageIndexInChapter + 1) range=\(NSStringFromRange(page.contentRange)) break=\(page.metadata.breakReason.rawValue) preview=\(preview)") - for note in page.metadata.diagnostics.prefix(4) { - print("[PaginationDebug] note=\(note)") - } - } - } - - chapters.append( - RDEPUBTextChapter( - chapterIndex: chapterIndex, - spineIndex: spineIndex, - href: item.href, - title: chapterTitle, - attributedContent: chapterAttributedContent, - fragmentOffsets: rendered.fragmentOffsets, - pageBreakReasons: pages.map(\.metadata.breakReason), - pages: pages - ) - ) - lastBuildPaginationDiagnostics.append( - RDEPUBTextChapterPaginationDiagnostic( - href: item.href, - title: chapterTitle, - pageCount: pages.count, - breakReasons: pages.map(\.metadata.breakReason), - attachmentPageCount: pages.filter { !$0.metadata.attachmentKinds.isEmpty }.count, - blockAdjustedPageCount: pages.filter { $0.metadata.breakReason == .blockBoundary || $0.metadata.breakReason == .attachmentBoundary }.count, - blockKinds: uniqueValues(from: pages.flatMap(\.metadata.blockKinds)), - semanticHints: uniqueValues(from: pages.flatMap(\.metadata.semanticHints)), - attachmentPlacements: uniqueValues(from: pages.flatMap(\.metadata.attachmentPlacements)), - sampleNotes: Array( - pages - .flatMap(\.metadata.diagnostics) - .prefix(4) - ) - ) - ) - flatPages.append(contentsOf: pages) - } - - let book = RDEPUBTextBook(chapters: chapters, pages: flatPages) - - sampler.totalBuildDuration = CFAbsoluteTimeGetCurrent() - buildStart - - // 保存分页缓存(只缓存页范围和分页原因,不缓存富文本) - if let cacheKey { - let paginationCache = chapters.map { chapter in - let pageRanges = chapter.pages.map(\.contentRange) - let breakReasons = chapter.pages.map(\.metadata.breakReason) - let semanticHints = Array(Set(chapter.pages.flatMap(\.metadata.semanticHints))) - return RDEPUBTextChapterPaginationCache( - href: chapter.href, - pageRanges: pageRanges, - breakReasons: breakReasons, - semanticHints: semanticHints - ) - } - cache?.save(paginationCache, key: cacheKey) - } - - print(sampler.summary()) - lastBuildPerformanceSamples = sampler.samples - - return book - } - - // MARK: - 章节标题解析 - - /// 从目录表中查找章节标题,找不到则回退到 spine item 的 title 或 href - private func resolvedChapterTitle(for item: RDEPUBSpineItem, toc: [EPUBTableOfContentsItem]) -> String { - if let title = flattenedTOCItems(from: toc).first(where: { tocItem in - tocItem.href.components(separatedBy: "#").first == item.href - })?.title.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty { - return title - } - let trimmedTitle = item.title.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedTitle.isEmpty ? item.href : trimmedTitle - } - - /// 递归展开嵌套目录为扁平列表 - private func flattenedTOCItems(from items: [EPUBTableOfContentsItem]) -> [EPUBTableOfContentsItem] { - items.flatMap { item in - [item] + flattenedTOCItems(from: item.children) - } - } - - // MARK: - 章节过滤 - - /// 判断是否应跳过该章节(空白的封面/扉页,无文本且无附件) - private func shouldSkipChapter(item: RDEPUBSpineItem, content: NSAttributedString, text: String) -> Bool { - let lowercasedHref = item.href.lowercased() - var hasAttachment = false - if content.length > 0 { - content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in - guard value != nil else { return } - hasAttachment = true - stop.pointee = true - } - } - if text.isEmpty && !hasAttachment && (lowercasedHref.contains("cover") || lowercasedHref.contains("title")) { - return true - } - return false - } - - // MARK: - 附件统计 - - /// 统计富文本中的附件数量 - private func attachmentCount(in content: NSAttributedString) -> Int { - guard content.length > 0 else { return 0 } - var count = 0 - content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, _ in - if value != nil { - count += 1 - } - } - return count - } - - /// 获取富文本中所有附件的 NSRange 列表 - private func attachmentRanges(in content: NSAttributedString) -> [NSRange] { - guard content.length > 0 else { return [] } - var ranges: [NSRange] = [] - content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, range, _ in - if value != nil { - ranges.append(range) - } - } - return ranges - } - - // MARK: - 封面章节检测 - - /// 判断是否为纯图片封面章节(href 包含 cover 且有附件但几乎无文本) - private func isAttachmentOnlyCoverChapter( - item: RDEPUBSpineItem, - content: NSAttributedString, - plainText: String - ) -> Bool { - let lowercasedHref = item.href.lowercased() - guard lowercasedHref.contains("cover") else { return false } - let trimmed = plainText.trimmingCharacters(in: .whitespacesAndNewlines) - return attachmentCount(in: content) > 0 && trimmed.count <= 1 - } - - private func debugPreview(for content: NSAttributedString, limit: Int) -> String { - let collapsed = content.string - .replacingOccurrences(of: "\n", with: " ") - .replacingOccurrences(of: "\r", with: " ") - .replacingOccurrences(of: "\t", with: " ") - .trimmingCharacters(in: .whitespacesAndNewlines) - guard !collapsed.isEmpty else { return "" } - if collapsed.count <= limit { - return collapsed - } - let head = collapsed.prefix(limit) - return "\(head)…" - } - - // MARK: - 尾页规范化 - - /// 规范化分页结果: - /// 1. 移除章节中间误产生的纯空白页 - /// 2. 丢弃纯空白的尾页 - /// 3. 将过短的尾页(≤2 字符)合并到前一页 - private func normalizeTrailingFrames( - _ frames: [RDEPUBTextLayoutFrame], - content: NSAttributedString, - href: String - ) -> [RDEPUBTextLayoutFrame] { - guard frames.count > 1 else { return frames } - - var normalized = frames - - // 先移除章节中间的纯空白页。这类页通常只包含换行/占位空白, - // 会把相邻正文页硬生生拆开,直接表现为“上一页底部空很多”。 - var compacted: [RDEPUBTextLayoutFrame] = [] - compacted.reserveCapacity(normalized.count) - for frame in normalized { - if shouldDropWhitespaceOnlyFrame(frame, in: content) { - let note = "normalized: dropped whitespace-only intermediate page \(NSStringFromRange(frame.contentRange))" - if var previous = compacted.popLast() { - previous.diagnostics.append(note) - compacted.append(previous) - } else { - print("[EPUB][Pagination] href=\(href) dropped leading/intermediate whitespace frame \(NSStringFromRange(frame.contentRange))") - } - continue - } - compacted.append(frame) - } - normalized = compacted - - // 丢弃纯空白的尾页 - while let lastFrame = normalized.last, - shouldDropWhitespaceOnlyFrame(lastFrame, in: content) { - normalized.removeLast() - let note = "normalized: dropped whitespace-only trailing page \(NSStringFromRange(lastFrame.contentRange))" - if var previousFrame = normalized.popLast() { - previousFrame.diagnostics.append(note) - normalized.append(previousFrame) - } else { - print("[EPUB][Pagination] href=\(href) dropped trailing frame \(NSStringFromRange(lastFrame.contentRange))") - } - } - - // 将过短的尾页合并到前一页 - guard normalized.count > 1, - let lastFrame = normalized.last, - let previousFrame = normalized.dropLast().last, - shouldMergeShortTrailingFrame(lastFrame, previousFrame: previousFrame, in: content) else { - return normalized - } - - let mergedFrame = mergeTrailingFrame(previousFrame, with: lastFrame) - normalized.removeLast(2) - normalized.append(mergedFrame) - return normalized - } - - /// 判断尾页是否为纯空白(无可显示字符且无附件) - private func shouldDropWhitespaceOnlyFrame( - _ frame: RDEPUBTextLayoutFrame, - in content: NSAttributedString - ) -> Bool { - guard frame.contentRange.length > 0, - attachmentCount(in: content, range: frame.contentRange) == 0 else { - return false - } - return visibleCharacterCount(in: content, range: frame.contentRange) == 0 - } - - /// 判断尾页是否过短需要合并到前一页。 - /// 条件:尾页 ≤ 2 个可见字符,且前一页的字符数是尾页的 8 倍以上(至少 12 个字符) - private func shouldMergeShortTrailingFrame( - _ trailingFrame: RDEPUBTextLayoutFrame, - previousFrame: RDEPUBTextLayoutFrame, - in content: NSAttributedString - ) -> Bool { - guard trailingFrame.contentRange.length > 0, - NSMaxRange(previousFrame.contentRange) == trailingFrame.contentRange.location else { - return false - } - - let visibleCount = visibleCharacterCount(in: content, range: trailingFrame.contentRange) - let trailingAttachmentCount = attachmentCount(in: content, range: trailingFrame.contentRange) - guard visibleCount <= 2, - trailingFrame.contentRange.length <= 2, - visibleCount > 0 || trailingAttachmentCount > 0 else { - return false - } - - let previousVisibleCount = visibleCharacterCount(in: content, range: previousFrame.contentRange) - return previousVisibleCount >= max(visibleCount * 8, 12) - } - - /// 将尾页合并到前一页,保留尾页的分页原因 - private func mergeTrailingFrame( - _ previousFrame: RDEPUBTextLayoutFrame, - with trailingFrame: RDEPUBTextLayoutFrame - ) -> RDEPUBTextLayoutFrame { - let mergedRange = NSRange( - location: previousFrame.contentRange.location, - length: NSMaxRange(trailingFrame.contentRange) - previousFrame.contentRange.location - ) - - return RDEPUBTextLayoutFrame( - contentRange: mergedRange, - breakReason: trailingFrame.breakReason, - blockRange: trailingFrame.blockRange ?? previousFrame.blockRange, - attachmentRanges: uniqueRanges(from: previousFrame.attachmentRanges + trailingFrame.attachmentRanges), - attachmentKinds: uniqueValues(from: previousFrame.attachmentKinds + trailingFrame.attachmentKinds), - blockKinds: uniqueValues(from: previousFrame.blockKinds + trailingFrame.blockKinds), - semanticHints: uniqueValues(from: previousFrame.semanticHints + trailingFrame.semanticHints), - attachmentPlacements: uniqueValues(from: previousFrame.attachmentPlacements + trailingFrame.attachmentPlacements), - trailingFragmentID: trailingFrame.trailingFragmentID ?? previousFrame.trailingFragmentID, - diagnostics: previousFrame.diagnostics - + trailingFrame.diagnostics - + ["normalized: merged short trailing page \(NSStringFromRange(trailingFrame.contentRange)) into previous page"] - ) - } - - /// 统计指定范围内可见字符数(排除空白和控制字符) - private func visibleCharacterCount( - in content: NSAttributedString, - range: NSRange - ) -> Int { - guard range.length > 0 else { return 0 } - let string = content.attributedSubstring(from: range).string - let filteredScalars = string.unicodeScalars.filter { scalar in - !CharacterSet.whitespacesAndNewlines.contains(scalar) - && !CharacterSet.controlCharacters.contains(scalar) - } - return filteredScalars.count - } - - // MARK: - 集合工具方法 - - /// 数组去重(保持顺序) - private func uniqueValues(from values: [T]) -> [T] { - values.reduce(into: [T]()) { result, value in - if !result.contains(value) { - result.append(value) - } - } - } - - /// NSRange 数组去重(保持顺序) - private func uniqueRanges(from ranges: [NSRange]) -> [NSRange] { - ranges.reduce(into: [NSRange]()) { result, value in - if !result.contains(value) { - result.append(value) - } - } - } - - /// 统计指定范围内附件数量 - private func attachmentCount(in content: NSAttributedString, range: NSRange) -> Int { - guard content.length > 0, range.length > 0 else { return 0 } - var count = 0 - content.enumerateAttribute(.attachment, in: range) { value, _, _ in - if value != nil { - count += 1 - } - } - return count - } - - // MARK: - 缓存键生成 - - /// 生成缓存键:基于书籍 ID、字号、行距、页面尺寸等参数的 SHA256 哈希 - private func makeCacheKey( - bookID: String, - pageSize: CGSize, - style: RDEPUBTextRenderStyle - ) -> String? { - guard let cache else { return nil } - return cache.cacheKey( - bookID: bookID, - fontSize: style.font.pointSize, - lineHeightMultiple: style.lineSpacing, - contentInsets: layoutConfig.edgeInsets, - pageSize: pageSize, - layoutConfigSignature: layoutConfig.cacheSignature - ) - } -} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift deleted file mode 100644 index 177a501..0000000 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift +++ /dev/null @@ -1,946 +0,0 @@ -import CoreText -import UIKit - -#if canImport(DTCoreText) -import DTCoreText -#endif - -/// CoreText 分页引擎:将富文本按页面尺寸拆分为多帧(每帧对应一页)。 -/// -/// 分页策略优先级(从高到低): -/// 1. avoidPageBreakInside — 不在保护块内分页(对标 WXRead 的行级回退扫描) -/// 2. keepWithNext — 标题等元素需与下一段同页 -/// 3. 语义边界 — pageBreakBefore/After 等显式分页标记 -/// 4. pageRelate — 微信读书式的跨页关联元素 -/// 5. 附件边界 — 块级附件应整体移到下一页 -/// 6. 帧限制 — 默认按 CoreText 可视范围分页 -/// -/// 支持两条渲染路径: -/// - DTCoreText 路径(首选):DTCoreTextLayouter → DTCoreTextLayoutFrame -/// - CoreText 回退路径:CTFramesetterCreateFrame -struct RDEPUBTextLayouter { - /// 待分页的富文本 - private let attributedString: NSAttributedString - /// 页面尺寸(决定每帧能容纳多少内容) - private let pageSize: CGSize - /// CoreText 帧设置器 - private let framesetter: CTFramesetter - /// 页面矩形路径(用于 CTFrame 排版) - private let path: CGPath - /// DTCoreText 路径可直接消费的单矩形布局区域 - private let dtLayoutRect: CGRect - /// 布局配置(avoidPageBreakInside、孤行控制等) - private let config: RDEPUBTextLayoutConfig - - init(attributedString: NSAttributedString, pageSize: CGSize, config: RDEPUBTextLayoutConfig = .default) { - self.attributedString = attributedString - self.pageSize = pageSize - self.config = config - self.framesetter = CTFramesetterCreateWithAttributedString(attributedString) - self.dtLayoutRect = config.contentRect(fallback: pageSize) - self.path = Self.makeLayoutPath(pageSize: pageSize, config: config) - } - - /// 执行分页,返回布局帧列表(每帧对应一页)。 - /// 根据编译环境选择 DTCoreText 或 CoreText 路径。 - func layoutFrames(fragmentOffsets: [String: Int] = [:]) -> [RDEPUBTextLayoutFrame] { - guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else { - return [] - } - -#if canImport(DTCoreText) - return layoutFramesUsingDTCoreText(fragmentOffsets: fragmentOffsets) -#else - return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) -#endif - } - - // MARK: - CoreText 分页路径(回退方案) - - /// 使用原生 CoreText API 进行分页。 - /// - /// 流程:CTFramesetterCreateFrame → 获取可视范围 → 语义边界调整 → 记录帧 - private func layoutFramesUsingCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] { - guard attributedString.length > 0, pageSize.width > 0, pageSize.height > 0 else { - return [] - } - - var frames: [RDEPUBTextLayoutFrame] = [] - var location = 0 - let resolvedSize = config.resolvedFrameSize(fallback: pageSize) - let usableWidth = resolvedSize.width - config.edgeInsets.left - config.edgeInsets.right - let usableHeight = resolvedSize.height - config.edgeInsets.top - config.edgeInsets.bottom - - guard usableWidth > 0, usableHeight > 0 else { - return [] - } - - while location < attributedString.length { - let framePath = CGMutablePath() - // 对齐 WXRead WRChapterPageCount: - // CoreText 分页路径使用 bottom inset 作为 y 原点,而不是 UIKit 语义的 top inset。 - let pageRect = CGRect( - x: config.edgeInsets.left, - y: config.edgeInsets.bottom, - width: usableWidth, - height: usableHeight - ) - framePath.addRect(pageRect) - let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), framePath, nil) - let proposedRange = proposedRangeUsingWXReadPageCount( - from: frame, - start: location, - usableHeight: usableHeight, - totalLength: attributedString.length - ) - guard proposedRange.length > 0 else { - break - } - - // 行级 avoidPageBreakInside 处理(WXRead 方案:从最后一行向前扫描) - let avoidAdjusted = trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange) - let lineRanges = lineRanges(from: frame) - - let adjusted = adjustedRange( - from: avoidAdjusted, - totalLength: attributedString.length, - lineRanges: lineRanges - ) - let trailingFragmentID = nearestTrailingFragmentID( - endingAt: adjusted.range.location + adjusted.range.length, - fragmentOffsets: fragmentOffsets - ) - frames.append( - RDEPUBTextLayoutFrame( - contentRange: adjusted.range, - breakReason: adjusted.breakReason, - blockRange: adjusted.blockRange, - attachmentRanges: adjusted.attachmentRanges, - attachmentKinds: adjusted.attachmentKinds, - blockKinds: adjusted.blockKinds, - semanticHints: adjusted.semanticHints, - attachmentPlacements: adjusted.attachmentPlacements, - trailingFragmentID: trailingFragmentID, - diagnostics: adjusted.diagnostics - ) - ) - - let nextLocation = adjusted.range.location + adjusted.range.length - guard nextLocation > location else { - location += max(proposedRange.length, 1) - continue - } - location = nextLocation - } - - return frames - } - - // MARK: - DTCoreText 分页路径(首选方案) - -#if canImport(DTCoreText) - /// 使用 DTCoreTextLayouter 进行分页,提供更精确的行级语义处理。 - private func layoutFramesUsingDTCoreText(fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame] { - guard config.numberOfColumns == 1 else { - return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) - } - - guard let layouter = DTCoreTextLayouter(attributedString: attributedString) else { - return layoutFramesUsingCoreText(fragmentOffsets: fragmentOffsets) - } - - layouter.shouldCacheLayoutFrames = false - - var frames: [RDEPUBTextLayoutFrame] = [] - var location = 0 - let pageRect = dtLayoutRect - - while location < attributedString.length { - guard let layoutFrame = layouter.layoutFrame(with: pageRect, range: NSRange(location: location, length: 0)) else { - break - } - - let visibleRange = layoutFrame.visibleStringRange() - guard visibleRange.length > 0 else { - break - } - - let proposedRange = NSRange(location: location, length: visibleRange.length) - let avoidAdjusted = trimmedRangeForAvoidPageBreakInside(from: layoutFrame, proposed: proposedRange) - let lineAdjusted = trimmedRangeForKeepWithNext(from: layoutFrame, proposed: avoidAdjusted) - let lineRanges = lineRanges(from: layoutFrame) - let adjusted = adjustedRange( - from: lineAdjusted, - totalLength: attributedString.length, - lineRanges: lineRanges - ) - let verifiedRange: NSRange - if adjusted.breakReason == .attachmentBoundary { - verifiedRange = verifiedDisplayRange(for: adjusted.range) - } else { - verifiedRange = adjusted.range - } - let trailingFragmentID = nearestTrailingFragmentID( - endingAt: verifiedRange.location + verifiedRange.length, - fragmentOffsets: fragmentOffsets - ) - let diagnostics = verifiedRange == adjusted.range - ? adjusted.diagnostics - : adjusted.diagnostics + ["verified-display-range \(NSStringFromRange(adjusted.range)) -> \(NSStringFromRange(verifiedRange))"] - - frames.append( - RDEPUBTextLayoutFrame( - contentRange: verifiedRange, - breakReason: adjusted.breakReason, - blockRange: blockRange(at: max(verifiedRange.location, verifiedRange.location + verifiedRange.length - 1)), - attachmentRanges: attachmentRanges(in: verifiedRange), - attachmentKinds: attachmentKinds(in: verifiedRange), - blockKinds: blockKinds(in: verifiedRange), - semanticHints: semanticHints(in: verifiedRange), - attachmentPlacements: attachmentPlacements(in: verifiedRange), - trailingFragmentID: trailingFragmentID, - diagnostics: diagnostics - ) - ) - - let nextLocation = verifiedRange.location + verifiedRange.length - guard nextLocation > location else { - location += max(visibleRange.length, 1) - continue - } - location = nextLocation - } - - return frames - } - - private func verifiedDisplayRange(for range: NSRange) -> NSRange { - guard let clampedRange = clampedRange(range), - clampedRange.length > 0, - !attachmentRanges(in: clampedRange).isEmpty else { - return range - } - - let pageContent = attributedString.attributedSubstring(from: clampedRange) - guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else { - return clampedRange - } - - layouter.shouldCacheLayoutFrames = false - guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else { - return clampedRange - } - - let visibleRange = layoutFrame.visibleStringRange() - guard visibleRange.length > 0, visibleRange.length < clampedRange.length else { - return clampedRange - } - - return NSRange(location: clampedRange.location, length: visibleRange.length) - } -#endif - - private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath { - let columnRects = config.columnRects(fallback: pageSize) - guard columnRects.count > 1 else { - return CGPath(rect: columnRects.first ?? CGRect(origin: .zero, size: pageSize), transform: nil) - } - - let path = CGMutablePath() - for rect in columnRects { - path.addRect(rect) - } - return path - } - - // MARK: - 语义边界调整 - - /// 对 CoreText 提出的分页范围进行语义边界调整。 - /// - /// 调整优先级: - /// 1. 若已达章节末尾,直接返回 chapterEnd - /// 2. 优先在语义边界(pageBreakBefore/After)分页 - /// 3. 其次在 pageRelate 跨页关联点分页 - /// 4. 再次在附件边界分页(块级附件需整体移动) - /// 5. 以上都不满足时,使用原始帧限制分页 - /// - /// 最小分页长度约束:不低于原始范围的 55%,避免单页内容过少。 - private func adjustedRange( - from proposedRange: NSRange, - totalLength: Int, - lineRanges: [NSRange] - ) -> ( - 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 = blockKinds(in: proposedRange) - let proposedSemanticHints = semanticHints(in: proposedRange) - let proposedAttachmentPlacements = attachmentPlacements(in: proposedRange) - guard pageEnd < totalLength else { - return ( - range: proposedRange, - breakReason: .chapterEnd, - blockRange: blockRange(at: max(proposedRange.location, pageEnd - 1)), - attachmentRanges: attachmentRanges(in: proposedRange), - attachmentKinds: attachmentKinds(in: proposedRange), - blockKinds: proposedBlockKinds, - semanticHints: proposedSemanticHints, - attachmentPlacements: proposedAttachmentPlacements, - diagnostics: diagnostics( - reason: .chapterEnd, - range: proposedRange, - attachmentRanges: attachmentRanges(in: proposedRange), - blockRange: blockRange(at: max(proposedRange.location, pageEnd - 1)), - blockKinds: proposedBlockKinds, - semanticHints: proposedSemanticHints, - attachmentPlacements: proposedAttachmentPlacements - ) - ) - } - - let currentBlockRange = blockRange(at: max(proposedRange.location, pageEnd - 1)) - let currentAttachmentRanges = attachmentRanges(in: proposedRange) - let currentAttachmentKinds = attachmentKinds(in: proposedRange) - let currentBlockKinds = proposedBlockKinds - let currentSemanticHints = proposedSemanticHints - let currentAttachmentPlacements = proposedAttachmentPlacements - - // 对齐 WXRead:默认按 CTFrame 已经容纳的行数分页,仅保留 pageRelate 这种 - // 微信读书特有的跨页关联规则;其他 keepWithNext/attachmentBoundary/通用语义边界 - // 先不参与截断,避免提前裁短页尾内容。 - if let pageRelateBoundary = preferredPageRelateBoundary( - after: proposedRange, - minimumEnd: proposedRange.location + 1, - lineRanges: lineRanges - ) { - 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: diagnostics( - reason: .semanticBoundary, - range: adjustedRange, - attachmentRanges: currentAttachmentRanges, - blockRange: currentBlockRange, - blockKinds: currentBlockKinds, - semanticHints: currentSemanticHints, - attachmentPlacements: currentAttachmentPlacements, - trigger: RDEPUBTextSemanticHint.pageRelate.rawValue - ) - ) - } - - // 4. 帧限制(默认分页) - return ( - range: proposedRange, - breakReason: .frameLimit, - blockRange: currentBlockRange, - attachmentRanges: currentAttachmentRanges, - attachmentKinds: currentAttachmentKinds, - blockKinds: currentBlockKinds, - semanticHints: currentSemanticHints, - attachmentPlacements: currentAttachmentPlacements, - diagnostics: diagnostics( - reason: .frameLimit, - range: proposedRange, - attachmentRanges: currentAttachmentRanges, - blockRange: currentBlockRange, - blockKinds: currentBlockKinds, - semanticHints: currentSemanticHints, - attachmentPlacements: currentAttachmentPlacements - ) - ) - } - - // MARK: - 语义边界查找 - - /// 在指定范围内查找最优的语义分页点(pageBreakBefore / pageBreakAfter)。 - /// - /// 只有当边界位置超过 minimumEnd(最小分页长度约束)时才有效。 - private func preferredSemanticBoundary( - in range: NSRange, - minimumEnd: Int - ) -> (location: Int, trigger: String)? { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return nil - } - var boundary: (location: Int, trigger: String)? - attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, attributeRange, stop in - guard let rawValue = value as? String else { return } - let hints = rawValue - .split(separator: ",") - .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } - guard !hints.isEmpty else { return } - - if hints.contains(.pageBreakBefore), - attributeRange.location > safeRange.location, - attributeRange.location >= minimumEnd { - boundary = (attributeRange.location, RDEPUBTextSemanticHint.pageBreakBefore.rawValue) - stop.pointee = true - return - } - - let attributeEnd = attributeRange.location + attributeRange.length - if hints.contains(.pageBreakAfter), - attributeEnd > minimumEnd, - attributeEnd < safeRange.location + safeRange.length { - boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue) - stop.pointee = true - return - } - } - return boundary - } - - /// 查找附件边界:只有块级附件(.attachment 或 .centered)才触发分页, - /// 行内脚注图标等不应导致整段移动。 - private func preferredAttachmentBoundary(in range: NSRange, minimumEnd: Int) -> Int? { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return nil - } - var boundary: Int? - attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, stop in - guard value != nil else { return } - - let location = attributeRange.location - let placement = attachmentPlacement(at: location) - let blockKind = blockKind(at: location) - - // 对标 WXRead:只有块级附件才应将整个块推到下一页。 - // 行内脚注图标等行内附件不应导致整段移动。 - // 只有真正的块级附件才需要整块挪页。 - // 行内/基线对齐的附件(例如脚注 note.png)不能触发附件边界分页。 - let isBlockLevelAttachment: Bool - switch placement { - case .centered: - isBlockLevelAttachment = true - case .inline, .baseline: - isBlockLevelAttachment = false - case nil: - isBlockLevelAttachment = blockKind == .attachment - } - guard isBlockLevelAttachment else { return } - - let boundaryRange = blockRange(at: location) ?? paragraphRange(containing: location) - if boundaryRange.location > safeRange.location, boundaryRange.location >= minimumEnd { - boundary = boundaryRange.location - stop.pointee = true - } - } - return boundary - } - - /// 查找 pageRelate 跨页关联边界:当下一页起始是一个 pageRelate 块且 - /// 该块恰好是当前页最后一行时,将最后一行移到下一页。 - private func preferredPageRelateBoundary( - after range: NSRange, - minimumEnd: Int, - lineRanges: [NSRange] - ) -> Int? { - let pageStartOfNext = range.location + range.length - guard pageStartOfNext > range.location, - pageStartOfNext < attributedString.length, - lineRanges.count >= 2, - semanticHints(at: pageStartOfNext).contains(.pageRelate) else { - return nil - } - - let boundaryBlockStart = blockRange(at: pageStartOfNext)?.location ?? 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 - } - - // MARK: - 属性查询工具 - - /// 获取指定位置的块级元素范围 - private func blockRange(at location: Int) -> NSRange? { - guard location >= 0, location < attributedString.length else { return nil } - let attributes = attributedString.attributes(at: location, effectiveRange: nil) - if let encodedRange = attributes[.rdPageBlockRange] as? String { - return NSRangeFromString(encodedRange) - } - return nil - } - - /// 获取指定位置的块级元素类型 - private func blockKind(at location: Int) -> RDEPUBTextBlockKind? { - guard location >= 0, location < attributedString.length else { return nil } - let attributes = attributedString.attributes(at: location, effectiveRange: nil) - guard let rawValue = attributes[.rdPageBlockKind] as? String else { return nil } - return RDEPUBTextBlockKind(rawValue: rawValue) - } - - /// 获取指定位置的附件布局方式 - private func attachmentPlacement(at location: Int) -> RDEPUBTextAttachmentPlacement? { - guard location >= 0, location < attributedString.length else { return nil } - let attributes = attributedString.attributes(at: location, effectiveRange: nil) - guard let rawValue = attributes[.rdPageAttachmentPlacement] as? String else { return nil } - return RDEPUBTextAttachmentPlacement(rawValue: rawValue) - } - - /// 获取包含指定位置的段落范围 - private func paragraphRange(containing location: Int) -> NSRange { - let source = attributedString.string as NSString - guard source.length > 0 else { return NSRange(location: 0, length: 0) } - let safeLocation = min(max(location, 0), max(source.length - 1, 0)) - return source.paragraphRange(for: NSRange(location: safeLocation, length: 0)) - } - - /// 获取指定范围内的所有附件字符范围 - private func attachmentRanges(in range: NSRange) -> [NSRange] { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return [] - } - var results: [NSRange] = [] - attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, _ in - guard value != nil else { return } - results.append(attributeRange) - } - return results - } - - /// 获取指定位置的语义提示列表 - private func semanticHints(at location: Int) -> [RDEPUBTextSemanticHint] { - guard location >= 0, location < attributedString.length else { return [] } - let attributes = attributedString.attributes(at: location, effectiveRange: nil) - guard let rawValue = attributes[.rdPageSemanticHints] as? String else { return [] } - return rawValue - .split(separator: ",") - .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } - } - - /// 获取指定范围内的附件类型列表(去重) - private func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return [] - } - var kinds: [RDEPUBTextAttachmentKind] = [] - attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, _, _ in - guard let rawValue = value as? String, - let kind = RDEPUBTextAttachmentKind(rawValue: rawValue), - !kinds.contains(kind) else { - return - } - kinds.append(kind) - } - return kinds - } - - /// 获取指定范围内的块级元素类型列表(去重) - private func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return [] - } - var kinds: [RDEPUBTextBlockKind] = [] - attributedString.enumerateAttribute(.rdPageBlockKind, in: safeRange) { value, _, _ in - guard let rawValue = value as? String, - let kind = RDEPUBTextBlockKind(rawValue: rawValue), - !kinds.contains(kind) else { - return - } - kinds.append(kind) - } - return kinds - } - - /// 获取指定范围内的语义提示列表(去重) - private func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return [] - } - var hints: [RDEPUBTextSemanticHint] = [] - attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, _, _ in - guard let rawValue = value as? String else { return } - for hint in rawValue.split(separator: ",").compactMap({ RDEPUBTextSemanticHint(rawValue: String($0)) }) where !hints.contains(hint) { - hints.append(hint) - } - } - return hints - } - - /// 获取指定范围内的附件布局方式列表(去重) - private func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] { - guard let safeRange = clampedRange(range), safeRange.length > 0 else { - return [] - } - var placements: [RDEPUBTextAttachmentPlacement] = [] - attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: safeRange) { value, _, _ in - guard let rawValue = value as? String, - let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue), - !placements.contains(placement) else { - return - } - placements.append(placement) - } - return placements - } - - /// 将范围裁剪到 attributedString 的合法边界内,避免属性枚举与子串提取越界。 - 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)) - } - - private func clampedProbeRange(for lineRange: NSRange) -> NSRange? { - clampedRange(NSRange(location: lineRange.location, length: max(lineRange.length, 1))) - } - - /// 查找指定位置之前最近的 fragment ID(用于阅读位置恢复) - private func nearestTrailingFragmentID( - endingAt location: Int, - fragmentOffsets: [String: Int] - ) -> String? { - fragmentOffsets - .filter { $0.value <= location } - .max { lhs, rhs in lhs.value < rhs.value }? - .key - } - - // MARK: - 行级 avoidPageBreakInside(WXRead 方案) - - /// 从 CTFrame 最后一行向前扫描,移除落在 avoidPageBreakInside 保护块内的尾部行。 - /// 对标 WXRead 的 WRCoreTextLayoutFrame.avoidPageBreakInsideByRemovingLastLinesIfNeeded。 - /// 最多移除 3 行(kMaxLinesToRemove),避免因保护块过大导致整页内容被清空。 - private func trimmedRangeForAvoidPageBreakInside( - from frame: CTFrame, - proposed: NSRange - ) -> NSRange { - guard config.avoidPageBreakInsideEnabled else { return proposed } - - let lines = CTFrameGetLines(frame) as! [CTLine] - guard !lines.isEmpty else { return proposed } - - var origins = [CGPoint](repeating: .zero, count: lines.count) - CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) - - // 从最后一行向前扫描,统计落在保护块内的连续尾部行数 - // kMaxLinesToRemove = 3(与 WXRead 一致) - let kMaxLinesToRemove = 3 - var linesToRemove = 0 - - for i in stride(from: lines.count - 1, through: 0, by: -1) { - let lineRange = CTLineGetStringRange(lines[i]) - let lineNSRange = NSRange(location: lineRange.location, length: lineRange.length) - - if lineIsInAvoidPageBreakInsideBlock(lineNSRange) { - linesToRemove += 1 - if linesToRemove >= kMaxLinesToRemove { - // 不超过 kMaxLinesToRemove 行,停在这里 - linesToRemove = kMaxLinesToRemove - break - } - } else { - break - } - } - - guard linesToRemove > 0 else { return proposed } - - let validLineCount = lines.count - linesToRemove - guard validLineCount > 0 else { - // 所有行都在保护块内,回退到原始范围 - return proposed - } - - let lastValidLine = lines[validLineCount - 1] - let lastLineRange = CTLineGetStringRange(lastValidLine) - let endLocation = lastLineRange.location + lastLineRange.length - let adjustedLength = endLocation - proposed.location - - guard adjustedLength > 0 else { return proposed } - - return NSRange(location: proposed.location, length: adjustedLength) - } - - /// CoreText 路径的 keepWithNext 处理:从最后行向前扫描 - private func trimmedRangeForKeepWithNext( - from frame: CTFrame, - proposed: NSRange - ) -> NSRange { - let lines = CTFrameGetLines(frame) as! [CTLine] - let lineRanges = lines.map { - let range = CTLineGetStringRange($0) - return NSRange(location: range.location, length: range.length) - } - return trimmedRangeForKeepWithNext(proposed: proposed, lineRanges: lineRanges) - } - - // MARK: - DTCoreText 行级处理 - -#if canImport(DTCoreText) - /// DTCoreText 路径的 avoidPageBreakInside 处理 - private func trimmedRangeForAvoidPageBreakInside( - from layoutFrame: DTCoreTextLayoutFrame, - proposed: NSRange - ) -> NSRange { - guard config.avoidPageBreakInsideEnabled else { return proposed } - - guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine], !lines.isEmpty else { - return proposed - } - - let kMaxLinesToRemove = 3 - var linesToRemove = 0 - - for line in lines.reversed() { - let lineRange = line.stringRange() - if lineIsInAvoidPageBreakInsideBlock(lineRange) { - linesToRemove += 1 - if linesToRemove >= kMaxLinesToRemove { - linesToRemove = kMaxLinesToRemove - break - } - } else { - break - } - } - - guard linesToRemove > 0 else { return proposed } - - let validLineCount = lines.count - linesToRemove - guard validLineCount > 0 else { return proposed } - - let lastValidLine = lines[validLineCount - 1] - let lastLineRange = lastValidLine.stringRange() - let endLocation = lastLineRange.location + lastLineRange.length - let adjustedLength = endLocation - proposed.location - - guard adjustedLength > 0 else { return proposed } - - return NSRange(location: proposed.location, length: adjustedLength) - } - - /// DTCoreText 路径的 keepWithNext 处理 - private func trimmedRangeForKeepWithNext( - from layoutFrame: DTCoreTextLayoutFrame, - proposed: NSRange - ) -> NSRange { - guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine], !lines.isEmpty else { - return proposed - } - let lineRanges = lines.map { $0.stringRange() } - return trimmedRangeForKeepWithNext(proposed: proposed, lineRanges: lineRanges) - } -#endif - - /// 从最后行向前扫描,移除落在 keepWithNext 保护块内的尾部行。 - /// 最多移除 3 行。 - private func trimmedRangeForKeepWithNext( - proposed: NSRange, - lineRanges: [NSRange] - ) -> NSRange { - guard !lineRanges.isEmpty else { return proposed } - - let kMaxLinesToRemove = 3 - var linesToRemove = 0 - - for lineRange in lineRanges.reversed() { - if lineIsInKeepWithNextBlock(lineRange) { - linesToRemove += 1 - if linesToRemove >= kMaxLinesToRemove { - linesToRemove = kMaxLinesToRemove - break - } - } else { - break - } - } - - guard linesToRemove > 0 else { return proposed } - - let validLineCount = lineRanges.count - linesToRemove - guard validLineCount > 0 else { return proposed } - - let lastValidLine = lineRanges[validLineCount - 1] - let endLocation = lastValidLine.location + lastValidLine.length - let adjustedLength = endLocation - proposed.location - guard adjustedLength > 0 else { return proposed } - return NSRange(location: proposed.location, length: adjustedLength) - } - - /// 检查某行是否落在 avoidPageBreakInside 保护块内 - private 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 - } - - /// 只有真正的块级内容才应触发 avoidPageBreakInside。 - /// 像脚注 note.png 这类行内图片会被标记为 `img`,但不该把整行推到下一页。 - 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:)) - - if blockKind == .attachment, placement != .centered { - return false - } - - return true - } - - /// 检查某行是否落在 keepWithNext 保护块内 - private 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 - } - - /// 获取 CTFrame 中所有行的字符范围 - private func lineRanges(from frame: CTFrame) -> [NSRange] { - let lines = CTFrameGetLines(frame) as! [CTLine] - return lines.map { - let lineRange = CTLineGetStringRange($0) - return NSRange(location: lineRange.location, length: lineRange.length) - } - } - - /// 逐句对齐 WXRead `WRChapterPageCount.recalculatePageRangesForAttributedString` 的分页循环: - /// 1. 取出 CTFrame 当前页的所有行 - /// 2. 读取每行 origin - /// 3. 用 `lineY - ascent > usableHeight` 判断是否越过可用高度 - /// 4. 累计能容纳行的字符数得到本页范围 - private func proposedRangeUsingWXReadPageCount( - from frame: CTFrame, - start location: Int, - usableHeight: CGFloat, - totalLength: Int - ) -> NSRange { - let lines = CTFrameGetLines(frame) as! [CTLine] - guard !lines.isEmpty else { - return NSRange(location: location, length: 0) - } - - var origins = [CGPoint](repeating: .zero, count: lines.count) - CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) - - var pageCharCount = 0 - for (index, line) in lines.enumerated() { - let lineRange = CTLineGetStringRange(line) - let lineY = origins[index].y - var ascent: CGFloat = 0 - var descent: CGFloat = 0 - CTLineGetTypographicBounds(line, &ascent, &descent, nil) - - if lineY - ascent > usableHeight { - break - } - - pageCharCount += lineRange.length - } - - if pageCharCount == 0 { - pageCharCount = 1 - } - - return NSRange(location: location, length: min(pageCharCount, totalLength - location)) - } - -#if canImport(DTCoreText) - /// 获取 DTCoreTextLayoutFrame 中所有行的字符范围 - private func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] { - guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else { - return [] - } - return lines.map { $0.stringRange() } - } -#endif - - // MARK: - 诊断日志 - - /// 生成分页诊断日志,记录分页原因、范围和语义信息 - private func diagnostics( - reason: RDEPUBTextPageBreakReason, - range: NSRange, - attachmentRanges: [NSRange], - blockRange: NSRange?, - blockKinds: [RDEPUBTextBlockKind], - semanticHints: [RDEPUBTextSemanticHint], - attachmentPlacements: [RDEPUBTextAttachmentPlacement], - trigger: String? = nil - ) -> [String] { - var items = ["page break: \(reason.rawValue)", "page range: \(NSStringFromRange(range))"] - if let blockRange { - items.append("block range: \(NSStringFromRange(blockRange))") - } - if !attachmentRanges.isEmpty { - items.append("attachment ranges: \(attachmentRanges.map(NSStringFromRange).joined(separator: ","))") - } - if !blockKinds.isEmpty { - items.append("block kinds: \(blockKinds.map(\.rawValue).joined(separator: ","))") - } - if !semanticHints.isEmpty { - items.append("semantic hints: \(semanticHints.map(\.rawValue).joined(separator: ","))") - } - if !attachmentPlacements.isEmpty { - items.append("attachment placements: \(attachmentPlacements.map(\.rawValue).joined(separator: ","))") - } - if let trigger, !trigger.isEmpty { - items.append("semantic trigger: \(trigger)") - } - return items - } -} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift index 33d96c5..3887e27 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift @@ -100,6 +100,8 @@ public struct RDEPUBTextLayoutConfig: Equatable { public var hyphenation: Bool /// 图片最大高度占页面高度的比例 public var imageMaxHeightRatio: CGFloat + /// 当调用方暂时拿不到 pageSize 时,用于估算附件尺寸的兜底 viewport。 + public var fallbackViewportSize: CGSize public init( frameWidth: CGFloat = 0, @@ -111,7 +113,8 @@ public struct RDEPUBTextLayoutConfig: Equatable { avoidWidows: Bool = true, avoidPageBreakInsideEnabled: Bool = true, hyphenation: Bool = true, - imageMaxHeightRatio: CGFloat = 0.85 + imageMaxHeightRatio: CGFloat = 0.85, + fallbackViewportSize: CGSize = CGSize(width: 375, height: 667) ) { self.frameWidth = frameWidth self.frameHeight = frameHeight @@ -123,6 +126,7 @@ public struct RDEPUBTextLayoutConfig: Equatable { self.avoidPageBreakInsideEnabled = avoidPageBreakInsideEnabled self.hyphenation = hyphenation self.imageMaxHeightRatio = imageMaxHeightRatio + self.fallbackViewportSize = fallbackViewportSize } /// 默认配置 @@ -172,7 +176,9 @@ public struct RDEPUBTextLayoutConfig: Equatable { avoidWidows ? "1" : "0", avoidPageBreakInsideEnabled ? "1" : "0", hyphenation ? "1" : "0", - String(format: "%.3f", imageMaxHeightRatio) + String(format: "%.3f", imageMaxHeightRatio), + String(format: "%.3f", fallbackViewportSize.width), + String(format: "%.3f", fallbackViewportSize.height) ].joined(separator: "|") } } diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift deleted file mode 100644 index b086463..0000000 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift +++ /dev/null @@ -1,1304 +0,0 @@ -import UIKit -import CoreText - -#if canImport(DTCoreText) -import DTCoreText -#endif - -/// 渲染器支持工具集:负责 HTML 预处理、fragment 标记注入/提取、CSS 组装和字体标准化。 -/// -/// 本枚举是渲染链路的预处理层,`RDEPUBTextBookBuilder` 在调用渲染器前通过 -/// `makeChapterRenderRequest()` 完成 HTML 的全部预处理工作。 -/// -/// 核心职责: -/// - HTML 规范化:清理冗余字符、规范化附件标记 -/// - Fragment 标记注入/提取:id 属性 → ${id=xxx} 标记 → 字符偏移量映射 -/// - 语义标记注入:HTML 标签 → ${rd-sem-start/end} 标记 → NSAttributedString 属性 -/// - CSS 组装:按优先级合并 default/replace/dark/epub/user 五层样式表 -/// - 字体标准化:将 EPUB 原始字体映射到用户设置的字体 -/// - 附件规范化:图片尺寸、脚注图标、封面图片等特殊处理 -enum RDEPUBTextRendererSupport { - /// 外部样式表链接正则 - private static let stylesheetLinkPattern = #"]*rel\s*=\s*["'][^"']*stylesheet[^"']*["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>"# - /// 图片源地址正则 - private static let imageSourcePattern = #"]*src\s*=\s*["']([^"']+)["'][^>]*>"# - /// 语义标记正则(${rd-sem-start:...} / ${rd-sem-end:...}) - private static let semanticMarkerPattern = #"\$\{rd-sem-(start|end):([^}]+)\}"# - /// 脚注附件日志已输出标记(避免重复日志) - private static var didLogFootnoteAttachment = false - /// 封面附件日志已输出标记(避免重复日志) - private static var didLogCoverAttachment = false - /// 已注册字体资源,避免重复调用 CTFontManager。 - private static var registeredFontPaths = Set() - - // MARK: - Fragment 标记注入/提取 - - /// 将 HTML 中的 id 属性元素注入 fragment 标记。 - /// - /// 将 `` 转换为 `${id=xxx}`, - /// 使 DTCoreText 渲染后能在富文本中定位到 fragment 偏移量。 - 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" - ) - } - - /// 从渲染后的富文本中提取 fragment 偏移量映射。 - /// - /// 扫描 `${id=xxx}` 标记,记录每个 fragment ID 的字符偏移量, - /// 然后删除标记文本,返回 [fragmentID: offset] 字典。 - 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 - } - - /// 将 HTML 中注入的语义标记(${rd-sem-start/end})应用到富文本属性中。 - /// - /// 标记包含 blockKind、semanticHints、attachmentPlacement 等信息, - /// 解析后写入对应的 .rdPage* 属性键,供分页引擎读取。 - static func applyPaginationSemantics(in attributedString: NSMutableAttributedString) { - guard let regex = try? NSRegularExpression(pattern: semanticMarkerPattern, options: []) else { - return - } - - let mutableString = NSMutableString(string: attributedString.string) - var searchRange = NSRange(location: 0, length: mutableString.length) - var openRanges: [String: (location: Int, semantics: RDPaginationSemantics)] = [:] - - while let match = regex.firstMatch(in: mutableString as String, options: [], range: searchRange) { - let kind = mutableString.substring(with: match.range(at: 1)) - let payload = mutableString.substring(with: match.range(at: 2)) - let markerLocation = match.range.location - - attributedString.deleteCharacters(in: match.range) - mutableString.deleteCharacters(in: match.range) - - if kind == "start" { - let semantics = parseSemanticMarkerPayload(payload) - openRanges[semantics.id] = (markerLocation, semantics) - } else { - let markerID = parseSemanticEndID(payload) - if let markerID, let opened = openRanges.removeValue(forKey: markerID) { - let length = max(markerLocation - opened.location, 0) - if length > 0 { - apply(semantics: opened.semantics, to: NSRange(location: opened.location, length: length), in: attributedString) - } - } - } - - searchRange = NSRange(location: markerLocation, length: mutableString.length - markerLocation) - } - } - - /// 规范化阅读属性:统一字体、行距、颜色,并注入分页语义属性。 - /// - /// 遍历富文本的所有属性区间: - /// 1. 将 EPUB 原始字体映射到用户设置字体(保留粗体/斜体特征) - /// 2. 统一行距和段间距 - /// 3. 应用用户设置的文本颜色 - /// 4. 规范化附件显示尺寸 - /// 5. 注入块级元素范围、序号、类型等分页语义属性 - static func normalizeReadingAttributes(in attributedString: NSMutableAttributedString, style: RDEPUBTextRenderStyle) { - let fullRange = NSRange(location: 0, length: attributedString.length) - var blockIndex = 0 - let sourceText = attributedString.string as NSString - attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in - let sourceFont = attributes[.font] as? UIFont - let normalizedFont = normalizedFont(from: sourceFont, baseFont: style.font) - 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 - updatedAttributes[.paragraphStyle] = paragraph - if let textColor = style.textColor { - updatedAttributes[.foregroundColor] = textColor - } - normalizeAttachmentDisplayIfNeeded(in: &updatedAttributes, font: normalizedFont) - let semanticBlockRange = (attributes[.rdPageBlockRange] as? String) - .flatMap(NSRangeFromString) - .flatMap { $0.length > 0 ? $0 : nil } - let paragraphRange = sourceText.length > 0 - ? sourceText.paragraphRange(for: NSRange(location: min(range.location, max(sourceText.length - 1, 0)), length: 0)) - : range - updatedAttributes[.rdPageBlockRange] = NSStringFromRange(semanticBlockRange ?? paragraphRange) - updatedAttributes[.rdPageBlockIndex] = blockIndex - if let attachmentKind = attachmentKind(for: attributes) { - updatedAttributes[.rdPageAttachmentKind] = attachmentKind.rawValue - } - if let placement = normalizeAttachmentPlacement(for: attributes) { - updatedAttributes[.rdPageAttachmentPlacement] = placement.rawValue - } - if let blockKind = normalizeBlockKind(for: attributes) { - updatedAttributes[.rdPageBlockKind] = blockKind.rawValue - } - if let hints = normalizeSemanticHints(for: attributes), !hints.isEmpty { - updatedAttributes[.rdPageSemanticHints] = hints.map(\.rawValue).joined(separator: ",") - } - attributedString.setAttributes(updatedAttributes, range: range) - blockIndex += 1 - } - } - - /// 回退渲染:当 DTCoreText 不可用时,将 HTML 源码当作纯文本处理。 - 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) - } - - /// 核心预处理方法:将原始 HTML 转换为完整的章节渲染请求。 - /// - /// 预处理流程: - /// 1. normalizeHTML:清理冗余字符、规范化附件 HTML 标记 - /// 2. injectPaginationSemanticMarkers:为 HTML 标签注入语义标记 - /// 3. inlineLinkedStyleSheets:将外部样式表内联到 HTML 中 - /// 4. 组装五层 CSS(default/replace/dark/epub/user)并注入 style 标签 - /// 5. injectBaseHref:注入 base 标签以解析相对路径 - /// 6. injectFragmentMarkers:注入 fragment 锚点标记 - /// 7. collectImageDiagnostics:收集图片资源引用诊断 - static func makeChapterRenderRequest( - href: String, - title: String, - rawHTML: String, - baseURL: URL?, - style: RDEPUBTextRenderStyle, - resourceResolver: RDEPUBResourceResolver?, - contentLanguageCode: String? = nil, - pageSize: CGSize? = nil, - layoutConfig: RDEPUBTextLayoutConfig? = nil - ) -> RDEPUBTextChapterRenderRequest { - let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML)) - let stylesheetHrefReplacements = inlineLinkedStyleSheets( - in: normalizedHTML, - chapterHref: href, - baseURL: baseURL, - resourceResolver: resourceResolver - ) - let layers = makeStyleSheetLayers( - style: style, - epubCSS: stylesheetHrefReplacements.inlinedCSS, - 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, - styleID: "rd-native-default-replace-dark", - css: layers - .filter { $0.kind != .user && $0.kind != .epub } - .map(\.css) - .joined(separator: "\n\n"), - position: .headStart - ) - let htmlWithEPUBLayer = injectStyleTag( - into: htmlWithDefaultLayers, - styleID: "rd-native-epub", - css: layers.first(where: { $0.kind == .epub })?.css ?? "", - position: .headEnd - ) - let composedHTML = injectStyleTag( - into: htmlWithEPUBLayer, - styleID: "rd-native-user", - css: layers.first(where: { $0.kind == .user })?.css ?? "", - position: .headEnd - ) - let markedHTML = injectFragmentMarkers(into: composedHTML) - let resourceDiagnostics = stylesheetHrefReplacements.diagnostics + collectImageDiagnostics( - in: markedHTML, - chapterHref: href, - baseURL: baseURL, - resourceResolver: resourceResolver - ) - - let context = RDEPUBTextChapterContext( - href: href, - title: title, - html: markedHTML, - baseURL: baseURL, - stylesheet: RDEPUBTextStyleSheetPackage(layers: layers), - resourceDiagnostics: resourceDiagnostics - ) - return RDEPUBTextChapterRenderRequest( - context: context, - style: style, - pageSize: pageSize, - layoutConfig: layoutConfig - ) - } - - /// HTML 规范化:清理冗余字符(CR、多余换行),规范化附件 HTML 标记 - static func normalizeHTML(_ html: String) -> String { - var cleanedHTML = html - let replacements: [(pattern: String, template: String)] = [ - (#"分页符"#, ""), - (#"\r"#, "\n"), - (#"\n+"#, "\n") - ] - - for replacement in replacements { - if let regex = try? NSRegularExpression(pattern: replacement.pattern, options: [.caseInsensitive]) { - cleanedHTML = regex.stringByReplacingMatches( - in: cleanedHTML, - options: [], - range: NSRange(location: 0, length: cleanedHTML.utf16.count), - withTemplate: replacement.template - ) - } - } - - cleanedHTML = normalizeAttachmentHTMLMarkers(in: cleanedHTML) - return cleanedHTML - } - -#if canImport(DTCoreText) - static func prepareHTMLElementForReaderRendering( - _ element: DTHTMLElement, - style: RDEPUBTextRenderStyle, - maxImageSize: CGSize? = nil - ) { - guard let attachment = element.textAttachment else { return } - let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize) - let fallbackSize = CGSize( - width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width), - height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85) - ) - normalizeAttachmentLayoutForWXRead( - attachment, - fontPointSize: pointSize, - maxImageSize: maxImageSize ?? fallbackSize - ) - if isFootnoteAttachment(attachment) { - element.displayStyle = .inline - } else if isCoverAttachment(attachment) { - element.displayStyle = .block - } - } -#endif - - private static func makeStyleSheetLayers( - style: RDEPUBTextRenderStyle, - epubCSS: String, - contentLanguageCode: String?, - sourceHTML: String - ) -> [RDEPUBTextStyleSheetLayer] { - let useLatinReplace = prefersLatinLanguageCSS( - languageCode: contentLanguageCode, - sourceHTML: sourceHTML - ) - var layers: [RDEPUBTextStyleSheetLayer] = [ - .init(kind: .default, css: defaultCSS()), - .init(kind: .replace, css: replaceCSS(useLatinVariant: useLatinReplace)) - ] - if isDarkTheme(style: style) { - layers.append(.init(kind: .dark, css: darkCSS(style: style))) - } - if !epubCSS.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - layers.append(.init(kind: .epub, css: epubCSS)) - } - layers.append(.init(kind: .user, css: userCSS(style: style))) - return layers - } - - private static func defaultCSS() -> String { - RDEPUBAssetRepository.string(for: .wxReadDefaultCSS) - } - - private static func replaceCSS(useLatinVariant: Bool) -> String { - let asset: RDEPUBAsset = useLatinVariant ? .wxReadLatinReplaceCSS : .wxReadReplaceCSS - return RDEPUBAssetRepository.string(for: asset) - } - - private static func inlineStyleCSS(in html: String) -> String { - guard let regex = try? NSRegularExpression(pattern: #"]*>([\s\S]*?)"#, 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 - - if let bodyPicContainerRegex = try? NSRegularExpression( - pattern: #"]*class\s*=\s*["'][^"']*\b(?:qrbodyPic|bodyPic)\b[^"']*["'][^>]*)>([\s\S]*?)"#, - options: [.caseInsensitive] - ) { - normalized = replaceMatches( - using: bodyPicContainerRegex, - in: normalized - ) { tag in - guard let imageTagRegex = try? NSRegularExpression(pattern: #"]*>"#, options: [.caseInsensitive]) else { - return tag - } - - return replaceMatches( - using: imageTagRegex, - in: tag - ) { imageTag in - mergeHTMLAttributes( - into: imageTag, - requiredClass: "bodyPic", - styleFragments: [ - "wr-vertical-center-style:2", - "max-width:100%", - "height:auto", - "display:block", - "margin-left:auto", - "margin-right:auto" - ] - ) - } - } - } - - if let footnoteRegex = try? NSRegularExpression( - pattern: #"]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#, - options: [.caseInsensitive] - ) { - normalized = replaceMatches( - using: footnoteRegex, - in: normalized - ) { tag in - mergeHTMLAttributes( - into: tag, - requiredClass: nil, - styleFragments: [ - "width:1em", - "height:1em", - "vertical-align:middle", - "display:inline-block" - ] - ) - } - } - - if let coverRegex = try? NSRegularExpression( - pattern: #"]*class\s*=\s*["'][^"']*\bfrontCover\b[^"']*["'][^>]*)>\s*(]*>)\s*"#, - options: [.caseInsensitive] - ) { - normalized = replaceMatches( - using: coverRegex, - in: normalized - ) { tag in - guard let imageTagRegex = try? NSRegularExpression(pattern: #"]*>"#, options: [.caseInsensitive]), - let imageMatch = imageTagRegex.firstMatch( - in: tag, - options: [], - range: NSRange(location: 0, length: (tag as NSString).length) - ), - let imageRange = Range(imageMatch.range, in: tag) else { - return tag - } - - let imageTag = String(tag[imageRange]) - let normalizedImageTag = mergeHTMLAttributes( - into: imageTag, - requiredClass: "rd-front-cover-image", - styleFragments: [ - "display:block", - "width:100%", - "height:auto", - "margin-left:auto", - "margin-right:auto" - ] - ) - return tag.replacingCharacters(in: imageRange, with: normalizedImageTag) - } - } - - return normalized - } - - private static func replaceMatches( - using regex: NSRegularExpression, - in source: String, - transform: (String) -> String - ) -> String { - let nsSource = source as NSString - let matches = regex.matches(in: source, options: [], range: NSRange(location: 0, length: nsSource.length)) - guard !matches.isEmpty else { return source } - - var rewritten = source - for match in matches.reversed() { - guard let range = Range(match.range, in: rewritten) else { continue } - let original = String(rewritten[range]) - rewritten.replaceSubrange(range, with: transform(original)) - } - return rewritten - } - - private static func string(from size: CGSize) -> String { - "{\(Int(round(size.width))), \(Int(round(size.height)))}" - } - - private static func mergeHTMLAttributes( - into tag: String, - requiredClass: String?, - styleFragments: [String] - ) -> String { - var rewritten = tag - if let requiredClass { - if let classRegex = try? NSRegularExpression(pattern: #"class\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]), - let match = classRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)), - match.numberOfRanges > 1 { - let existingClasses = (rewritten as NSString).substring(with: match.range(at: 1)) - if !existingClasses.localizedCaseInsensitiveContains(requiredClass) { - let replacement = #"class="\#(existingClasses) \#(requiredClass)""# - if let range = Range(match.range, in: rewritten) { - rewritten.replaceSubrange(range, with: replacement) - } - } - } else if let closing = rewritten.lastIndex(of: ">") { - rewritten.insert(contentsOf: #" class="\#(requiredClass)""#, at: closing) - } - } - - let styleValue = styleFragments.joined(separator: ";") + ";" - if let styleRegex = try? NSRegularExpression(pattern: #"style\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]), - let match = styleRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)), - match.numberOfRanges > 1 { - let existing = (rewritten as NSString).substring(with: match.range(at: 1)).trimmingCharacters(in: .whitespacesAndNewlines) - let merged = existing.isEmpty ? styleValue : existing + (existing.hasSuffix(";") ? "" : ";") + styleValue - let replacement = #"style="\#(merged)""# - if let range = Range(match.range, in: rewritten) { - rewritten.replaceSubrange(range, with: replacement) - } - } else if let closing = rewritten.lastIndex(of: ">") { - rewritten.insert(contentsOf: #" style="\#(styleValue)""#, at: closing) - } - - return rewritten - } - - private static func normalizeAttachmentDisplayIfNeeded( - in attributes: inout [NSAttributedString.Key: Any], - font: UIFont - ) { - guard let attachment = attributes[.attachment] else { return } - -#if canImport(DTCoreText) - if let textAttachment = attachment as? DTTextAttachment { - normalizeAttachmentLayoutForWXRead(textAttachment, fontPointSize: font.pointSize) - attributes[.attachment] = textAttachment - return - } -#endif - - if let textAttachment = attachment as? NSTextAttachment, textAttachment.bounds.height <= 0 { - let targetHeight = max(round(font.pointSize * 0.86), 1) - textAttachment.bounds = CGRect(x: 0, y: 0, width: targetHeight, height: targetHeight) - attributes[.attachment] = textAttachment - } - } - - private static func darkCSS(style: RDEPUBTextRenderStyle) -> String { - let background = style.backgroundColor?.ss_cssString ?? "rgba(0, 0, 0, 1.000)" - let text = style.textColor?.ss_cssString ?? "rgba(255, 255, 255, 1.000)" - return RDEPUBAssetRepository.string(for: .wxReadDarkCSS) + "\n\n" + """ - html, body { - background: \(background) !important; - color: \(text) !important; - } - a { - color: \(text) !important; - } - """ - } - - private static func userCSS(style: RDEPUBTextRenderStyle) -> String { - let lineHeight = max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1) - let text = style.textColor.map { "color: \($0.ss_cssString) !important;" } ?? "" - let background = style.backgroundColor.map { "background: \($0.ss_cssString) !important;" } ?? "" - return """ - html, body { - font-family: "\(style.font.familyName)" !important; - font-size: \(String(format: "%.3f", style.font.pointSize))px !important; - line-height: \(String(format: "%.3f", lineHeight)) !important; - \(text) - \(background) - } - """ - } - - private static func isDarkTheme(style: RDEPUBTextRenderStyle) -> Bool { - guard let backgroundColor = style.backgroundColor else { - return false - } - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - var alpha: CGFloat = 0 - backgroundColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - let luminance = (0.299 * red) + (0.587 * green) + (0.114 * blue) - return luminance < 0.5 - } - - private static func prefersLatinLanguageCSS( - languageCode: String?, - sourceHTML: String - ) -> Bool { - let candidateCodes = inferredLanguageCodes( - explicitLanguageCode: languageCode, - sourceHTML: sourceHTML - ) - - if candidateCodes.contains(where: isExplicitLatinLanguageCode) { - return true - } - if candidateCodes.contains(where: isExplicitCJKLanguageCode) { - return false - } - - let textSample = plainTextSample(from: sourceHTML) - guard !textSample.isEmpty else { return false } - - var alphabeticCount = 0 - var latinCount = 0 - for scalar in textSample.unicodeScalars { - guard CharacterSet.letters.contains(scalar) else { continue } - alphabeticCount += 1 - if isLatinScalar(scalar) { - latinCount += 1 - } - } - - guard alphabeticCount >= 80 else { return false } - return (Double(latinCount) / Double(alphabeticCount)) >= 0.6 - } - - private static func inferredLanguageCodes( - explicitLanguageCode: String?, - sourceHTML: String - ) -> [String] { - var codes: [String] = [] - if let explicitLanguageCode { - let normalized = explicitLanguageCode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - if !normalized.isEmpty { - codes.append(normalized) - } - } - - if let regex = try? NSRegularExpression( - pattern: #"\b(?:xml:lang|lang)\s*=\s*["']([^"']+)["']"#, - options: [.caseInsensitive] - ) { - let nsHTML = sourceHTML as NSString - let range = NSRange(location: 0, length: min(nsHTML.length, 8_000)) - for match in regex.matches(in: sourceHTML, options: [], range: range) { - guard match.numberOfRanges > 1 else { continue } - let code = nsHTML.substring(with: match.range(at: 1)) - .trimmingCharacters(in: .whitespacesAndNewlines) - .lowercased() - if !code.isEmpty { - codes.append(code) - } - } - } - - return Array(NSOrderedSet(array: codes)) as? [String] ?? codes - } - - private static func isExplicitLatinLanguageCode(_ code: String) -> Bool { - let normalized = code.lowercased() - if normalized.contains("latn") { - return true - } - - let prefix = normalized.split(separator: "-").first.map(String.init) ?? normalized - let latinPrefixes: Set = [ - "en", "fr", "de", "es", "it", "pt", "nl", "sv", "da", "no", "fi", - "is", "ga", "cy", "pl", "cs", "sk", "sl", "hr", "hu", "ro", "tr", - "vi", "id", "ms", "tl", "sw", "af", "sq", "et", "lv", "lt" - ] - return latinPrefixes.contains(prefix) - } - - private static func isExplicitCJKLanguageCode(_ code: String) -> Bool { - let prefix = code.lowercased().split(separator: "-").first.map(String.init) ?? code.lowercased() - return ["zh", "ja", "ko"].contains(prefix) - } - - private static func plainTextSample(from html: String) -> String { - let maxLength = min(html.count, 20_000) - let sample = String(html.prefix(maxLength)) - let withoutTags = sample.replacingOccurrences( - of: #"<[^>]+>"#, - with: " ", - options: .regularExpression - ) - return withoutTags.replacingOccurrences( - of: #"&[A-Za-z0-9#]+;"#, - with: " ", - options: .regularExpression - ) - } - - private static func isLatinScalar(_ scalar: UnicodeScalar) -> Bool { - switch scalar.value { - case 0x0041...0x007A, - 0x00C0...0x00FF, - 0x0100...0x024F, - 0x1E00...0x1EFF: - return true - default: - return false - } - } - - private static func injectBaseHref(into html: String, baseURL: URL?) -> String { - guard let baseURL else { - return html - } - let baseTag = "" - if html.range(of: "", options: [.caseInsensitive]) != nil { - return html.replacingOccurrences(of: "", with: "\n\(baseTag)", options: [.caseInsensitive]) - } - if let htmlTagRange = html.range(of: "", range: htmlTagRange.lowerBound..\n\(baseTag)\n") - } - return "\n\(baseTag)\n\n" + html - } - - private static func inlineLinkedStyleSheets( - in html: String, - chapterHref: String, - baseURL: URL?, - resourceResolver: RDEPUBResourceResolver? - ) -> (html: String, inlinedCSS: String, diagnostics: [RDEPUBTextResourceReferenceDiagnostic]) { - guard let regex = try? NSRegularExpression(pattern: stylesheetLinkPattern, options: [.caseInsensitive]) else { - return (html, "", []) - } - - let nsHTML = html as NSString - let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)) - guard !matches.isEmpty else { - return (html, "", []) - } - - var rewrittenHTML = html - var inlinedCSSBlocks: [String] = [] - var diagnostics: [RDEPUBTextResourceReferenceDiagnostic] = [] - - for match in matches.reversed() { - guard match.numberOfRanges > 1 else { continue } - let href = nsHTML.substring(with: match.range(at: 1)) - let resolution = resolveReference( - href, - kind: .stylesheet, - chapterHref: chapterHref, - baseURL: baseURL, - resourceResolver: resourceResolver - ) - diagnostics.append(resolution.diagnostic) - - let replacement: String - if let fileURL = resolution.resolvedFileURL, - let css = try? String(contentsOf: fileURL), - resolution.diagnostic.existsOnDisk { - let cssWithResolvedURLs = rewriteCSSResourceURLs( - in: css, - styleSheetFileURL: fileURL - ) - inlinedCSSBlocks.append(cssWithResolvedURLs) - replacement = "" - } else { - replacement = "" - } - - if let range = Range(match.range, in: rewrittenHTML) { - rewrittenHTML.replaceSubrange(range, with: replacement) - } - } - - return (rewrittenHTML, inlinedCSSBlocks.reversed().joined(separator: "\n\n"), diagnostics.reversed()) - } - - private static func collectImageDiagnostics( - in html: String, - chapterHref: String, - baseURL: URL?, - resourceResolver: RDEPUBResourceResolver? - ) -> [RDEPUBTextResourceReferenceDiagnostic] { - guard let regex = try? NSRegularExpression(pattern: imageSourcePattern, options: [.caseInsensitive]) else { - return [] - } - let nsHTML = html as NSString - return regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)).compactMap { match in - guard match.numberOfRanges > 1 else { return nil } - let href = nsHTML.substring(with: match.range(at: 1)) - return resolveReference( - href, - kind: .image, - chapterHref: chapterHref, - baseURL: baseURL, - resourceResolver: resourceResolver - ).diagnostic - } - } - - private static func rewriteCSSResourceURLs( - in css: String, - styleSheetFileURL: URL - ) -> String { - guard let regex = try? NSRegularExpression(pattern: #"url\(([^)]+)\)"#, options: [.caseInsensitive]) else { - return css - } - - let nsCSS = css as NSString - let matches = regex.matches(in: css, options: [], range: NSRange(location: 0, length: nsCSS.length)) - guard !matches.isEmpty else { - return css - } - - var rewrittenCSS = css - for match in matches.reversed() { - guard match.numberOfRanges > 1 else { continue } - let rawValue = nsCSS.substring(with: match.range(at: 1)) - .trimmingCharacters(in: .whitespacesAndNewlines) - .trimmingCharacters(in: CharacterSet(charactersIn: "\"'")) - guard !rawValue.isEmpty else { continue } - if rawValue.hasPrefix("data:") || rawValue.hasPrefix("http://") || rawValue.hasPrefix("https://") || rawValue.hasPrefix("file://") || rawValue.hasPrefix("#") { - continue - } - guard let resolvedURL = URL(string: rawValue, relativeTo: styleSheetFileURL.deletingLastPathComponent())?.standardizedFileURL else { - continue - } - let replacement = "url(\"\(resolvedURL.absoluteString)\")" - if let range = Range(match.range, in: rewrittenCSS) { - rewrittenCSS.replaceSubrange(range, with: replacement) - } - } - return rewrittenCSS - } - - private static func resolveReference( - _ reference: String, - kind: RDEPUBTextResourceReferenceKind, - chapterHref: String, - baseURL: URL?, - resourceResolver: RDEPUBResourceResolver? - ) -> (normalizedHref: String?, resolvedFileURL: URL?, diagnostic: RDEPUBTextResourceReferenceDiagnostic) { - let trimmedReference = reference.trimmingCharacters(in: .whitespacesAndNewlines) - let normalizedHref = resourceResolver?.normalizedHref(trimmedReference, relativeToHref: chapterHref) - let resolvedFileURL = resourceResolver?.fileURL(forReference: trimmedReference, relativeToHref: chapterHref) - ?? URL(string: trimmedReference, relativeTo: baseURL)?.standardizedFileURL - let existsOnDisk = resolvedFileURL.map { FileManager.default.fileExists(atPath: $0.path) } ?? false - let diagnostic = RDEPUBTextResourceReferenceDiagnostic( - kind: kind, - chapterHref: chapterHref, - originalReference: trimmedReference, - normalizedHref: normalizedHref, - resolvedFileURL: resolvedFileURL, - existsOnDisk: existsOnDisk - ) - return (normalizedHref, resolvedFileURL, diagnostic) - } - - private enum StyleInjectionPosition { - case headStart - case headEnd - } - - private static func injectStyleTag( - into html: String, - styleID: String, - css: String, - position: StyleInjectionPosition - ) -> String { - let trimmedCSS = css.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmedCSS.isEmpty else { - return html - } - - let styleTag = "" - switch position { - case .headStart: - if html.range(of: "", options: [.caseInsensitive]) != nil { - return html.replacingOccurrences(of: "", with: "\n\(styleTag)", options: [.caseInsensitive]) - } - case .headEnd: - if html.range(of: "", options: [.caseInsensitive]) != nil { - return html.replacingOccurrences(of: "", with: "\(styleTag)\n", options: [.caseInsensitive]) - } - } - - if html.range(of: " 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 - } - - private static func attachmentKind(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextAttachmentKind? { - if let attachment = attributes[.attachment] as? NSTextAttachment { - if attachment.image != nil || attachment.fileType?.lowercased().contains("image") == true { - return .image - } - return .generic - } - - for value in attributes.values { - let typeName = String(describing: type(of: value)).lowercased() - if typeName.contains("attachment") { - return typeName.contains("image") ? .image : .generic - } - } - - return nil - } - - private static func injectPaginationSemanticMarkers(into html: String) -> String { - guard let regex = try? NSRegularExpression(pattern: #"<[^>]+>"#, options: [.caseInsensitive]) else { - return html - } - - let nsHTML = html as NSString - let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)) - guard !matches.isEmpty else { - return html - } - - var output = "" - var cursor = 0 - var openTagStack: [(name: String, id: String)] = [] - var nextMarkerID = 0 - - for match in matches { - let tagRange = match.range - guard tagRange.location >= cursor else { continue } - output += nsHTML.substring(with: NSRange(location: cursor, length: tagRange.location - cursor)) - - let tag = nsHTML.substring(with: tagRange) - let loweredTag = tag.lowercased() - let tagName = htmlTagName(from: loweredTag) - - if loweredTag.hasPrefix("") { - output += startMarker + tag + semanticEndMarker(id: markerID) - } else { - openTagStack.append((name: tagName, id: markerID)) - output += tag + startMarker - } - } else { - output += tag - } - - cursor = tagRange.location + tagRange.length - } - - output += nsHTML.substring(from: cursor) - return output - } - - private static func htmlTagName(from loweredTag: String) -> String? { - let trimmed = loweredTag.trimmingCharacters(in: .whitespacesAndNewlines) - guard trimmed.hasPrefix("<") else { return nil } - let body = trimmed.dropFirst().drop(while: { $0 == "/" || $0 == "!" || $0 == "?" }) - let name = body.prefix { $0.isLetter || $0.isNumber } - return name.isEmpty ? nil : String(name) - } - - private static func paginationSemantics(forTagName tagName: String, rawTag: String) -> RDPaginationSemantics? { - let loweredTag = rawTag.lowercased() - let blockKind = inferredBlockKind(forTagName: tagName, rawTag: loweredTag) - let hints = inferredHints(forTagName: tagName, rawTag: loweredTag) - let placement = inferredAttachmentPlacement(forTagName: tagName, rawTag: loweredTag) - - guard blockKind != nil || !hints.isEmpty || placement != nil else { - return nil - } - - return RDPaginationSemantics( - id: "", - blockKind: blockKind, - hints: hints, - attachmentPlacement: placement - ) - } - - private static func inferredBlockKind(forTagName tagName: String, rawTag: String) -> RDEPUBTextBlockKind? { - if rawTag.contains("bodypic") || tagName == "img" || tagName == "figure" { - return .attachment - } - switch tagName { - case "h1", "h2", "h3", "h4", "h5", "h6": - return .generic - case "blockquote": - return .blockquote - case "ul", "ol", "li": - return .list - case "table", "thead", "tbody", "tfoot", "tr", "td", "th": - return .table - case "pre", "code": - return .code - case "p": - return .paragraph - case "div": - if rawTag.contains("code") || rawTag.contains("highlight") { - return .code - } - if rawTag.contains("quote") || rawTag.contains("blockquote") { - return .blockquote - } - if rawTag.contains("table") { - return .table - } - if rawTag.contains("list") { - return .list - } - return .generic - default: - return nil - } - } - - private static func inferredHints(forTagName tagName: String, rawTag: String) -> [RDEPUBTextSemanticHint] { - var hints: [RDEPUBTextSemanticHint] = [] - if rawTag.contains("avoidpagebreakinside") || - rawTag.contains("break-inside: avoid") || - rawTag.contains("page-break-inside: avoid") || - ["blockquote", "pre", "code", "table", "ul", "ol", "figure", "img"].contains(tagName) { - hints.append(.avoidPageBreakInside) - } - if ["h1", "h2", "h3", "h4", "h5", "h6"].contains(tagName) || - rawTag.contains("subhead") || - rawTag.contains("firsttitle") || - rawTag.contains("secondtitle") || - rawTag.contains("thirdtitle") || - rawTag.contains("fourthtitle") || - rawTag.contains("fifthtitle") || - rawTag.contains("sixthtitle") { - hints.append(.keepWithNext) - } - if rawTag.contains("pagebreakbefore") || - rawTag.contains("page-break-before: always") || - rawTag.contains("break-before: page") { - hints.append(.pageBreakBefore) - } - if rawTag.contains("pagebreakafter") || - rawTag.contains("page-break-after: always") || - rawTag.contains("break-after: page") { - hints.append(.pageBreakAfter) - } - if rawTag.contains("pageRelate".lowercased()) || rawTag.contains("weread-page-relate") { - hints.append(.pageRelate) - } - return hints - .reduce(into: [RDEPUBTextSemanticHint]()) { result, hint in - if !result.contains(hint) { - result.append(hint) - } - } - .sorted { $0.rawValue < $1.rawValue } - } - - private static func inferredAttachmentPlacement(forTagName tagName: String, rawTag: String) -> RDEPUBTextAttachmentPlacement? { - guard tagName == "img" || rawTag.contains("bodypic") || rawTag.contains("wr-vertical-center") else { - return nil - } - if rawTag.contains("wr-vertical-center-style: 2") || rawTag.contains("bodypic") { - return .centered - } - if rawTag.contains("wr-vertical-center-style: 1") || rawTag.contains("wr-vertical-center") { - return .baseline - } - return .inline - } - - private static func isVoidHTMLTag(_ tagName: String) -> Bool { - ["img", "br", "hr", "input", "meta", "link"].contains(tagName) - } - - private static func semanticStartMarker(id: String, semantics: RDPaginationSemantics) -> String { - var segments = ["id=\(id)"] - if let blockKind = semantics.blockKind { - segments.append("block=\(blockKind.rawValue)") - } - if !semantics.hints.isEmpty { - segments.append("hints=\(semantics.hints.map(\.rawValue).joined(separator: ","))") - } - if let placement = semantics.attachmentPlacement { - segments.append("placement=\(placement.rawValue)") - } - return "${rd-sem-start:\(segments.joined(separator: ";"))}" - } - - private static func semanticEndMarker(id: String) -> String { - "${rd-sem-end:id=\(id)}" - } - - private static func parseSemanticMarkerPayload(_ payload: String) -> RDPaginationSemantics { - var values: [String: String] = [:] - payload.split(separator: ";").forEach { entry in - let parts = entry.split(separator: "=", maxSplits: 1) - guard parts.count == 2 else { return } - values[String(parts[0])] = String(parts[1]) - } - let blockKind = values["block"].flatMap(RDEPUBTextBlockKind.init(rawValue:)) - let hints = values["hints"]? - .split(separator: ",") - .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } ?? [] - let placement = values["placement"].flatMap(RDEPUBTextAttachmentPlacement.init(rawValue:)) - return RDPaginationSemantics( - id: values["id"] ?? UUID().uuidString, - blockKind: blockKind, - hints: hints, - attachmentPlacement: placement - ) - } - - private static func parseSemanticEndID(_ payload: String) -> String? { - payload.split(separator: ";").first { $0.hasPrefix("id=") }.map { String($0.dropFirst(3)) } - } - - private static func apply( - semantics: RDPaginationSemantics, - to range: NSRange, - in attributedString: NSMutableAttributedString - ) { - var attributes: [NSAttributedString.Key: Any] = [:] - attributes[.rdPageBlockRange] = NSStringFromRange(range) - if let blockKind = semantics.blockKind { - attributes[.rdPageBlockKind] = blockKind.rawValue - } - if !semantics.hints.isEmpty { - attributes[.rdPageSemanticHints] = semantics.hints.map(\.rawValue).joined(separator: ",") - } - if let placement = semantics.attachmentPlacement { - attributes[.rdPageAttachmentPlacement] = placement.rawValue - } - guard !attributes.isEmpty else { return } - attributedString.addAttributes(attributes, range: range) - } - - private static func normalizeBlockKind(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextBlockKind? { - if let rawValue = attributes[.rdPageBlockKind] as? String, - let blockKind = RDEPUBTextBlockKind(rawValue: rawValue) { - return blockKind - } - return nil - } - - private static func normalizeSemanticHints(for attributes: [NSAttributedString.Key: Any]) -> [RDEPUBTextSemanticHint]? { - if let rawValue = attributes[.rdPageSemanticHints] as? String { - let hints = rawValue - .split(separator: ",") - .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } - return hints.isEmpty ? nil : hints - } - return nil - } - - private static func normalizeAttachmentPlacement(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextAttachmentPlacement? { - if let rawValue = attributes[.rdPageAttachmentPlacement] as? String, - let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue) { - return placement - } - if let attachmentKind = attachmentKind(for: attributes), attachmentKind == .image { - return .inline - } - return nil - } - - private struct RDPaginationSemantics { - var id: String - var blockKind: RDEPUBTextBlockKind? - var hints: [RDEPUBTextSemanticHint] - var attachmentPlacement: RDEPUBTextAttachmentPlacement? - } - -#if canImport(DTCoreText) - static func normalizeAttachmentLayoutForWXRead( - _ attachment: DTTextAttachment, - fontPointSize: CGFloat, - maxImageSize: CGSize? = nil - ) { - let pointSize = max(fontPointSize, 1) - let originalSize = attachment.originalSize - - if isFootnoteAttachment(attachment) { - let targetWidth = max(round(pointSize), 1) - let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1 - let targetHeight = max(round(targetWidth / max(aspectRatio, 0.1)), 1) - attachment.displaySize = CGSize(width: targetWidth, height: targetHeight) - attachment.verticalAlignment = .baseline - - if !didLogFootnoteAttachment { - didLogFootnoteAttachment = true - print("[EPUB][Attachment] footnote original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize)) font=\(pointSize)") - } - return - } - - if isCoverAttachment(attachment) { - let maxSize = maxImageSize ?? CGSize(width: round(UIScreen.main.bounds.width - 40), height: round(UIScreen.main.bounds.height - 56)) - if originalSize.width > 0, originalSize.height > 0 { - let scale = min(maxSize.width / originalSize.width, maxSize.height / originalSize.height) - attachment.displaySize = CGSize( - width: round(originalSize.width * scale), - height: round(originalSize.height * scale) - ) - } else { - attachment.displaySize = maxSize - } - attachment.verticalAlignment = .baseline - - if !didLogCoverAttachment { - didLogCoverAttachment = true - print("[EPUB][Attachment] cover original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize))") - } - return - } - - var resolvedSize = attachment.displaySize - if resolvedSize.width <= 0 || resolvedSize.height <= 0 { - resolvedSize = originalSize - } - if resolvedSize.width <= 0 || resolvedSize.height <= 0 { - resolvedSize = CGSize(width: pointSize, height: pointSize) - } - - if let maxImageSize, - resolvedSize.width > 0, - resolvedSize.height > 0, - (resolvedSize.width > maxImageSize.width || resolvedSize.height > maxImageSize.height) { - let scale = min(maxImageSize.width / resolvedSize.width, maxImageSize.height / resolvedSize.height) - resolvedSize = CGSize( - width: round(resolvedSize.width * scale), - height: round(resolvedSize.height * scale) - ) - } - - attachment.displaySize = CGSize(width: round(resolvedSize.width), height: round(resolvedSize.height)) - attachment.verticalAlignment = .center - } - - private static func isFootnoteAttachment(_ attachment: DTTextAttachment) -> Bool { - let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased() - let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased() - ?? ((attachment.attributes["src"] as? String) ?? "").lowercased() - return lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" - } - - private static func isCoverAttachment(_ attachment: DTTextAttachment) -> Bool { - let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased() - let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased() - ?? ((attachment.attributes["src"] as? String) ?? "").lowercased() - return lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg" - } -#endif -} diff --git a/Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift b/Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift index 49ac3ba..a750868 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift @@ -43,7 +43,20 @@ public final class RDPlainTextBookBuilder { for (index, spec) in chapterSpecs.enumerated() { let html = wrapTextAsHTML(spec.content) - let rendered = try renderer.renderChapter(html: html, baseURL: nil, style: style) + let request = RDEPUBTextChapterRenderRequest( + context: RDEPUBTextChapterContext( + href: "chapter_\(index).xhtml", + title: spec.title ?? "第 \(index + 1) 章", + html: html, + baseURL: nil, + stylesheet: RDEPUBTextStyleSheetPackage(layers: []), + resourceDiagnostics: [] + ), + style: style, + pageSize: pageSize, + layoutConfig: layoutConfig + ) + let rendered = try renderer.renderChapter(request: request) let content = NSMutableAttributedString(attributedString: rendered.attributedString) let layoutFrames = content.length > 0 ? content.rd_paginatedFrames(size: pageSize, config: layoutConfig) : [] diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBAttachmentNormalizer.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBAttachmentNormalizer.swift new file mode 100644 index 0000000..95aa829 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBAttachmentNormalizer.swift @@ -0,0 +1,187 @@ +import UIKit + +#if canImport(DTCoreText) +import DTCoreText +#endif + +struct RDEPUBAttachmentNormalizer { + /// 脚注附件日志已输出标记(避免重复日志) + private static var didLogFootnoteAttachment = false + /// 封面附件日志已输出标记(避免重复日志) + private static var didLogCoverAttachment = false + +#if canImport(DTCoreText) + func normalize( + _ attachment: DTTextAttachment, + fontPointSize: CGFloat, + maxImageSize: CGSize + ) { + Self.normalizeAttachmentLayoutForWXRead( + attachment, + fontPointSize: fontPointSize, + maxImageSize: maxImageSize + ) + } +#endif + + // MARK: - DTCoreText 附件布局规范化 + +#if canImport(DTCoreText) + /// 对 DTTextAttachment 做尺寸规范化:脚注缩放、封面缩放、通用图片缩放。 + static func normalizeAttachmentLayoutForWXRead( + _ attachment: DTTextAttachment, + fontPointSize: CGFloat, + maxImageSize: CGSize? = nil + ) { + let pointSize = max(fontPointSize, 1) + let originalSize = attachment.originalSize + + if isFootnoteAttachment(attachment) { + let targetWidth = max(round(pointSize), 1) + let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1 + let targetHeight = max(round(targetWidth / max(aspectRatio, 0.1)), 1) + attachment.displaySize = CGSize(width: targetWidth, height: targetHeight) + attachment.verticalAlignment = .baseline + + if !didLogFootnoteAttachment { + didLogFootnoteAttachment = true + print("[EPUB][Attachment] footnote original=\(RDEPUBHTMLNormalizer.string(from: originalSize)) display=\(RDEPUBHTMLNormalizer.string(from: attachment.displaySize)) font=\(pointSize)") + } + return + } + + if isCoverAttachment(attachment) { + let maxSize = maxImageSize ?? defaultMaxImageSize(fontPointSize: pointSize) + if originalSize.width > 0, originalSize.height > 0 { + let scale = min(maxSize.width / originalSize.width, maxSize.height / originalSize.height) + attachment.displaySize = CGSize( + width: round(originalSize.width * scale), + height: round(originalSize.height * scale) + ) + } else { + attachment.displaySize = maxSize + } + attachment.verticalAlignment = .baseline + + if !didLogCoverAttachment { + didLogCoverAttachment = true + print("[EPUB][Attachment] cover original=\(RDEPUBHTMLNormalizer.string(from: originalSize)) display=\(RDEPUBHTMLNormalizer.string(from: attachment.displaySize))") + } + return + } + + var resolvedSize = attachment.displaySize + if resolvedSize.width <= 0 || resolvedSize.height <= 0 { + resolvedSize = originalSize + } + if resolvedSize.width <= 0 || resolvedSize.height <= 0 { + resolvedSize = CGSize(width: pointSize, height: pointSize) + } + + if let maxImageSize, + resolvedSize.width > 0, + resolvedSize.height > 0, + (resolvedSize.width > maxImageSize.width || resolvedSize.height > maxImageSize.height) { + let scale = min(maxImageSize.width / resolvedSize.width, maxImageSize.height / resolvedSize.height) + resolvedSize = CGSize( + width: round(resolvedSize.width * scale), + height: round(resolvedSize.height * scale) + ) + } + + attachment.displaySize = CGSize(width: round(resolvedSize.width), height: round(resolvedSize.height)) + attachment.verticalAlignment = .center + } + + /// DTCoreText 元素配置:在 willFlushCallback 中调用。 + static func prepareHTMLElementForReaderRendering( + _ element: DTHTMLElement, + style: RDEPUBTextRenderStyle, + maxImageSize: CGSize? = nil + ) { + guard let attachment = element.textAttachment else { return } + let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize) + let fallbackSize = CGSize( + width: defaultMaxImageSize(fontPointSize: pointSize).width, + height: defaultMaxImageSize(fontPointSize: pointSize).height + ) + normalizeAttachmentLayoutForWXRead( + attachment, + fontPointSize: pointSize, + maxImageSize: maxImageSize ?? fallbackSize + ) + if isFootnoteAttachment(attachment) { + element.displayStyle = .inline + } else if isCoverAttachment(attachment) { + element.displayStyle = .block + } + } + + private static func isFootnoteAttachment(_ attachment: DTTextAttachment) -> Bool { + let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased() + let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased() + ?? ((attachment.attributes["src"] as? String) ?? "").lowercased() + return lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" + } + + private static func isCoverAttachment(_ attachment: DTTextAttachment) -> Bool { + let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased() + let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased() + ?? ((attachment.attributes["src"] as? String) ?? "").lowercased() + return lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg" + } + + private static func defaultMaxImageSize(fontPointSize: CGFloat) -> CGSize { + let referenceViewport = CGSize(width: 375, height: 667) + let horizontalInset = max(round(fontPointSize), 16) + let verticalInset = max(round(fontPointSize * 1.5), 28) + return CGSize( + width: max(round(referenceViewport.width - horizontalInset * 2), 1), + height: max(round((referenceViewport.height - verticalInset * 2) * 0.85), 1) + ) + } +#endif + + // MARK: - 通用附件规范化 + + /// 规范化 NSAttributedString 中附件的显示尺寸。 + static func normalizeAttachmentDisplayIfNeeded( + in attributes: inout [NSAttributedString.Key: Any], + font: UIFont + ) { + guard let attachment = attributes[.attachment] else { return } + +#if canImport(DTCoreText) + if let textAttachment = attachment as? DTTextAttachment { + normalizeAttachmentLayoutForWXRead(textAttachment, fontPointSize: font.pointSize) + attributes[.attachment] = textAttachment + return + } +#endif + + if let textAttachment = attachment as? NSTextAttachment, textAttachment.bounds.height <= 0 { + let targetHeight = max(round(font.pointSize * 0.86), 1) + textAttachment.bounds = CGRect(x: 0, y: 0, width: targetHeight, height: targetHeight) + attributes[.attachment] = textAttachment + } + } + + /// 推断附件类型。 + static func attachmentKind(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextAttachmentKind? { + if let attachment = attributes[.attachment] as? NSTextAttachment { + if attachment.image != nil || attachment.fileType?.lowercased().contains("image") == true { + return .image + } + return .generic + } + + for value in attributes.values { + let typeName = String(describing: type(of: value)).lowercased() + if typeName.contains("attachment") { + return typeName.contains("image") ? .image : .generic + } + } + + return nil + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFontNormalizer.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFontNormalizer.swift new file mode 100644 index 0000000..ec1023f --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFontNormalizer.swift @@ -0,0 +1,89 @@ +import UIKit +import CoreText + +struct RDEPUBFontNormalizer { + /// 已注册字体资源,避免重复调用 CTFontManager。 + private static var registeredFontPaths = Set() + + func registerEmbeddedFonts( + html: String, + inlinedCSS: String, + input: RDEPUBTypesettingInput + ) { + Self.registerEmbeddedFonts( + in: inlinedCSS + "\n" + Self.inlineStyleCSS(in: html), + chapterHref: input.href, + resourceResolver: input.resourceResolver + ) + } + + // MARK: - 字体注册 + + /// 从 CSS 中解析 @font-face 规则,注册嵌入字体。 + 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) + } + } + } + + 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) + } + + // MARK: - 字体标准化 + + /// 将 EPUB 原始字体映射到用户设置字体,保留粗体/斜体特征。 + 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 + } + + /// 提取 HTML 中内联 "#, 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") + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFragmentMarkerInjector.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFragmentMarkerInjector.swift new file mode 100644 index 0000000..febae4f --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBFragmentMarkerInjector.swift @@ -0,0 +1,59 @@ +import Foundation + +struct RDEPUBFragmentMarkerInjector: RDEPUBTypesettingStage { + func process(_ html: String, context: RDEPUBTypesettingInput) -> String { + Self.injectFragmentMarkers(into: html) + } + + func extractOffsets(from attributedString: NSMutableAttributedString) -> [String: Int] { + Self.extractFragmentOffsets(from: attributedString) + } + + // MARK: - Fragment 标记注入 + + /// 将 HTML 中的 id 属性元素注入 fragment 标记。 + /// `` → `${id=xxx}` + 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" + ) + } + + // MARK: - Fragment 偏移量提取 + + /// 从渲染后的富文本中提取 fragment 偏移量映射。 + /// 扫描 `${id=xxx}` 标记,记录偏移量,然后删除标记文本。 + 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 + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBHTMLNormalizer.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBHTMLNormalizer.swift new file mode 100644 index 0000000..67d8b60 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBHTMLNormalizer.swift @@ -0,0 +1,214 @@ +import Foundation + +struct RDEPUBHTMLNormalizer: RDEPUBTypesettingStage { + func process(_ html: String, context: RDEPUBTypesettingInput) -> String { + Self.normalizeHTML(html) + } + + // MARK: - HTML 规范化入口 + + /// 清理冗余字符(CR、多余换行),规范化附件 HTML 标记。 + static func normalizeHTML(_ html: String) -> String { + var cleanedHTML = html + let replacements: [(pattern: String, template: String)] = [ + (#"分页符"#, ""), + (#"\r"#, "\n"), + (#"\n+"#, "\n") + ] + + for replacement in replacements { + if let regex = try? NSRegularExpression(pattern: replacement.pattern, options: [.caseInsensitive]) { + cleanedHTML = regex.stringByReplacingMatches( + in: cleanedHTML, + options: [], + range: NSRange(location: 0, length: cleanedHTML.utf16.count), + withTemplate: replacement.template + ) + } + } + + cleanedHTML = normalizeAttachmentHTMLMarkers(in: cleanedHTML) + return cleanedHTML + } + + // MARK: - 附件 HTML 标记规范化 + + /// 处理 bodyPic div、脚注 img、封面 h1+img 等特殊 HTML 结构。 + private static func normalizeAttachmentHTMLMarkers(in html: String) -> String { + var normalized = html + + if let bodyPicContainerRegex = try? NSRegularExpression( + pattern: #"]*class\s*=\s*["'][^"']*\b(?:qrbodyPic|bodyPic)\b[^"']*["'][^>]*)>([\s\S]*?)"#, + options: [.caseInsensitive] + ) { + normalized = replaceMatches( + using: bodyPicContainerRegex, + in: normalized + ) { tag in + guard let imageTagRegex = try? NSRegularExpression(pattern: #"]*>"#, options: [.caseInsensitive]) else { + return tag + } + + return replaceMatches( + using: imageTagRegex, + in: tag + ) { imageTag in + mergeHTMLAttributes( + into: imageTag, + requiredClass: "bodyPic", + styleFragments: [ + "wr-vertical-center-style:2", + "max-width:100%", + "height:auto", + "display:block", + "margin-left:auto", + "margin-right:auto" + ] + ) + } + } + } + + if let footnoteRegex = try? NSRegularExpression( + pattern: #"]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#, + options: [.caseInsensitive] + ) { + normalized = replaceMatches( + using: footnoteRegex, + in: normalized + ) { tag in + mergeHTMLAttributes( + into: tag, + requiredClass: nil, + styleFragments: [ + "width:1em", + "height:1em", + "vertical-align:middle", + "display:inline-block" + ] + ) + } + } + + if let coverRegex = try? NSRegularExpression( + pattern: #"]*class\s*=\s*["'][^"']*\bfrontCover\b[^"']*["'][^>]*)>\s*(]*>)\s*"#, + options: [.caseInsensitive] + ) { + normalized = replaceMatches( + using: coverRegex, + in: normalized + ) { tag in + guard let imageTagRegex = try? NSRegularExpression(pattern: #"]*>"#, options: [.caseInsensitive]), + let imageMatch = imageTagRegex.firstMatch( + in: tag, + options: [], + range: NSRange(location: 0, length: (tag as NSString).length) + ), + let imageRange = Range(imageMatch.range, in: tag) else { + return tag + } + + let imageTag = String(tag[imageRange]) + let normalizedImageTag = mergeHTMLAttributes( + into: imageTag, + requiredClass: "rd-front-cover-image", + styleFragments: [ + "display:block", + "width:100%", + "height:auto", + "margin-left:auto", + "margin-right:auto" + ] + ) + return tag.replacingCharacters(in: imageRange, with: normalizedImageTag) + } + } + + return normalized + } + + // MARK: - HTML 工具方法 + + /// 通用反向正则替换:遍历所有匹配,对每个匹配的原始文本调用 transform。 + static func replaceMatches( + using regex: NSRegularExpression, + in source: String, + transform: (String) -> String + ) -> String { + let nsSource = source as NSString + let matches = regex.matches(in: source, options: [], range: NSRange(location: 0, length: nsSource.length)) + guard !matches.isEmpty else { return source } + + var rewritten = source + for match in matches.reversed() { + guard let range = Range(match.range, in: rewritten) else { continue } + let original = String(rewritten[range]) + rewritten.replaceSubrange(range, with: transform(original)) + } + return rewritten + } + + /// 合并 HTML 标签的 class 和 style 属性。 + static func mergeHTMLAttributes( + into tag: String, + requiredClass: String?, + styleFragments: [String] + ) -> String { + var rewritten = tag + if let requiredClass { + if let classRegex = try? NSRegularExpression(pattern: #"class\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]), + let match = classRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)), + match.numberOfRanges > 1 { + let existingClasses = (rewritten as NSString).substring(with: match.range(at: 1)) + if !existingClasses.localizedCaseInsensitiveContains(requiredClass) { + let replacement = #"class="\#(existingClasses) \#(requiredClass)""# + if let range = Range(match.range, in: rewritten) { + rewritten.replaceSubrange(range, with: replacement) + } + } + } else if let closing = rewritten.lastIndex(of: ">") { + rewritten.insert(contentsOf: #" class="\#(requiredClass)""#, at: closing) + } + } + + let styleValue = styleFragments.joined(separator: ";") + ";" + if let styleRegex = try? NSRegularExpression(pattern: #"style\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]), + let match = styleRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)), + match.numberOfRanges > 1 { + let existing = (rewritten as NSString).substring(with: match.range(at: 1)).trimmingCharacters(in: .whitespacesAndNewlines) + let merged = existing.isEmpty ? styleValue : existing + (existing.hasSuffix(";") ? "" : ";") + styleValue + let replacement = #"style="\#(merged)""# + if let range = Range(match.range, in: rewritten) { + rewritten.replaceSubrange(range, with: replacement) + } + } else if let closing = rewritten.lastIndex(of: ">") { + rewritten.insert(contentsOf: #" style="\#(styleValue)""#, at: closing) + } + + return rewritten + } + + /// 注入 `` 标签以解析相对路径。 + static func injectBaseHref(into html: String, baseURL: URL?) -> String { + guard let baseURL else { + return html + } + let baseTag = "" + if html.range(of: "", options: [.caseInsensitive]) != nil { + return html.replacingOccurrences(of: "", with: "\n\(baseTag)", options: [.caseInsensitive]) + } + if let htmlTagRange = html.range(of: "", range: htmlTagRange.lowerBound..\n\(baseTag)\n") + } + return "\n\(baseTag)\n\n" + html + } + + /// 工具方法:从字符串范围构建 CGSize 描述。 + static func string(from size: CGSize) -> String { + "{\(Int(round(size.width))), \(Int(round(size.height)))}" + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBRenderDiagnosticsCollector.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBRenderDiagnosticsCollector.swift new file mode 100644 index 0000000..e268b5e --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBRenderDiagnosticsCollector.swift @@ -0,0 +1,162 @@ +import Foundation + +struct RDEPUBRenderDiagnosticsCollector { + /// 外部样式表链接正则 + private static let stylesheetLinkPattern = #"]*rel\s*=\s*["'][^"']*stylesheet[^"']*["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>"# + /// 图片源地址正则 + private static let imageSourcePattern = #"]*src\s*=\s*["']([^"']+)["'][^>]*>"# + + func collect( + in html: String, + input: RDEPUBTypesettingInput + ) -> [RDEPUBTextResourceReferenceDiagnostic] { + Self.collectImageDiagnostics( + in: html, + chapterHref: input.href, + baseURL: input.baseURL, + resourceResolver: input.resourceResolver + ) + } + + // MARK: - 图片诊断 + + /// 扫描 HTML 中的 标签,构建资源引用诊断。 + static func collectImageDiagnostics( + in html: String, + chapterHref: String, + baseURL: URL?, + resourceResolver: RDEPUBResourceResolver? + ) -> [RDEPUBTextResourceReferenceDiagnostic] { + guard let regex = try? NSRegularExpression(pattern: imageSourcePattern, options: [.caseInsensitive]) else { + return [] + } + let nsHTML = html as NSString + return regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)).compactMap { match in + guard match.numberOfRanges > 1 else { return nil } + let href = nsHTML.substring(with: match.range(at: 1)) + return resolveReference( + href, + kind: .image, + chapterHref: chapterHref, + baseURL: baseURL, + resourceResolver: resourceResolver + ).diagnostic + } + } + + // MARK: - 外部样式表内联 + + /// 查找 `` 标签,内联 CSS 内容。 + static func inlineLinkedStyleSheets( + in html: String, + chapterHref: String, + baseURL: URL?, + resourceResolver: RDEPUBResourceResolver? + ) -> (html: String, inlinedCSS: String, diagnostics: [RDEPUBTextResourceReferenceDiagnostic]) { + guard let regex = try? NSRegularExpression(pattern: stylesheetLinkPattern, options: [.caseInsensitive]) else { + return (html, "", []) + } + + let nsHTML = html as NSString + let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)) + guard !matches.isEmpty else { + return (html, "", []) + } + + var rewrittenHTML = html + var inlinedCSSBlocks: [String] = [] + var diagnostics: [RDEPUBTextResourceReferenceDiagnostic] = [] + + for match in matches.reversed() { + guard match.numberOfRanges > 1 else { continue } + let href = nsHTML.substring(with: match.range(at: 1)) + let resolution = resolveReference( + href, + kind: .stylesheet, + chapterHref: chapterHref, + baseURL: baseURL, + resourceResolver: resourceResolver + ) + diagnostics.append(resolution.diagnostic) + + if let fileURL = resolution.resolvedFileURL, + let css = try? String(contentsOf: fileURL), + resolution.diagnostic.existsOnDisk { + let cssWithResolvedURLs = rewriteCSSResourceURLs( + in: css, + styleSheetFileURL: fileURL + ) + inlinedCSSBlocks.append(cssWithResolvedURLs) + } + + if let range = Range(match.range, in: rewrittenHTML) { + rewrittenHTML.replaceSubrange(range, with: "") + } + } + + return (rewrittenHTML, inlinedCSSBlocks.reversed().joined(separator: "\n\n"), diagnostics.reversed()) + } + + // MARK: - CSS 资源 URL 重写 + + /// 重写 CSS 中的相对 url() 引用,解析为绝对文件路径。 + static func rewriteCSSResourceURLs( + in css: String, + styleSheetFileURL: URL + ) -> String { + guard let regex = try? NSRegularExpression(pattern: #"url\(([^)]+)\)"#, options: [.caseInsensitive]) else { + return css + } + + let nsCSS = css as NSString + let matches = regex.matches(in: css, options: [], range: NSRange(location: 0, length: nsCSS.length)) + guard !matches.isEmpty else { + return css + } + + var rewrittenCSS = css + for match in matches.reversed() { + guard match.numberOfRanges > 1 else { continue } + let rawValue = nsCSS.substring(with: match.range(at: 1)) + .trimmingCharacters(in: .whitespacesAndNewlines) + .trimmingCharacters(in: CharacterSet(charactersIn: "\"'")) + guard !rawValue.isEmpty else { continue } + if rawValue.hasPrefix("data:") || rawValue.hasPrefix("http://") || rawValue.hasPrefix("https://") || rawValue.hasPrefix("file://") || rawValue.hasPrefix("#") { + continue + } + guard let resolvedURL = URL(string: rawValue, relativeTo: styleSheetFileURL.deletingLastPathComponent())?.standardizedFileURL else { + continue + } + let replacement = "url(\"\(resolvedURL.absoluteString)\")" + if let range = Range(match.range, in: rewrittenCSS) { + rewrittenCSS.replaceSubrange(range, with: replacement) + } + } + return rewrittenCSS + } + + // MARK: - 资源引用解析 + + static func resolveReference( + _ reference: String, + kind: RDEPUBTextResourceReferenceKind, + chapterHref: String, + baseURL: URL?, + resourceResolver: RDEPUBResourceResolver? + ) -> (normalizedHref: String?, resolvedFileURL: URL?, diagnostic: RDEPUBTextResourceReferenceDiagnostic) { + let trimmedReference = reference.trimmingCharacters(in: .whitespacesAndNewlines) + let normalizedHref = resourceResolver?.normalizedHref(trimmedReference, relativeToHref: chapterHref) + let resolvedFileURL = resourceResolver?.fileURL(forReference: trimmedReference, relativeToHref: chapterHref) + ?? URL(string: trimmedReference, relativeTo: baseURL)?.standardizedFileURL + let existsOnDisk = resolvedFileURL.map { FileManager.default.fileExists(atPath: $0.path) } ?? false + let diagnostic = RDEPUBTextResourceReferenceDiagnostic( + kind: kind, + chapterHref: chapterHref, + originalReference: trimmedReference, + normalizedHref: normalizedHref, + resolvedFileURL: resolvedFileURL, + existsOnDisk: existsOnDisk + ) + return (normalizedHref, resolvedFileURL, diagnostic) + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift new file mode 100644 index 0000000..efd8c1d --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift @@ -0,0 +1,332 @@ +import Foundation +import UIKit + +struct RDEPUBSemanticMarkerInjector: RDEPUBTypesettingStage { + /// 语义标记正则(${rd-sem-start:...} / ${rd-sem-end:...}) + private static let semanticMarkerPattern = #"\$\{rd-sem-(start|end):([^}]+)\}"# + + func process(_ html: String, context: RDEPUBTypesettingInput) -> String { + Self.injectPaginationSemanticMarkers(into: html) + } + + func apply(to attributedString: NSMutableAttributedString) { + Self.applyPaginationSemantics(in: attributedString) + } + + // MARK: - 语义标记注入(HTML 阶段) + + /// 为 HTML 标签注入 ${rd-sem-start/end} 语义标记。 + static func injectPaginationSemanticMarkers(into html: String) -> String { + guard let regex = try? NSRegularExpression(pattern: #"<[^>]+>"#, options: [.caseInsensitive]) else { + return html + } + + let nsHTML = html as NSString + let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsHTML.length)) + guard !matches.isEmpty else { + return html + } + + var output = "" + var cursor = 0 + var openTagStack: [(name: String, id: String)] = [] + var nextMarkerID = 0 + + for match in matches { + let tagRange = match.range + guard tagRange.location >= cursor else { continue } + output += nsHTML.substring(with: NSRange(location: cursor, length: tagRange.location - cursor)) + + let tag = nsHTML.substring(with: tagRange) + let loweredTag = tag.lowercased() + let tagName = htmlTagName(from: loweredTag) + + if loweredTag.hasPrefix("") { + output += startMarker + tag + semanticEndMarker(id: markerID) + } else { + openTagStack.append((name: tagName, id: markerID)) + output += tag + startMarker + } + } else { + output += tag + } + + cursor = tagRange.location + tagRange.length + } + + output += nsHTML.substring(from: cursor) + return output + } + + // MARK: - 语义标记应用(渲染后阶段) + + /// 将 HTML 中注入的语义标记解析后写入 NSAttributedString 属性。 + static func applyPaginationSemantics(in attributedString: NSMutableAttributedString) { + guard let regex = try? NSRegularExpression(pattern: semanticMarkerPattern, options: []) else { + return + } + + let mutableString = NSMutableString(string: attributedString.string) + var searchRange = NSRange(location: 0, length: mutableString.length) + var openRanges: [String: (location: Int, semantics: RDPaginationSemantics)] = [:] + + while let match = regex.firstMatch(in: mutableString as String, options: [], range: searchRange) { + let kind = mutableString.substring(with: match.range(at: 1)) + let payload = mutableString.substring(with: match.range(at: 2)) + let markerLocation = match.range.location + + attributedString.deleteCharacters(in: match.range) + mutableString.deleteCharacters(in: match.range) + + if kind == "start" { + let semantics = parseSemanticMarkerPayload(payload) + openRanges[semantics.id] = (markerLocation, semantics) + } else { + let markerID = parseSemanticEndID(payload) + if let markerID, let opened = openRanges.removeValue(forKey: markerID) { + let length = max(markerLocation - opened.location, 0) + if length > 0 { + apply(semantics: opened.semantics, to: NSRange(location: opened.location, length: length), in: attributedString) + } + } + } + + searchRange = NSRange(location: markerLocation, length: mutableString.length - markerLocation) + } + } + + // MARK: - 语义推断 + + static func htmlTagName(from loweredTag: String) -> String? { + let trimmed = loweredTag.trimmingCharacters(in: .whitespacesAndNewlines) + guard trimmed.hasPrefix("<") else { return nil } + let body = trimmed.dropFirst().drop(while: { $0 == "/" || $0 == "!" || $0 == "?" }) + let name = body.prefix { $0.isLetter || $0.isNumber } + return name.isEmpty ? nil : String(name) + } + + private static func paginationSemantics(forTagName tagName: String, rawTag: String) -> RDPaginationSemantics? { + let loweredTag = rawTag.lowercased() + let blockKind = inferredBlockKind(forTagName: tagName, rawTag: loweredTag) + let hints = inferredHints(forTagName: tagName, rawTag: loweredTag) + let placement = inferredAttachmentPlacement(forTagName: tagName, rawTag: loweredTag) + + guard blockKind != nil || !hints.isEmpty || placement != nil else { + return nil + } + + return RDPaginationSemantics( + id: "", + blockKind: blockKind, + hints: hints, + attachmentPlacement: placement + ) + } + + private static func inferredBlockKind(forTagName tagName: String, rawTag: String) -> RDEPUBTextBlockKind? { + if rawTag.contains("bodypic") || tagName == "img" || tagName == "figure" { + return .attachment + } + switch tagName { + case "h1", "h2", "h3", "h4", "h5", "h6": + return .generic + case "blockquote": + return .blockquote + case "ul", "ol", "li": + return .list + case "table", "thead", "tbody", "tfoot", "tr", "td", "th": + return .table + case "pre", "code": + return .code + case "p": + return .paragraph + case "div": + if rawTag.contains("code") || rawTag.contains("highlight") { + return .code + } + if rawTag.contains("quote") || rawTag.contains("blockquote") { + return .blockquote + } + if rawTag.contains("table") { + return .table + } + if rawTag.contains("list") { + return .list + } + return .generic + default: + return nil + } + } + + private static func inferredHints(forTagName tagName: String, rawTag: String) -> [RDEPUBTextSemanticHint] { + var hints: [RDEPUBTextSemanticHint] = [] + if rawTag.contains("avoidpagebreakinside") || + rawTag.contains("break-inside: avoid") || + rawTag.contains("page-break-inside: avoid") || + ["blockquote", "pre", "code", "table", "ul", "ol", "figure", "img"].contains(tagName) { + hints.append(.avoidPageBreakInside) + } + if ["h1", "h2", "h3", "h4", "h5", "h6"].contains(tagName) || + rawTag.contains("subhead") || + rawTag.contains("firsttitle") || + rawTag.contains("secondtitle") || + rawTag.contains("thirdtitle") || + rawTag.contains("fourthtitle") || + rawTag.contains("fifthtitle") || + rawTag.contains("sixthtitle") { + hints.append(.keepWithNext) + } + if rawTag.contains("pagebreakbefore") || + rawTag.contains("page-break-before: always") || + rawTag.contains("break-before: page") { + hints.append(.pageBreakBefore) + } + if rawTag.contains("pagebreakafter") || + rawTag.contains("page-break-after: always") || + rawTag.contains("break-after: page") { + hints.append(.pageBreakAfter) + } + if rawTag.contains("pageRelate".lowercased()) || rawTag.contains("weread-page-relate") { + hints.append(.pageRelate) + } + return hints + .reduce(into: [RDEPUBTextSemanticHint]()) { result, hint in + if !result.contains(hint) { + result.append(hint) + } + } + .sorted { $0.rawValue < $1.rawValue } + } + + private static func inferredAttachmentPlacement(forTagName tagName: String, rawTag: String) -> RDEPUBTextAttachmentPlacement? { + guard tagName == "img" || rawTag.contains("bodypic") || rawTag.contains("wr-vertical-center") else { + return nil + } + if rawTag.contains("wr-vertical-center-style: 2") || rawTag.contains("bodypic") { + return .centered + } + if rawTag.contains("wr-vertical-center-style: 1") || rawTag.contains("wr-vertical-center") { + return .baseline + } + return .inline + } + + private static func isVoidHTMLTag(_ tagName: String) -> Bool { + ["img", "br", "hr", "input", "meta", "link"].contains(tagName) + } + + private static func semanticStartMarker(id: String, semantics: RDPaginationSemantics) -> String { + var segments = ["id=\(id)"] + if let blockKind = semantics.blockKind { + segments.append("block=\(blockKind.rawValue)") + } + if !semantics.hints.isEmpty { + segments.append("hints=\(semantics.hints.map(\.rawValue).joined(separator: ","))") + } + if let placement = semantics.attachmentPlacement { + segments.append("placement=\(placement.rawValue)") + } + return "${rd-sem-start:\(segments.joined(separator: ";"))}" + } + + private static func semanticEndMarker(id: String) -> String { + "${rd-sem-end:id=\(id)}" + } + + private static func parseSemanticMarkerPayload(_ payload: String) -> RDPaginationSemantics { + var values: [String: String] = [:] + payload.split(separator: ";").forEach { entry in + let parts = entry.split(separator: "=", maxSplits: 1) + guard parts.count == 2 else { return } + values[String(parts[0])] = String(parts[1]) + } + let blockKind = values["block"].flatMap(RDEPUBTextBlockKind.init(rawValue:)) + let hints = values["hints"]? + .split(separator: ",") + .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } ?? [] + let placement = values["placement"].flatMap(RDEPUBTextAttachmentPlacement.init(rawValue:)) + return RDPaginationSemantics( + id: values["id"] ?? UUID().uuidString, + blockKind: blockKind, + hints: hints, + attachmentPlacement: placement + ) + } + + private static func parseSemanticEndID(_ payload: String) -> String? { + payload.split(separator: ";").first { $0.hasPrefix("id=") }.map { String($0.dropFirst(3)) } + } + + private static func apply( + semantics: RDPaginationSemantics, + to range: NSRange, + in attributedString: NSMutableAttributedString + ) { + var attributes: [NSAttributedString.Key: Any] = [:] + attributes[.rdPageBlockRange] = NSStringFromRange(range) + if let blockKind = semantics.blockKind { + attributes[.rdPageBlockKind] = blockKind.rawValue + } + if !semantics.hints.isEmpty { + attributes[.rdPageSemanticHints] = semantics.hints.map(\.rawValue).joined(separator: ",") + } + if let placement = semantics.attachmentPlacement { + attributes[.rdPageAttachmentPlacement] = placement.rawValue + } + guard !attributes.isEmpty else { return } + attributedString.addAttributes(attributes, range: range) + } + + /// 推断块级元素类型。 + static func normalizeBlockKind(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextBlockKind? { + if let rawValue = attributes[.rdPageBlockKind] as? String, + let blockKind = RDEPUBTextBlockKind(rawValue: rawValue) { + return blockKind + } + return nil + } + + /// 推断语义提示列表。 + static func normalizeSemanticHints(for attributes: [NSAttributedString.Key: Any]) -> [RDEPUBTextSemanticHint]? { + if let rawValue = attributes[.rdPageSemanticHints] as? String { + let hints = rawValue + .split(separator: ",") + .compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) } + return hints.isEmpty ? nil : hints + } + return nil + } + + /// 推断附件放置方式。 + static func normalizeAttachmentPlacement(for attributes: [NSAttributedString.Key: Any]) -> RDEPUBTextAttachmentPlacement? { + if let rawValue = attributes[.rdPageAttachmentPlacement] as? String, + let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue) { + return placement + } + if let attachmentKind = RDEPUBAttachmentNormalizer.attachmentKind(for: attributes), attachmentKind == .image { + return .inline + } + return nil + } + + // MARK: - 内部类型 + + struct RDPaginationSemantics { + var id: String + var blockKind: RDEPUBTextBlockKind? + var hints: [RDEPUBTextSemanticHint] + var attachmentPlacement: RDEPUBTextAttachmentPlacement? + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swift new file mode 100644 index 0000000..52ba1e2 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBStyleSheetComposer.swift @@ -0,0 +1,288 @@ +import UIKit + +struct RDEPUBStyleSheetComposition { + var html: String + var layers: [RDEPUBTextStyleSheetLayer] + var inlinedCSS: String + var diagnostics: [RDEPUBTextResourceReferenceDiagnostic] +} + +struct RDEPUBStyleSheetComposer { + func compose(html: String, input: RDEPUBTypesettingInput) -> RDEPUBStyleSheetComposition { + let stylesheetHrefReplacements = RDEPUBRenderDiagnosticsCollector.inlineLinkedStyleSheets( + in: html, + chapterHref: input.href, + baseURL: input.baseURL, + resourceResolver: input.resourceResolver + ) + let layers = Self.makeStyleSheetLayers( + style: input.style, + epubCSS: stylesheetHrefReplacements.inlinedCSS, + contentLanguageCode: input.contentLanguageCode, + sourceHTML: input.rawHTML + ) + let htmlWithBase = RDEPUBHTMLNormalizer.injectBaseHref( + into: stylesheetHrefReplacements.html, + baseURL: input.baseURL + ) + let htmlWithDefaultLayers = Self.injectStyleTag( + into: htmlWithBase, + styleID: "rd-native-default-replace-dark", + css: layers + .filter { $0.kind != .user && $0.kind != .epub } + .map(\.css) + .joined(separator: "\n\n"), + position: .headStart + ) + let htmlWithEPUBLayer = Self.injectStyleTag( + into: htmlWithDefaultLayers, + styleID: "rd-native-epub", + css: layers.first(where: { $0.kind == .epub })?.css ?? "", + position: .headEnd + ) + let composedHTML = Self.injectStyleTag( + into: htmlWithEPUBLayer, + styleID: "rd-native-user", + css: layers.first(where: { $0.kind == .user })?.css ?? "", + position: .headEnd + ) + + return RDEPUBStyleSheetComposition( + html: composedHTML, + layers: layers, + inlinedCSS: stylesheetHrefReplacements.inlinedCSS, + diagnostics: stylesheetHrefReplacements.diagnostics + ) + } + + // MARK: - CSS 层组装 + + /// 构建五层 CSS 数组(default/replace/dark/epub/user)。 + static func makeStyleSheetLayers( + style: RDEPUBTextRenderStyle, + epubCSS: String, + contentLanguageCode: String?, + sourceHTML: String + ) -> [RDEPUBTextStyleSheetLayer] { + let useLatinReplace = prefersLatinLanguageCSS( + languageCode: contentLanguageCode, + sourceHTML: sourceHTML + ) + var layers: [RDEPUBTextStyleSheetLayer] = [ + .init(kind: .default, css: defaultCSS()), + .init(kind: .replace, css: replaceCSS(useLatinVariant: useLatinReplace)) + ] + if isDarkTheme(style: style) { + layers.append(.init(kind: .dark, css: darkCSS(style: style))) + } + if !epubCSS.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + layers.append(.init(kind: .epub, css: epubCSS)) + } + layers.append(.init(kind: .user, css: userCSS(style: style))) + return layers + } + + // MARK: - Style 注入 + + enum StyleInjectionPosition { + case headStart + case headEnd + } + + /// 向 HTML 注入 " + switch position { + case .headStart: + if html.range(of: "", options: [.caseInsensitive]) != nil { + return html.replacingOccurrences(of: "", with: "\n\(styleTag)", options: [.caseInsensitive]) + } + case .headEnd: + if html.range(of: "", options: [.caseInsensitive]) != nil { + return html.replacingOccurrences(of: "", with: "\(styleTag)\n", options: [.caseInsensitive]) + } + } + + if html.range(of: " String { + RDEPUBAssetRepository.string(for: .wxReadDefaultCSS) + } + + private static func replaceCSS(useLatinVariant: Bool) -> String { + let asset: RDEPUBAsset = useLatinVariant ? .wxReadLatinReplaceCSS : .wxReadReplaceCSS + return RDEPUBAssetRepository.string(for: asset) + } + + private static func darkCSS(style: RDEPUBTextRenderStyle) -> String { + let background = style.backgroundColor?.ss_cssString ?? "rgba(0, 0, 0, 1.000)" + let text = style.textColor?.ss_cssString ?? "rgba(255, 255, 255, 1.000)" + return RDEPUBAssetRepository.string(for: .wxReadDarkCSS) + "\n\n" + """ + html, body { + background: \(background) !important; + color: \(text) !important; + } + a { + color: \(text) !important; + } + """ + } + + private static func userCSS(style: RDEPUBTextRenderStyle) -> String { + let lineHeight = max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1) + let text = style.textColor.map { "color: \($0.ss_cssString) !important;" } ?? "" + let background = style.backgroundColor.map { "background: \($0.ss_cssString) !important;" } ?? "" + return """ + html, body { + font-family: "\(style.font.familyName)" !important; + font-size: \(String(format: "%.3f", style.font.pointSize))px !important; + line-height: \(String(format: "%.3f", lineHeight)) !important; + \(text) + \(background) + } + """ + } + + private static func isDarkTheme(style: RDEPUBTextRenderStyle) -> Bool { + guard let backgroundColor = style.backgroundColor else { + return false + } + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + backgroundColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + let luminance = (0.299 * red) + (0.587 * green) + (0.114 * blue) + return luminance < 0.5 + } + + // MARK: - 语言检测 + + private static func prefersLatinLanguageCSS( + languageCode: String?, + sourceHTML: String + ) -> Bool { + let candidateCodes = inferredLanguageCodes( + explicitLanguageCode: languageCode, + sourceHTML: sourceHTML + ) + + if candidateCodes.contains(where: isExplicitLatinLanguageCode) { + return true + } + if candidateCodes.contains(where: isExplicitCJKLanguageCode) { + return false + } + + let textSample = plainTextSample(from: sourceHTML) + guard !textSample.isEmpty else { return false } + + var alphabeticCount = 0 + var latinCount = 0 + for scalar in textSample.unicodeScalars { + guard CharacterSet.letters.contains(scalar) else { continue } + alphabeticCount += 1 + if isLatinScalar(scalar) { + latinCount += 1 + } + } + + guard alphabeticCount >= 80 else { return false } + return (Double(latinCount) / Double(alphabeticCount)) >= 0.6 + } + + private static func inferredLanguageCodes( + explicitLanguageCode: String?, + sourceHTML: String + ) -> [String] { + var codes: [String] = [] + if let explicitLanguageCode { + let normalized = explicitLanguageCode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if !normalized.isEmpty { + codes.append(normalized) + } + } + + if let regex = try? NSRegularExpression( + pattern: #"\b(?:xml:lang|lang)\s*=\s*["']([^"']+)["']"#, + options: [.caseInsensitive] + ) { + let nsHTML = sourceHTML as NSString + let range = NSRange(location: 0, length: min(nsHTML.length, 8_000)) + for match in regex.matches(in: sourceHTML, options: [], range: range) { + guard match.numberOfRanges > 1 else { continue } + let code = nsHTML.substring(with: match.range(at: 1)) + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() + if !code.isEmpty { + codes.append(code) + } + } + } + + return Array(NSOrderedSet(array: codes)) as? [String] ?? codes + } + + private static func isExplicitLatinLanguageCode(_ code: String) -> Bool { + let normalized = code.lowercased() + if normalized.contains("latn") { + return true + } + + let prefix = normalized.split(separator: "-").first.map(String.init) ?? normalized + let latinPrefixes: Set = [ + "en", "fr", "de", "es", "it", "pt", "nl", "sv", "da", "no", "fi", + "is", "ga", "cy", "pl", "cs", "sk", "sl", "hr", "hu", "ro", "tr", + "vi", "id", "ms", "tl", "sw", "af", "sq", "et", "lv", "lt" + ] + return latinPrefixes.contains(prefix) + } + + private static func isExplicitCJKLanguageCode(_ code: String) -> Bool { + let prefix = code.lowercased().split(separator: "-").first.map(String.init) ?? code.lowercased() + return ["zh", "ja", "ko"].contains(prefix) + } + + private static func plainTextSample(from html: String) -> String { + let maxLength = min(html.count, 20_000) + let sample = String(html.prefix(maxLength)) + let withoutTags = sample.replacingOccurrences( + of: #"<[^>]+>"#, + with: " ", + options: .regularExpression + ) + return withoutTags.replacingOccurrences( + of: #"&[A-Za-z0-9#]+;"#, + with: " ", + options: .regularExpression + ) + } + + private static func isLatinScalar(_ scalar: UnicodeScalar) -> Bool { + switch scalar.value { + case 0x0041...0x007A, + 0x00C0...0x00FF, + 0x0100...0x024F, + 0x1E00...0x1EFF: + return true + default: + return false + } + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTextRendererSupport.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTextRendererSupport.swift new file mode 100644 index 0000000..b97be20 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTextRendererSupport.swift @@ -0,0 +1,167 @@ +import UIKit +import CoreText + +#if canImport(DTCoreText) +import DTCoreText +#endif + +/// 渲染器支持 Facade:串联各 stage 完成 HTML 预处理,并保留后渲染归一化入口。 +/// +/// 预处理管线由各 stage 文件持有实现,本文件只作为公开入口协调调用。 +/// `RDEPUBDTCoreTextRenderer` 直接调用各 stage 的静态方法。 +enum RDEPUBTextRendererSupport { + + // MARK: - 预处理管线入口 + + /// 核心预处理方法:将原始 HTML 转换为完整的章节渲染请求。 + /// + /// 内部流程: + /// 1. HTMLNormalizer.normalizeHTML + /// 2. SemanticMarkerInjector.injectPaginationSemanticMarkers + /// 3. DiagnosticsCollector.inlineLinkedStyleSheets + /// 4. StyleSheetComposer.makeStyleSheetLayers + injectStyleTag + /// 5. FontNormalizer.registerEmbeddedFonts + /// 6. HTMLNormalizer.injectBaseHref + /// 7. FragmentMarkerInjector.injectFragmentMarkers + /// 8. DiagnosticsCollector.collectImageDiagnostics + static func makeChapterRenderRequest( + href: String, + title: String, + rawHTML: String, + baseURL: URL?, + style: RDEPUBTextRenderStyle, + resourceResolver: RDEPUBResourceResolver?, + contentLanguageCode: String? = nil, + pageSize: CGSize? = nil, + layoutConfig: RDEPUBTextLayoutConfig? = nil + ) -> RDEPUBTextChapterRenderRequest { + let normalizedHTML = RDEPUBSemanticMarkerInjector.injectPaginationSemanticMarkers( + into: RDEPUBHTMLNormalizer.normalizeHTML(rawHTML) + ) + let stylesheetHrefReplacements = RDEPUBRenderDiagnosticsCollector.inlineLinkedStyleSheets( + in: normalizedHTML, + chapterHref: href, + baseURL: baseURL, + resourceResolver: resourceResolver + ) + let layers = RDEPUBStyleSheetComposer.makeStyleSheetLayers( + style: style, + epubCSS: stylesheetHrefReplacements.inlinedCSS, + contentLanguageCode: contentLanguageCode, + sourceHTML: rawHTML + ) + RDEPUBFontNormalizer.registerEmbeddedFonts( + in: stylesheetHrefReplacements.inlinedCSS + "\n" + RDEPUBFontNormalizer.inlineStyleCSS(in: normalizedHTML), + chapterHref: href, + resourceResolver: resourceResolver + ) + let htmlWithBase = RDEPUBHTMLNormalizer.injectBaseHref(into: stylesheetHrefReplacements.html, baseURL: baseURL) + let htmlWithDefaultLayers = RDEPUBStyleSheetComposer.injectStyleTag( + into: htmlWithBase, + styleID: "rd-native-default-replace-dark", + css: layers + .filter { $0.kind != .user && $0.kind != .epub } + .map(\.css) + .joined(separator: "\n\n"), + position: .headStart + ) + let htmlWithEPUBLayer = RDEPUBStyleSheetComposer.injectStyleTag( + into: htmlWithDefaultLayers, + styleID: "rd-native-epub", + css: layers.first(where: { $0.kind == .epub })?.css ?? "", + position: .headEnd + ) + let composedHTML = RDEPUBStyleSheetComposer.injectStyleTag( + into: htmlWithEPUBLayer, + styleID: "rd-native-user", + css: layers.first(where: { $0.kind == .user })?.css ?? "", + position: .headEnd + ) + let markedHTML = RDEPUBFragmentMarkerInjector.injectFragmentMarkers(into: composedHTML) + let resourceDiagnostics = stylesheetHrefReplacements.diagnostics + RDEPUBRenderDiagnosticsCollector.collectImageDiagnostics( + in: markedHTML, + chapterHref: href, + baseURL: baseURL, + resourceResolver: resourceResolver + ) + + let context = RDEPUBTextChapterContext( + href: href, + title: title, + html: markedHTML, + baseURL: baseURL, + stylesheet: RDEPUBTextStyleSheetPackage(layers: layers), + resourceDiagnostics: resourceDiagnostics + ) + return RDEPUBTextChapterRenderRequest( + context: context, + style: style, + pageSize: pageSize, + layoutConfig: layoutConfig + ) + } + + // MARK: - 后渲染归一化(由 RDEPUBDTCoreTextRenderer 调用) + + /// 规范化阅读属性:统一字体、行距、颜色,并注入分页语义属性。 + static func normalizeReadingAttributes(in attributedString: NSMutableAttributedString, style: RDEPUBTextRenderStyle) { + let fullRange = NSRange(location: 0, length: attributedString.length) + var blockIndex = 0 + let sourceText = attributedString.string as NSString + attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in + let sourceFont = attributes[.font] as? UIFont + let normalizedFont = RDEPUBFontNormalizer.normalizedFont(from: sourceFont, baseFont: style.font) + 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 + updatedAttributes[.paragraphStyle] = paragraph + if let textColor = style.textColor { + updatedAttributes[.foregroundColor] = textColor + } + RDEPUBAttachmentNormalizer.normalizeAttachmentDisplayIfNeeded(in: &updatedAttributes, font: normalizedFont) + let semanticBlockRange = (attributes[.rdPageBlockRange] as? String) + .flatMap(NSRangeFromString) + .flatMap { $0.length > 0 ? $0 : nil } + let paragraphRange = sourceText.length > 0 + ? sourceText.paragraphRange(for: NSRange(location: min(range.location, max(sourceText.length - 1, 0)), length: 0)) + : range + updatedAttributes[.rdPageBlockRange] = NSStringFromRange(semanticBlockRange ?? paragraphRange) + updatedAttributes[.rdPageBlockIndex] = blockIndex + if let attachmentKind = RDEPUBAttachmentNormalizer.attachmentKind(for: attributes) { + updatedAttributes[.rdPageAttachmentKind] = attachmentKind.rawValue + } + if let placement = RDEPUBSemanticMarkerInjector.normalizeAttachmentPlacement(for: attributes) { + updatedAttributes[.rdPageAttachmentPlacement] = placement.rawValue + } + if let blockKind = RDEPUBSemanticMarkerInjector.normalizeBlockKind(for: attributes) { + updatedAttributes[.rdPageBlockKind] = blockKind.rawValue + } + if let hints = RDEPUBSemanticMarkerInjector.normalizeSemanticHints(for: attributes), !hints.isEmpty { + updatedAttributes[.rdPageSemanticHints] = hints.map(\.rawValue).joined(separator: ",") + } + attributedString.setAttributes(updatedAttributes, range: range) + blockIndex += 1 + } + } + + /// 回退渲染:当 DTCoreText 不可用时,将 HTML 源码当作纯文本处理。 + 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) + } + + /// 构建共享段落样式。 + static func paragraphStyle(lineSpacing: CGFloat) -> NSMutableParagraphStyle { + let style = NSMutableParagraphStyle() + style.lineSpacing = lineSpacing + style.paragraphSpacing = max(6, lineSpacing / 2) + return style + } +} diff --git a/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTypesettingPipeline.swift b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTypesettingPipeline.swift new file mode 100644 index 0000000..d165463 --- /dev/null +++ b/Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBTypesettingPipeline.swift @@ -0,0 +1,65 @@ +import UIKit + +struct RDEPUBTypesettingInput { + var href: String + var title: String + var rawHTML: String + var baseURL: URL? + var style: RDEPUBTextRenderStyle + var resourceResolver: RDEPUBResourceResolver? + var contentLanguageCode: String? + var pageSize: CGSize? + var layoutConfig: RDEPUBTextLayoutConfig? +} + +struct RDEPUBTypesettingOutput { + var request: RDEPUBTextChapterRenderRequest + var diagnostics: [RDEPUBTextResourceReferenceDiagnostic] +} + +protocol RDEPUBTypesettingStage { + func process(_ html: String, context: RDEPUBTypesettingInput) -> String +} + +struct RDEPUBTextTypesetterPipeline { + func makeRequest(from input: RDEPUBTypesettingInput) -> RDEPUBTypesettingOutput { + let htmlNormalizer = RDEPUBHTMLNormalizer() + let semanticMarkerInjector = RDEPUBSemanticMarkerInjector() + let styleSheetComposer = RDEPUBStyleSheetComposer() + let fontNormalizer = RDEPUBFontNormalizer() + let fragmentMarkerInjector = RDEPUBFragmentMarkerInjector() + let diagnosticsCollector = RDEPUBRenderDiagnosticsCollector() + + let normalizedHTML = semanticMarkerInjector.process( + htmlNormalizer.process(input.rawHTML, context: input), + context: input + ) + let styleSheetComposition = styleSheetComposer.compose(html: normalizedHTML, input: input) + fontNormalizer.registerEmbeddedFonts( + html: normalizedHTML, + inlinedCSS: styleSheetComposition.inlinedCSS, + input: input + ) + let markedHTML = fragmentMarkerInjector.process(styleSheetComposition.html, context: input) + let diagnostics = styleSheetComposition.diagnostics + diagnosticsCollector.collect(in: markedHTML, input: input) + + let context = RDEPUBTextChapterContext( + href: input.href, + title: input.title, + html: markedHTML, + baseURL: input.baseURL, + stylesheet: RDEPUBTextStyleSheetPackage(layers: styleSheetComposition.layers), + resourceDiagnostics: diagnostics + ) + let request = RDEPUBTextChapterRenderRequest( + context: context, + style: input.style, + pageSize: input.pageSize, + layoutConfig: input.layoutConfig + ) + return RDEPUBTypesettingOutput( + request: request, + diagnostics: diagnostics + ) + } +} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift new file mode 100644 index 0000000..ea9d36d --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift @@ -0,0 +1,176 @@ +import UIKit + +// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径) + +extension RDEPUBReaderController: RDEPUBWebContentViewDelegate { + func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) { + guard readerView.currentPage >= 0, + activePages.indices.contains(readerView.currentPage) else { + return + } + + let currentPage = activePages[readerView.currentPage] + guard readingSession?.pageContains(spineIndex: spineIndex, in: currentPage) == true else { + return + } + + persist(location: location) + readingSession?.updateReadingContext( + pageNumber: readerView.currentPage + 1, + location: location, + spineIndex: spineIndex, + chapterIndex: currentPage.chapterIndex, + bookIdentifier: currentBookIdentifier + ) + } + + func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) { + if let selection { + updateCurrentSelection(scopedSelection(selection, relativeToSpineIndex: spineIndex)) + } else { + updateCurrentSelection(nil) + } + } + + func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) { + handleSelectionMenuAction(action, selection: currentSelection) + } + + func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) { + guard let readingSession, + let pageNumber = readingSession.queueNavigation( + to: location, + relativeToSpineIndex: fromSpineIndex, + bookIdentifier: currentBookIdentifier + ) else { + return + } + readerView.transitionToPage(pageNum: max(pageNumber - 1, 0), animated: true) + } + + func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL) { + delegate?.epubReader(self, didActivateExternalLink: url) + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + + func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String) { + print("EPUB JS Error: \(message)") + } +} + +// MARK: - 文本内容视图代理(Native Text 渲染路径) + +extension RDEPUBReaderController: RDEPUBTextContentViewDelegate { + func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) { + guard let selection else { + updateCurrentSelection(nil) + return + } + updateCurrentSelection(normalizedTextSelection(selection)) + } + + func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) { + handleSelectionMenuAction(action, selection: currentSelection) + contentView.clearSelection() + } + + private func normalizedTextSelection(_ selection: RDEPUBSelection) -> RDEPUBSelection? { + guard let textBook, + let chapterData = textBook.chapterData(for: selection.location.href) else { + return scopedSelection(selection, relativeToSpineIndex: nil) + } + guard let payload = RDEPUBTextOffsetRangeInfo.decode(from: selection.rangeInfo) else { + return scopedSelection(selection, relativeToSpineIndex: nil) + } + + let contentLength = max(chapterData.attributedContent.length, 1) + let lastInclusiveOffset = max(contentLength - 1, 1) + let start = max(0, min(payload.start, lastInclusiveOffset)) + let endExclusive = max(start + 1, min(payload.end, contentLength)) + let absoluteRange = NSRange(location: start, length: endExclusive - start) + let location = chapterData.location(for: absoluteRange, bookIdentifier: currentBookIdentifier) + return RDEPUBSelection( + bookIdentifier: currentBookIdentifier, + location: location, + text: selection.text, + rangeInfo: selection.rangeInfo, + createdAt: selection.createdAt + ) + } + + func pageNumber(for location: RDEPUBLocation) -> Int? { + if let textBook, let publication { + if let anchor = location.rangeAnchor?.start { + if let page = textBook.indexTable.pageNumber(for: anchor, in: textBook) { + return page + 1 + } + } + + let normalizedLocation = publication.resourceResolver.normalizedLocation( + location, + relativeToSpineIndex: nil, + bookIdentifier: currentBookIdentifier + ) ?? location + return textBook.pageNumber( + for: normalizedLocation, + resolver: publication.resourceResolver, + bookIdentifier: currentBookIdentifier + ) + } + + return readingSession?.queueNavigation( + to: location, + relativeToSpineIndex: nil, + bookIdentifier: currentBookIdentifier + ) + } + + func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? { + guard let textBook, + let publication, + let page = textBook.page(at: pageNumber) else { + return nil + } + + let location = textBook.chapterData(forPageNumber: pageNumber)?.location(forPage: page, bookIdentifier: currentBookIdentifier) + ?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier) + guard let location else { return nil } + + return publication.resourceResolver.normalizedLocation( + location, + relativeToSpineIndex: nil, + bookIdentifier: currentBookIdentifier + ) ?? location + } + + func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) { + guard let textBook, + let page = textBook.page(at: pageNumber) else { + readingSession?.transition(to: .idle) + return + } + + readingSession?.updateReadingContext( + pageNumber: pageNumber, + location: location, + spineIndex: page.spineIndex, + chapterIndex: page.chapterIndex, + bookIdentifier: currentBookIdentifier + ) + } + + func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> RDEPUBNativeTextSnapshot { + let chapters = textBook.chapterInfos + let pages = textBook.pages.map { + EPUBPage( + spineIndex: $0.spineIndex, + chapterIndex: $0.chapterIndex, + pageIndexInChapter: $0.pageIndexInChapter, + totalPagesInChapter: $0.totalPagesInChapter, + chapterTitle: $0.chapterTitle, + fixedSpread: nil + ) + } + return (pages, chapters) + } +} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift new file mode 100644 index 0000000..9ed4c6e --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift @@ -0,0 +1,133 @@ +import UIKit + +// MARK: - RDReaderView 数据源与代理 + +extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate { + public func pageCountOfReaderView(readerView: RDReaderView) -> Int { + textBook?.pages.count ?? activePages.count + } + + public func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView { + if let textBook, let page = textBook.page(at: pageNum + 1) { + let contentView = (containerView as? RDEPUBTextContentView) ?? RDEPUBTextContentView() + contentView.delegate = self + contentView.configure( + page: page, + pageNumber: pageNum + 1, + totalPages: textBook.pages.count, + configuration: configuration, + highlights: textHighlights(for: page), + searchState: searchState + ) + return contentView + } + + guard let publication, + let request = request(for: pageNum) else { + return containerView ?? UIView() + } + + let contentView = (containerView as? RDEPUBWebContentView) ?? RDEPUBWebContentView() + contentView.delegate = self + contentView.configure( + publication: publication, + request: request, + pageNumber: pageNum + 1, + totalPages: activePages.count, + theme: configuration.theme + ) + return contentView + } + + public func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? { + textBook == nil + ? NSStringFromClass(RDEPUBWebContentView.self) + : NSStringFromClass(RDEPUBTextContentView.self) + } + + private func textHighlights(for page: RDEPUBTextPage) -> [RDEPUBHighlight] { + if let textBook, + let chapterData = textBook.chapterData(for: page.href) { + return chapterData.highlights(on: page, from: activeHighlights) + } + + guard let publication else { + return activeHighlights.filter { $0.location.href == page.href } + } + let pageHref = publication.resourceResolver.normalizedHref(page.href) ?? page.href + return activeHighlights.filter { + (publication.resourceResolver.normalizedHref($0.location.href) ?? $0.location.href) == pageHref + } + } + + func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? { + readerContext.textChapterData(forNormalizedHref: href) + } + + public func topToolView(readerView: RDReaderView) -> UIView? { + topToolView + } + + public func bottomToolView(readerView: RDReaderView) -> UIView? { + bottomToolView + } + + public func pageNum(readerView: RDReaderView, pageNum: Int) { + updateCurrentSelection(nil) + reconcileTextPaginationSizeIfNeeded(for: pageNum) + + let totalPages = pageCountOfReaderView(readerView: readerView) + if totalPages > 0, pageNum == totalPages - 1 { + delegate?.epubReaderDidReachEnd(self) + } + + if textBook != nil, + let location = resolvedTextLocation(forPageNumber: pageNum + 1) { + persist(location: location) + synchronizeTextReadingState(pageNumber: pageNum + 1, location: location) + return + } + + if let location = fallbackLocation(for: pageNum) { + persist(location: location) + } + if readingSession?.navigatorState == .jumping || readingSession?.navigatorState == .moving { + readingSession?.transition(to: .idle) + } + } + + public func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) { + _ = isLandscape + runtime.viewportMonitor.capturePendingPresentationRestoreLocation() + } + + private func reconcileTextPaginationSizeIfNeeded(for pageNum: Int) { + guard textBook != nil, + !isRepaginating, + !isReconcilingTextPaginationSize, + pageNum >= 0, + let lastTextPaginationPageSize else { + return + } + + let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) + guard resolvedSize.width > 0, + resolvedSize.height > 0 else { + return + } + + let sizeChanged = abs(resolvedSize.width - lastTextPaginationPageSize.width) > 0.5 + || abs(resolvedSize.height - lastTextPaginationPageSize.height) > 0.5 + guard sizeChanged else { + return + } + + isReconcilingTextPaginationSize = true + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.isReconcilingTextPaginationSize = false + guard self.textBook != nil, !self.isRepaginating else { return } + self.repaginatePreservingCurrentLocation() + } + } +} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+PublicAPI.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+PublicAPI.swift new file mode 100644 index 0000000..6f55aba --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+PublicAPI.swift @@ -0,0 +1,176 @@ +import UIKit + +// MARK: - Public Reader Commands + +extension RDEPUBReaderController { + /// 重新加载书籍,重置所有状态并重新解析 EPUB + public func reloadBook() { + runtime.reloadBook() + } + + /// 跳转到指定阅读位置 + /// - Parameter location: 目标阅读位置 + public func go(to location: RDEPUBLocation) { + guard publication != nil else { return } + _ = runtime.go(to: location) + } + + /// 跳转到指定页码 + /// - Parameters: + /// - pageNumber: 目标页码(从 1 开始) + /// - animated: 是否动画过渡 + /// - Returns: 是否跳转成功 + @discardableResult + public func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool { + runtime.go(toPageNumber: pageNumber, animated: animated) + } + + /// 清除当前文本选中状态 + public func clearSelection() { + runtime.clearSelection() + } + + public func bookmark(withID id: String) -> RDEPUBBookmark? { + runtime.bookmark(withID: id) + } + + public func highlight(withID id: String) -> RDEPUBHighlight? { + runtime.highlight(withID: id) + } + + public func nativeTextSemanticSummary() -> String? { + guard let textBook, + let page = textBook.page(at: max(readerView.currentPage + 1, 1)) ?? textBook.pages.first else { + return nil + } + + let metadata = page.metadata + var parts = [ + "page \(page.absolutePageIndex + 1)", + "break \(metadata.breakReason.rawValue)", + metadata.blockKinds.isEmpty ? nil : "block kinds [\(metadata.blockKinds.map(\.rawValue).joined(separator: ","))]", + metadata.semanticHints.isEmpty ? nil : "hints [\(metadata.semanticHints.map(\.rawValue).joined(separator: ","))]", + metadata.attachmentPlacements.isEmpty ? nil : "placements [\(metadata.attachmentPlacements.map(\.rawValue).joined(separator: ","))]" + ].compactMap { $0 } + if let firstDiagnostic = metadata.diagnostics.first { + parts.append(firstDiagnostic) + } + return parts.joined(separator: " · ") + } + + /// 添加高亮标注,从当前选中文本或指定选区创建 + /// - Parameters: + /// - selection: 文本选区,默认使用 currentSelection + /// - color: 高亮颜色(CSS 格式),默认黄色 + /// - note: 可选批注文字 + /// - Returns: 创建的高亮对象,重复时返回 nil + @discardableResult + public func addHighlight( + from selection: RDEPUBSelection? = nil, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + runtime.addHighlight(from: selection, color: color, note: note) + } + + @discardableResult + public func addAnnotation( + from selection: RDEPUBSelection? = nil, + style: RDEPUBHighlightStyle, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + runtime.addAnnotation(from: selection, style: style, color: color, note: note) + } + + @discardableResult + public func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { + runtime.upsertHighlight(highlight) + } + + @discardableResult + public func removeHighlight(id: String) -> RDEPUBHighlight? { + runtime.removeHighlight(id: id) + } + + @discardableResult + public func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? { + runtime.updateHighlightNote(id: id, note: note) + } + + @discardableResult + public func go(toHighlightID id: String, animated: Bool = true) -> Bool { + runtime.go(toHighlightID: id, animated: animated) + } + + public func removeAllHighlights() { + runtime.removeAllHighlights() + } + + @discardableResult + public func go(toTableOfContentsItem item: EPUBTableOfContentsItem, animated: Bool = true) -> Bool { + go(toTableOfContentsHref: item.href, animated: animated) + } + + @discardableResult + public func go(toTableOfContentsItem item: RDEPUBReaderTableOfContentsItem, animated: Bool = true) -> Bool { + go(toTableOfContentsHref: item.href, animated: animated) + } + + @discardableResult + public func go(toTableOfContentsHref href: String, animated: Bool = true) -> Bool { + guard publication != nil else { return false } + + let components = href.components(separatedBy: "#") + let baseHref = components.first ?? href + let fragment = components.count > 1 ? components[1] : nil + let location = RDEPUBLocation( + bookIdentifier: currentBookIdentifier, + href: baseHref, + progression: 0, + fragment: fragment + ) + return restoreReadingLocation(location, animated: animated) + } + + @discardableResult + public func addBookmark(note: String? = nil) -> RDEPUBBookmark? { + runtime.addBookmark(note: note) + } + + @discardableResult + public func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? { + runtime.toggleBookmark(note: note) + } + + @discardableResult + public func removeBookmark(id: String) -> RDEPUBBookmark? { + runtime.removeBookmark(id: id) + } + + @discardableResult + public func go(toBookmarkID id: String, animated: Bool = true) -> Bool { + runtime.go(toBookmarkID: id, animated: animated) + } + + /// 执行全文搜索,自动跳转到第一个匹配项 + /// - Parameter keyword: 搜索关键词,空字符串会清除搜索 + public func search(keyword: String) { + runtime.search(keyword: keyword) + } + + @discardableResult + public func searchNext() -> Bool { + runtime.searchNext() + } + + @discardableResult + public func searchPrevious() -> Bool { + runtime.searchPrevious() + } + + public func clearSearch() { + runtime.clearSearch() + } +} + diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RenderSupport.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RenderSupport.swift new file mode 100644 index 0000000..0220711 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RenderSupport.swift @@ -0,0 +1,80 @@ +import UIKit + +extension RDEPUBReaderController { + func currentLayoutContext() -> RDEPUBNavigatorLayoutContext { + readerContext.currentLayoutContext() + } + + func currentPreferences() -> RDEPUBPreferences { + readerContext.currentPreferences() + } + + func currentTextPageSize() -> CGSize { + readerContext.currentTextPageSize() + } + + func currentTextRenderStyle() -> RDEPUBTextRenderStyle { + readerContext.currentTextRenderStyle() + } + + func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { + readerContext.currentTextLayoutConfig(pageSize: pageSize) + } + + func resolvedTextRenderer() -> RDEPUBTextRenderer { + readerContext.resolvedTextRenderer() + } + + func ensurePaginationHostView() -> UIView { + let viewportSize = currentLayoutContext().viewportSize + let hostFrame = CGRect(x: -viewportSize.width - 32, y: 0, width: viewportSize.width, height: viewportSize.height) + if paginationHostView.superview == nil { + view.addSubview(paginationHostView) + view.sendSubviewToBack(paginationHostView) + } + paginationHostView.frame = hostFrame + return paginationHostView + } + + func request(for pageIndex: Int) -> RDEPUBRenderRequest? { + guard let publication, activePages.indices.contains(pageIndex) else { + return nil + } + let page = activePages[pageIndex] + let pendingLocation = readingSession?.pendingLocation(forPageNumber: pageIndex + 1, spineIndex: page.spineIndex) + return currentPreferences().renderRequest( + for: page, + publication: publication, + viewportSize: currentLayoutContext().viewportSize, + targetLocation: pendingLocation, + highlights: highlights(for: page), + searchPresentation: searchPresentation(for: page) + ) + } + + func fallbackLocation(for pageIndex: Int) -> RDEPUBLocation? { + guard activePages.indices.contains(pageIndex) else { return nil } + return readingSession?.fallbackLocation(for: activePages[pageIndex], bookIdentifier: currentBookIdentifier) + } + + private func highlights(for page: EPUBPage) -> [RDEPUBHighlight] { + guard let publication else { return [] } + if let spread = page.fixedSpread { + let hrefs = Set(spread.resources.compactMap { publication.resourceResolver.normalizedHref($0.href) }) + return activeHighlights.filter { highlight in + guard let normalizedHref = publication.resourceResolver.normalizedHref(highlight.location.href) else { + return false + } + return hrefs.contains(normalizedHref) + } + } + + guard publication.spine.indices.contains(page.spineIndex) else { return [] } + let href = publication.spine[page.spineIndex].href + let normalizedHref = publication.resourceResolver.normalizedHref(href) + return activeHighlights.filter { highlight in + publication.resourceResolver.normalizedHref(highlight.location.href) == normalizedHref + } + } +} + diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RuntimeBridge.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RuntimeBridge.swift new file mode 100644 index 0000000..9d93492 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+RuntimeBridge.swift @@ -0,0 +1,234 @@ +import UIKit + +extension RDEPUBReaderController { + func applyReaderViewConfiguration() { + let resolvedDirection = resolvedPageDirection() + let presentationDidChange = readerView.currentDisplayType != configuration.displayType + || readerView.landscapeDualPageEnabled != configuration.landscapeDualPageEnabled + || readerView.pageDirection != resolvedDirection + let preservedLocation = presentationDidChange + ? (runtime.viewportMonitor.consumePendingPresentationRestoreLocation() ?? currentVisibleLocation() ?? persistenceLocation()) + : nil + view.backgroundColor = configuration.theme.contentBackgroundColor + readerView.landscapeDualPageEnabled = configuration.landscapeDualPageEnabled + readerView.pageDirection = resolvedDirection + updateReaderChrome() + if presentationDidChange { + readerView.switchReaderDisplayType(configuration.displayType) + if let preservedLocation { + _ = restoreReadingLocation(preservedLocation) + } + } + } + + func startInitialLoadIfNeeded() { + runtime.startInitialLoadIfNeeded() + } + + func loadPublication() { + runtime.loadPublication() + } + + func applyParsedPublication( + parser: RDEPUBParser, + publication: RDEPUBPublication, + bookIdentifier: String, + restoreLocation: RDEPUBLocation?, + bookmarks: [RDEPUBBookmark], + highlights: [RDEPUBHighlight] + ) { + runtime.applyParsedPublication( + parser: parser, + publication: publication, + bookIdentifier: bookIdentifier, + restoreLocation: restoreLocation, + bookmarks: bookmarks, + highlights: highlights + ) + } + + func paginatePublication(restoreLocation: RDEPUBLocation?) { + runtime.paginatePublication(restoreLocation: restoreLocation) + } + + func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) { + runtime.applyTextBook(textBook, restoreLocation: restoreLocation) + } + + func applyPaginationSnapshot( + _ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]), + restoreLocation: RDEPUBLocation? + ) { + runtime.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) + } + + func finishPagination(restoreLocation: RDEPUBLocation?) { + runtime.finishPagination(restoreLocation: restoreLocation) + } + + func repaginatePreservingCurrentLocation() { + runtime.repaginatePreservingCurrentLocation() + } + + @discardableResult + func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { + runtime.restoreReadingLocation(location, animated: animated) + } + + func currentVisibleLocation() -> RDEPUBLocation? { + runtime.currentVisibleLocation() + } + + func persistenceLocation() -> RDEPUBLocation? { + readerContext.persistenceLocation() + } + + func persist(location: RDEPUBLocation) { + readerContext.persist(location: location) + } + + func updateCurrentSelection(_ selection: RDEPUBSelection?) { + runtime.annotationCoordinator.updateCurrentSelection(selection) + } + + func scopedSelection( + _ selection: RDEPUBSelection, + relativeToSpineIndex spineIndex: Int? + ) -> RDEPUBSelection? { + runtime.annotationCoordinator.scopedSelection(selection, relativeToSpineIndex: spineIndex) + } + + func refreshVisibleContentPreservingLocation() { + runtime.refreshVisibleContentPreservingLocation() + } + + func rebuildExternalTextBook() { + runtime.rebuildExternalTextBook() + } + + func updateReaderChrome() { + runtime.updateReaderChrome() + } + + func updateBookmarkChrome() { + runtime.updateBookmarkChrome() + } + + func presentBookmarksManager() { + runtime.presentBookmarksManager() + } + + func presentHighlightsManager() { + runtime.presentHighlightsManager() + } + + func presentAnnotationCreation() { + runtime.presentAnnotationCreation() + } + + func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) { + runtime.handleSelectionMenuAction(action, selection: selection) + } + + func presentSettings() { + runtime.presentSettings() + } + + func updateConfiguration(_ update: (inout RDEPUBReaderConfiguration) -> Void) { + var nextConfiguration = configuration + update(&nextConfiguration) + configuration = nextConfiguration + } + + func setScreenBrightness(_ brightness: CGFloat) { + currentBrightness = max(0, min(1, brightness)) + persistReaderSettingsIfNeeded() + } + + func persistReaderSettingsIfNeeded() { + let settings = RDEPUBReaderSettings.capture( + configuration: configuration, + brightness: currentBrightness + ) + persistence?.saveReaderSettings(settings) + } + + func requiresRepagination( + from oldConfiguration: RDEPUBReaderConfiguration, + to newConfiguration: RDEPUBReaderConfiguration + ) -> Bool { + oldConfiguration.fontSize != newConfiguration.fontSize || + oldConfiguration.lineHeightMultiple != newConfiguration.lineHeightMultiple || + oldConfiguration.numberOfColumns != newConfiguration.numberOfColumns || + oldConfiguration.columnGap != newConfiguration.columnGap || + oldConfiguration.reflowableContentInsets != newConfiguration.reflowableContentInsets || + oldConfiguration.fixedContentInset != newConfiguration.fixedContentInset || + oldConfiguration.fixedLayoutFit != newConfiguration.fixedLayoutFit || + oldConfiguration.fixedLayoutSpreadMode != newConfiguration.fixedLayoutSpreadMode || + oldConfiguration.textRenderingEngine != newConfiguration.textRenderingEngine + } + + func requiresVisibleRefresh( + from oldConfiguration: RDEPUBReaderConfiguration, + to newConfiguration: RDEPUBReaderConfiguration + ) -> Bool { + oldConfiguration.theme != newConfiguration.theme + } + + func presentTableOfContents() { + runtime.presentTableOfContents() + } + + func handleBackAction() { + print("[Debug] handleBackAction called") + runtime.handleBackAction() + } + + func handle(error: Error) { + isRepaginating = false + hideLoading() + readerContext.clearActiveSnapshot() + textBook = nil + readerView.reloadData() + errorLabel.text = error.localizedDescription + errorLabel.isHidden = false + delegate?.epubReader(self, didFailWithError: error) + } + + func showLoading() { + errorLabel.isHidden = true + loadingIndicator.startAnimating() + } + + func hideLoading() { + loadingIndicator.stopAnimating() + } + + func currentViewportSignature() -> RDEPUBViewportSignature? { + runtime.currentViewportSignature() + } + + func handleViewportChangeIfNeeded( + reason: RDEPUBViewportChangeReason, + viewportSignature: RDEPUBViewportSignature? = nil + ) { + runtime.handleViewportChangeIfNeeded(reason: reason, viewportSignature: viewportSignature) + } + + func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? { + runtime.searchPresentation(for: page) + } + + private func resolvedPageDirection() -> RDReaderView.PageDirection { + publication?.readingProgression == .rtl ? .rightToLeft : .leftToRight + } +} + +// MARK: - 手势识别器代理(用于 NavigationController 返回手势) + +extension RDEPUBReaderController: UIGestureRecognizerDelegate { + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + true + } +} + diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+TableOfContents.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+TableOfContents.swift new file mode 100644 index 0000000..c2558b7 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController+TableOfContents.swift @@ -0,0 +1,84 @@ +import Foundation + +extension RDEPUBReaderController { + func resolvedCurrentTableOfContentsItem() -> RDEPUBReaderTableOfContentsItem? { + let items = flattenedTableOfContents + guard !items.isEmpty else { return nil } + + let currentPageNumber = max(readerView.currentPage + 1, 1) + let pageAnchoredMatch = items.last { item in + guard let pageNumber = item.pageNumber else { return false } + return pageNumber <= currentPageNumber + } + if let pageAnchoredMatch { + return pageAnchoredMatch + } + + guard let publication, + let currentLocation = currentVisibleLocation(), + let normalizedCurrentHref = publication.resourceResolver.normalizedHref(currentLocation.href) else { + return nil + } + + if let textBook, + let chapterData = textBook.chapterData( + for: currentLocation, + resolver: publication.resourceResolver, + bookIdentifier: currentBookIdentifier + ), + let tocItem = chapterData.primaryTableOfContentsItem( + from: publication.tableOfContents, + normalizer: { publication.resourceResolver.normalizedHref($0) } + ) { + return items.last { $0.href == tocItem.href } ?? items.last { + guard let normalizedItemHref = publication.resourceResolver.normalizedHref($0.href.components(separatedBy: "#").first ?? $0.href) else { + return false + } + return normalizedItemHref == normalizedCurrentHref + } + } + + return items.last { item in + guard let normalizedItemHref = publication.resourceResolver.normalizedHref(item.href.components(separatedBy: "#").first ?? item.href) else { + return false + } + return normalizedItemHref == normalizedCurrentHref + } + } + + func flattenedTableOfContentsItems( + from items: [EPUBTableOfContentsItem], + depth: Int = 0 + ) -> [RDEPUBReaderTableOfContentsItem] { + items.flatMap { item in + let location = RDEPUBLocation(bookIdentifier: currentBookIdentifier, href: item.href, progression: 0) + let pageNumber: Int? + if let textBook, let publication, + let chapterData = textBook.chapterData(for: location, resolver: publication.resourceResolver, bookIdentifier: currentBookIdentifier) { + let normalizedLocation = publication.resourceResolver.normalizedLocation( + location, + bookIdentifier: currentBookIdentifier + ) ?? location + pageNumber = chapterData.pageNumber(for: normalizedLocation) + ?? textBook.pageNumber( + for: location, + resolver: publication.resourceResolver, + bookIdentifier: currentBookIdentifier + ) + } else if let readingSession { + pageNumber = readingSession.pageIndex(for: location, bookIdentifier: currentBookIdentifier).map { $0 + 1 } + } else { + pageNumber = nil + } + + let current = RDEPUBReaderTableOfContentsItem( + title: item.title, + href: item.href, + depth: depth, + pageNumber: pageNumber + ) + return [current] + flattenedTableOfContentsItems(from: item.children, depth: depth + 1) + } + } +} + diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift index 9c77276..e1d71f6 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift @@ -20,36 +20,6 @@ import UIKit // MARK: - 阅读器主控制器 public final class RDEPUBReaderController: UIViewController { - // MARK: - 内部辅助类型 - - /// 视口签名结构体,用于检测视口尺寸或安全区是否发生显著变化 - /// 当变化超过阈值时触发重新分页 - private struct RDEPUBViewportSignature: Equatable { - let width: CGFloat - let height: CGFloat - let safeTop: CGFloat - let safeLeft: CGFloat - let safeBottom: CGFloat - let safeRight: CGFloat - - func differsSignificantly(from other: RDEPUBViewportSignature, threshold: CGFloat = 1) -> Bool { - abs(width - other.width) > threshold || - abs(height - other.height) > threshold || - abs(safeTop - other.safeTop) > threshold || - abs(safeLeft - other.safeLeft) > threshold || - abs(safeBottom - other.safeBottom) > threshold || - abs(safeRight - other.safeRight) > threshold - } - } - - /// 视口变化的原因枚举,用于决定延迟处理的策略 - private enum RDEPUBViewportChangeReason { - case viewLayout - case orientationTransition - case pendingRepagination - } - - private typealias NativeTextSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo]) // MARK: - 公开属性 @@ -59,6 +29,7 @@ public final class RDEPUBReaderController: UIViewController { /// 阅读器配置,修改后自动判断是否需要重新分页或刷新内容 public var configuration: RDEPUBReaderConfiguration { didSet { + readerContext.configuration = configuration persistReaderSettingsIfNeeded() guard isViewLoaded else { return } let oldConfiguration = oldValue @@ -94,7 +65,10 @@ public final class RDEPUBReaderController: UIViewController { } /// 当前用户选中的文本(只读),用于创建高亮/批注 - public private(set) var currentSelection: RDEPUBSelection? + public internal(set) var currentSelection: RDEPUBSelection? { + get { readerContext.currentSelection } + set { readerContext.currentSelection = newValue } + } /// 所有高亮标注 public var highlights: [RDEPUBHighlight] { @@ -131,16 +105,14 @@ public final class RDEPUBReaderController: UIViewController { // MARK: - 私有属性 /// EPUB 文件 URL - private let epubURL: URL - fileprivate let persistence: RDEPUBReaderPersistence? - private let readerView = RDReaderView() - private let loadingIndicator: UIActivityIndicatorView = { - if #available(iOS 13.0, *) { - return UIActivityIndicatorView(style: .large) - } - return UIActivityIndicatorView(style: .gray) + let epubURL: URL + let persistence: RDEPUBReaderPersistence? + let dependencies: RDEPUBReaderDependencies + let readerView = RDReaderView() + let loadingIndicator: UIActivityIndicatorView = { + UIActivityIndicatorView(style: .large) }() - private let errorLabel: UILabel = { + let errorLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 label.textAlignment = .center @@ -148,36 +120,85 @@ public final class RDEPUBReaderController: UIViewController { label.isHidden = true return label }() - private let paginationHostView = UIView() + let paginationHostView = UIView() + lazy var readerContext = RDEPUBReaderContext(controller: self) - private var parser: RDEPUBParser? - fileprivate var publication: RDEPUBPublication? - private var readingSession: RDEPUBReadingSession? - private var textBook: RDEPUBTextBook? - private var activePages: [EPUBPage] = [] - private var activeChapters: [EPUBChapterInfo] = [] - fileprivate var activeBookmarks: [RDEPUBBookmark] = [] - private var activeHighlights: [RDEPUBHighlight] = [] - fileprivate lazy var topToolView = makeTopToolView() - fileprivate lazy var bottomToolView = makeBottomToolView() - fileprivate var currentBookIdentifier: String? - private let textBookCache = RDEPUBTextBookCache() - private var currentBrightness: CGFloat - private var didStartInitialLoad = false - private var isRepaginating = false - private var lastTextPaginationPageSize: CGSize? - private var isReconcilingTextPaginationSize = false - private var paginationToken = UUID() - private var paginator: RDEPUBPaginator? - private var searchState: RDEPUBSearchState? - private var lastAppliedViewportSignature: RDEPUBViewportSignature? - private var pendingViewportChangeReason: RDEPUBViewportChangeReason? - private var pendingPresentationRestoreLocation: RDEPUBLocation? - private var isWaitingForViewportTransitionCompletion = false - /// 标记是否通过外部 TextBook 初始化(跳过 EPUB 解析流程) - private var isExternalTextBook = false - /// 外部 TextBook 的原始文件 URL(用于重建 TextBook) - private var textFileURL: URL? + var parser: RDEPUBParser? { + get { readerContext.parser } + set { readerContext.parser = newValue } + } + var publication: RDEPUBPublication? { + get { readerContext.publication } + set { readerContext.publication = newValue } + } + var readingSession: RDEPUBReadingSession? { + get { readerContext.readingSession } + set { readerContext.readingSession = newValue } + } + var textBook: RDEPUBTextBook? { + get { readerContext.textBook } + set { readerContext.textBook = newValue } + } + var activePages: [EPUBPage] { + readerContext.activePages + } + var activeChapters: [EPUBChapterInfo] { + readerContext.activeChapters + } + var activeBookmarks: [RDEPUBBookmark] { + get { readerContext.activeBookmarks } + set { readerContext.activeBookmarks = newValue } + } + var activeHighlights: [RDEPUBHighlight] { + get { readerContext.activeHighlights } + set { readerContext.activeHighlights = newValue } + } + lazy var topToolView = runtime.makeTopToolView() + lazy var bottomToolView = runtime.makeBottomToolView() + var currentBookIdentifier: String? { + get { readerContext.currentBookIdentifier } + set { readerContext.currentBookIdentifier = newValue } + } + var textBookCache: RDEPUBTextBookCache { readerContext.textBookCache } + var currentBrightness: CGFloat { + get { readerContext.currentBrightness } + set { readerContext.currentBrightness = newValue } + } + var didStartInitialLoad: Bool { + get { readerContext.didStartInitialLoad } + set { readerContext.didStartInitialLoad = newValue } + } + var isRepaginating: Bool { + get { readerContext.isRepaginating } + set { readerContext.isRepaginating = newValue } + } + var lastTextPaginationPageSize: CGSize? { + get { readerContext.lastTextPaginationPageSize } + set { readerContext.lastTextPaginationPageSize = newValue } + } + var isReconcilingTextPaginationSize = false + var paginationToken: UUID { + get { readerContext.paginationToken } + set { readerContext.paginationToken = newValue } + } + var paginator: RDEPUBPaginator? { + get { readerContext.paginator } + set { readerContext.paginator = newValue } + } + var searchState: RDEPUBSearchState? { + get { readerContext.searchState } + set { readerContext.searchState = newValue } + } + var isExternalTextBook: Bool { + get { readerContext.isExternalTextBook } + set { readerContext.isExternalTextBook = newValue } + } + var textFileURL: URL? { + get { readerContext.textFileURL } + set { readerContext.textFileURL = newValue } + } + lazy var readerAssemblyCoordinator = RDEPUBReaderAssemblyCoordinator(context: readerContext) + lazy var runtime = RDEPUBReaderRuntime(context: readerContext) // MARK: - 初始化 @@ -190,15 +211,22 @@ public final class RDEPUBReaderController: UIViewController { public init( epubURL: URL, configuration: RDEPUBReaderConfiguration = .default, - persistence: RDEPUBReaderPersistence? = RDEPUBUserDefaultsPersistence() + persistence: RDEPUBReaderPersistence? = RDEPUBUserDefaultsPersistence(), + dependencies: RDEPUBReaderDependencies = .live ) { let persistedSettings = persistence?.loadReaderSettings() self.epubURL = epubURL self.configuration = persistedSettings?.applying(to: configuration) ?? configuration self.persistence = persistence - self.currentBrightness = max(0, min(1, persistedSettings?.brightness ?? CGFloat(UIScreen.main.brightness))) + self.dependencies = dependencies + let brightness = max(0, min(1, persistedSettings?.brightness ?? dependencies.environment.currentBrightness)) super.init(nibName: nil, bundle: nil) - UIScreen.main.brightness = currentBrightness + // Sync context after super.init + readerContext.dependencies = dependencies + readerContext.configuration = self.configuration + readerContext.epubURL = epubURL + readerContext.persistence = persistence + self.currentBrightness = brightness } required init?(coder: NSCoder) { @@ -220,13 +248,18 @@ public final class RDEPUBReaderController: UIViewController { title: String, textFileURL: URL? = nil, configuration: RDEPUBReaderConfiguration = .default, - persistence: RDEPUBReaderPersistence? = RDEPUBUserDefaultsPersistence() + persistence: RDEPUBReaderPersistence? = RDEPUBUserDefaultsPersistence(), + dependencies: RDEPUBReaderDependencies = .live ) { let persistedSettings = persistence?.loadReaderSettings() let resolvedConfig = persistedSettings?.applying(to: configuration) ?? configuration - let brightness = max(0, min(1, persistedSettings?.brightness ?? CGFloat(UIScreen.main.brightness))) - self.init(epubURL: URL(string: "about:blank")!, configuration: resolvedConfig, persistence: persistence) + self.init( + epubURL: URL(string: "about:blank")!, + configuration: resolvedConfig, + persistence: persistence, + dependencies: dependencies + ) self.isExternalTextBook = true self.textBook = textBook self.textFileURL = textFileURL @@ -237,21 +270,9 @@ public final class RDEPUBReaderController: UIViewController { public override func viewDidLoad() { super.viewDidLoad() - UIScreen.main.brightness = currentBrightness - view.backgroundColor = configuration.theme.contentBackgroundColor - setupReaderView() - setupLoadingIndicator() - setupErrorLabel() - delegate?.epubReader(self, configureTopToolView: topToolView) - - if isExternalTextBook { - let restoreLocation = currentBookIdentifier.flatMap { persistence?.loadLocation(for: $0) } - if let id = currentBookIdentifier { - activeBookmarks = persistence?.loadBookmarks(for: id) ?? [] - activeHighlights = persistence?.loadHighlights(for: id) ?? [] - } - finishPagination(restoreLocation: restoreLocation) - } + currentBrightness = currentBrightness + readerAssemblyCoordinator.assembleInterface() + readerAssemblyCoordinator.finishExternalTextBookLaunchIfNeeded() } public override func viewWillAppear(_ animated: Bool) { @@ -273,1810 +294,12 @@ public final class RDEPUBReaderController: UIViewController { public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - guard let viewportSignature = currentViewportSignature() else { return } - - if !didStartInitialLoad { - lastAppliedViewportSignature = viewportSignature - startInitialLoadIfNeeded() - return - } - - guard publication != nil || isExternalTextBook else { - lastAppliedViewportSignature = viewportSignature - return - } - - guard !isWaitingForViewportTransitionCompletion else { - return - } - - handleViewportChangeIfNeeded(reason: .viewLayout, viewportSignature: viewportSignature) + runtime.viewportMonitor.viewDidLayoutSubviews() } public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - - guard didStartInitialLoad else { return } - pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation() - isWaitingForViewportTransitionCompletion = true - - coordinator.animate(alongsideTransition: nil) { [weak self] _ in - guard let self else { return } - self.isWaitingForViewportTransitionCompletion = false - self.view.layoutIfNeeded() - self.handleViewportChangeIfNeeded(reason: .orientationTransition) - } - } - - // MARK: - 公开 API - - /// 重新加载书籍,重置所有状态并重新解析 EPUB - public func reloadBook() { - didStartInitialLoad = false - parser = nil - publication = nil - readingSession = nil - textBook = nil - activePages = [] - activeChapters = [] - activeBookmarks = [] - activeHighlights = [] - searchState = nil - lastAppliedViewportSignature = currentViewportSignature() - pendingViewportChangeReason = nil - pendingPresentationRestoreLocation = nil - isWaitingForViewportTransitionCompletion = false - updateCurrentSelection(nil) - readerView.reloadData() - startInitialLoadIfNeeded() - } - - /// 跳转到指定阅读位置 - /// - Parameter location: 目标阅读位置 - public func go(to location: RDEPUBLocation) { - guard publication != nil else { return } - restoreReadingLocation(location) - } - - /// 跳转到指定页码 - /// - Parameters: - /// - pageNumber: 目标页码(从 1 开始) - /// - animated: 是否动画过渡 - /// - Returns: 是否跳转成功 - @discardableResult - public func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool { - guard pageNumber > 0 else { return false } - - if textBook != nil { - guard let location = resolvedTextLocation(forPageNumber: pageNumber) else { - return false - } - return restoreReadingLocation(location, animated: animated) - } - - guard activePages.indices.contains(pageNumber - 1) else { - return false - } - readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated) - if let location = currentVisibleLocation() { - persist(location: location) - } - return true - } - - /// 清除当前文本选中状态 - public func clearSelection() { - updateCurrentSelection(nil) - } - - public func bookmark(withID id: String) -> RDEPUBBookmark? { - activeBookmarks.first { $0.id == id } - } - - public func highlight(withID id: String) -> RDEPUBHighlight? { - activeHighlights.first { $0.id == id } - } - - public func nativeTextSemanticSummary() -> String? { - guard let textBook, - let page = textBook.page(at: max(readerView.currentPage + 1, 1)) ?? textBook.pages.first else { - return nil - } - - let metadata = page.metadata - var parts = [ - "page \(page.absolutePageIndex + 1)", - "break \(metadata.breakReason.rawValue)", - metadata.blockKinds.isEmpty ? nil : "block kinds [\(metadata.blockKinds.map(\.rawValue).joined(separator: ","))]", - metadata.semanticHints.isEmpty ? nil : "hints [\(metadata.semanticHints.map(\.rawValue).joined(separator: ","))]", - metadata.attachmentPlacements.isEmpty ? nil : "placements [\(metadata.attachmentPlacements.map(\.rawValue).joined(separator: ","))]" - ].compactMap { $0 } - if let firstDiagnostic = metadata.diagnostics.first { - parts.append(firstDiagnostic) - } - return parts.joined(separator: " · ") - } - - /// 添加高亮标注,从当前选中文本或指定选区创建 - /// - Parameters: - /// - selection: 文本选区,默认使用 currentSelection - /// - color: 高亮颜色(CSS 格式),默认黄色 - /// - note: 可选批注文字 - /// - Returns: 创建的高亮对象,重复时返回 nil - @discardableResult - public func addHighlight( - from selection: RDEPUBSelection? = nil, - color: String = "#F8E16C", - note: String? = nil - ) -> RDEPUBHighlight? { - addAnnotation(from: selection, style: .highlight, color: color, note: note) - } - - @discardableResult - public func addAnnotation( - from selection: RDEPUBSelection? = nil, - style: RDEPUBHighlightStyle, - color: String = "#F8E16C", - note: String? = nil - ) -> RDEPUBHighlight? { - let sourceSelection = selection ?? currentSelection - guard let sourceSelection, - let scopedSelection = scopedSelection(sourceSelection, relativeToSpineIndex: nil), - !scopedSelection.isEmpty else { - return nil - } - - let newHighlight = RDEPUBHighlight( - bookIdentifier: currentBookIdentifier, - location: scopedSelection.location, - text: scopedSelection.text, - rangeInfo: scopedSelection.rangeInfo, - style: style, - color: color, - note: note - ) - - let isDuplicate = activeHighlights.contains { highlight in - highlight.location.href == newHighlight.location.href && - highlight.location.fragment == newHighlight.location.fragment && - highlight.text == newHighlight.text && - highlight.rangeInfo == newHighlight.rangeInfo && - highlight.style == newHighlight.style - } - guard !isDuplicate else { - return nil - } - - activeHighlights.append(newHighlight) - persistHighlightsAndRefreshContent() - updateCurrentSelection(nil) - return newHighlight - } - - @discardableResult - public func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { - guard let scopedHighlight = scopedHighlight(highlight) else { - return nil - } - - if let index = activeHighlights.firstIndex(where: { $0.id == scopedHighlight.id }) { - activeHighlights[index] = scopedHighlight - } else { - activeHighlights.append(scopedHighlight) - } - persistHighlightsAndRefreshContent() - return scopedHighlight - } - - @discardableResult - public func removeHighlight(id: String) -> RDEPUBHighlight? { - guard let index = activeHighlights.firstIndex(where: { $0.id == id }) else { - return nil - } - let removed = activeHighlights.remove(at: index) - persistHighlightsAndRefreshContent() - return removed - } - - @discardableResult - public func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? { - guard let index = activeHighlights.firstIndex(where: { $0.id == id }) else { - return nil - } - activeHighlights[index].note = normalizedNote(note) - persistHighlightsAndRefreshContent() - return activeHighlights[index] - } - - @discardableResult - public func go(toHighlightID id: String, animated: Bool = true) -> Bool { - guard let highlight = highlight(withID: id) else { - return false - } - return restoreReadingLocation(highlight.location, animated: animated) - } - - public func removeAllHighlights() { - guard !activeHighlights.isEmpty else { return } - activeHighlights.removeAll() - persistHighlightsAndRefreshContent() - } - - @discardableResult - public func go(toTableOfContentsItem item: EPUBTableOfContentsItem, animated: Bool = true) -> Bool { - go(toTableOfContentsHref: item.href, animated: animated) - } - - @discardableResult - public func go(toTableOfContentsItem item: RDEPUBReaderTableOfContentsItem, animated: Bool = true) -> Bool { - go(toTableOfContentsHref: item.href, animated: animated) - } - - @discardableResult - public func go(toTableOfContentsHref href: String, animated: Bool = true) -> Bool { - guard publication != nil else { return false } - - let components = href.components(separatedBy: "#") - let baseHref = components.first ?? href - let fragment = components.count > 1 ? components[1] : nil - let location = RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: baseHref, - progression: 0, - fragment: fragment - ) - return restoreReadingLocation(location, animated: animated) - } - - private func setupReaderView() { - readerView.dataSource = self - readerView.delegate = self - readerView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(readerView) - NSLayoutConstraint.activate([ - readerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - readerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - readerView.topAnchor.constraint(equalTo: view.topAnchor), - readerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - - readerView.register(contentView: RDEPUBTextContentView.self, contentViewWithReuseIdentifier: NSStringFromClass(RDEPUBTextContentView.self)) - readerView.register(contentView: RDEPUBWebContentView.self, contentViewWithReuseIdentifier: NSStringFromClass(RDEPUBWebContentView.self)) - applyReaderViewConfiguration() - } - - private func makeTopToolView() -> RDEPUBReaderTopToolView { - let toolView = RDEPUBReaderTopToolView() - toolView.onBack = { [weak self] in - self?.handleBackAction() - } - toolView.onToggleBookmark = { [weak self] in - _ = self?.toggleBookmark() - } - return toolView - } - - private func makeBottomToolView() -> RDEPUBReaderBottomToolView { - let toolView = RDEPUBReaderBottomToolView() - toolView.onShowTableOfContents = { [weak self] in - self?.presentTableOfContents() - } - toolView.onShowBookmarks = { [weak self] in - self?.presentBookmarksManager() - } - toolView.onShowHighlights = { [weak self] in - self?.presentHighlightsManager() - } - toolView.onAddHighlight = { [weak self] in - self?.presentAnnotationCreation() - } - toolView.onShowSettings = { [weak self] in - self?.presentSettings() - } - return toolView - } - - private func setupLoadingIndicator() { - loadingIndicator.hidesWhenStopped = true - loadingIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(loadingIndicator) - NSLayoutConstraint.activate([ - loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - } - - private func setupErrorLabel() { - errorLabel.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(errorLabel) - NSLayoutConstraint.activate([ - errorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), - errorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), - errorLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor) - ]) - } - - private func applyReaderViewConfiguration() { - let resolvedDirection = resolvedPageDirection() - let presentationDidChange = readerView.currentDisplayType != configuration.displayType - || readerView.landscapeDualPageEnabled != configuration.landscapeDualPageEnabled - || readerView.pageDirection != resolvedDirection - let preservedLocation = presentationDidChange - ? (pendingPresentationRestoreLocation ?? currentVisibleLocation() ?? persistenceLocation()) - : nil - view.backgroundColor = configuration.theme.contentBackgroundColor - readerView.landscapeDualPageEnabled = configuration.landscapeDualPageEnabled - readerView.pageDirection = resolvedDirection - updateReaderChrome() - if presentationDidChange { - readerView.switchReaderDisplayType(configuration.displayType) - if let preservedLocation { - _ = restoreReadingLocation(preservedLocation) - } - pendingPresentationRestoreLocation = nil - } - } - - private func startInitialLoadIfNeeded() { - guard !didStartInitialLoad, readerView.bounds.width > 0, readerView.bounds.height > 0 else { - return - } - didStartInitialLoad = true - loadPublication() - } - - private func loadPublication() { - showLoading() - let loadToken = UUID() - paginationToken = loadToken - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - let parser = RDEPUBParser() - - do { - try parser.parse(epubURL: self.epubURL) - let publication = parser.makePublication() - let bookIdentifier = parser.metadata.identifier ?? self.epubURL.lastPathComponent - let restoreLocation = self.persistence?.loadLocation(for: bookIdentifier) - let bookmarks = self.persistence?.loadBookmarks(for: bookIdentifier) ?? [] - let highlights = self.persistence?.loadHighlights(for: bookIdentifier) ?? [] - - DispatchQueue.main.async { - guard self.paginationToken == loadToken else { return } - self.applyParsedPublication( - parser: parser, - publication: publication, - bookIdentifier: bookIdentifier, - restoreLocation: restoreLocation, - bookmarks: bookmarks, - highlights: highlights - ) - } - } catch { - DispatchQueue.main.async { - guard self.paginationToken == loadToken else { return } - self.handle(error: error) - } - } - } - } - - private func applyParsedPublication( - parser: RDEPUBParser, - publication: RDEPUBPublication, - bookIdentifier: String, - restoreLocation: RDEPUBLocation?, - bookmarks: [RDEPUBBookmark], - highlights: [RDEPUBHighlight] - ) { - self.parser = parser - self.publication = publication - self.currentBookIdentifier = bookIdentifier - self.activeBookmarks = bookmarks - self.activeHighlights = highlights - self.readingSession = RDEPUBReadingSession(publication: publication) - self.readingSession?.transition(to: .loading) - self.title = parser.metadata.title.isEmpty ? epubURL.deletingPathExtension().lastPathComponent : parser.metadata.title - applyReaderViewConfiguration() - updateReaderChrome() - delegate?.epubReader(self, didOpen: publication) - paginatePublication(restoreLocation: restoreLocation) - } - - private func paginatePublication(restoreLocation: RDEPUBLocation?) { - guard let parser, let publication, let readingSession else { return } - - isRepaginating = true - errorLabel.isHidden = true - showLoading() - let token = UUID() - paginationToken = token - - if publication.readingProfile == .textReflowable { - let renderer = resolvedTextRenderer() - let pageSize = currentTextPageSize() - lastTextPaginationPageSize = pageSize - let layoutConfig = currentTextLayoutConfig(pageSize: pageSize) - let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig) - let renderStyle = currentTextRenderStyle() - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - do { - let textBook = try builder.build( - parser: parser, - publication: publication, - pageSize: pageSize, - style: renderStyle - ) - DispatchQueue.main.async { - guard self.paginationToken == token else { return } - self.applyTextBook(textBook, restoreLocation: restoreLocation) - } - } catch { - DispatchQueue.main.async { - guard self.paginationToken == token else { return } - self.handle(error: error) - } - } - } - return - } - - if publication.layout == .fixed { - let snapshot = readingSession.makePaginationSnapshot( - pageCounts: Array(repeating: 1, count: publication.spine.count), - preferences: currentPreferences(), - layoutContext: currentLayoutContext() - ) - applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) - return - } - - let paginator = RDEPUBPaginator() - self.paginator = paginator - paginator.calculate( - parser: parser, - hostingView: ensurePaginationHostView(), - presentation: currentPreferences().presentationStyle(viewportSize: currentLayoutContext().viewportSize) - ) { [weak self] pageCounts in - guard let self = self, self.paginationToken == token else { return } - let snapshot = readingSession.makePaginationSnapshot( - pageCounts: pageCounts, - preferences: self.currentPreferences(), - layoutContext: self.currentLayoutContext() - ) - self.paginator = nil - self.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) - } - } - - private func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) { - self.textBook = textBook - let snapshot = nativeTextSnapshot(from: textBook) - self.activePages = snapshot.pages - self.activeChapters = snapshot.chapters - readingSession?.setActiveSnapshot(snapshot) - - guard !textBook.pages.isEmpty else { - handle(error: RDEPUBParserError.emptySpine) - return - } - - finishPagination(restoreLocation: restoreLocation) - } - - private func applyPaginationSnapshot( - _ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]), - restoreLocation: RDEPUBLocation? - ) { - textBook = nil - activePages = snapshot.pages - activeChapters = snapshot.chapters - readingSession?.setActiveSnapshot(snapshot) - - guard !snapshot.pages.isEmpty else { - handle(error: RDEPUBParserError.emptySpine) - return - } - - finishPagination(restoreLocation: restoreLocation) - } - - private func finishPagination(restoreLocation: RDEPUBLocation?) { - isRepaginating = false - hideLoading() - readerView.reloadData() - if let targetLocation = restoreLocation { - restoreReadingLocation(targetLocation) - } else { - readerView.transitionToPage(pageNum: 0) - readingSession?.transition(to: .idle) - } - - if pendingViewportChangeReason != nil { - let pendingReason = pendingViewportChangeReason ?? .pendingRepagination - pendingViewportChangeReason = nil - DispatchQueue.main.async { [weak self] in - self?.handleViewportChangeIfNeeded(reason: pendingReason) - } - } - } - - private func repaginatePreservingCurrentLocation() { - guard publication != nil else { return } - let restoreLocation = pendingPresentationRestoreLocation ?? currentVisibleLocation() ?? persistenceLocation() - pendingPresentationRestoreLocation = nil - paginatePublication(restoreLocation: restoreLocation) - } - - @discardableResult - fileprivate func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { - guard let targetPageNumber = pageNumber(for: location) else { - readerView.transitionToPage(pageNum: 0) - readingSession?.transition(to: .idle) - return false - } - - if textBook == nil { - _ = readingSession?.queueNavigation( - to: location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) - } else { - readingSession?.transition(to: .jumping) - } - readerView.transitionToPage(pageNum: max(targetPageNumber - 1, 0), animated: animated) - return true - } - - fileprivate func currentVisibleLocation() -> RDEPUBLocation? { - if textBook != nil, readerView.currentPage >= 0 { - return resolvedTextLocation(forPageNumber: readerView.currentPage + 1) - } - return readingSession?.currentReadingLocation(bookIdentifier: currentBookIdentifier) - } - - private func persistenceLocation() -> RDEPUBLocation? { - guard let currentBookIdentifier else { return nil } - return persistence?.loadLocation(for: currentBookIdentifier) - } - - private func persist(location: RDEPUBLocation) { - guard let currentBookIdentifier else { return } - persistence?.saveLocation(location, for: currentBookIdentifier) - delegate?.epubReader(self, didUpdateLocation: location) - delegate?.epubReader(self, didUpdateCurrentTableOfContentsItem: resolvedCurrentTableOfContentsItem()) - updateBookmarkChrome() - } - - private func updateCurrentSelection(_ selection: RDEPUBSelection?) { - currentSelection = selection?.isEmpty == false ? selection : nil - bottomToolView.setAddHighlightEnabled(configuration.allowsHighlights && currentSelection != nil) - delegate?.epubReader(self, didChangeSelection: currentSelection) + runtime.viewportMonitor.viewWillTransition(with: coordinator) } - private func scopedSelection( - _ selection: RDEPUBSelection, - relativeToSpineIndex spineIndex: Int? - ) -> RDEPUBSelection? { - guard let publication else { return nil } - let normalizedLocation = publication.resourceResolver.normalizedLocation( - selection.location, - relativeToSpineIndex: spineIndex, - bookIdentifier: currentBookIdentifier - ) ?? RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: selection.location.href, - progression: selection.location.progression, - lastProgression: selection.location.lastProgression, - fragment: selection.location.fragment, - rangeAnchor: selection.location.rangeAnchor - ) - return RDEPUBSelection( - bookIdentifier: currentBookIdentifier, - location: normalizedLocation, - text: selection.text, - rangeInfo: selection.rangeInfo, - createdAt: selection.createdAt - ) - } - - private func scopedHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { - guard let publication else { return nil } - let normalizedLocation = publication.resourceResolver.normalizedLocation( - highlight.location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) ?? RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: highlight.location.href, - progression: highlight.location.progression, - lastProgression: highlight.location.lastProgression, - fragment: highlight.location.fragment, - rangeAnchor: highlight.location.rangeAnchor - ) - return RDEPUBHighlight( - id: highlight.id, - bookIdentifier: currentBookIdentifier, - location: normalizedLocation, - text: highlight.text, - rangeInfo: highlight.rangeInfo, - style: highlight.style, - color: highlight.color, - note: highlight.note, - createdAt: highlight.createdAt - ) - } - - private func persistHighlightsAndRefreshContent() { - guard let currentBookIdentifier else { return } - persistence?.saveHighlights(activeHighlights, for: currentBookIdentifier) - delegate?.epubReader(self, didUpdateHighlights: activeHighlights) - updateReaderChrome() - refreshVisibleContentPreservingLocation() - } - - private func refreshVisibleContentPreservingLocation() { - let restoreLocation = currentVisibleLocation() ?? persistenceLocation() - readerView.reloadData() - if let restoreLocation { - _ = restoreReadingLocation(restoreLocation) - } - } - - private func rebuildExternalTextBook() { - guard let textFileURL else { return } - let restoreLocation = currentVisibleLocation() ?? persistenceLocation() - let pageSize = currentTextPageSize() - let style = currentTextRenderStyle() - let builder = RDPlainTextBookBuilder(layoutConfig: currentTextLayoutConfig(pageSize: pageSize)) - if let newBook = try? builder.build(textFileURL: textFileURL, pageSize: pageSize, style: style) { - applyTextBook(newBook, restoreLocation: restoreLocation) - } - } - - private func updateReaderChrome() { - topToolView.apply(theme: configuration.theme) - topToolView.setTitle(title ?? parser?.metadata.title ?? epubURL.deletingPathExtension().lastPathComponent) - topToolView.setBookmarkEnabled(currentBookIdentifier != nil) - bottomToolView.apply(theme: configuration.theme) - bottomToolView.updateVisibility( - showsTableOfContents: configuration.showsTableOfContents, - allowsHighlights: configuration.allowsHighlights, - showsSettingsPanel: configuration.showsSettingsPanel - ) - bottomToolView.setBookmarksEnabled(!activeBookmarks.isEmpty) - bottomToolView.setAddHighlightEnabled(configuration.allowsHighlights && currentSelection != nil) - bottomToolView.setHighlightsEnabled(configuration.allowsHighlights && !activeHighlights.isEmpty) - updateBookmarkChrome() - } - - private func presentHighlightsManager() { - guard configuration.allowsHighlights else { return } - guard !activeHighlights.isEmpty else { return } - - let highlightsController = RDEPUBReaderHighlightsViewController( - highlights: activeHighlights, - theme: configuration.theme, - sectionTitleProvider: { [weak self] highlight in - self?.titleForHighlight(highlight) - } - ) - highlightsController.onSelectHighlight = { [weak self, weak highlightsController] highlight in - guard let self else { return } - highlightsController?.dismiss(animated: true) { - _ = self.go(to: highlight.location) - } - } - highlightsController.onUpdateHighlight = { [weak self] highlight in - _ = self?.updateHighlightNote(id: highlight.id, note: highlight.note) - } - highlightsController.onDeleteHighlight = { [weak self] highlight in - _ = self?.removeHighlight(id: highlight.id) - } - - let navigationController = UINavigationController(rootViewController: highlightsController) - navigationController.modalPresentationStyle = .pageSheet - present(navigationController, animated: true) - } - - private func presentAnnotationCreation() { - guard configuration.allowsHighlights, - let currentSelection else { - return - } - presentAnnotationActionSheet(for: currentSelection) - } - - private func presentAnnotationActionSheet(for selection: RDEPUBSelection) { - let alert = UIAlertController(title: "创建标注", message: nil, preferredStyle: .actionSheet) - alert.addAction(UIAlertAction(title: "高亮", style: .default) { [weak self] _ in - self?.createAnnotation(from: selection, style: .highlight) - }) - alert.addAction(UIAlertAction(title: "划线", style: .default) { [weak self] _ in - self?.createAnnotation(from: selection, style: .underline) - }) - alert.addAction(UIAlertAction(title: "批注", style: .default) { [weak self] _ in - self?.presentAnnotationNoteEditor(for: selection) - }) - alert.addAction(UIAlertAction(title: "取消", style: .cancel)) - - if let popover = alert.popoverPresentationController { - popover.sourceView = bottomToolView - popover.sourceRect = bottomToolView.bounds - } - - present(alert, animated: true) - } - - private func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) { - guard let selection else { return } - switch action { - case .copy: - UIPasteboard.general.string = selection.text - updateCurrentSelection(nil) - case .highlight: - createAnnotation(from: selection, style: .highlight) - case .annotate: - presentAnnotationNoteEditor(for: selection) - } - } - - private func createAnnotation(from selection: RDEPUBSelection, style: RDEPUBHighlightStyle, note: String? = nil) { - _ = addAnnotation(from: selection, style: style, note: note) - } - - private func presentAnnotationNoteEditor(for selection: RDEPUBSelection) { - let alert = UIAlertController(title: "添加批注", message: selection.text, preferredStyle: .alert) - alert.addTextField { textField in - textField.placeholder = "输入批注内容" - } - alert.addAction(UIAlertAction(title: "取消", style: .cancel)) - alert.addAction(UIAlertAction(title: "保存", style: .default) { [weak self, weak alert] _ in - self?.createAnnotation( - from: selection, - style: .highlight, - note: alert?.textFields?.first?.text - ) - }) - present(alert, animated: true) - } - - private func presentSettings() { - guard configuration.showsSettingsPanel else { return } - let settingsController = RDEPUBReaderSettingsViewController( - configuration: configuration, - brightness: currentBrightness - ) - settingsController.onBrightnessChange = { [weak self] brightness in - self?.setScreenBrightness(brightness) - } - settingsController.onFontSizeChange = { [weak self] fontSize in - self?.updateConfiguration { $0.fontSize = fontSize } - } - settingsController.onLineHeightChange = { [weak self] lineHeightMultiple in - self?.updateConfiguration { $0.lineHeightMultiple = lineHeightMultiple } - } - settingsController.onColumnCountChange = { [weak self] numberOfColumns in - self?.updateConfiguration { $0.numberOfColumns = numberOfColumns } - } - settingsController.onDisplayTypeChange = { [weak self] displayType in - self?.updateConfiguration { $0.displayType = displayType } - } - settingsController.onThemeChange = { [weak self] theme in - self?.updateConfiguration { $0.theme = theme } - } - - let navigationController = UINavigationController(rootViewController: settingsController) - navigationController.modalPresentationStyle = .pageSheet - present(navigationController, animated: true) - } - - private func titleForHighlight(_ highlight: RDEPUBHighlight) -> String? { - guard let publication, - let normalizedHighlightHref = publication.resourceResolver.normalizedHref(highlight.location.href) else { - return nil - } - - return flattenedTableOfContents.first { item in - let rawHref = item.href.components(separatedBy: "#").first ?? item.href - return publication.resourceResolver.normalizedHref(rawHref) == normalizedHighlightHref - }?.title - } - - private func updateConfiguration(_ update: (inout RDEPUBReaderConfiguration) -> Void) { - var nextConfiguration = configuration - update(&nextConfiguration) - configuration = nextConfiguration - } - - private func setScreenBrightness(_ brightness: CGFloat) { - currentBrightness = max(0, min(1, brightness)) - UIScreen.main.brightness = currentBrightness - persistReaderSettingsIfNeeded() - } - - private func persistReaderSettingsIfNeeded() { - let settings = RDEPUBReaderSettings.capture( - configuration: configuration, - brightness: currentBrightness - ) - persistence?.saveReaderSettings(settings) - } - - private func normalizedNote(_ note: String?) -> String? { - let trimmed = note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return trimmed.isEmpty ? nil : trimmed - } - - private func requiresRepagination( - from oldConfiguration: RDEPUBReaderConfiguration, - to newConfiguration: RDEPUBReaderConfiguration - ) -> Bool { - oldConfiguration.fontSize != newConfiguration.fontSize || - oldConfiguration.lineHeightMultiple != newConfiguration.lineHeightMultiple || - oldConfiguration.numberOfColumns != newConfiguration.numberOfColumns || - oldConfiguration.columnGap != newConfiguration.columnGap || - oldConfiguration.reflowableContentInsets != newConfiguration.reflowableContentInsets || - oldConfiguration.fixedContentInset != newConfiguration.fixedContentInset || - oldConfiguration.fixedLayoutFit != newConfiguration.fixedLayoutFit || - oldConfiguration.fixedLayoutSpreadMode != newConfiguration.fixedLayoutSpreadMode || - oldConfiguration.textRenderingEngine != newConfiguration.textRenderingEngine - } - - private func requiresVisibleRefresh( - from oldConfiguration: RDEPUBReaderConfiguration, - to newConfiguration: RDEPUBReaderConfiguration - ) -> Bool { - oldConfiguration.theme != newConfiguration.theme - } - - private func presentTableOfContents() { - guard configuration.showsTableOfContents else { return } - let items = flattenedTableOfContents - guard !items.isEmpty else { return } - - let chapterController = RDEPUBReaderChapterListController( - items: items, - currentItem: currentTableOfContentsItem, - theme: configuration.theme - ) - chapterController.onSelectItem = { [weak self] item in - guard let self else { return } - chapterController.dismiss(animated: true) { - _ = self.go(toTableOfContentsItem: item, animated: true) - } - } - - let navigationController = UINavigationController(rootViewController: chapterController) - navigationController.modalPresentationStyle = .pageSheet - present(navigationController, animated: true) - } - - private func handleBackAction() { - if let navigationController, navigationController.viewControllers.first != self { - navigationController.popViewController(animated: true) - return - } - if let navigationController, navigationController.presentingViewController != nil { - navigationController.dismiss(animated: true) - return - } - dismiss(animated: true) - } - - private func resolvedCurrentTableOfContentsItem() -> RDEPUBReaderTableOfContentsItem? { - let items = flattenedTableOfContents - guard !items.isEmpty else { return nil } - - let currentPageNumber = max(readerView.currentPage + 1, 1) - let pageAnchoredMatch = items.last { item in - guard let pageNumber = item.pageNumber else { return false } - return pageNumber <= currentPageNumber - } - if let pageAnchoredMatch { - return pageAnchoredMatch - } - - guard let publication, - let currentLocation = currentVisibleLocation(), - let normalizedCurrentHref = publication.resourceResolver.normalizedHref(currentLocation.href) else { - return nil - } - - if let textBook, - let chapterData = textBook.chapterData( - for: currentLocation, - resolver: publication.resourceResolver, - bookIdentifier: currentBookIdentifier - ), - let tocItem = chapterData.primaryTableOfContentsItem( - from: publication.tableOfContents, - normalizer: { publication.resourceResolver.normalizedHref($0) } - ) { - return items.last { $0.href == tocItem.href } ?? items.last { - guard let normalizedItemHref = publication.resourceResolver.normalizedHref($0.href.components(separatedBy: "#").first ?? $0.href) else { - return false - } - return normalizedItemHref == normalizedCurrentHref - } - } - - return items.last { item in - guard let normalizedItemHref = publication.resourceResolver.normalizedHref(item.href.components(separatedBy: "#").first ?? item.href) else { - return false - } - return normalizedItemHref == normalizedCurrentHref - } - } - - private func flattenedTableOfContentsItems( - from items: [EPUBTableOfContentsItem], - depth: Int = 0 - ) -> [RDEPUBReaderTableOfContentsItem] { - items.flatMap { item in - let location = RDEPUBLocation(bookIdentifier: currentBookIdentifier, href: item.href, progression: 0) - let pageNumber: Int? - if let textBook, let publication, - let chapterData = textBook.chapterData(for: location, resolver: publication.resourceResolver, bookIdentifier: currentBookIdentifier) { - let normalizedLocation = publication.resourceResolver.normalizedLocation( - location, - bookIdentifier: currentBookIdentifier - ) ?? location - pageNumber = chapterData.pageNumber(for: normalizedLocation) - ?? textBook.pageNumber( - for: location, - resolver: publication.resourceResolver, - bookIdentifier: currentBookIdentifier - ) - } else if let readingSession { - pageNumber = readingSession.pageIndex(for: location, bookIdentifier: currentBookIdentifier).map { $0 + 1 } - } else { - pageNumber = nil - } - - let current = RDEPUBReaderTableOfContentsItem( - title: item.title, - href: item.href, - depth: depth, - pageNumber: pageNumber - ) - return [current] + flattenedTableOfContentsItems(from: item.children, depth: depth + 1) - } - } - - private func currentLayoutContext() -> RDEPUBNavigatorLayoutContext { - let containerSize = readerView.bounds.size == .zero ? view.bounds.size : readerView.bounds.size - return RDEPUBNavigatorLayoutContext( - containerSize: containerSize, - pagesPerScreen: readerView.pagesPerScreen, - safeAreaInsets: view.safeAreaInsets, - userInterfaceIdiom: traitCollection.userInterfaceIdiom, - reflowableContentInsets: configuration.reflowableContentInsets - ) - } - - private func currentPreferences() -> RDEPUBPreferences { - configuration.makePreferences() - } - - private func currentTextPageSize() -> CGSize { - let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil - let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) - if ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug") { - print("[PaginationDebug] currentTextPageSize currentPage=\(readerView.currentPage) resolved=\(NSCoder.string(for: resolvedSize)) readerBounds=\(NSCoder.string(for: readerView.bounds)) viewBounds=\(NSCoder.string(for: view.bounds)) safe=\(NSCoder.string(for: view.safeAreaInsets))") - } - if resolvedSize.width > 0, resolvedSize.height > 0 { - return resolvedSize - } - return currentLayoutContext().viewportSize - } - - private func currentTextRenderStyle() -> RDEPUBTextRenderStyle { - let font = UIFont.systemFont(ofSize: configuration.fontSize) - let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4) - return RDEPUBTextRenderStyle( - font: font, - lineSpacing: lineSpacing, - textColor: configuration.theme.contentTextColor, - backgroundColor: configuration.theme.contentBackgroundColor - ) - } - - private func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { - return RDEPUBTextLayoutConfig( - frameWidth: max(pageSize.width, 1), - frameHeight: max(pageSize.height, 1), - edgeInsets: configuration.reflowableContentInsets, - numberOfColumns: configuration.numberOfColumns, - columnGap: configuration.columnGap, - avoidOrphans: true, - avoidWidows: true, - avoidPageBreakInsideEnabled: true, - hyphenation: true, - imageMaxHeightRatio: 0.85 - ) - } - - private func resolvedTextRenderer() -> RDEPUBTextRenderer { - RDEPUBDTCoreTextRenderer() - } - - private func resolvedPageDirection() -> RDReaderView.PageDirection { - publication?.readingProgression == .rtl ? .rightToLeft : .leftToRight - } - - private func ensurePaginationHostView() -> UIView { - let viewportSize = currentLayoutContext().viewportSize - let hostFrame = CGRect(x: -viewportSize.width - 32, y: 0, width: viewportSize.width, height: viewportSize.height) - if paginationHostView.superview == nil { - view.addSubview(paginationHostView) - view.sendSubviewToBack(paginationHostView) - } - paginationHostView.frame = hostFrame - return paginationHostView - } - - private func request(for pageIndex: Int) -> RDEPUBRenderRequest? { - guard let publication, activePages.indices.contains(pageIndex) else { - return nil - } - let page = activePages[pageIndex] - let pendingLocation = readingSession?.pendingLocation(forPageNumber: pageIndex + 1, spineIndex: page.spineIndex) - return currentPreferences().renderRequest( - for: page, - publication: publication, - viewportSize: currentLayoutContext().viewportSize, - targetLocation: pendingLocation, - highlights: highlights(for: page), - searchPresentation: searchPresentation(for: page) - ) - } - - private func highlights(for page: EPUBPage) -> [RDEPUBHighlight] { - guard let publication else { return [] } - if let spread = page.fixedSpread { - let hrefs = Set(spread.resources.compactMap { publication.resourceResolver.normalizedHref($0.href) }) - return activeHighlights.filter { highlight in - guard let normalizedHref = publication.resourceResolver.normalizedHref(highlight.location.href) else { - return false - } - return hrefs.contains(normalizedHref) - } - } - - guard publication.spine.indices.contains(page.spineIndex) else { return [] } - let href = publication.spine[page.spineIndex].href - let normalizedHref = publication.resourceResolver.normalizedHref(href) - return activeHighlights.filter { highlight in - publication.resourceResolver.normalizedHref(highlight.location.href) == normalizedHref - } - } - - private func fallbackLocation(for pageIndex: Int) -> RDEPUBLocation? { - guard activePages.indices.contains(pageIndex) else { return nil } - return readingSession?.fallbackLocation(for: activePages[pageIndex], bookIdentifier: currentBookIdentifier) - } - - private func handle(error: Error) { - isRepaginating = false - hideLoading() - activePages = [] - activeChapters = [] - textBook = nil - readerView.reloadData() - errorLabel.text = error.localizedDescription - errorLabel.isHidden = false - delegate?.epubReader(self, didFailWithError: error) - } - - private func showLoading() { - errorLabel.isHidden = true - loadingIndicator.startAnimating() - } - - private func hideLoading() { - loadingIndicator.stopAnimating() - } - - private func currentViewportSignature() -> RDEPUBViewportSignature? { - let containerSize = readerView.bounds.size == .zero ? view.bounds.size : readerView.bounds.size - guard containerSize.width > 0, containerSize.height > 0 else { return nil } - let safeAreaInsets = view.safeAreaInsets - return RDEPUBViewportSignature( - width: containerSize.width, - height: containerSize.height, - safeTop: safeAreaInsets.top, - safeLeft: safeAreaInsets.left, - safeBottom: safeAreaInsets.bottom, - safeRight: safeAreaInsets.right - ) - } - - private func handleViewportChangeIfNeeded( - reason: RDEPUBViewportChangeReason, - viewportSignature: RDEPUBViewportSignature? = nil - ) { - guard didStartInitialLoad, - let signature = viewportSignature ?? currentViewportSignature() else { - return - } - - if isRepaginating { - pendingViewportChangeReason = reason - return - } - - if let lastAppliedViewportSignature, - !signature.differsSignificantly(from: lastAppliedViewportSignature) { - return - } - - lastAppliedViewportSignature = signature - - if isExternalTextBook { - rebuildExternalTextBook() - return - } - - guard publication != nil else { return } - repaginatePreservingCurrentLocation() - } -} - -// MARK: - 书签管理 - -extension RDEPUBReaderController { - @discardableResult - public func addBookmark(note: String? = nil) -> RDEPUBBookmark? { - guard let location = scopedBookmarkLocation(currentVisibleLocation()) else { - return nil - } - guard bookmark(matching: location) == nil else { - return nil - } - - let newBookmark = RDEPUBBookmark( - bookIdentifier: currentBookIdentifier, - location: location, - chapterTitle: titleForBookmarkLocation(location), - note: normalizedBookmarkNote(note) - ) - activeBookmarks.append(newBookmark) - persistBookmarksAndRefreshChrome() - return newBookmark - } - - @discardableResult - public func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? { - guard let location = scopedBookmarkLocation(currentVisibleLocation()) else { - return nil - } - - if let existingBookmark = bookmark(matching: location) { - _ = removeBookmark(id: existingBookmark.id) - return nil - } - - return addBookmark(note: note) - } - - @discardableResult - public func removeBookmark(id: String) -> RDEPUBBookmark? { - guard let index = activeBookmarks.firstIndex(where: { $0.id == id }) else { - return nil - } - let removed = activeBookmarks.remove(at: index) - persistBookmarksAndRefreshChrome() - return removed - } - - @discardableResult - public func go(toBookmarkID id: String, animated: Bool = true) -> Bool { - guard let bookmark = bookmark(withID: id) else { - return false - } - return restoreReadingLocation(bookmark.location, animated: animated) - } - - private func updateBookmarkChrome() { - topToolView.setBookmarkSelected(currentBookmark() != nil) - bottomToolView.setBookmarksEnabled(!activeBookmarks.isEmpty) - } - - private func presentBookmarksManager() { - guard !activeBookmarks.isEmpty else { return } - - let bookmarksController = RDEPUBReaderBookmarksViewController( - bookmarks: activeBookmarks, - theme: configuration.theme - ) - bookmarksController.onSelectBookmark = { [weak self, weak bookmarksController] bookmark in - guard let self else { return } - bookmarksController?.dismiss(animated: true) { - _ = self.restoreReadingLocation(bookmark.location, animated: true) - } - } - bookmarksController.onDeleteBookmark = { [weak self] bookmark in - _ = self?.removeBookmark(id: bookmark.id) - } - - let navigationController = UINavigationController(rootViewController: bookmarksController) - navigationController.modalPresentationStyle = .pageSheet - present(navigationController, animated: true) - } - - private func titleForBookmarkLocation(_ location: RDEPUBLocation) -> String? { - if let currentLocation = currentVisibleLocation(), - bookmarkHref(for: currentLocation) == bookmarkHref(for: location) { - return currentTableOfContentsItem?.title - } - - return flattenedTableOfContents.last { item in - bookmarkHref(forTableOfContentsHref: item.href) == bookmarkHref(for: location) - }?.title - } - - private func persistBookmarksAndRefreshChrome() { - guard let currentBookIdentifier else { return } - persistence?.saveBookmarks(activeBookmarks, for: currentBookIdentifier) - delegate?.epubReader(self, didUpdateBookmarks: activeBookmarks) - updateBookmarkChrome() - } - - private func currentBookmark() -> RDEPUBBookmark? { - guard let location = scopedBookmarkLocation(currentVisibleLocation()) else { - return nil - } - return bookmark(matching: location) - } - - private func bookmark(matching location: RDEPUBLocation?) -> RDEPUBBookmark? { - guard let location else { return nil } - return activeBookmarks.first { bookmarkMatchesLocation($0, location: location) } - } - - private func bookmarkMatchesLocation(_ bookmark: RDEPUBBookmark, location: RDEPUBLocation) -> Bool { - guard bookmarkHref(for: bookmark.location) == bookmarkHref(for: location) else { - return false - } - - if let bookmarkAnchor = bookmark.location.rangeAnchor, - let locationAnchor = location.rangeAnchor { - return bookmarkAnchor == locationAnchor - } - - if let bookmarkFragment = bookmark.location.fragment, - let locationFragment = location.fragment { - return bookmarkFragment == locationFragment - } - - let progressionDelta = abs(bookmark.location.navigationProgression - location.navigationProgression) - let threshold: Double = publication?.layout == .fixed ? 0.01 : 0.05 - return progressionDelta <= threshold - } - - private func bookmarkHref(for location: RDEPUBLocation) -> String { - publication?.resourceResolver.normalizedHref(location.href) ?? location.href - } - - private func bookmarkHref(forTableOfContentsHref href: String) -> String { - let rawHref = href.components(separatedBy: "#").first ?? href - return publication?.resourceResolver.normalizedHref(rawHref) ?? rawHref - } - - private func scopedBookmarkLocation(_ location: RDEPUBLocation?) -> RDEPUBLocation? { - guard let location else { return nil } - guard let publication else { - return RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: location.href, - progression: location.progression, - lastProgression: location.lastProgression, - fragment: location.fragment, - rangeAnchor: location.rangeAnchor - ) - } - - return publication.resourceResolver.normalizedLocation( - location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) ?? RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: location.href, - progression: location.progression, - lastProgression: location.lastProgression, - fragment: location.fragment, - rangeAnchor: location.rangeAnchor - ) - } - - private func normalizedBookmarkNote(_ note: String?) -> String? { - let trimmed = note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return trimmed.isEmpty ? nil : trimmed - } - - // MARK: - 搜索功能 - - /// 执行全文搜索,自动跳转到第一个匹配项 - /// - Parameter keyword: 搜索关键词,空字符串会清除搜索 - public func search(keyword: String) { - let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines) - guard !normalizedKeyword.isEmpty else { - clearSearch() - return - } - - let matches = resolvedSearchMatches(for: normalizedKeyword) - searchState = RDEPUBSearchState( - keyword: normalizedKeyword, - matches: matches, - currentMatchIndex: matches.isEmpty ? nil : 0 - ) - notifySearchStateChanged() - - if matches.isEmpty { - refreshVisibleContentPreservingLocation() - } else { - _ = navigateToCurrentSearchMatch(animated: false) - } - } - - @discardableResult - public func searchNext() -> Bool { - advanceSearch(by: 1) - } - - @discardableResult - public func searchPrevious() -> Bool { - advanceSearch(by: -1) - } - - public func clearSearch() { - searchState = nil - notifySearchStateChanged() - refreshVisibleContentPreservingLocation() - } - - private func resolvedSearchMatches(for keyword: String) -> [RDEPUBSearchMatch] { - if let textBook, let publication { - return RDEPUBTextSearchEngine(textBook: textBook, publication: publication).search(keyword: keyword) - } - if let parser, let publication { - return RDEPUBHTMLSearchEngine(parser: parser, publication: publication).search(keyword: keyword) - } - return [] - } - - private func advanceSearch(by delta: Int) -> Bool { - guard var searchState, !searchState.matches.isEmpty else { - return false - } - - let currentIndex = searchState.currentMatchIndex ?? 0 - let nextIndex = (currentIndex + delta + searchState.matches.count) % searchState.matches.count - searchState.currentMatchIndex = nextIndex - self.searchState = searchState - notifySearchStateChanged() - return navigateToCurrentSearchMatch(animated: true) - } - - private func notifySearchStateChanged() { - delegate?.epubReader(self, didUpdateSearchResult: searchState?.result) - delegate?.epubReader(self, didChangeCurrentSearchMatch: searchState?.currentMatch) - } - - private func navigateToCurrentSearchMatch(animated: Bool) -> Bool { - guard let searchMatch = searchState?.currentMatch else { - refreshVisibleContentPreservingLocation() - return false - } - - if let targetPageNumber = pageNumber(for: searchMatch), - readerView.currentPage == targetPageNumber - 1 { - refreshVisibleContentPreservingLocation() - return true - } - - let location = RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: searchMatch.href, - progression: searchMatch.progression, - lastProgression: searchMatch.progression, - fragment: nil, - rangeAnchor: searchMatch.rangeAnchor - ) - return restoreReadingLocation(location, animated: animated) - } - - private func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? { - if let chapterData = textChapterData(forNormalizedHref: searchMatch.href) { - if let pageNumber = chapterData.pageNumber(for: searchMatch) { - return pageNumber - } - - if let rangeLocation = searchMatch.rangeLocation, - let page = chapterData.page(containing: rangeLocation) { - return page.absolutePageIndex + 1 - } - } - - let location = RDEPUBLocation( - bookIdentifier: currentBookIdentifier, - href: searchMatch.href, - progression: searchMatch.progression, - lastProgression: searchMatch.progression, - fragment: nil, - rangeAnchor: searchMatch.rangeAnchor - ) - - if let textBook, let publication { - return textBook.pageNumber( - for: location, - resolver: publication.resourceResolver, - bookIdentifier: currentBookIdentifier - ) - } - - return readingSession?.pageIndex(for: location, bookIdentifier: currentBookIdentifier).map { $0 + 1 } - } - - private func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? { - guard let searchState, let publication else { - return nil - } - - let pageHrefs: [String] - if let fixedSpread = page.fixedSpread { - pageHrefs = fixedSpread.resources.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href } - } else if publication.spine.indices.contains(page.spineIndex) { - pageHrefs = [publication.resourceResolver.normalizedHref(publication.spine[page.spineIndex].href) ?? publication.spine[page.spineIndex].href] - } else { - pageHrefs = [] - } - - guard !pageHrefs.isEmpty else { - return nil - } - - let currentMatch = searchState.currentMatch - let normalizedCurrentHref = currentMatch.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href } - let resources = pageHrefs.map { href in - let matchCount = searchState.matches.filter { - (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == href - }.count - let activeLocalMatchIndex = normalizedCurrentHref == href ? currentMatch?.localMatchIndex : nil - return RDEPUBSearchPresentationResource( - href: href, - matchCount: matchCount, - activeLocalMatchIndex: activeLocalMatchIndex - ) - } - return RDEPUBSearchPresentation(keyword: searchState.keyword, resources: resources) - } -} - -// MARK: - RDReaderView 数据源与代理 - -extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate { - public func pageCountOfReaderView(readerView: RDReaderView) -> Int { - textBook?.pages.count ?? activePages.count - } - - public func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView { - if let textBook, let page = textBook.page(at: pageNum + 1) { - let contentView = (containerView as? RDEPUBTextContentView) ?? RDEPUBTextContentView() - contentView.delegate = self - contentView.configure( - page: page, - pageNumber: pageNum + 1, - totalPages: textBook.pages.count, - configuration: configuration, - highlights: textHighlights(for: page), - searchState: searchState - ) - return contentView - } - - guard let publication, - let request = request(for: pageNum) else { - return containerView ?? UIView() - } - - let contentView = (containerView as? RDEPUBWebContentView) ?? RDEPUBWebContentView() - contentView.delegate = self - contentView.configure( - publication: publication, - request: request, - pageNumber: pageNum + 1, - totalPages: activePages.count, - theme: configuration.theme - ) - return contentView - } - - public func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? { - textBook == nil - ? NSStringFromClass(RDEPUBWebContentView.self) - : NSStringFromClass(RDEPUBTextContentView.self) - } - - private func textHighlights(for page: RDEPUBTextPage) -> [RDEPUBHighlight] { - if let textBook, - let chapterData = textBook.chapterData(for: page.href) { - return chapterData.highlights(on: page, from: activeHighlights) - } - - guard let publication else { - return activeHighlights.filter { $0.location.href == page.href } - } - let pageHref = publication.resourceResolver.normalizedHref(page.href) ?? page.href - return activeHighlights.filter { - (publication.resourceResolver.normalizedHref($0.location.href) ?? $0.location.href) == pageHref - } - } - - private func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? { - guard let textBook, let publication else { return nil } - let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href - return textBook.chapters.lazy - .first(where: { (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == normalizedHref }) - .flatMap { textBook.chapterData(for: $0.href) } - } - - public func topToolView(readerView: RDReaderView) -> UIView? { - topToolView - } - - public func bottomToolView(readerView: RDReaderView) -> UIView? { - bottomToolView - } - - public func pageNum(readerView: RDReaderView, pageNum: Int) { - updateCurrentSelection(nil) - reconcileTextPaginationSizeIfNeeded(for: pageNum) - - let totalPages = pageCountOfReaderView(readerView: readerView) - if totalPages > 0, pageNum == totalPages - 1 { - delegate?.epubReaderDidReachEnd(self) - } - - if textBook != nil, - let location = resolvedTextLocation(forPageNumber: pageNum + 1) { - persist(location: location) - synchronizeTextReadingState(pageNumber: pageNum + 1, location: location) - return - } - - if let location = fallbackLocation(for: pageNum) { - persist(location: location) - } - if readingSession?.navigatorState == .jumping || readingSession?.navigatorState == .moving { - readingSession?.transition(to: .idle) - } - } - - public func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) { - _ = isLandscape - pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation() - } - - private func reconcileTextPaginationSizeIfNeeded(for pageNum: Int) { - guard textBook != nil, - !isRepaginating, - !isReconcilingTextPaginationSize, - pageNum >= 0, - let lastTextPaginationPageSize else { - return - } - - let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) - guard resolvedSize.width > 0, - resolvedSize.height > 0 else { - return - } - - let sizeChanged = abs(resolvedSize.width - lastTextPaginationPageSize.width) > 0.5 - || abs(resolvedSize.height - lastTextPaginationPageSize.height) > 0.5 - guard sizeChanged else { - return - } - - isReconcilingTextPaginationSize = true - DispatchQueue.main.async { [weak self] in - guard let self else { return } - self.isReconcilingTextPaginationSize = false - guard self.textBook != nil, !self.isRepaginating else { return } - self.repaginatePreservingCurrentLocation() - } - } -} - -// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径) - -extension RDEPUBReaderController: RDEPUBWebContentViewDelegate { - func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) { - guard readerView.currentPage >= 0, - activePages.indices.contains(readerView.currentPage) else { - return - } - - let currentPage = activePages[readerView.currentPage] - guard readingSession?.pageContains(spineIndex: spineIndex, in: currentPage) == true else { - return - } - - persist(location: location) - readingSession?.updateReadingContext( - pageNumber: readerView.currentPage + 1, - location: location, - spineIndex: spineIndex, - chapterIndex: currentPage.chapterIndex, - bookIdentifier: currentBookIdentifier - ) - } - - func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) { - if let selection { - updateCurrentSelection(scopedSelection(selection, relativeToSpineIndex: spineIndex)) - } else { - updateCurrentSelection(nil) - } - } - - func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) { - handleSelectionMenuAction(action, selection: currentSelection) - } - - func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) { - guard let readingSession, - let pageNumber = readingSession.queueNavigation( - to: location, - relativeToSpineIndex: fromSpineIndex, - bookIdentifier: currentBookIdentifier - ) else { - return - } - readerView.transitionToPage(pageNum: max(pageNumber - 1, 0), animated: true) - } - - func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL) { - delegate?.epubReader(self, didActivateExternalLink: url) - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - - func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String) { - print("EPUB JS Error: \(message)") - } -} - -// MARK: - 文本内容视图代理(Native Text 渲染路径) - -extension RDEPUBReaderController: RDEPUBTextContentViewDelegate { - func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) { - guard let selection else { - updateCurrentSelection(nil) - return - } - updateCurrentSelection(normalizedTextSelection(selection)) - } - - func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) { - handleSelectionMenuAction(action, selection: currentSelection) - contentView.clearSelection() - } - - private func normalizedTextSelection(_ selection: RDEPUBSelection) -> RDEPUBSelection? { - guard let textBook, - let chapterData = textBook.chapterData(for: selection.location.href) else { - return scopedSelection(selection, relativeToSpineIndex: nil) - } - guard let payload = RDEPUBTextOffsetRangeInfo.decode(from: selection.rangeInfo) else { - return scopedSelection(selection, relativeToSpineIndex: nil) - } - - let contentLength = max(chapterData.attributedContent.length, 1) - let lastInclusiveOffset = max(contentLength - 1, 1) - let start = max(0, min(payload.start, lastInclusiveOffset)) - let endExclusive = max(start + 1, min(payload.end, contentLength)) - let absoluteRange = NSRange(location: start, length: endExclusive - start) - let location = chapterData.location(for: absoluteRange, bookIdentifier: currentBookIdentifier) - return RDEPUBSelection( - bookIdentifier: currentBookIdentifier, - location: location, - text: selection.text, - rangeInfo: selection.rangeInfo, - createdAt: selection.createdAt - ) - } - - private func pageNumber(for location: RDEPUBLocation) -> Int? { - if let textBook, let publication { - // Anchor-based lookup (character-level precision) - if let anchor = location.rangeAnchor?.start { - if let page = textBook.indexTable.pageNumber(for: anchor, in: textBook) { - return page + 1 - } - } - - let normalizedLocation = publication.resourceResolver.normalizedLocation( - location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) ?? location - return textBook.pageNumber( - for: normalizedLocation, - resolver: publication.resourceResolver, - bookIdentifier: currentBookIdentifier - ) - } - - return readingSession?.queueNavigation( - to: location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) - } - - private func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? { - guard let textBook, - let publication, - let page = textBook.page(at: pageNumber) else { - return nil - } - - let location = textBook.chapterData(forPageNumber: pageNumber)?.location(forPage: page, bookIdentifier: currentBookIdentifier) - ?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier) - guard let location else { return nil } - - return publication.resourceResolver.normalizedLocation( - location, - relativeToSpineIndex: nil, - bookIdentifier: currentBookIdentifier - ) ?? location - } - - private func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) { - guard let textBook, - let page = textBook.page(at: pageNumber) else { - readingSession?.transition(to: .idle) - return - } - - readingSession?.updateReadingContext( - pageNumber: pageNumber, - location: location, - spineIndex: page.spineIndex, - chapterIndex: page.chapterIndex, - bookIdentifier: currentBookIdentifier - ) - } - - private func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> NativeTextSnapshot { - let chapters = textBook.chapterInfos - let pages = textBook.pages.map { - EPUBPage( - spineIndex: $0.spineIndex, - chapterIndex: $0.chapterIndex, - pageIndexInChapter: $0.pageIndexInChapter, - totalPagesInChapter: $0.totalPagesInChapter, - chapterTitle: $0.chapterTitle, - fixedSpread: nil - ) - } - return (pages, chapters) - } -} - -// MARK: - 手势识别器代理(用于 NavigationController 返回手势) - -extension RDEPUBReaderController: UIGestureRecognizerDelegate { - public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - true - } } diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift index e4bf870..40475c5 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift @@ -100,6 +100,7 @@ public final class RDEPUBReaderTopToolView: RDEPUBReaderToolView { } @objc private func backAction() { + print("[Debug] backAction fired, onBack: \(String(describing: onBack))") onBack?() } diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift b/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift deleted file mode 100644 index f09a11e..0000000 --- a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift +++ /dev/null @@ -1,752 +0,0 @@ -import UIKit -import Foundation - -#if canImport(DTCoreText) -import DTCoreText -#endif - -// MARK: - 文本内容视图代理 - -/// 文本内容视图的代理协议 -/// 通知控制器文本选择变化和选择菜单操作 -protocol RDEPUBTextContentViewDelegate: AnyObject { - /// 用户选中文本发生变化时调用 - func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?) - /// 用户从选择菜单中触发操作(拷贝/高亮/批注) - func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) -} - -// MARK: - 可选中文本视图 - -/// 自定义 UITextView,替换系统默认的 UIMenuItem 为自定义操作(拷贝、高亮、批注) -final class RDEPUBSelectableTextView: UITextView { - /// 选择菜单操作回调 - var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)? - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - switch action { - case #selector(rd_copy(_:)), - #selector(rd_highlight(_:)), - #selector(rd_annotate(_:)): - return selectedRange.location != NSNotFound && selectedRange.length > 0 - default: - return false - } - } - - @objc func rd_copy(_ sender: Any?) { - onSelectionAction?(.copy) - } - - @objc func rd_highlight(_ sender: Any?) { - onSelectionAction?(.highlight) - } - - @objc func rd_annotate(_ sender: Any?) { - onSelectionAction?(.annotate) - } -} - -// MARK: - DTCoreText 直接绘制视图 - -/// 基于 DTCoreText 的 Core Text 直接绘制视图 -/// 将 DTCoreText 的排版结果直接绘制到 UIView 上,跳过 UITextView 的间接渲染 -#if canImport(DTCoreText) -final class RDEPUBDirectCoreTextPageView: UIView { - var layoutFrame: DTCoreTextLayoutFrame? { - didSet { - setNeedsDisplay() - } - } - - var drawOptions: DTCoreTextLayoutFrameDrawingOptions = DTCoreTextLayoutFrameDrawingOptions(rawValue: 1)! { - didSet { - setNeedsDisplay() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - backgroundColor = .clear - isOpaque = false - contentMode = .redraw - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext(), - let layoutFrame else { return } - - context.saveGState() - layoutFrame.draw(in: context, options: drawOptions) - context.restoreGState() - } -} -#endif - -// MARK: - 文本内容视图 - -/// EPUB 流式排版的文本内容视图 -/// 支持两种渲染路径: -/// 1. DTCoreText 路径:直接绘制到 CoreText 视图,支持精确的排版控制 -/// 2. 回退路径:通过 UITextView 的 attributedText 渲染 -/// -/// 内置能力:高亮覆盖、搜索高亮、文本选择、长按菜单、封面图显示 -final class RDEPUBTextContentView: UIView { - private var contentInsets: UIEdgeInsets = .zero - private var currentPage: RDEPUBTextPage? - private var highlightedRanges: [RDEPUBHighlight] = [] - private var currentSearchState: RDEPUBSearchState? - private var isSelectionFromInteraction = false - private var selectionMenuAnchorRect: CGRect? - weak var delegate: RDEPUBTextContentViewDelegate? - -#if canImport(DTCoreText) - private let coreTextContentView: RDEPUBDirectCoreTextPageView = { - let view = RDEPUBDirectCoreTextPageView() - view.backgroundColor = .clear - view.isOpaque = false - return view - }() - - private var coreTextDisplayContent: NSAttributedString? - private var coreTextDisplayRange: NSRange? -#endif - - private let interactionController = RDEPUBPageInteractionController() - - private let backgroundOverlayView: RDEPUBSelectionOverlayView = { - let view = RDEPUBSelectionOverlayView() - return view - }() - - private let overlayView: RDEPUBSelectionOverlayView = { - let view = RDEPUBSelectionOverlayView() - return view - }() - - private var selectionAnchorPoint: CGPoint? - - private let textView: RDEPUBSelectableTextView = { - let view = RDEPUBSelectableTextView() - view.isEditable = false - view.isScrollEnabled = false - view.isSelectable = true - view.backgroundColor = .clear - view.textContainerInset = .zero - view.textContainer.lineFragmentPadding = 0 - return view - }() - - private let coverImageView: UIImageView = { - let view = UIImageView() - view.contentMode = .scaleAspectFit - view.isHidden = true - return view - }() - - private let pageNumberLabel: UILabel = { - let label = UILabel() - label.font = UIFont.systemFont(ofSize: 13) - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - addSubview(coverImageView) -#if canImport(DTCoreText) - addSubview(backgroundOverlayView) - addSubview(coreTextContentView) -#endif - addSubview(overlayView) - addSubview(textView) - addSubview(pageNumberLabel) - textView.delegate = self - textView.onSelectionAction = { [weak self] action in - guard let self else { return } - self.delegate?.textContentView(self, didRequestSelectionAction: action) - } - - let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:))) - longPress.minimumPressDuration = 0.4 - addGestureRecognizer(longPress) - - let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) - tap.numberOfTapsRequired = 1 - addGestureRecognizer(tap) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var canBecomeFirstResponder: Bool { true } - - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { -#if canImport(DTCoreText) - switch action { - case #selector(rd_copy(_:)), - #selector(rd_highlight(_:)), - #selector(rd_annotate(_:)): - return overlayView.selectionRange?.length ?? 0 > 0 - default: - return false - } -#else - return super.canPerformAction(action, withSender: sender) -#endif - } - - override func layoutSubviews() { - super.layoutSubviews() - -#if canImport(DTCoreText) - backgroundOverlayView.frame = bounds.inset(by: contentInsets) - coreTextContentView.frame = bounds.inset(by: contentInsets) - updateCoreTextLayoutFrameIfNeeded() -#endif - overlayView.frame = bounds.inset(by: contentInsets) - textView.frame = bounds.inset(by: contentInsets) - coverImageView.frame = bounds.inset(by: contentInsets) - - let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20)) - pageNumberLabel.frame = CGRect( - x: bounds.width - labelSize.width - 24, - y: bounds.height - labelSize.height - 20, - width: labelSize.width, - height: labelSize.height - ) - } - - func configure( - page: RDEPUBTextPage, - pageNumber: Int, - totalPages: Int, - configuration: RDEPUBReaderConfiguration, - highlights: [RDEPUBHighlight] = [], - searchState: RDEPUBSearchState? = nil - ) { - currentPage = page - highlightedRanges = highlights - currentSearchState = searchState - contentInsets = configuration.reflowableContentInsets - backgroundColor = configuration.theme.contentBackgroundColor - pageNumberLabel.textColor = configuration.theme.contentTextColor - pageNumberLabel.text = "\(pageNumber) / \(totalPages)" - - if configureCoverIfNeeded(for: page) { -#if canImport(DTCoreText) - coreTextContentView.isHidden = true - coreTextContentView.layoutFrame = nil - coreTextDisplayContent = nil - coreTextDisplayRange = nil - textView.isHidden = true - textView.isUserInteractionEnabled = false -#endif - textView.attributedText = nil - textView.selectedRange = NSRange(location: 0, length: 0) - delegate?.textContentView(self, didChangeSelection: nil) - setNeedsLayout() - return - } - - coverImageView.isHidden = true - coverImageView.image = nil - - let selectionContent = normalizedPageContent(from: page) - let selectionRange = NSRange(location: 0, length: selectionContent.length) - selectionContent.addAttribute( - .foregroundColor, - value: configuration.theme.contentTextColor, - range: selectionRange - ) - -#if canImport(DTCoreText) - let displayContent = normalizedPageContent(from: page) - let fullRange = NSRange(location: 0, length: displayContent.length) - displayContent.addAttribute( - .foregroundColor, - value: configuration.theme.contentTextColor, - range: fullRange - ) - coreTextContentView.isHidden = false - coreTextContentView.backgroundColor = .clear - coreTextDisplayContent = displayContent - coreTextDisplayRange = NSRange(location: 0, length: displayContent.length) - textView.isHidden = true - textView.isUserInteractionEnabled = false - textView.attributedText = nil - updateCoreTextLayoutFrameIfNeeded() -#else - applyHighlights(to: selectionContent, page: page, contentBaseOffset: page.pageStartOffset) - applySearchHighlights(to: selectionContent, page: page, searchState: searchState, contentBaseOffset: page.pageStartOffset) - textView.isHidden = false - textView.isUserInteractionEnabled = true -#endif - -#if !canImport(DTCoreText) - textView.tintColor = configuration.theme.toolControlTextColor - textView.attributedText = selectionProxyContent(from: selectionContent) - textView.selectedRange = NSRange(location: 0, length: 0) -#endif - - overlayView.configure(page: page, selectionColor: overlayView.selectionColor, snapshot: interactionController.snapshot) -#if canImport(DTCoreText) - backgroundOverlayView.configure(page: page, selectionColor: overlayView.selectionColor, snapshot: interactionController.snapshot) - let (bgDecorations, fgDecorations) = buildOverlayDecorations(page: page) - backgroundOverlayView.applyDecorations(bgDecorations) - overlayView.applyDecorations(fgDecorations) -#endif - - delegate?.textContentView(self, didChangeSelection: nil) - setNeedsLayout() - } - - func clearSelection() { - textView.selectedRange = NSRange(location: 0, length: 0) - overlayView.clearSelection() - backgroundOverlayView.clearSelection() - selectionAnchorPoint = nil - selectionMenuAnchorRect = nil - isSelectionFromInteraction = false - UIMenuController.shared.setMenuVisible(false, animated: true) - delegate?.textContentView(self, didChangeSelection: nil) - } - - // MARK: - Gesture Handling - - @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { - let point = gesture.location(in: overlayView) - - switch gesture.state { - case .began: - selectionAnchorPoint = point - isSelectionFromInteraction = true - handleSelectionFromInteraction(point: point, anchorPoint: nil) - - case .changed: - guard let anchor = selectionAnchorPoint else { return } - handleSelectionFromInteraction(point: point, anchorPoint: anchor) - - case .ended: - isSelectionFromInteraction = false - showSelectionMenuIfNeeded() - - default: - break - } - } - - private func handleSelectionFromInteraction(point: CGPoint, anchorPoint: CGPoint?) { - guard let page = currentPage else { return } - - let range: NSRange? - if let anchor = anchorPoint { - range = interactionController.selectionRange(from: anchor, to: point) - } else if let idx = interactionController.characterIndex(at: point) { - range = NSRange(location: idx, length: 1) - } else { - range = nil - } - - guard let range else { return } - let rects = interactionController.selectionRects(for: range) - overlayView.updateSelection(absoluteRange: range, rects: rects) - selectionMenuAnchorRect = interactionController.menuAnchorRect(for: range) - notifySelectionChange(range: range, page: page) - } - - @objc private func handleTap(_ gesture: UITapGestureRecognizer) { - clearSelection() - } - - @objc private func rd_copy(_ sender: Any?) { - delegate?.textContentView(self, didRequestSelectionAction: .copy) - } - - @objc private func rd_highlight(_ sender: Any?) { - delegate?.textContentView(self, didRequestSelectionAction: .highlight) - } - - @objc private func rd_annotate(_ sender: Any?) { - delegate?.textContentView(self, didRequestSelectionAction: .annotate) - } - - private func notifySelectionChange(range: NSRange, page: RDEPUBTextPage) { - let source = page.chapterContent.string as NSString - let selectedText = source.substring(with: range).trimmingCharacters(in: .whitespacesAndNewlines) - guard !selectedText.isEmpty else { - delegate?.textContentView(self, didChangeSelection: nil) - return - } - - let chapterLength = max(page.chapterContent.length - 1, 1) - let chapterStart = max(range.location, 0) - let chapterEnd = max(chapterStart + range.length - 1, chapterStart) - let selection = RDEPUBSelection( - location: RDEPUBLocation( - href: page.href, - progression: Double(chapterStart) / Double(chapterLength), - lastProgression: Double(chapterEnd) / Double(chapterLength), - fragment: nil - ), - text: selectedText, - rangeInfo: RDEPUBTextOffsetRangeInfo(href: page.href, start: range.location, end: range.location + range.length).jsonString() - ) - delegate?.textContentView(self, didChangeSelection: selection) - } - - private func showSelectionMenuIfNeeded() { -#if canImport(DTCoreText) - guard overlayView.selectionRange?.length ?? 0 > 0, - let anchorRect = selectionMenuAnchorRect ?? overlayView.selectionRange.flatMap({ interactionController.menuAnchorRect(for: $0) }) else { - return - } - - becomeFirstResponder() - let menuRect = overlayView.convert(anchorRect, to: self) - let menuController = UIMenuController.shared - menuController.menuItems = [ - UIMenuItem(title: "拷贝", action: #selector(RDEPUBTextContentView.rd_copy(_:))), - UIMenuItem(title: "高亮", action: #selector(RDEPUBTextContentView.rd_highlight(_:))), - UIMenuItem(title: "批注", action: #selector(RDEPUBTextContentView.rd_annotate(_:))) - ] - menuController.setTargetRect(menuRect, in: self) - menuController.setMenuVisible(true, animated: true) -#endif - } - - private func applyHighlights(to content: NSMutableAttributedString, page: RDEPUBTextPage) { - applyHighlights(to: content, page: page, contentBaseOffset: page.pageStartOffset) - } - - private func applyHighlights( - to content: NSMutableAttributedString, - page: RDEPUBTextPage, - contentBaseOffset: Int - ) { - let pageRange = absoluteOffsetRange(for: page) - let pageStart = pageRange.lowerBound - let pageEndExclusive = pageRange.upperBound - - for highlight in highlightedRanges where highlight.location.href == page.href { - guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } - let overlapStart = max(range.location, pageStart) - let overlapEnd = min(range.location + range.length, pageEndExclusive) - guard overlapStart < overlapEnd else { continue } - - let relativeRange = NSRange( - location: overlapStart - contentBaseOffset, - length: overlapEnd - overlapStart - ) - switch highlight.style { - case .highlight: - content.addAttribute( - .backgroundColor, - value: UIColor(rdHexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45), - range: relativeRange - ) - case .underline: - content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange) - if let color = UIColor(rdHexString: highlight.color, alpha: 1) { - content.addAttribute(.underlineColor, value: color, range: relativeRange) - } - } - } - } - - private func applySearchHighlights( - to content: NSMutableAttributedString, - page: RDEPUBTextPage, - searchState: RDEPUBSearchState? - ) { - applySearchHighlights( - to: content, - page: page, - searchState: searchState, - contentBaseOffset: page.pageStartOffset - ) - } - - private func applySearchHighlights( - to content: NSMutableAttributedString, - page: RDEPUBTextPage, - searchState: RDEPUBSearchState?, - contentBaseOffset: Int - ) { - guard let searchState else { return } - - let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55) - let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75) - let pageRange = absoluteOffsetRange(for: page) - let pageStart = pageRange.lowerBound - let pageEndExclusive = pageRange.upperBound - - for match in searchState.matches where match.href == page.href { - guard let matchStart = match.rangeLocation else { continue } - let matchEnd = matchStart + match.rangeLength - let overlapStart = max(matchStart, pageStart) - let overlapEnd = min(matchEnd, pageEndExclusive) - guard overlapStart < overlapEnd else { continue } - - let relativeRange = NSRange(location: Int(overlapStart - contentBaseOffset), length: Int(overlapEnd - overlapStart)) - let color = match == searchState.currentMatch ? activeColor : normalColor - content.addAttribute(.backgroundColor, value: color, range: relativeRange) - } - } - - private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range { - let lowerBound = page.pageStartOffset - let upperBound = page.pageEndOffset + 1 - return lowerBound.. (background: [RDEPUBTextOverlayDecoration], foreground: [RDEPUBTextOverlayDecoration]) { - var background: [RDEPUBTextOverlayDecoration] = [] - var foreground: [RDEPUBTextOverlayDecoration] = [] - let pageRange = absoluteOffsetRange(for: page) - let pageStart = pageRange.lowerBound - let pageEndExclusive = pageRange.upperBound - - // Search results → background (behind text) - if let searchState = currentSearchState { - let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55) - let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75) - - for match in searchState.matches where match.href == page.href { - guard let matchStart = match.rangeLocation else { continue } - let matchEnd = matchStart + match.rangeLength - let overlapStart = max(matchStart, pageStart) - let overlapEnd = min(matchEnd, pageEndExclusive) - guard overlapStart < overlapEnd else { continue } - - let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) - let rects = interactionController.selectionRects(for: absoluteRange) - guard !rects.isEmpty else { continue } - - let isActive = match == searchState.currentMatch - let kind: RDEPUBTextOverlayDecoration.Kind = isActive ? .activeSearch : .search - let color = isActive ? activeColor : normalColor - background.append(RDEPUBTextOverlayDecoration(kind: kind, absoluteRange: absoluteRange, rects: rects, color: color)) - } - } - - // Highlights → background (filled) or foreground (underline) - for highlight in highlightedRanges where highlight.location.href == page.href { - guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } - let overlapStart = max(range.location, pageStart) - let overlapEnd = min(range.location + range.length, pageEndExclusive) - guard overlapStart < overlapEnd else { continue } - - let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) - let rects = interactionController.selectionRects(for: absoluteRange) - guard !rects.isEmpty else { continue } - - let color = UIColor(rdHexString: highlight.color, alpha: 0.45) - ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45) - let decoration = RDEPUBTextOverlayDecoration( - kind: highlight.style == .underline ? .underline : .highlight, - absoluteRange: absoluteRange, - rects: rects, - color: color - ) - - if decoration.kind == .underline { - foreground.append(decoration) - } else { - background.append(decoration) - } - } - - return (background, foreground) - } -#endif - - private func configureCoverIfNeeded(for page: RDEPUBTextPage) -> Bool { - guard page.pageIndexInChapter == 0, - page.href.lowercased().contains("cover"), - let image = coverImage(from: page.content) else { - return false - } - - coverImageView.image = image - coverImageView.isHidden = false -#if canImport(DTCoreText) - coreTextContentView.isHidden = true - coreTextContentView.layoutFrame = nil - coreTextDisplayContent = nil - coreTextDisplayRange = nil - textView.isHidden = true - textView.isUserInteractionEnabled = false -#endif - textView.attributedText = nil - return true - } - - private func coverImage(from content: NSAttributedString) -> UIImage? { - guard content.length > 0 else { return nil } - var resolvedImage: UIImage? - content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in - guard let image = image(from: value) else { return } - resolvedImage = image - stop.pointee = true - } - return resolvedImage - } - - private func image(from attachmentValue: Any?) -> UIImage? { -#if canImport(DTCoreText) - if let attachment = attachmentValue as? DTTextAttachment, - let url = attachment.contentURL { - return UIImage(contentsOfFile: url.path) - } -#endif - if let attachment = attachmentValue as? NSTextAttachment { - if let image = attachment.image { - return image - } - if let data = attachment.contents { - return UIImage(data: data) - } - if let fileWrapper = attachment.fileWrapper, - let data = fileWrapper.regularFileContents { - return UIImage(data: data) - } - } - return nil - } - - private func selectionProxyContent(from content: NSAttributedString) -> NSAttributedString { - let proxy = NSMutableAttributedString(attributedString: content) - let fullRange = NSRange(location: 0, length: proxy.length) - proxy.removeAttribute(.backgroundColor, range: fullRange) - proxy.addAttribute(.foregroundColor, value: UIColor.clear, range: fullRange) - - var attachmentRanges: [NSRange] = [] - proxy.enumerateAttribute(.attachment, in: fullRange) { value, range, _ in - guard value != nil else { return } - attachmentRanges.append(range) - } - - for range in attachmentRanges.reversed() { - let replacement = NSAttributedString( - string: String(repeating: " ", count: max(range.length, 1)), - attributes: [ - .font: proxy.attribute(.font, at: max(range.location - 1, 0), effectiveRange: nil) as Any, - .foregroundColor: UIColor.clear - ] - ) - proxy.replaceCharacters(in: range, with: replacement) - } - - return proxy - } - - private func normalizedPageContent(from page: RDEPUBTextPage) -> NSMutableAttributedString { - let content = NSMutableAttributedString(attributedString: page.content) - guard shouldNormalizeContinuationParagraph(for: page) else { - return content - } - - let text = content.string as NSString - let firstParagraphRange = text.paragraphRange(for: NSRange(location: 0, length: 0)) - guard firstParagraphRange.length > 0 else { - return content - } - - content.enumerateAttribute(.paragraphStyle, in: firstParagraphRange) { value, range, _ in - guard let style = value as? NSParagraphStyle else { return } - let mutableStyle = (style.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle() - mutableStyle.firstLineHeadIndent = mutableStyle.headIndent - mutableStyle.paragraphSpacingBefore = 0 - content.addAttribute(.paragraphStyle, value: mutableStyle.copy() as Any, range: range) - } - - return content - } - - private func shouldNormalizeContinuationParagraph(for page: RDEPUBTextPage) -> Bool { - let pageStart = page.pageStartOffset - guard pageStart > 0, pageStart < page.chapterContent.length else { - return false - } - - let chapterText = page.chapterContent.string as NSString - guard let previousScalar = UnicodeScalar(chapterText.character(at: pageStart - 1)) else { - return false - } - - return !CharacterSet.newlines.contains(previousScalar) - } - -#if canImport(DTCoreText) - private func updateCoreTextLayoutFrameIfNeeded() { - guard !coreTextContentView.isHidden, - let displayContent = coreTextDisplayContent, - let displayRange = coreTextDisplayRange, - let page = currentPage, - coreTextContentView.bounds.width > 0, - coreTextContentView.bounds.height > 0 else { - interactionController.configure(layoutFrame: nil, page: currentPage) - return - } - - guard let layouter = DTCoreTextLayouter(attributedString: displayContent) else { - coreTextContentView.layoutFrame = nil - interactionController.configure(layoutFrame: nil, page: page) - return - } - layouter.shouldCacheLayoutFrames = false - let layoutFrame = layouter.layoutFrame(with: coreTextContentView.bounds, range: displayRange) - coreTextContentView.layoutFrame = layoutFrame - interactionController.configure(layoutFrame: layoutFrame, page: page) - overlayView.updateSnapshot(interactionController.snapshot) - backgroundOverlayView.updateSnapshot(interactionController.snapshot) - } -#endif - -} - -extension RDEPUBTextContentView: UITextViewDelegate { - func textViewDidChangeSelection(_ textView: UITextView) { - guard !isSelectionFromInteraction else { return } - guard let page = currentPage else { - delegate?.textContentView(self, didChangeSelection: nil) - return - } - - let selectedRange = textView.selectedRange - guard selectedRange.location != NSNotFound, - selectedRange.length > 0, - let attributedText = textView.attributedText else { - delegate?.textContentView(self, didChangeSelection: nil) - return - } - - let source = attributedText.string as NSString - let selectedText = source.substring(with: selectedRange).trimmingCharacters(in: .whitespacesAndNewlines) - guard !selectedText.isEmpty else { - delegate?.textContentView(self, didChangeSelection: nil) - return - } - - let globalStart = page.pageStartOffset + selectedRange.location - let globalEnd = globalStart + selectedRange.length - let totalLength = max(page.chapterContent.length - 1, 1) - let selection = RDEPUBSelection( - location: RDEPUBLocation( - href: page.href, - progression: Double(globalStart) / Double(totalLength), - lastProgression: Double(max(globalEnd - 1, globalStart)) / Double(totalLength), - fragment: nil - ), - text: selectedText, - rangeInfo: RDEPUBTextOffsetRangeInfo(href: page.href, start: globalStart, end: globalEnd).jsonString() - ) - delegate?.textContentView(self, didChangeSelection: selection) - } -} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBViewportTypes.swift b/Sources/RDReaderView/EPUBUI/RDEPUBViewportTypes.swift new file mode 100644 index 0000000..f2a2213 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/RDEPUBViewportTypes.swift @@ -0,0 +1,29 @@ +import UIKit + +/// 视口签名结构体,用于检测视口尺寸或安全区是否发生显著变化 +/// 当变化超过阈值时触发重新分页 +struct RDEPUBViewportSignature: Equatable { + let width: CGFloat + let height: CGFloat + let safeTop: CGFloat + let safeLeft: CGFloat + let safeBottom: CGFloat + let safeRight: CGFloat + + func differsSignificantly(from other: RDEPUBViewportSignature, threshold: CGFloat = 1) -> Bool { + abs(width - other.width) > threshold || + abs(height - other.height) > threshold || + abs(safeTop - other.safeTop) > threshold || + abs(safeLeft - other.safeLeft) > threshold || + abs(safeBottom - other.safeBottom) > threshold || + abs(safeRight - other.safeRight) > threshold + } +} + +/// 视口变化的原因枚举,用于决定延迟处理的策略 +enum RDEPUBViewportChangeReason { + case viewLayout + case orientationTransition +} + +typealias RDEPUBNativeTextSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo]) diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift new file mode 100644 index 0000000..a45d626 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift @@ -0,0 +1,496 @@ +import UIKit + +final class RDEPUBReaderAnnotationCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + private var controller: RDEPUBReaderController? { + context.controller + } + + func bookmark(withID id: String) -> RDEPUBBookmark? { + guard let controller else { return nil } + return controller.activeBookmarks.first { $0.id == id } + } + + func highlight(withID id: String) -> RDEPUBHighlight? { + guard let controller else { return nil } + return controller.activeHighlights.first { $0.id == id } + } + + func updateCurrentSelection(_ selection: RDEPUBSelection?) { + guard let controller else { return } + controller.currentSelection = selection?.isEmpty == false ? selection : nil + controller.bottomToolView.setAddHighlightEnabled( + controller.configuration.allowsHighlights && controller.currentSelection != nil + ) + controller.delegate?.epubReader(controller, didChangeSelection: controller.currentSelection) + } + + @discardableResult + func addHighlight( + from selection: RDEPUBSelection? = nil, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + addAnnotation(from: selection, style: .highlight, color: color, note: note) + } + + @discardableResult + func addAnnotation( + from selection: RDEPUBSelection? = nil, + style: RDEPUBHighlightStyle, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + guard let controller else { return nil } + let sourceSelection = selection ?? controller.currentSelection + guard let sourceSelection, + let scopedSelection = scopedSelection(sourceSelection, relativeToSpineIndex: nil), + !scopedSelection.isEmpty else { + return nil + } + + let newHighlight = RDEPUBHighlight( + bookIdentifier: controller.currentBookIdentifier, + location: scopedSelection.location, + text: scopedSelection.text, + rangeInfo: scopedSelection.rangeInfo, + style: style, + color: color, + note: note + ) + + let isDuplicate = controller.activeHighlights.contains { highlight in + highlight.location.href == newHighlight.location.href && + highlight.location.fragment == newHighlight.location.fragment && + highlight.text == newHighlight.text && + highlight.rangeInfo == newHighlight.rangeInfo && + highlight.style == newHighlight.style + } + guard !isDuplicate else { + return nil + } + + controller.activeHighlights.append(newHighlight) + persistHighlightsAndRefreshContent() + updateCurrentSelection(nil) + return newHighlight + } + + @discardableResult + func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { + guard let controller else { return nil } + guard let scopedHighlight = scopedHighlight(highlight) else { + return nil + } + + if let index = controller.activeHighlights.firstIndex(where: { $0.id == scopedHighlight.id }) { + controller.activeHighlights[index] = scopedHighlight + } else { + controller.activeHighlights.append(scopedHighlight) + } + persistHighlightsAndRefreshContent() + return scopedHighlight + } + + @discardableResult + func removeHighlight(id: String) -> RDEPUBHighlight? { + guard let controller else { return nil } + guard let index = controller.activeHighlights.firstIndex(where: { $0.id == id }) else { + return nil + } + let removed = controller.activeHighlights.remove(at: index) + persistHighlightsAndRefreshContent() + return removed + } + + @discardableResult + func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? { + guard let controller else { return nil } + guard let index = controller.activeHighlights.firstIndex(where: { $0.id == id }) else { + return nil + } + controller.activeHighlights[index].note = normalizedNote(note) + persistHighlightsAndRefreshContent() + return controller.activeHighlights[index] + } + + @discardableResult + func go(toHighlightID id: String, animated: Bool = true) -> Bool { + guard let controller else { return false } + guard let highlight = highlight(withID: id) else { + return false + } + return controller.restoreReadingLocation(highlight.location, animated: animated) + } + + func removeAllHighlights() { + guard let controller else { return } + guard !controller.activeHighlights.isEmpty else { return } + controller.activeHighlights.removeAll() + persistHighlightsAndRefreshContent() + } + + func scopedSelection( + _ selection: RDEPUBSelection, + relativeToSpineIndex spineIndex: Int? + ) -> RDEPUBSelection? { + guard let controller else { return nil } + guard let publication = controller.publication else { return nil } + let normalizedLocation = publication.resourceResolver.normalizedLocation( + selection.location, + relativeToSpineIndex: spineIndex, + bookIdentifier: controller.currentBookIdentifier + ) ?? RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: selection.location.href, + progression: selection.location.progression, + lastProgression: selection.location.lastProgression, + fragment: selection.location.fragment, + rangeAnchor: selection.location.rangeAnchor + ) + return RDEPUBSelection( + bookIdentifier: controller.currentBookIdentifier, + location: normalizedLocation, + text: selection.text, + rangeInfo: selection.rangeInfo, + createdAt: selection.createdAt + ) + } + + func presentHighlightsManager() { + guard let controller else { return } + guard controller.configuration.allowsHighlights else { return } + guard !controller.activeHighlights.isEmpty else { return } + + let highlightsController = RDEPUBReaderHighlightsViewController( + highlights: controller.activeHighlights, + theme: controller.configuration.theme, + sectionTitleProvider: { [weak self] highlight in + self?.titleForHighlight(highlight) + } + ) + highlightsController.onSelectHighlight = { [weak self, weak highlightsController] highlight in + guard let controller = self?.controller else { return } + highlightsController?.dismiss(animated: true) { + controller.go(to: highlight.location) + } + } + highlightsController.onUpdateHighlight = { [weak self] highlight in + _ = self?.controller?.updateHighlightNote(id: highlight.id, note: highlight.note) + } + highlightsController.onDeleteHighlight = { [weak self] highlight in + _ = self?.controller?.removeHighlight(id: highlight.id) + } + + let navigationController = UINavigationController(rootViewController: highlightsController) + navigationController.modalPresentationStyle = .pageSheet + controller.present(navigationController, animated: true) + } + + func presentAnnotationCreation() { + guard let controller else { return } + guard controller.configuration.allowsHighlights, + let currentSelection = controller.currentSelection else { + return + } + presentAnnotationActionSheet(for: currentSelection) + } + + func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) { + guard let selection else { return } + switch action { + case .copy: + UIPasteboard.general.string = selection.text + updateCurrentSelection(nil) + case .highlight: + createAnnotation(from: selection, style: .highlight) + case .annotate: + presentAnnotationNoteEditor(for: selection) + } + } + + @discardableResult + func addBookmark(note: String? = nil) -> RDEPUBBookmark? { + guard let controller else { return nil } + guard let location = scopedBookmarkLocation(controller.currentVisibleLocation()) else { + return nil + } + guard bookmark(matching: location) == nil else { + return nil + } + + let newBookmark = RDEPUBBookmark( + bookIdentifier: controller.currentBookIdentifier, + location: location, + chapterTitle: titleForBookmarkLocation(location), + note: normalizedBookmarkNote(note) + ) + controller.activeBookmarks.append(newBookmark) + persistBookmarksAndRefreshChrome() + return newBookmark + } + + @discardableResult + func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? { + guard let controller else { return nil } + guard let location = scopedBookmarkLocation(controller.currentVisibleLocation()) else { + return nil + } + + if let existingBookmark = bookmark(matching: location) { + _ = removeBookmark(id: existingBookmark.id) + return nil + } + + return addBookmark(note: note) + } + + @discardableResult + func removeBookmark(id: String) -> RDEPUBBookmark? { + guard let controller else { return nil } + guard let index = controller.activeBookmarks.firstIndex(where: { $0.id == id }) else { + return nil + } + let removed = controller.activeBookmarks.remove(at: index) + persistBookmarksAndRefreshChrome() + return removed + } + + @discardableResult + func go(toBookmarkID id: String, animated: Bool = true) -> Bool { + guard let controller else { return false } + guard let bookmark = bookmark(withID: id) else { + return false + } + return controller.restoreReadingLocation(bookmark.location, animated: animated) + } + + func updateBookmarkChrome() { + guard let controller else { return } + controller.topToolView.setBookmarkSelected(currentBookmark() != nil) + controller.bottomToolView.setBookmarksEnabled(!controller.activeBookmarks.isEmpty) + } + + func presentBookmarksManager() { + guard let controller else { return } + guard !controller.activeBookmarks.isEmpty else { return } + + let bookmarksController = RDEPUBReaderBookmarksViewController( + bookmarks: controller.activeBookmarks, + theme: controller.configuration.theme + ) + bookmarksController.onSelectBookmark = { [weak self, weak bookmarksController] bookmark in + guard let controller = self?.controller else { return } + bookmarksController?.dismiss(animated: true) { + _ = controller.restoreReadingLocation(bookmark.location, animated: true) + } + } + bookmarksController.onDeleteBookmark = { [weak self] bookmark in + _ = self?.controller?.removeBookmark(id: bookmark.id) + } + + let navigationController = UINavigationController(rootViewController: bookmarksController) + navigationController.modalPresentationStyle = .pageSheet + controller.present(navigationController, animated: true) + } + + private func scopedHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { + guard let controller else { return nil } + guard let publication = controller.publication else { return nil } + let normalizedLocation = publication.resourceResolver.normalizedLocation( + highlight.location, + relativeToSpineIndex: nil, + bookIdentifier: controller.currentBookIdentifier + ) ?? RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: highlight.location.href, + progression: highlight.location.progression, + lastProgression: highlight.location.lastProgression, + fragment: highlight.location.fragment, + rangeAnchor: highlight.location.rangeAnchor + ) + return RDEPUBHighlight( + id: highlight.id, + bookIdentifier: controller.currentBookIdentifier, + location: normalizedLocation, + text: highlight.text, + rangeInfo: highlight.rangeInfo, + style: highlight.style, + color: highlight.color, + note: highlight.note, + createdAt: highlight.createdAt + ) + } + + private func persistHighlightsAndRefreshContent() { + guard let controller else { return } + guard let currentBookIdentifier = controller.currentBookIdentifier else { return } + controller.persistence?.saveHighlights(controller.activeHighlights, for: currentBookIdentifier) + controller.delegate?.epubReader(controller, didUpdateHighlights: controller.activeHighlights) + controller.updateReaderChrome() + controller.refreshVisibleContentPreservingLocation() + } + + private func presentAnnotationActionSheet(for selection: RDEPUBSelection) { + guard let controller else { return } + let alert = UIAlertController(title: "创建标注", message: nil, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: "高亮", style: .default) { [weak self] _ in + self?.createAnnotation(from: selection, style: .highlight) + }) + alert.addAction(UIAlertAction(title: "划线", style: .default) { [weak self] _ in + self?.createAnnotation(from: selection, style: .underline) + }) + alert.addAction(UIAlertAction(title: "批注", style: .default) { [weak self] _ in + self?.presentAnnotationNoteEditor(for: selection) + }) + alert.addAction(UIAlertAction(title: "取消", style: .cancel)) + + if let popover = alert.popoverPresentationController { + popover.sourceView = controller.bottomToolView + popover.sourceRect = controller.bottomToolView.bounds + } + + controller.present(alert, animated: true) + } + + private func createAnnotation(from selection: RDEPUBSelection, style: RDEPUBHighlightStyle, note: String? = nil) { + _ = addAnnotation(from: selection, style: style, note: note) + } + + private func presentAnnotationNoteEditor(for selection: RDEPUBSelection) { + guard let controller else { return } + let alert = UIAlertController(title: "添加批注", message: selection.text, preferredStyle: .alert) + alert.addTextField { textField in + textField.placeholder = "输入批注内容" + } + alert.addAction(UIAlertAction(title: "取消", style: .cancel)) + alert.addAction(UIAlertAction(title: "保存", style: .default) { [weak self, weak alert] _ in + self?.createAnnotation( + from: selection, + style: .highlight, + note: alert?.textFields?.first?.text + ) + }) + controller.present(alert, animated: true) + } + + private func titleForHighlight(_ highlight: RDEPUBHighlight) -> String? { + guard let controller else { return nil } + guard let publication = controller.publication, + let normalizedHighlightHref = publication.resourceResolver.normalizedHref(highlight.location.href) else { + return nil + } + + return controller.flattenedTableOfContents.first { item in + let rawHref = item.href.components(separatedBy: "#").first ?? item.href + return publication.resourceResolver.normalizedHref(rawHref) == normalizedHighlightHref + }?.title + } + + private func normalizedNote(_ note: String?) -> String? { + let trimmed = note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return trimmed.isEmpty ? nil : trimmed + } + + private func titleForBookmarkLocation(_ location: RDEPUBLocation) -> String? { + guard let controller else { return nil } + if let currentLocation = controller.currentVisibleLocation(), + bookmarkHref(for: currentLocation) == bookmarkHref(for: location) { + return controller.currentTableOfContentsItem?.title + } + + return controller.flattenedTableOfContents.last { item in + bookmarkHref(forTableOfContentsHref: item.href) == bookmarkHref(for: location) + }?.title + } + + private func persistBookmarksAndRefreshChrome() { + guard let controller else { return } + guard let currentBookIdentifier = controller.currentBookIdentifier else { return } + controller.persistence?.saveBookmarks(controller.activeBookmarks, for: currentBookIdentifier) + controller.delegate?.epubReader(controller, didUpdateBookmarks: controller.activeBookmarks) + updateBookmarkChrome() + } + + private func currentBookmark() -> RDEPUBBookmark? { + guard let controller else { return nil } + guard let location = scopedBookmarkLocation(controller.currentVisibleLocation()) else { + return nil + } + return bookmark(matching: location) + } + + private func bookmark(matching location: RDEPUBLocation?) -> RDEPUBBookmark? { + guard let controller else { return nil } + guard let location else { return nil } + return controller.activeBookmarks.first { bookmarkMatchesLocation($0, location: location) } + } + + private func bookmarkMatchesLocation(_ bookmark: RDEPUBBookmark, location: RDEPUBLocation) -> Bool { + guard let controller else { return false } + guard bookmarkHref(for: bookmark.location) == bookmarkHref(for: location) else { + return false + } + + if let bookmarkAnchor = bookmark.location.rangeAnchor, + let locationAnchor = location.rangeAnchor { + return bookmarkAnchor == locationAnchor + } + + if let bookmarkFragment = bookmark.location.fragment, + let locationFragment = location.fragment { + return bookmarkFragment == locationFragment + } + + let progressionDelta = abs(bookmark.location.navigationProgression - location.navigationProgression) + let threshold: Double = controller.publication?.layout == .fixed ? 0.01 : 0.05 + return progressionDelta <= threshold + } + + private func bookmarkHref(for location: RDEPUBLocation) -> String { + controller?.publication?.resourceResolver.normalizedHref(location.href) ?? location.href + } + + private func bookmarkHref(forTableOfContentsHref href: String) -> String { + let rawHref = href.components(separatedBy: "#").first ?? href + return controller?.publication?.resourceResolver.normalizedHref(rawHref) ?? rawHref + } + + private func scopedBookmarkLocation(_ location: RDEPUBLocation?) -> RDEPUBLocation? { + guard let controller else { return nil } + guard let location else { return nil } + guard let publication = controller.publication else { + return RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: location.href, + progression: location.progression, + lastProgression: location.lastProgression, + fragment: location.fragment, + rangeAnchor: location.rangeAnchor + ) + } + + return publication.resourceResolver.normalizedLocation( + location, + relativeToSpineIndex: nil, + bookIdentifier: controller.currentBookIdentifier + ) ?? RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: location.href, + progression: location.progression, + lastProgression: location.lastProgression, + fragment: location.fragment, + rangeAnchor: location.rangeAnchor + ) + } + + private func normalizedBookmarkNote(_ note: String?) -> String? { + let trimmed = note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return trimmed.isEmpty ? nil : trimmed + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAssemblyCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAssemblyCoordinator.swift new file mode 100644 index 0000000..5b005e4 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAssemblyCoordinator.swift @@ -0,0 +1,71 @@ +import UIKit + +final class RDEPUBReaderAssemblyCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + func assembleInterface() { + guard let controller = context.controller, + let readerView = context.readerView else { return } + + controller.view.backgroundColor = context.configuration.theme.contentBackgroundColor + setupReaderView(readerView, in: controller.view) + setupLoadingIndicator(controller.loadingIndicator, in: controller.view) + setupErrorLabel(controller.errorLabel, in: controller.view) + controller.delegate?.epubReader(controller, configureTopToolView: controller.topToolView) + } + + func finishExternalTextBookLaunchIfNeeded() { + guard let runtime = context.runtime, + context.isExternalTextBook else { + return + } + + let restoreLocation = context.currentBookIdentifier.flatMap { context.persistence?.loadLocation(for: $0) } + if let id = context.currentBookIdentifier { + context.activeBookmarks = context.persistence?.loadBookmarks(for: id) ?? [] + context.activeHighlights = context.persistence?.loadHighlights(for: id) ?? [] + } + runtime.finishPagination(restoreLocation: restoreLocation) + } + + private func setupReaderView(_ readerView: RDReaderView, in containerView: UIView) { + readerView.dataSource = context.controller as? RDReaderDataSource + readerView.delegate = context.controller as? RDReaderDelegate + readerView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(readerView) + NSLayoutConstraint.activate([ + readerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + readerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + readerView.topAnchor.constraint(equalTo: containerView.topAnchor), + readerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + readerView.register(contentView: RDEPUBTextContentView.self, contentViewWithReuseIdentifier: NSStringFromClass(RDEPUBTextContentView.self)) + readerView.register(contentView: RDEPUBWebContentView.self, contentViewWithReuseIdentifier: NSStringFromClass(RDEPUBWebContentView.self)) + context.controller?.applyReaderViewConfiguration() + } + + private func setupLoadingIndicator(_ loadingIndicator: UIActivityIndicatorView, in containerView: UIView) { + loadingIndicator.hidesWhenStopped = true + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(loadingIndicator) + NSLayoutConstraint.activate([ + loadingIndicator.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + loadingIndicator.centerYAnchor.constraint(equalTo: containerView.centerYAnchor) + ]) + } + + private func setupErrorLabel(_ errorLabel: UILabel, in containerView: UIView) { + errorLabel.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(errorLabel) + NSLayoutConstraint.activate([ + errorLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 24), + errorLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -24), + errorLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor) + ]) + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift new file mode 100644 index 0000000..6a03147 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift @@ -0,0 +1,168 @@ +import UIKit + +final class RDEPUBReaderChromeCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + private var controller: RDEPUBReaderController? { + context.controller + } + + func makeTopToolView() -> RDEPUBReaderTopToolView { + let toolView = RDEPUBReaderTopToolView() + toolView.onBack = { [weak self] in + print("[Debug] onBack fired, controller: \(String(describing: self?.controller))") + self?.handleBackAction() + } + toolView.onToggleBookmark = { [weak self] in + _ = self?.context.runtime?.toggleBookmark() + } + return toolView + } + + func makeBottomToolView() -> RDEPUBReaderBottomToolView { + let toolView = RDEPUBReaderBottomToolView() + toolView.onShowTableOfContents = { [weak self] in + self?.presentTableOfContents() + } + toolView.onShowBookmarks = { [weak self] in + self?.context.runtime?.presentBookmarksManager() + } + toolView.onShowHighlights = { [weak self] in + self?.context.runtime?.presentHighlightsManager() + } + toolView.onAddHighlight = { [weak self] in + self?.context.runtime?.presentAnnotationCreation() + } + toolView.onShowSettings = { [weak self] in + self?.presentSettings() + } + return toolView + } + + func updateReaderChrome() { + guard let controller else { return } + controller.topToolView.apply(theme: controller.configuration.theme) + controller.topToolView.setTitle( + controller.title + ?? controller.parser?.metadata.title + ?? controller.epubURL.deletingPathExtension().lastPathComponent + ) + controller.topToolView.setBookmarkEnabled(controller.currentBookIdentifier != nil) + controller.bottomToolView.apply(theme: controller.configuration.theme) + controller.bottomToolView.updateVisibility( + showsTableOfContents: controller.configuration.showsTableOfContents, + allowsHighlights: controller.configuration.allowsHighlights, + showsSettingsPanel: controller.configuration.showsSettingsPanel + ) + controller.bottomToolView.setBookmarksEnabled(!controller.activeBookmarks.isEmpty) + controller.bottomToolView.setAddHighlightEnabled( + controller.configuration.allowsHighlights && controller.currentSelection != nil + ) + controller.bottomToolView.setHighlightsEnabled( + controller.configuration.allowsHighlights && !controller.activeHighlights.isEmpty + ) + controller.updateBookmarkChrome() + } + + func presentSettings() { + guard let controller else { return } + guard controller.configuration.showsSettingsPanel else { return } + let settingsController = RDEPUBReaderSettingsViewController( + configuration: controller.configuration, + brightness: controller.currentBrightness + ) + settingsController.onBrightnessChange = { [weak controller] brightness in + controller?.setScreenBrightness(brightness) + } + settingsController.onFontSizeChange = { [weak controller] fontSize in + controller?.updateConfiguration { $0.fontSize = fontSize } + } + settingsController.onLineHeightChange = { [weak controller] lineHeightMultiple in + controller?.updateConfiguration { $0.lineHeightMultiple = lineHeightMultiple } + } + settingsController.onColumnCountChange = { [weak controller] numberOfColumns in + controller?.updateConfiguration { $0.numberOfColumns = numberOfColumns } + } + settingsController.onDisplayTypeChange = { [weak controller] displayType in + controller?.updateConfiguration { $0.displayType = displayType } + } + settingsController.onThemeChange = { [weak controller] theme in + controller?.updateConfiguration { $0.theme = theme } + } + + let navigationController = UINavigationController(rootViewController: settingsController) + navigationController.modalPresentationStyle = .pageSheet + controller.present(navigationController, animated: true) + } + + func presentTableOfContents() { + guard let controller else { return } + guard controller.configuration.showsTableOfContents else { return } + let items = controller.flattenedTableOfContents + guard !items.isEmpty else { return } + + let chapterController = RDEPUBReaderChapterListController( + items: items, + currentItem: controller.currentTableOfContentsItem, + theme: controller.configuration.theme + ) + chapterController.onSelectItem = { [weak controller] item in + guard let controller else { return } + chapterController.dismiss(animated: true) { + _ = controller.go(toTableOfContentsItem: item, animated: true) + } + } + + let navigationController = UINavigationController(rootViewController: chapterController) + navigationController.modalPresentationStyle = .pageSheet + controller.present(navigationController, animated: true) + } + + func handleBackAction() { + guard let controller else { return } + close(controller) + } + + private func close(_ controller: UIViewController) { + let target = closestDismissTarget(from: controller) + if let navigationController = target.navigationController, + navigationController.viewControllers.first !== target { + navigationController.popViewController(animated: true) + return + } + + if let navigationController = target.navigationController, + navigationController.presentingViewController != nil { + navigationController.dismiss(animated: true) + return + } + + if target.presentingViewController != nil { + target.dismiss(animated: true) + return + } + + controller.dismiss(animated: true) + } + + private func closestDismissTarget(from controller: UIViewController) -> UIViewController { + var candidate: UIViewController = controller + var current = controller.parent + while let parent = current { + if let navigationController = parent.navigationController, + navigationController.viewControllers.contains(parent) { + return parent + } + if parent.presentingViewController != nil || parent.navigationController?.presentingViewController != nil { + return parent + } + candidate = parent + current = parent.parent + } + return candidate + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift new file mode 100644 index 0000000..85f946d --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift @@ -0,0 +1,209 @@ +import UIKit + +/// 阅读器共享状态中心:所有 coordinator 通过 context 访问业务状态和便捷方法。 +/// +/// context 持有: +/// - 业务状态(parser、publication、textBook、pages 等) +/// - UI 配置(configuration、brightness) +/// - 持久化策略(persistence) +/// - 便捷方法(renderStyle、layoutConfig 等) +/// - 弱引用 controller(仅用于 UIKit 呈现操作) +final class RDEPUBReaderContext { + // MARK: - 引用 + + weak var controller: RDEPUBReaderController? + weak var readerView: RDReaderView? + var dependencies: RDEPUBReaderDependencies = .live + var runtime: RDEPUBReaderRuntime? { + controller?.runtime + } + + // MARK: - 业务状态 + + var parser: RDEPUBParser? + var publication: RDEPUBPublication? + var readingSession: RDEPUBReadingSession? + var textBook: RDEPUBTextBook? + var activeBookmarks: [RDEPUBBookmark] = [] + var activeHighlights: [RDEPUBHighlight] = [] + var currentBookIdentifier: String? + var paginationToken = UUID() + var paginator: RDEPUBPaginator? + var searchState: RDEPUBSearchState? + var lastTextPaginationPageSize: CGSize? + var currentSelection: RDEPUBSelection? + + // MARK: - 控制器状态(从 controller 下沉) + + var configuration: RDEPUBReaderConfiguration = .default + var persistence: RDEPUBReaderPersistence? + var epubURL: URL = URL(string: "about:blank")! + var isRepaginating: Bool = false + var didStartInitialLoad: Bool = false + var isExternalTextBook: Bool = false + var textFileURL: URL? + var textBookCache = RDEPUBTextBookCache() + + // MARK: - 初始化 + + init(controller: RDEPUBReaderController) { + self.controller = controller + self.readerView = controller.readerView + } + + // MARK: - 便捷方法 + + func currentLayoutContext() -> RDEPUBNavigatorLayoutContext { + let containerSize = readerView?.bounds.size ?? .zero + let viewSize = controller?.view.bounds.size ?? containerSize + let resolvedSize = containerSize == .zero ? viewSize : containerSize + return RDEPUBNavigatorLayoutContext( + containerSize: resolvedSize, + pagesPerScreen: readerView?.pagesPerScreen ?? 1, + safeAreaInsets: controller?.view.safeAreaInsets ?? .zero, + userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone, + reflowableContentInsets: configuration.reflowableContentInsets + ) + } + + func currentPreferences() -> RDEPUBPreferences { + configuration.makePreferences() + } + + func currentTextPageSize() -> CGSize { + let pageNum = (readerView?.currentPage ?? -1) >= 0 ? readerView?.currentPage : nil + if let readerView, let pageNum { + let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) + if resolvedSize.width > 0, resolvedSize.height > 0 { + return resolvedSize + } + } + return currentLayoutContext().viewportSize + } + + func currentTextRenderStyle() -> RDEPUBTextRenderStyle { + let font = UIFont.systemFont(ofSize: configuration.fontSize) + let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4) + return RDEPUBTextRenderStyle( + font: font, + lineSpacing: lineSpacing, + textColor: configuration.theme.contentTextColor, + backgroundColor: configuration.theme.contentBackgroundColor + ) + } + + func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { + return RDEPUBTextLayoutConfig( + frameWidth: max(pageSize.width, 1), + frameHeight: max(pageSize.height, 1), + edgeInsets: configuration.reflowableContentInsets, + numberOfColumns: configuration.numberOfColumns, + columnGap: configuration.columnGap, + avoidOrphans: true, + avoidWidows: true, + avoidPageBreakInsideEnabled: true, + hyphenation: true, + imageMaxHeightRatio: 0.85, + fallbackViewportSize: dependencies.environment.fallbackViewportSize + ) + } + + func resolvedTextRenderer() -> RDEPUBTextRenderer { + dependencies.makeTextRenderer(configuration.textRenderingEngine) + } + + var activePages: [EPUBPage] { + readingSession?.activePages ?? [] + } + + var activeChapters: [EPUBChapterInfo] { + readingSession?.activeChapters ?? [] + } + + var currentBrightness: CGFloat { + get { dependencies.environment.currentBrightness } + set { dependencies.environment.currentBrightness = newValue } + } + + func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) { + readingSession?.setActiveSnapshot(snapshot) + } + + func clearActiveSnapshot() { + readingSession?.resetRuntimeState() + } + + func makeParser() -> RDEPUBParser { + dependencies.makeParser() + } + + func makePaginator() -> RDEPUBPaginator { + dependencies.makePaginator() + } + + func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder { + dependencies.makeTextBookBuilder(resolvedTextRenderer(), textBookCache, layoutConfig) + } + + func makePlainTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder { + dependencies.makePlainTextBookBuilder(resolvedTextRenderer(), layoutConfig) + } + + func currentVisibleLocation() -> RDEPUBLocation? { + controller?.currentVisibleLocation() + } + + func persistenceLocation() -> RDEPUBLocation? { + guard let currentBookIdentifier else { return nil } + return persistence?.loadLocation(for: currentBookIdentifier) + } + + func persist(location: RDEPUBLocation) { + guard let currentBookIdentifier else { return } + persistence?.saveLocation(location, for: currentBookIdentifier) + } + + func textChapterData(forNormalizedHref href: String) -> RDEPUBChapterData? { + guard let textBook, let publication else { return nil } + let normalizedHref = publication.resourceResolver.normalizedHref(href) ?? href + return textBook.chapters.lazy + .first(where: { (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == normalizedHref }) + .flatMap { textBook.chapterData(for: $0.href) } + } + + func showLoading() { + controller?.showLoading() + } + + func hideLoading() { + controller?.hideLoading() + } + + func handle(error: Error) { + controller?.handle(error: error) + } + + func updateReaderChrome() { + controller?.updateReaderChrome() + } + + func refreshVisibleContentPreservingLocation() { + controller?.refreshVisibleContentPreservingLocation() + } + + func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { + controller?.restoreReadingLocation(location, animated: animated) ?? false + } + + func repaginatePreservingCurrentLocation() { + controller?.repaginatePreservingCurrentLocation() + } + + func applyReaderViewConfiguration() { + controller?.applyReaderViewConfiguration() + } + + func updateBookmarkChrome() { + controller?.updateBookmarkChrome() + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderDependencies.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderDependencies.swift new file mode 100644 index 0000000..14b5907 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderDependencies.swift @@ -0,0 +1,64 @@ +import UIKit + +public protocol RDEPUBReaderDisplayEnvironment: AnyObject { + var currentBrightness: CGFloat { get set } + var fallbackViewportSize: CGSize { get } +} + +public final class RDEPUBUIScreenEnvironment: RDEPUBReaderDisplayEnvironment { + public init() {} + + public var currentBrightness: CGFloat { + get { CGFloat(UIScreen.main.brightness) } + set { UIScreen.main.brightness = newValue } + } + + public var fallbackViewportSize: CGSize { + UIScreen.main.bounds.size + } +} + +public struct RDEPUBReaderDependencies { + public var environment: any RDEPUBReaderDisplayEnvironment + public var makeParser: () -> RDEPUBParser + public var makePaginator: () -> RDEPUBPaginator + public var makeTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder + public var makePlainTextBookBuilder: (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder + public var makeTextRenderer: (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer + + public init( + environment: any RDEPUBReaderDisplayEnvironment, + makeParser: @escaping () -> RDEPUBParser, + makePaginator: @escaping () -> RDEPUBPaginator, + makeTextBookBuilder: @escaping (RDEPUBTextRenderer, RDEPUBTextBookCache?, RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder, + makePlainTextBookBuilder: @escaping (RDEPUBTextRenderer, RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder, + makeTextRenderer: @escaping (RDEPUBTextRenderingEngine) -> RDEPUBTextRenderer + ) { + self.environment = environment + self.makeParser = makeParser + self.makePaginator = makePaginator + self.makeTextBookBuilder = makeTextBookBuilder + self.makePlainTextBookBuilder = makePlainTextBookBuilder + self.makeTextRenderer = makeTextRenderer + } + + public static var live: RDEPUBReaderDependencies { + RDEPUBReaderDependencies( + environment: RDEPUBUIScreenEnvironment(), + makeParser: { RDEPUBParser() }, + makePaginator: { RDEPUBPaginator() }, + makeTextBookBuilder: { renderer, cache, layoutConfig in + RDEPUBTextBookBuilder(renderer: renderer, cache: cache, layoutConfig: layoutConfig) + }, + makePlainTextBookBuilder: { renderer, layoutConfig in + RDPlainTextBookBuilder(renderer: renderer, layoutConfig: layoutConfig) + }, + makeTextRenderer: { engine in + switch engine { + case .dtCoreText: + return RDEPUBDTCoreTextRenderer() + } + } + ) + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLoadCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLoadCoordinator.swift new file mode 100644 index 0000000..c29bf15 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLoadCoordinator.swift @@ -0,0 +1,84 @@ +import Foundation + +final class RDEPUBReaderLoadCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + func startInitialLoadIfNeeded() { + guard let controller = context.controller, + let readerView = context.readerView, + !controller.didStartInitialLoad, + readerView.bounds.width > 0, + readerView.bounds.height > 0 else { + return + } + controller.didStartInitialLoad = true + loadPublication() + } + + func loadPublication() { + guard let controller = context.controller else { return } + context.showLoading() + let loadToken = UUID() + context.paginationToken = loadToken + + DispatchQueue.global(qos: .userInitiated).async { [weak controller] in + guard let controller else { return } + let parser = self.context.makeParser() + + do { + try parser.parse(epubURL: controller.epubURL) + let publication = parser.makePublication() + let bookIdentifier = parser.metadata.identifier ?? controller.epubURL.lastPathComponent + let restoreLocation = controller.persistence?.loadLocation(for: bookIdentifier) + let bookmarks = controller.persistence?.loadBookmarks(for: bookIdentifier) ?? [] + let highlights = controller.persistence?.loadHighlights(for: bookIdentifier) ?? [] + + DispatchQueue.main.async { + guard self.context.paginationToken == loadToken else { return } + self.context.runtime?.applyParsedPublication( + parser: parser, + publication: publication, + bookIdentifier: bookIdentifier, + restoreLocation: restoreLocation, + bookmarks: bookmarks, + highlights: highlights + ) + } + } catch { + DispatchQueue.main.async { + guard self.context.paginationToken == loadToken else { return } + self.context.handle(error: error) + } + } + } + } + + func applyParsedPublication( + parser: RDEPUBParser, + publication: RDEPUBPublication, + bookIdentifier: String, + restoreLocation: RDEPUBLocation?, + bookmarks: [RDEPUBBookmark], + highlights: [RDEPUBHighlight] + ) { + guard let controller = context.controller else { return } + context.parser = parser + context.publication = publication + context.currentBookIdentifier = bookIdentifier + context.activeBookmarks = bookmarks + context.activeHighlights = highlights + context.readingSession = RDEPUBReadingSession(publication: publication) + context.readingSession?.transition(to: .loading) + controller.title = parser.metadata.title.isEmpty + ? controller.epubURL.deletingPathExtension().lastPathComponent + : parser.metadata.title + controller.applyReaderViewConfiguration() + context.updateReaderChrome() + controller.delegate?.epubReader(controller, didOpen: publication) + context.runtime?.paginatePublication(restoreLocation: restoreLocation) + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swift new file mode 100644 index 0000000..e622e41 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swift @@ -0,0 +1,60 @@ +import Foundation + +final class RDEPUBReaderLocationCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + @discardableResult + func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { + guard let controller = context.controller, + let readerView = context.readerView else { return false } + guard let targetPageNumber = controller.pageNumber(for: location) else { + readerView.transitionToPage(pageNum: 0) + context.readingSession?.transition(to: .idle) + return false + } + + if context.textBook == nil { + _ = context.readingSession?.queueNavigation( + to: location, + relativeToSpineIndex: nil, + bookIdentifier: context.currentBookIdentifier + ) + } else { + context.readingSession?.transition(to: .jumping) + } + readerView.transitionToPage(pageNum: max(targetPageNumber - 1, 0), animated: animated) + return true + } + + func currentVisibleLocation() -> RDEPUBLocation? { + guard let controller = context.controller, + let readerView = context.readerView else { + return nil + } + if context.textBook != nil, readerView.currentPage >= 0 { + return controller.resolvedTextLocation(forPageNumber: readerView.currentPage + 1) + } + return context.readingSession?.currentReadingLocation(bookIdentifier: context.currentBookIdentifier) + } + + func persistenceLocation() -> RDEPUBLocation? { + guard let controller = context.controller, + let currentBookIdentifier = context.currentBookIdentifier else { + return nil + } + return controller.persistence?.loadLocation(for: currentBookIdentifier) + } + + func persist(location: RDEPUBLocation) { + guard let controller = context.controller, + let currentBookIdentifier = context.currentBookIdentifier else { return } + controller.persistence?.saveLocation(location, for: currentBookIdentifier) + controller.delegate?.epubReader(controller, didUpdateLocation: location) + controller.delegate?.epubReader(controller, didUpdateCurrentTableOfContentsItem: controller.currentTableOfContentsItem) + controller.updateBookmarkChrome() + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift new file mode 100644 index 0000000..7ccd2a1 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift @@ -0,0 +1,156 @@ +import Foundation + +final class RDEPUBReaderPaginationCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + func paginatePublication(restoreLocation: RDEPUBLocation?) { + guard let controller = context.controller, + let parser = context.parser, + let publication = context.publication, + let readingSession = context.readingSession else { + return + } + + controller.isRepaginating = true + controller.errorLabel.isHidden = true + controller.showLoading() + let token = UUID() + context.paginationToken = token + + if publication.readingProfile == .textReflowable { + let pageSize = controller.currentTextPageSize() + context.lastTextPaginationPageSize = pageSize + let layoutConfig = controller.currentTextLayoutConfig(pageSize: pageSize) + let builder = context.makeTextBookBuilder(layoutConfig: layoutConfig) + let renderStyle = controller.currentTextRenderStyle() + + DispatchQueue.global(qos: .userInitiated).async { [weak controller] in + guard let controller else { return } + do { + let textBook = try builder.build( + parser: parser, + publication: publication, + pageSize: pageSize, + style: renderStyle + ) + DispatchQueue.main.async { + guard self.context.paginationToken == token else { return } + self.context.runtime?.applyTextBook(textBook, restoreLocation: restoreLocation) + } + } catch { + DispatchQueue.main.async { + guard self.context.paginationToken == token else { return } + self.context.handle(error: error) + } + } + } + return + } + + if publication.layout == .fixed { + let snapshot = readingSession.makePaginationSnapshot( + pageCounts: Array(repeating: 1, count: publication.spine.count), + preferences: controller.currentPreferences(), + layoutContext: controller.currentLayoutContext() + ) + context.runtime?.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) + return + } + + let paginator = context.makePaginator() + context.paginator = paginator + paginator.calculate( + parser: parser, + hostingView: controller.ensurePaginationHostView(), + presentation: controller.currentPreferences().presentationStyle(viewportSize: controller.currentLayoutContext().viewportSize) + ) { [weak controller] pageCounts in + guard let controller, self.context.paginationToken == token else { return } + let snapshot = readingSession.makePaginationSnapshot( + pageCounts: pageCounts, + preferences: controller.currentPreferences(), + layoutContext: controller.currentLayoutContext() + ) + self.context.paginator = nil + self.context.runtime?.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) + } + } + + func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) { + guard let controller = context.controller else { return } + context.textBook = textBook + let snapshot = controller.nativeTextSnapshot(from: textBook) + context.replaceActiveSnapshot(snapshot) + + guard !textBook.pages.isEmpty else { + context.handle(error: RDEPUBParserError.emptySpine) + return + } + + finishPagination(restoreLocation: restoreLocation) + } + + func applyPaginationSnapshot( + _ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]), + restoreLocation: RDEPUBLocation? + ) { + guard let controller = context.controller else { return } + context.textBook = nil + context.replaceActiveSnapshot(snapshot) + + guard !snapshot.pages.isEmpty else { + context.handle(error: RDEPUBParserError.emptySpine) + return + } + + finishPagination(restoreLocation: restoreLocation) + } + + func finishPagination(restoreLocation: RDEPUBLocation?) { + guard let controller = context.controller, + let readerView = context.readerView else { return } + controller.isRepaginating = false + controller.hideLoading() + readerView.reloadData() + if let targetLocation = restoreLocation { + controller.restoreReadingLocation(targetLocation) + } else { + readerView.transitionToPage(pageNum: 0) + context.readingSession?.transition(to: .idle) + } + + context.runtime?.viewportMonitor.processPendingChangeAfterPagination() + } + + func repaginatePreservingCurrentLocation() { + guard context.publication != nil else { return } + let restoreLocation = context.runtime?.viewportMonitor.consumePendingPresentationRestoreLocation() + ?? context.currentVisibleLocation() + ?? context.persistenceLocation() + paginatePublication(restoreLocation: restoreLocation) + } + + func refreshVisibleContentPreservingLocation() { + guard let readerView = context.readerView else { return } + let restoreLocation = context.currentVisibleLocation() ?? context.persistenceLocation() + readerView.reloadData() + if let restoreLocation { + _ = context.restoreReadingLocation(restoreLocation) + } + } + + func rebuildExternalTextBook() { + guard let controller = context.controller, + let textFileURL = controller.textFileURL else { return } + let restoreLocation = context.currentVisibleLocation() ?? context.persistenceLocation() + let pageSize = controller.currentTextPageSize() + let style = controller.currentTextRenderStyle() + let builder = context.makePlainTextBookBuilder(layoutConfig: controller.currentTextLayoutConfig(pageSize: pageSize)) + if let newBook = try? builder.build(textFileURL: textFileURL, pageSize: pageSize, style: style) { + context.runtime?.applyTextBook(newBook, restoreLocation: restoreLocation) + } + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift new file mode 100644 index 0000000..b5b9d15 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift @@ -0,0 +1,281 @@ +import UIKit + +final class RDEPUBReaderRuntime { + private unowned let context: RDEPUBReaderContext + + lazy var loadCoordinator = RDEPUBReaderLoadCoordinator(context: context) + lazy var paginationCoordinator = RDEPUBReaderPaginationCoordinator(context: context) + lazy var locationCoordinator = RDEPUBReaderLocationCoordinator(context: context) + lazy var searchCoordinator = RDEPUBReaderSearchCoordinator(context: context) + lazy var chromeCoordinator = RDEPUBReaderChromeCoordinator(context: context) + lazy var annotationCoordinator = RDEPUBReaderAnnotationCoordinator(context: context) + lazy var viewportMonitor = RDEPUBReaderViewportMonitor(context: context) + + init(context: RDEPUBReaderContext) { + self.context = context + } + + func makeTopToolView() -> RDEPUBReaderTopToolView { + chromeCoordinator.makeTopToolView() + } + + func makeBottomToolView() -> RDEPUBReaderBottomToolView { + chromeCoordinator.makeBottomToolView() + } + + func startInitialLoadIfNeeded() { + loadCoordinator.startInitialLoadIfNeeded() + } + + func reloadBook() { + guard let readerView = context.readerView else { return } + context.didStartInitialLoad = false + context.parser = nil + context.publication = nil + context.clearActiveSnapshot() + context.readingSession = nil + context.textBook = nil + context.activeBookmarks = [] + context.activeHighlights = [] + context.searchState = nil + viewportMonitor.resetForReload() + annotationCoordinator.updateCurrentSelection(nil) + readerView.reloadData() + startInitialLoadIfNeeded() + } + + func go(to location: RDEPUBLocation, animated: Bool = false) -> Bool { + locationCoordinator.restoreReadingLocation(location, animated: animated) + } + + @discardableResult + func go(toPageNumber pageNumber: Int, animated: Bool = false) -> Bool { + guard let controller = context.controller, + let readerView = context.readerView, + pageNumber > 0 else { + return false + } + + if context.textBook != nil { + guard let location = controller.resolvedTextLocation(forPageNumber: pageNumber) else { + return false + } + return locationCoordinator.restoreReadingLocation(location, animated: animated) + } + + guard context.activePages.indices.contains(pageNumber - 1) else { + return false + } + readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated) + if let location = locationCoordinator.currentVisibleLocation() { + context.persist(location: location) + } + return true + } + + func clearSelection() { + annotationCoordinator.updateCurrentSelection(nil) + } + + func bookmark(withID id: String) -> RDEPUBBookmark? { + annotationCoordinator.bookmark(withID: id) + } + + func highlight(withID id: String) -> RDEPUBHighlight? { + annotationCoordinator.highlight(withID: id) + } + + @discardableResult + func addHighlight( + from selection: RDEPUBSelection? = nil, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + annotationCoordinator.addHighlight(from: selection, color: color, note: note) + } + + @discardableResult + func addAnnotation( + from selection: RDEPUBSelection? = nil, + style: RDEPUBHighlightStyle, + color: String = "#F8E16C", + note: String? = nil + ) -> RDEPUBHighlight? { + annotationCoordinator.addAnnotation(from: selection, style: style, color: color, note: note) + } + + @discardableResult + func upsertHighlight(_ highlight: RDEPUBHighlight) -> RDEPUBHighlight? { + annotationCoordinator.upsertHighlight(highlight) + } + + @discardableResult + func removeHighlight(id: String) -> RDEPUBHighlight? { + annotationCoordinator.removeHighlight(id: id) + } + + @discardableResult + func updateHighlightNote(id: String, note: String?) -> RDEPUBHighlight? { + annotationCoordinator.updateHighlightNote(id: id, note: note) + } + + @discardableResult + func go(toHighlightID id: String, animated: Bool = true) -> Bool { + annotationCoordinator.go(toHighlightID: id, animated: animated) + } + + func removeAllHighlights() { + annotationCoordinator.removeAllHighlights() + } + + @discardableResult + func addBookmark(note: String? = nil) -> RDEPUBBookmark? { + annotationCoordinator.addBookmark(note: note) + } + + @discardableResult + func toggleBookmark(note: String? = nil) -> RDEPUBBookmark? { + annotationCoordinator.toggleBookmark(note: note) + } + + @discardableResult + func removeBookmark(id: String) -> RDEPUBBookmark? { + annotationCoordinator.removeBookmark(id: id) + } + + @discardableResult + func go(toBookmarkID id: String, animated: Bool = true) -> Bool { + annotationCoordinator.go(toBookmarkID: id, animated: animated) + } + + func updateBookmarkChrome() { + annotationCoordinator.updateBookmarkChrome() + } + + func presentBookmarksManager() { + annotationCoordinator.presentBookmarksManager() + } + + func presentHighlightsManager() { + annotationCoordinator.presentHighlightsManager() + } + + func presentAnnotationCreation() { + annotationCoordinator.presentAnnotationCreation() + } + + func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction, selection: RDEPUBSelection?) { + annotationCoordinator.handleSelectionMenuAction(action, selection: selection) + } + + func search(keyword: String) { + searchCoordinator.search(keyword: keyword) + } + + @discardableResult + func searchNext() -> Bool { + searchCoordinator.searchNext() + } + + @discardableResult + func searchPrevious() -> Bool { + searchCoordinator.searchPrevious() + } + + func clearSearch() { + searchCoordinator.clearSearch() + } + + func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? { + searchCoordinator.searchPresentation(for: page) + } + + func updateReaderChrome() { + chromeCoordinator.updateReaderChrome() + } + + func presentSettings() { + chromeCoordinator.presentSettings() + } + + func presentTableOfContents() { + chromeCoordinator.presentTableOfContents() + } + + func handleBackAction() { + chromeCoordinator.handleBackAction() + } + + func loadPublication() { + loadCoordinator.loadPublication() + } + + func applyParsedPublication( + parser: RDEPUBParser, + publication: RDEPUBPublication, + bookIdentifier: String, + restoreLocation: RDEPUBLocation?, + bookmarks: [RDEPUBBookmark], + highlights: [RDEPUBHighlight] + ) { + loadCoordinator.applyParsedPublication( + parser: parser, + publication: publication, + bookIdentifier: bookIdentifier, + restoreLocation: restoreLocation, + bookmarks: bookmarks, + highlights: highlights + ) + } + + func paginatePublication(restoreLocation: RDEPUBLocation?) { + paginationCoordinator.paginatePublication(restoreLocation: restoreLocation) + } + + func applyTextBook(_ textBook: RDEPUBTextBook, restoreLocation: RDEPUBLocation?) { + paginationCoordinator.applyTextBook(textBook, restoreLocation: restoreLocation) + } + + func applyPaginationSnapshot( + _ snapshot: (pages: [EPUBPage], chapters: [EPUBChapterInfo]), + restoreLocation: RDEPUBLocation? + ) { + paginationCoordinator.applyPaginationSnapshot(snapshot, restoreLocation: restoreLocation) + } + + func finishPagination(restoreLocation: RDEPUBLocation?) { + paginationCoordinator.finishPagination(restoreLocation: restoreLocation) + } + + func repaginatePreservingCurrentLocation() { + paginationCoordinator.repaginatePreservingCurrentLocation() + } + + func refreshVisibleContentPreservingLocation() { + paginationCoordinator.refreshVisibleContentPreservingLocation() + } + + func rebuildExternalTextBook() { + paginationCoordinator.rebuildExternalTextBook() + } + + @discardableResult + func restoreReadingLocation(_ location: RDEPUBLocation, animated: Bool = false) -> Bool { + locationCoordinator.restoreReadingLocation(location, animated: animated) + } + + func currentVisibleLocation() -> RDEPUBLocation? { + locationCoordinator.currentVisibleLocation() + } + + func currentViewportSignature() -> RDEPUBViewportSignature? { + viewportMonitor.currentViewportSignature() + } + + func handleViewportChangeIfNeeded( + reason: RDEPUBViewportChangeReason, + viewportSignature: RDEPUBViewportSignature? = nil + ) { + viewportMonitor.handleViewportChangeIfNeeded(reason: reason, viewportSignature: viewportSignature) + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderSearchCoordinator.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderSearchCoordinator.swift new file mode 100644 index 0000000..ac1ecd8 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderSearchCoordinator.swift @@ -0,0 +1,183 @@ +import Foundation + +final class RDEPUBReaderSearchCoordinator { + private unowned let context: RDEPUBReaderContext + + init(context: RDEPUBReaderContext) { + self.context = context + } + + private var controller: RDEPUBReaderController? { + context.controller + } + + func search(keyword: String) { + guard let controller else { return } + let normalizedKeyword = keyword.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalizedKeyword.isEmpty else { + clearSearch() + return + } + + let matches = resolvedSearchMatches(for: normalizedKeyword) + controller.searchState = RDEPUBSearchState( + keyword: normalizedKeyword, + matches: matches, + currentMatchIndex: matches.isEmpty ? nil : 0 + ) + notifySearchStateChanged() + + if matches.isEmpty { + controller.refreshVisibleContentPreservingLocation() + } else { + _ = navigateToCurrentSearchMatch(animated: false) + } + } + + @discardableResult + func searchNext() -> Bool { + advanceSearch(by: 1) + } + + @discardableResult + func searchPrevious() -> Bool { + advanceSearch(by: -1) + } + + func clearSearch() { + guard let controller else { return } + controller.searchState = nil + notifySearchStateChanged() + controller.refreshVisibleContentPreservingLocation() + } + + func searchPresentation(for page: EPUBPage) -> RDEPUBSearchPresentation? { + guard let controller else { return nil } + guard let searchState = controller.searchState, + let publication = controller.publication else { + return nil + } + + let pageHrefs: [String] + if let fixedSpread = page.fixedSpread { + pageHrefs = fixedSpread.resources.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href } + } else if publication.spine.indices.contains(page.spineIndex) { + pageHrefs = [ + publication.resourceResolver.normalizedHref(publication.spine[page.spineIndex].href) + ?? publication.spine[page.spineIndex].href + ] + } else { + pageHrefs = [] + } + + guard !pageHrefs.isEmpty else { + return nil + } + + let currentMatch = searchState.currentMatch + let normalizedCurrentHref = currentMatch.map { publication.resourceResolver.normalizedHref($0.href) ?? $0.href } + let resources = pageHrefs.map { href in + let matchCount = searchState.matches.filter { + (publication.resourceResolver.normalizedHref($0.href) ?? $0.href) == href + }.count + let activeLocalMatchIndex = normalizedCurrentHref == href ? currentMatch?.localMatchIndex : nil + return RDEPUBSearchPresentationResource( + href: href, + matchCount: matchCount, + activeLocalMatchIndex: activeLocalMatchIndex + ) + } + return RDEPUBSearchPresentation(keyword: searchState.keyword, resources: resources) + } + + private func resolvedSearchMatches(for keyword: String) -> [RDEPUBSearchMatch] { + guard let controller else { return [] } + if let textBook = controller.textBook, let publication = controller.publication { + return RDEPUBTextSearchEngine(textBook: textBook, publication: publication).search(keyword: keyword) + } + if let parser = controller.parser, let publication = controller.publication { + return RDEPUBHTMLSearchEngine(parser: parser, publication: publication).search(keyword: keyword) + } + return [] + } + + private func advanceSearch(by delta: Int) -> Bool { + guard let controller else { return false } + guard var searchState = controller.searchState, !searchState.matches.isEmpty else { + return false + } + + let currentIndex = searchState.currentMatchIndex ?? 0 + let nextIndex = (currentIndex + delta + searchState.matches.count) % searchState.matches.count + searchState.currentMatchIndex = nextIndex + controller.searchState = searchState + notifySearchStateChanged() + return navigateToCurrentSearchMatch(animated: true) + } + + private func notifySearchStateChanged() { + guard let controller else { return } + controller.delegate?.epubReader(controller, didUpdateSearchResult: controller.searchState?.result) + controller.delegate?.epubReader(controller, didChangeCurrentSearchMatch: controller.searchState?.currentMatch) + } + + private func navigateToCurrentSearchMatch(animated: Bool) -> Bool { + guard let controller else { return false } + guard let searchMatch = controller.searchState?.currentMatch else { + controller.refreshVisibleContentPreservingLocation() + return false + } + + if let targetPageNumber = pageNumber(for: searchMatch), + controller.readerView.currentPage == targetPageNumber - 1 { + controller.refreshVisibleContentPreservingLocation() + return true + } + + let location = RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: searchMatch.href, + progression: searchMatch.progression, + lastProgression: searchMatch.progression, + fragment: nil, + rangeAnchor: searchMatch.rangeAnchor + ) + return controller.restoreReadingLocation(location, animated: animated) + } + + private func pageNumber(for searchMatch: RDEPUBSearchMatch) -> Int? { + guard let controller else { return nil } + if let chapterData = controller.textChapterData(forNormalizedHref: searchMatch.href) { + if let pageNumber = chapterData.pageNumber(for: searchMatch) { + return pageNumber + } + + if let rangeLocation = searchMatch.rangeLocation, + let page = chapterData.page(containing: rangeLocation) { + return page.absolutePageIndex + 1 + } + } + + let location = RDEPUBLocation( + bookIdentifier: controller.currentBookIdentifier, + href: searchMatch.href, + progression: searchMatch.progression, + lastProgression: searchMatch.progression, + fragment: nil, + rangeAnchor: searchMatch.rangeAnchor + ) + + if let textBook = controller.textBook, let publication = controller.publication { + return textBook.pageNumber( + for: location, + resolver: publication.resourceResolver, + bookIdentifier: controller.currentBookIdentifier + ) + } + + return controller.readingSession?.pageIndex( + for: location, + bookIdentifier: controller.currentBookIdentifier + ).map { $0 + 1 } + } +} diff --git a/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderViewportMonitor.swift b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderViewportMonitor.swift new file mode 100644 index 0000000..9433176 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderViewportMonitor.swift @@ -0,0 +1,125 @@ +import UIKit + +final class RDEPUBReaderViewportMonitor { + private unowned let context: RDEPUBReaderContext + + private var lastAppliedViewportSignature: RDEPUBViewportSignature? + private var pendingViewportChangeReason: RDEPUBViewportChangeReason? + private var pendingPresentationRestoreLocation: RDEPUBLocation? + private var isWaitingForViewportTransitionCompletion = false + + init(context: RDEPUBReaderContext) { + self.context = context + } + + private var controller: RDEPUBReaderController? { + context.controller + } + + func viewDidLayoutSubviews() { + guard let controller else { return } + guard let viewportSignature = currentViewportSignature() else { return } + + if !controller.didStartInitialLoad { + lastAppliedViewportSignature = viewportSignature + controller.startInitialLoadIfNeeded() + return + } + + guard controller.publication != nil || controller.isExternalTextBook else { + lastAppliedViewportSignature = viewportSignature + return + } + + guard !isWaitingForViewportTransitionCompletion else { + return + } + + handleViewportChangeIfNeeded(reason: .viewLayout, viewportSignature: viewportSignature) + } + + func viewWillTransition(with coordinator: UIViewControllerTransitionCoordinator) { + guard let controller else { return } + guard controller.didStartInitialLoad else { return } + pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation() + isWaitingForViewportTransitionCompletion = true + + coordinator.animate(alongsideTransition: nil) { [weak self] _ in + guard let self, let controller = self.controller else { return } + self.isWaitingForViewportTransitionCompletion = false + controller.view.layoutIfNeeded() + self.handleViewportChangeIfNeeded(reason: .orientationTransition) + } + } + + func resetForReload() { + lastAppliedViewportSignature = currentViewportSignature() + pendingViewportChangeReason = nil + pendingPresentationRestoreLocation = nil + isWaitingForViewportTransitionCompletion = false + } + + func consumePendingPresentationRestoreLocation() -> RDEPUBLocation? { + defer { pendingPresentationRestoreLocation = nil } + return pendingPresentationRestoreLocation + } + + func capturePendingPresentationRestoreLocation() { + guard let controller else { return } + pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation() + } + + func processPendingChangeAfterPagination() { + guard let pendingReason = pendingViewportChangeReason else { return } + pendingViewportChangeReason = nil + DispatchQueue.main.async { [weak self] in + self?.handleViewportChangeIfNeeded(reason: pendingReason) + } + } + + func currentViewportSignature() -> RDEPUBViewportSignature? { + guard let controller else { return nil } + let containerSize = controller.readerView.bounds.size == .zero ? controller.view.bounds.size : controller.readerView.bounds.size + guard containerSize.width > 0, containerSize.height > 0 else { return nil } + let insets = controller.view.safeAreaInsets + return RDEPUBViewportSignature( + width: containerSize.width, + height: containerSize.height, + safeTop: insets.top, + safeLeft: insets.left, + safeBottom: insets.bottom, + safeRight: insets.right + ) + } + + func handleViewportChangeIfNeeded( + reason: RDEPUBViewportChangeReason, + viewportSignature: RDEPUBViewportSignature? = nil + ) { + guard let controller else { return } + guard controller.didStartInitialLoad, + let signature = viewportSignature ?? currentViewportSignature() else { + return + } + + if controller.isRepaginating { + pendingViewportChangeReason = reason + return + } + + if let lastAppliedViewportSignature, + !signature.differsSignificantly(from: lastAppliedViewportSignature) { + return + } + + lastAppliedViewportSignature = signature + + if controller.isExternalTextBook { + controller.rebuildExternalTextBook() + return + } + + guard controller.publication != nil else { return } + controller.repaginatePreservingCurrentLocation() + } +} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderConfiguration.swift b/Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBReaderConfiguration.swift rename to Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderSettings.swift b/Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderSettings.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBReaderSettings.swift rename to Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderSettings.swift diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderSettingsViewController.swift b/Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderSettingsViewController.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBReaderSettingsViewController.swift rename to Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderSettingsViewController.swift diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderTheme.swift b/Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderTheme.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBReaderTheme.swift rename to Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderTheme.swift diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBPageInteractionController.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBPageInteractionController.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBPageInteractionController.swift rename to Sources/RDReaderView/EPUBUI/TextPage/RDEPUBPageInteractionController.swift diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBPageLayoutSnapshot.swift similarity index 100% rename from Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift rename to Sources/RDReaderView/EPUBUI/TextPage/RDEPUBPageLayoutSnapshot.swift diff --git a/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectableTextView.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectableTextView.swift new file mode 100644 index 0000000..9172002 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectableTextView.swift @@ -0,0 +1,30 @@ +import UIKit + +/// 自定义 UITextView,替换系统默认的 UIMenuItem 为自定义操作(拷贝、高亮、批注) +final class RDEPUBSelectableTextView: UITextView { + /// 选择菜单操作回调 + var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)? + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + switch action { + case #selector(rd_copy(_:)), + #selector(rd_highlight(_:)), + #selector(rd_annotate(_:)): + return selectedRange.location != NSNotFound && selectedRange.length > 0 + default: + return false + } + } + + @objc func rd_copy(_ sender: Any?) { + onSelectionAction?(.copy) + } + + @objc func rd_highlight(_ sender: Any?) { + onSelectionAction?(.highlight) + } + + @objc func rd_annotate(_ sender: Any?) { + onSelectionAction?(.annotate) + } +} diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBSelectionOverlayView.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectionOverlayView.swift similarity index 99% rename from Sources/RDReaderView/EPUBUI/RDEPUBSelectionOverlayView.swift rename to Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectionOverlayView.swift index b8ab531..5adfe85 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBSelectionOverlayView.swift +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBSelectionOverlayView.swift @@ -36,7 +36,7 @@ struct RDEPUBTextOverlayDecoration { /// 文本选择和装饰的覆盖层绘制视图 /// 位于文本内容上方,负责绘制选区、高亮、搜索结果等视觉效果 /// 使用 Core Graphics 直接绘制,支持填充矩形和下划线两种绘制模式 -final class RDEPUBSelectionOverlayView: UIView { +class RDEPUBSelectionOverlayView: UIView { private(set) var page: RDEPUBTextPage? private var snapshot: RDEPUBPageLayoutSnapshot? private(set) var selectionRange: NSRange? diff --git a/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextAnnotationOverlay.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextAnnotationOverlay.swift new file mode 100644 index 0000000..0b51ca0 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextAnnotationOverlay.swift @@ -0,0 +1,136 @@ +import UIKit + +/// 前景覆盖层,负责绘制高亮、搜索命中和当前选区。 +final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView { + func applyHighlights( + _ highlights: [RDEPUBHighlight], + to content: NSMutableAttributedString, + page: RDEPUBTextPage, + contentBaseOffset: Int + ) { + let pageRange = absoluteOffsetRange(for: page) + let pageStart = pageRange.lowerBound + let pageEndExclusive = pageRange.upperBound + + for highlight in highlights where highlight.location.href == page.href { + guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } + let overlapStart = max(range.location, pageStart) + let overlapEnd = min(range.location + range.length, pageEndExclusive) + guard overlapStart < overlapEnd else { continue } + + let relativeRange = NSRange( + location: overlapStart - contentBaseOffset, + length: overlapEnd - overlapStart + ) + switch highlight.style { + case .highlight: + content.addAttribute( + .backgroundColor, + value: UIColor(rdHexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45), + range: relativeRange + ) + case .underline: + content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange) + if let color = UIColor(rdHexString: highlight.color, alpha: 1) { + content.addAttribute(.underlineColor, value: color, range: relativeRange) + } + } + } + } + + func applySearchHighlights( + to content: NSMutableAttributedString, + page: RDEPUBTextPage, + searchState: RDEPUBSearchState?, + contentBaseOffset: Int + ) { + guard let searchState else { return } + + let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55) + let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75) + let pageRange = absoluteOffsetRange(for: page) + let pageStart = pageRange.lowerBound + let pageEndExclusive = pageRange.upperBound + + for match in searchState.matches where match.href == page.href { + guard let matchStart = match.rangeLocation else { continue } + let matchEnd = matchStart + match.rangeLength + let overlapStart = max(matchStart, pageStart) + let overlapEnd = min(matchEnd, pageEndExclusive) + guard overlapStart < overlapEnd else { continue } + + let relativeRange = NSRange(location: Int(overlapStart - contentBaseOffset), length: Int(overlapEnd - overlapStart)) + let color = match == searchState.currentMatch ? activeColor : normalColor + content.addAttribute(.backgroundColor, value: color, range: relativeRange) + } + } + + func buildDecorations( + page: RDEPUBTextPage, + highlights: [RDEPUBHighlight], + searchState: RDEPUBSearchState?, + interactionController: RDEPUBPageInteractionController + ) -> (background: [RDEPUBTextOverlayDecoration], foreground: [RDEPUBTextOverlayDecoration]) { + var background: [RDEPUBTextOverlayDecoration] = [] + var foreground: [RDEPUBTextOverlayDecoration] = [] + let pageRange = absoluteOffsetRange(for: page) + let pageStart = pageRange.lowerBound + let pageEndExclusive = pageRange.upperBound + + if let searchState { + let normalColor = UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.55) + let activeColor = UIColor(red: 255 / 255, green: 159 / 255, blue: 67 / 255, alpha: 0.75) + + for match in searchState.matches where match.href == page.href { + guard let matchStart = match.rangeLocation else { continue } + let matchEnd = matchStart + match.rangeLength + let overlapStart = max(matchStart, pageStart) + let overlapEnd = min(matchEnd, pageEndExclusive) + guard overlapStart < overlapEnd else { continue } + + let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) + let rects = interactionController.selectionRects(for: absoluteRange) + guard !rects.isEmpty else { continue } + + let isActive = match == searchState.currentMatch + let kind: RDEPUBTextOverlayDecoration.Kind = isActive ? .activeSearch : .search + let color = isActive ? activeColor : normalColor + background.append(RDEPUBTextOverlayDecoration(kind: kind, absoluteRange: absoluteRange, rects: rects, color: color)) + } + } + + for highlight in highlights where highlight.location.href == page.href { + guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } + let overlapStart = max(range.location, pageStart) + let overlapEnd = min(range.location + range.length, pageEndExclusive) + guard overlapStart < overlapEnd else { continue } + + let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) + let rects = interactionController.selectionRects(for: absoluteRange) + guard !rects.isEmpty else { continue } + + let color = UIColor(rdHexString: highlight.color, alpha: 0.45) + ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45) + let decoration = RDEPUBTextOverlayDecoration( + kind: highlight.style == .underline ? .underline : .highlight, + absoluteRange: absoluteRange, + rects: rects, + color: color + ) + + if decoration.kind == .underline { + foreground.append(decoration) + } else { + background.append(decoration) + } + } + + return (background, foreground) + } + + private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range { + let lowerBound = page.pageStartOffset + let upperBound = page.pageEndOffset + 1 + return lowerBound.. Bool { +#if canImport(DTCoreText) + switch action { + case #selector(rd_copy(_:)), + #selector(rd_highlight(_:)), + #selector(rd_annotate(_:)): + return selectionController.canPerformSelectionAction(in: overlayView) + default: + return false + } +#else + return super.canPerformAction(action, withSender: sender) +#endif + } + + override func layoutSubviews() { + super.layoutSubviews() + +#if canImport(DTCoreText) + backgroundOverlayView.frame = bounds.inset(by: contentInsets) + coreTextContentView.frame = bounds.inset(by: contentInsets) + updateCoreTextLayoutFrameIfNeeded() +#endif + overlayView.frame = bounds.inset(by: contentInsets) + textView.frame = bounds.inset(by: contentInsets) + coverImageView.frame = bounds.inset(by: contentInsets) + + let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20)) + pageNumberLabel.frame = CGRect( + x: bounds.width - labelSize.width - 24, + y: bounds.height - labelSize.height - 20, + width: labelSize.width, + height: labelSize.height + ) + } + + func configure( + page: RDEPUBTextPage, + pageNumber: Int, + totalPages: Int, + configuration: RDEPUBReaderConfiguration, + highlights: [RDEPUBHighlight] = [], + searchState: RDEPUBSearchState? = nil + ) { + currentPage = page + contentInsets = configuration.reflowableContentInsets + backgroundColor = configuration.theme.contentBackgroundColor + pageNumberLabel.textColor = configuration.theme.contentTextColor + pageNumberLabel.text = "\(pageNumber) / \(totalPages)" + + if configureCoverIfNeeded(for: page) { +#if canImport(DTCoreText) + coreTextContentView.isHidden = true + coreTextContentView.layoutFrame = nil + coreTextDisplayContent = nil + coreTextDisplayRange = nil + textView.isHidden = true + textView.isUserInteractionEnabled = false +#endif + textView.attributedText = nil + textView.selectedRange = NSRange(location: 0, length: 0) + delegate?.textContentView(self, didChangeSelection: nil) + setNeedsLayout() + return + } + + coverImageView.isHidden = true + coverImageView.image = nil + + let selectionContent = normalizedPageContent(from: page) + let selectionRange = NSRange(location: 0, length: selectionContent.length) + selectionContent.addAttribute( + .foregroundColor, + value: configuration.theme.contentTextColor, + range: selectionRange + ) + +#if canImport(DTCoreText) + let displayContent = normalizedPageContent(from: page) + let fullRange = NSRange(location: 0, length: displayContent.length) + displayContent.addAttribute( + .foregroundColor, + value: configuration.theme.contentTextColor, + range: fullRange + ) + coreTextContentView.isHidden = false + coreTextContentView.backgroundColor = .clear + coreTextDisplayContent = displayContent + coreTextDisplayRange = NSRange(location: 0, length: displayContent.length) + textView.isHidden = true + textView.isUserInteractionEnabled = false + textView.attributedText = nil + updateCoreTextLayoutFrameIfNeeded() +#else + overlayView.applyHighlights(highlights, to: selectionContent, page: page, contentBaseOffset: page.pageStartOffset) + overlayView.applySearchHighlights(to: selectionContent, page: page, searchState: searchState, contentBaseOffset: page.pageStartOffset) + textView.isHidden = false + textView.isUserInteractionEnabled = true +#endif + +#if !canImport(DTCoreText) + textView.tintColor = configuration.theme.toolControlTextColor + textView.attributedText = selectionProxyContent(from: selectionContent) + textView.selectedRange = NSRange(location: 0, length: 0) +#endif + + overlayView.configure(page: page, selectionColor: overlayView.selectionColor, snapshot: interactionController.snapshot) +#if canImport(DTCoreText) + backgroundOverlayView.configure(page: page, selectionColor: overlayView.selectionColor, snapshot: interactionController.snapshot) + let (bgDecorations, fgDecorations) = overlayView.buildDecorations( + page: page, + highlights: highlights, + searchState: searchState, + interactionController: interactionController + ) + backgroundOverlayView.applyDecorations(bgDecorations) + overlayView.applyDecorations(fgDecorations) +#endif + + delegate?.textContentView(self, didChangeSelection: nil) + setNeedsLayout() + } + + func clearSelection() { + selectionController.clearSelection( + textView: textView, + overlayView: overlayView, + backgroundOverlayView: backgroundOverlayView + ) + } + + // MARK: - Gesture Handling + + @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) { + selectionController.handleLongPress( + gesture, + page: currentPage, + overlayView: overlayView, + interactionController: interactionController + ) + + if gesture.state == .ended { + showSelectionMenuIfNeeded() + } + } + + @objc private func handleTap(_ gesture: UITapGestureRecognizer) { + selectionController.handleTap( + textView: textView, + overlayView: overlayView, + backgroundOverlayView: backgroundOverlayView + ) + } + + @objc private func rd_copy(_ sender: Any?) { + delegate?.textContentView(self, didRequestSelectionAction: .copy) + } + + @objc private func rd_highlight(_ sender: Any?) { + delegate?.textContentView(self, didRequestSelectionAction: .highlight) + } + + @objc private func rd_annotate(_ sender: Any?) { + delegate?.textContentView(self, didRequestSelectionAction: .annotate) + } + + private func showSelectionMenuIfNeeded() { +#if canImport(DTCoreText) + selectionController.showSelectionMenuIfNeeded( + in: self, + overlayView: overlayView, + interactionController: interactionController, + copyAction: #selector(RDEPUBTextContentView.rd_copy(_:)), + highlightAction: #selector(RDEPUBTextContentView.rd_highlight(_:)), + annotateAction: #selector(RDEPUBTextContentView.rd_annotate(_:)) + ) +#endif + } + + private func configureCoverIfNeeded(for page: RDEPUBTextPage) -> Bool { + guard page.pageIndexInChapter == 0, + page.href.lowercased().contains("cover"), + let image = coverImage(from: page.content) else { + return false + } + + coverImageView.image = image + coverImageView.isHidden = false +#if canImport(DTCoreText) + coreTextContentView.isHidden = true + coreTextContentView.layoutFrame = nil + coreTextDisplayContent = nil + coreTextDisplayRange = nil + textView.isHidden = true + textView.isUserInteractionEnabled = false +#endif + textView.attributedText = nil + return true + } + + private func coverImage(from content: NSAttributedString) -> UIImage? { + guard content.length > 0 else { return nil } + var resolvedImage: UIImage? + content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in + guard let image = image(from: value) else { return } + resolvedImage = image + stop.pointee = true + } + return resolvedImage + } + + private func image(from attachmentValue: Any?) -> UIImage? { +#if canImport(DTCoreText) + if let attachment = attachmentValue as? DTTextAttachment, + let url = attachment.contentURL { + return UIImage(contentsOfFile: url.path) + } +#endif + if let attachment = attachmentValue as? NSTextAttachment { + if let image = attachment.image { + return image + } + if let data = attachment.contents { + return UIImage(data: data) + } + if let fileWrapper = attachment.fileWrapper, + let data = fileWrapper.regularFileContents { + return UIImage(data: data) + } + } + return nil + } + + private func selectionProxyContent(from content: NSAttributedString) -> NSAttributedString { + let proxy = NSMutableAttributedString(attributedString: content) + let fullRange = NSRange(location: 0, length: proxy.length) + proxy.removeAttribute(.backgroundColor, range: fullRange) + proxy.addAttribute(.foregroundColor, value: UIColor.clear, range: fullRange) + + var attachmentRanges: [NSRange] = [] + proxy.enumerateAttribute(.attachment, in: fullRange) { value, range, _ in + guard value != nil else { return } + attachmentRanges.append(range) + } + + for range in attachmentRanges.reversed() { + let replacement = NSAttributedString( + string: String(repeating: " ", count: max(range.length, 1)), + attributes: [ + .font: proxy.attribute(.font, at: max(range.location - 1, 0), effectiveRange: nil) as Any, + .foregroundColor: UIColor.clear + ] + ) + proxy.replaceCharacters(in: range, with: replacement) + } + + return proxy + } + + private func normalizedPageContent(from page: RDEPUBTextPage) -> NSMutableAttributedString { + let content = NSMutableAttributedString(attributedString: page.content) + guard shouldNormalizeContinuationParagraph(for: page) else { + return content + } + + let text = content.string as NSString + let firstParagraphRange = text.paragraphRange(for: NSRange(location: 0, length: 0)) + guard firstParagraphRange.length > 0 else { + return content + } + + content.enumerateAttribute(.paragraphStyle, in: firstParagraphRange) { value, range, _ in + guard let style = value as? NSParagraphStyle else { return } + let mutableStyle = (style.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle() + mutableStyle.firstLineHeadIndent = mutableStyle.headIndent + mutableStyle.paragraphSpacingBefore = 0 + content.addAttribute(.paragraphStyle, value: mutableStyle.copy() as Any, range: range) + } + + return content + } + + private func shouldNormalizeContinuationParagraph(for page: RDEPUBTextPage) -> Bool { + let pageStart = page.pageStartOffset + guard pageStart > 0, pageStart < page.chapterContent.length else { + return false + } + + let chapterText = page.chapterContent.string as NSString + guard let previousScalar = UnicodeScalar(chapterText.character(at: pageStart - 1)) else { + return false + } + + return !CharacterSet.newlines.contains(previousScalar) + } + +#if canImport(DTCoreText) + private func updateCoreTextLayoutFrameIfNeeded() { + guard !coreTextContentView.isHidden, + let displayContent = coreTextDisplayContent, + let displayRange = coreTextDisplayRange, + let page = currentPage, + coreTextContentView.bounds.width > 0, + coreTextContentView.bounds.height > 0 else { + interactionController.configure(layoutFrame: nil, page: currentPage) + return + } + + guard let layouter = DTCoreTextLayouter(attributedString: displayContent) else { + coreTextContentView.layoutFrame = nil + interactionController.configure(layoutFrame: nil, page: page) + return + } + layouter.shouldCacheLayoutFrames = false + let layoutFrame = layouter.layoutFrame(with: coreTextContentView.bounds, range: displayRange) + coreTextContentView.layoutFrame = layoutFrame + interactionController.configure(layoutFrame: layoutFrame, page: page) + overlayView.updateSnapshot(interactionController.snapshot) + backgroundOverlayView.updateSnapshot(interactionController.snapshot) + } +#endif + +} diff --git a/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageDecorationView.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageDecorationView.swift new file mode 100644 index 0000000..bf93f02 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageDecorationView.swift @@ -0,0 +1,4 @@ +import UIKit + +/// 背景覆盖层,负责绘制页内装饰和位于正文下方的提示层。 +final class RDEPUBTextPageDecorationView: RDEPUBSelectionOverlayView {} diff --git a/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift new file mode 100644 index 0000000..ee68d18 --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift @@ -0,0 +1,41 @@ +import UIKit + +#if canImport(DTCoreText) +import DTCoreText + +/// 基于 DTCoreText 的 Core Text 直接绘制视图 +/// 将 DTCoreText 的排版结果直接绘制到 UIView 上,跳过 UITextView 的间接渲染。 +final class RDEPUBTextPageRenderView: UIView { + var layoutFrame: DTCoreTextLayoutFrame? { + didSet { + setNeedsDisplay() + } + } + + var drawOptions: DTCoreTextLayoutFrameDrawingOptions = DTCoreTextLayoutFrameDrawingOptions(rawValue: 1)! { + didSet { + setNeedsDisplay() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .clear + isOpaque = false + contentMode = .redraw + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext(), + let layoutFrame else { return } + + context.saveGState() + layoutFrame.draw(in: context, options: drawOptions) + context.restoreGState() + } +} +#endif diff --git a/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift new file mode 100644 index 0000000..16f668d --- /dev/null +++ b/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift @@ -0,0 +1,189 @@ +import UIKit + +/// 负责管理文本选区、长按交互和菜单定位。 +final class RDEPUBTextSelectionController: NSObject, UITextViewDelegate { + private var isSelectionFromInteraction = false + private var selectionAnchorPoint: CGPoint? + private var selectionMenuAnchorRect: CGRect? + + var onSelectionChanged: ((RDEPUBSelection?) -> Void)? + + func canPerformSelectionAction(in overlayView: RDEPUBSelectionOverlayView) -> Bool { + overlayView.selectionRange?.length ?? 0 > 0 + } + + func clearSelection( + textView: UITextView, + overlayView: RDEPUBSelectionOverlayView, + backgroundOverlayView: RDEPUBSelectionOverlayView? = nil + ) { + textView.selectedRange = NSRange(location: 0, length: 0) + overlayView.clearSelection() + backgroundOverlayView?.clearSelection() + selectionAnchorPoint = nil + selectionMenuAnchorRect = nil + isSelectionFromInteraction = false + UIMenuController.shared.setMenuVisible(false, animated: true) + onSelectionChanged?(nil) + } + + func handleLongPress( + _ gesture: UILongPressGestureRecognizer, + page: RDEPUBTextPage?, + overlayView: RDEPUBSelectionOverlayView, + interactionController: RDEPUBPageInteractionController + ) { + let point = gesture.location(in: overlayView) + + switch gesture.state { + case .began: + selectionAnchorPoint = point + isSelectionFromInteraction = true + handleSelectionFromInteraction( + point: point, + anchorPoint: nil, + page: page, + overlayView: overlayView, + interactionController: interactionController + ) + + case .changed: + guard let anchor = selectionAnchorPoint else { return } + handleSelectionFromInteraction( + point: point, + anchorPoint: anchor, + page: page, + overlayView: overlayView, + interactionController: interactionController + ) + + case .ended: + isSelectionFromInteraction = false + + default: + break + } + } + + func handleTap( + textView: UITextView, + overlayView: RDEPUBSelectionOverlayView, + backgroundOverlayView: RDEPUBSelectionOverlayView? = nil + ) { + clearSelection( + textView: textView, + overlayView: overlayView, + backgroundOverlayView: backgroundOverlayView + ) + } + + func showSelectionMenuIfNeeded( + in hostView: UIView, + overlayView: RDEPUBSelectionOverlayView, + interactionController: RDEPUBPageInteractionController, + copyAction: Selector, + highlightAction: Selector, + annotateAction: Selector + ) { + guard overlayView.selectionRange?.length ?? 0 > 0, + let anchorRect = selectionMenuAnchorRect ?? overlayView.selectionRange.flatMap({ interactionController.menuAnchorRect(for: $0) }) else { + return + } + + hostView.becomeFirstResponder() + let menuRect = overlayView.convert(anchorRect, to: hostView) + let menuController = UIMenuController.shared + menuController.menuItems = [ + UIMenuItem(title: "拷贝", action: copyAction), + UIMenuItem(title: "高亮", action: highlightAction), + UIMenuItem(title: "批注", action: annotateAction) + ] + menuController.setTargetRect(menuRect, in: hostView) + menuController.setMenuVisible(true, animated: true) + } + + func textViewDidChangeSelection(_ textView: UITextView, page: RDEPUBTextPage?) { + guard !isSelectionFromInteraction else { return } + guard let page else { + onSelectionChanged?(nil) + return + } + + let selectedRange = textView.selectedRange + guard selectedRange.location != NSNotFound, + selectedRange.length > 0, + let attributedText = textView.attributedText else { + onSelectionChanged?(nil) + return + } + + let source = attributedText.string as NSString + let selectedText = source.substring(with: selectedRange).trimmingCharacters(in: .whitespacesAndNewlines) + guard !selectedText.isEmpty else { + onSelectionChanged?(nil) + return + } + + let globalStart = page.pageStartOffset + selectedRange.location + let globalEnd = globalStart + selectedRange.length + let totalLength = max(page.chapterContent.length - 1, 1) + let selection = RDEPUBSelection( + location: RDEPUBLocation( + href: page.href, + progression: Double(globalStart) / Double(totalLength), + lastProgression: Double(max(globalEnd - 1, globalStart)) / Double(totalLength), + fragment: nil + ), + text: selectedText, + rangeInfo: RDEPUBTextOffsetRangeInfo(href: page.href, start: globalStart, end: globalEnd).jsonString() + ) + onSelectionChanged?(selection) + } + + private func handleSelectionFromInteraction( + point: CGPoint, + anchorPoint: CGPoint?, + page: RDEPUBTextPage?, + overlayView: RDEPUBSelectionOverlayView, + interactionController: RDEPUBPageInteractionController + ) { + guard let page else { return } + + let range: NSRange? + if let anchorPoint { + range = interactionController.selectionRange(from: anchorPoint, to: point) + } else if let idx = interactionController.characterIndex(at: point) { + range = NSRange(location: idx, length: 1) + } else { + range = nil + } + + guard let range else { return } + let rects = interactionController.selectionRects(for: range) + overlayView.updateSelection(absoluteRange: range, rects: rects) + selectionMenuAnchorRect = interactionController.menuAnchorRect(for: range) + onSelectionChanged?(makeSelection(from: range, page: page)) + } + + private func makeSelection(from range: NSRange, page: RDEPUBTextPage) -> RDEPUBSelection? { + let source = page.chapterContent.string as NSString + let selectedText = source.substring(with: range).trimmingCharacters(in: .whitespacesAndNewlines) + guard !selectedText.isEmpty else { + return nil + } + + let chapterLength = max(page.chapterContent.length - 1, 1) + let chapterStart = max(range.location, 0) + let chapterEnd = max(chapterStart + range.length - 1, chapterStart) + return RDEPUBSelection( + location: RDEPUBLocation( + href: page.href, + progression: Double(chapterStart) / Double(chapterLength), + lastProgression: Double(chapterEnd) / Double(chapterLength), + fragment: nil + ), + text: selectedText, + rangeInfo: RDEPUBTextOffsetRangeInfo(href: page.href, start: range.location, end: range.location + range.length).jsonString() + ) + } +} diff --git a/Sources/RDReaderView/ReaderView/Paging/RDReaderPagingController.swift b/Sources/RDReaderView/ReaderView/Paging/RDReaderPagingController.swift new file mode 100644 index 0000000..c088901 --- /dev/null +++ b/Sources/RDReaderView/ReaderView/Paging/RDReaderPagingController.swift @@ -0,0 +1,59 @@ +import UIKit + +/// 翻页控制器:管理 UIPageViewController 的创建、切换、故障修复和翻页请求队列。 +/// +/// RDReaderView 持有此控制器,将 pageCurl 模式的容器管理职责下沉。 +struct RDReaderPagingController { + /// 当前 pageCurl 翻页请求模型 + struct PageTransitionRequest: Equatable { + let pageNum: Int + let animated: Bool + } + + /// 排队中的翻页请求(pageCurl 动画过程中收到新跳转时排队) + var pendingTransitionRequest: PageTransitionRequest? + + /// 是否正在执行页面转场动画 + var isTransitioning: Bool = false + + /// UI 是否已经构建完成(避免重复构建) + var didBuildUI = false + + // MARK: - PageViewController 工厂 + + /// 创建带指定 spine 位置的 UIPageViewController + static func createPageViewController(isDualPage: Bool) -> UIPageViewController { + let options: [UIPageViewController.OptionsKey: Any]? + if isDualPage { + options = [.spineLocation: NSNumber(value: UIPageViewController.SpineLocation.mid.rawValue)] + } else { + options = nil + } + let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: options) + pageVC.isDoubleSided = isDualPage + return pageVC + } + + // MARK: - 翻页请求队列管理 + + /// pageCurl 动画过程中如果收到新的跳转请求,则排队等待当前动画结束。 + mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool { + guard currentDisplayType == .pageCurl, isTransitioning else { return false } + pendingTransitionRequest = request + return true + } + + /// 收尾 pageCurl 转场,返回排队中的后续请求(如果有的话)。 + mutating func finishPageCurlTransition() -> PageTransitionRequest? { + isTransitioning = false + guard let pending = pendingTransitionRequest else { return nil } + pendingTransitionRequest = nil + return pending + } + + /// 重置所有排队状态(用于故障修复)。 + mutating func resetPendingState() { + isTransitioning = false + pendingTransitionRequest = nil + } +} diff --git a/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift b/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift new file mode 100644 index 0000000..3438eda --- /dev/null +++ b/Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift @@ -0,0 +1,256 @@ +import UIKit + +final class RDReaderPreloadController { + var radius: Int = 1 + + private let preloadHostView = UIView() + private var preloadedPageViews: [Int: UIView] = [:] + private var pageCurlCachedViews: [Int: UIView] = [:] + private var cacheSignature: CacheSignature? + + struct Environment { + let displayType: RDReaderView.DisplayType + let isLandscape: Bool + let pagesPerScreen: Int + let boundsSize: CGSize + let landscapeDualPageEnabled: Bool + let coverPageIndex: Int? + let totalPages: Int + let spreadResolver: RDReaderSpreadResolver + } + + private struct CacheSignature: Equatable { + let displayType: RDReaderView.DisplayType + let isLandscape: Bool + let pagesPerScreen: Int + let boundsSize: CGSize + } + + func setHostFrame(_ frame: CGRect) { + preloadHostView.frame = frame + } + + func ensureHostView(in parentView: UIView) { + guard preloadHostView.superview == nil else { return } + preloadHostView.isHidden = true + preloadHostView.isUserInteractionEnabled = false + preloadHostView.clipsToBounds = true + preloadHostView.frame = parentView.bounds + preloadHostView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + parentView.insertSubview(preloadHostView, at: 0) + } + + func initializeSignature(_ environment: Environment) { + cacheSignature = currentCacheSignature(environment) + } + + func invalidate(environment: Environment) { + pageCurlCachedViews.values.forEach { $0.removeFromSuperview() } + preloadedPageViews.values.forEach { $0.removeFromSuperview() } + pageCurlCachedViews.removeAll() + preloadedPageViews.removeAll() + cacheSignature = currentCacheSignature(environment) + } + + func pageViewForDisplay( + pageNum: Int, + environment: Environment, + contentViewProvider: (Int, UIView?) -> UIView? + ) -> UIView { + let reusableView = detachedReusablePageView(for: pageNum) + let view = contentViewProvider(pageNum, reusableView) ?? reusableView ?? UIView() + pageCurlCachedViews[pageNum] = view + return view + } + + func takePreloadedView(for pageNum: Int) -> UIView? { + let preloaded = preloadedPageViews.removeValue(forKey: pageNum) + preloaded?.removeFromSuperview() + return preloaded + } + + func prime( + around pageNum: Int, + preferredForward: Bool? = nil, + parentView: UIView, + environment: Environment, + contentViewProvider: (Int, UIView?) -> UIView? + ) { + refreshCacheSignatureIfNeeded(environment) + let targets = forecastTargets(around: pageNum, preferredForward: preferredForward, environment: environment) + let keepSet = Set(targets).union(visiblePageNumbers(for: pageNum, environment: environment)) + trimCachedPageViews(keeping: keepSet) + guard !targets.isEmpty else { return } + + ensureHostView(in: parentView) + preloadHostView.frame = parentView.bounds + + for targetPage in targets { + let existing = detachedReusablePageView(for: targetPage) + let contentView = contentViewProvider(targetPage, existing) ?? existing ?? UIView() + preloadedPageViews[targetPage] = contentView + if contentView.superview !== preloadHostView { + contentView.removeFromSuperview() + preloadHostView.addSubview(contentView) + } + contentView.frame = preloadHostView.bounds + } + } + + private func currentCacheSignature(_ environment: Environment) -> CacheSignature { + CacheSignature( + displayType: environment.displayType, + isLandscape: environment.isLandscape, + pagesPerScreen: environment.pagesPerScreen, + boundsSize: environment.boundsSize + ) + } + + private func refreshCacheSignatureIfNeeded(_ environment: Environment) { + let signature = currentCacheSignature(environment) + if cacheSignature != signature { + invalidate(environment: environment) + } else if cacheSignature == nil { + cacheSignature = signature + } + } + + private func shouldCachePage(_ pageNum: Int, environment: Environment) -> Bool { + pageNum >= 0 + && pageNum != RDReaderView.blankPageNum + && pageNum != RDReaderView.blankEndPageNum + && pageNum < environment.totalPages + } + + private func isDualPage(environment: Environment) -> Bool { + environment.landscapeDualPageEnabled + && environment.isLandscape + && environment.displayType != .verticalScroll + } + + private func spreadStart(for pageNum: Int, environment: Environment) -> Int? { + guard shouldCachePage(pageNum, environment: environment) else { return nil } + guard isDualPage(environment: environment) else { return pageNum } + return environment.spreadResolver.dualPagePair( + for: pageNum, + totalPages: environment.totalPages, + coverPageIndex: environment.coverPageIndex + ).left + } + + private func spreadPageNumbers(startingAt pageNum: Int, environment: Environment) -> Set { + guard shouldCachePage(pageNum, environment: environment) else { return [] } + guard isDualPage(environment: environment) else { return [pageNum] } + let pair = environment.spreadResolver.dualPagePair( + for: pageNum, + totalPages: environment.totalPages, + coverPageIndex: environment.coverPageIndex + ) + var pages: Set = [pair.left] + if let right = pair.right, shouldCachePage(right, environment: environment) { + pages.insert(right) + } + return pages + } + + private func adjacentSpreadStart(from pageNum: Int, forward: Bool, environment: Environment) -> Int? { + guard environment.totalPages > 0, + let spreadStart = spreadStart(for: pageNum, environment: environment) else { + return nil + } + + guard isDualPage(environment: environment) else { + let target = forward ? spreadStart + 1 : spreadStart - 1 + return shouldCachePage(target, environment: environment) ? target : nil + } + + if forward { + return environment.spreadResolver.adjacentDualPage( + from: spreadStart, + totalPages: environment.totalPages, + coverPageIndex: environment.coverPageIndex, + forward: true + ) + } + + let pair = environment.spreadResolver.dualPagePair( + for: spreadStart, + totalPages: environment.totalPages, + coverPageIndex: environment.coverPageIndex + ) + let prevEnd = pair.left - 1 + guard prevEnd >= 0 else { return nil } + return environment.spreadResolver.dualPagePair( + for: prevEnd, + totalPages: environment.totalPages, + coverPageIndex: environment.coverPageIndex + ).left + } + + private func visiblePageNumbers(for anchorPage: Int, environment: Environment) -> Set { + spreadPageNumbers(startingAt: anchorPage, environment: environment) + } + + private func detachedReusablePageView(for pageNum: Int) -> UIView? { + if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) { + preloaded.removeFromSuperview() + return preloaded + } + + guard let cached = pageCurlCachedViews[pageNum], cached.superview == nil else { + return nil + } + + pageCurlCachedViews.removeValue(forKey: pageNum) + return cached + } + + private func trimCachedPageViews(keeping pageNumbers: Set) { + pageCurlCachedViews = pageCurlCachedViews.filter { key, value in + let keep = pageNumbers.contains(key) + if !keep { + value.removeFromSuperview() + } + return keep + } + + preloadedPageViews = preloadedPageViews.filter { key, value in + let keep = pageNumbers.contains(key) + if !keep { + value.removeFromSuperview() + } + return keep + } + } + + private func forecastTargets(around pageNum: Int, preferredForward: Bool?, environment: Environment) -> [Int] { + guard environment.totalPages > 0, + shouldCachePage(pageNum, environment: environment) else { + return [] + } + + var targets = Set() + + var previousAnchor = pageNum + for _ in 0.. Bool { + guard landscapeDualPageEnabled, isLandscape, let coverIndex = coverPageIndex else { return false } + return pageNum == coverIndex + } + + func dualPagePair( + for pageNum: Int, + totalPages: Int, + coverPageIndex: Int? + ) -> (left: Int, right: Int?) { + if let coverIndex = coverPageIndex { + if pageNum == coverIndex { + return (coverIndex, nil) + } + let adjustedIndex = pageNum - (coverIndex + 1) + let pairStart = coverIndex + 1 + (adjustedIndex / 2) * 2 + let left = pairStart + let right = pairStart + 1 < totalPages ? pairStart + 1 : nil + return (left, right) + } + + let left = (pageNum / 2) * 2 + let right = left + 1 < totalPages ? left + 1 : nil + return (left, right) + } + + func adjacentDualPage( + from pageNum: Int, + totalPages: Int, + coverPageIndex: Int?, + forward: Bool + ) -> Int? { + let pair = dualPagePair(for: pageNum, totalPages: totalPages, coverPageIndex: coverPageIndex) + if forward { + let nextStart = (pair.right ?? pair.left) + 1 + return nextStart < totalPages ? nextStart : nil + } + + let prevEnd = pair.left - 1 + guard prevEnd >= 0 else { return nil } + return dualPagePair(for: prevEnd, totalPages: totalPages, coverPageIndex: coverPageIndex).left + } + + func nextPage( + from currentPage: Int, + totalPages: Int, + pagesPerScreen: Int, + coverPageIndex: Int?, + forward: Bool + ) -> Int? { + if pagesPerScreen > 1 { + return adjacentDualPage( + from: currentPage, + totalPages: totalPages, + coverPageIndex: coverPageIndex, + forward: forward + ) + } + + let target = currentPage + (forward ? 1 : -1) + guard target >= 0, target < totalPages else { return nil } + return target + } +} diff --git a/Sources/RDReaderView/ReaderView/Paging/RDReaderTapRegionHandler.swift b/Sources/RDReaderView/ReaderView/Paging/RDReaderTapRegionHandler.swift new file mode 100644 index 0000000..1ee5a45 --- /dev/null +++ b/Sources/RDReaderView/ReaderView/Paging/RDReaderTapRegionHandler.swift @@ -0,0 +1,27 @@ +import CoreGraphics + +struct RDReaderTapRegionHandler { + func resolveTapEvent( + point: CGPoint, + viewFrame: CGRect, + isToolViewVisible: Bool + ) -> RDReaderView.TapEvent { + let leftFrame = CGRect(x: 0, y: 0, width: viewFrame.width / 3, height: viewFrame.height) + let centerFrame = CGRect(x: viewFrame.width / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) + let rightFrame = CGRect(x: viewFrame.width * 2 / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) + + if leftFrame.contains(point) { + return isToolViewVisible ? .center : .left + } + + if centerFrame.contains(point) { + return .center + } + + if rightFrame.contains(point) { + return isToolViewVisible ? .center : .right + } + + return .none + } +} diff --git a/Sources/RDReaderView/ReaderView/RDReaderView+CollectionView.swift b/Sources/RDReaderView/ReaderView/RDReaderView+CollectionView.swift new file mode 100644 index 0000000..554101b --- /dev/null +++ b/Sources/RDReaderView/ReaderView/RDReaderView+CollectionView.swift @@ -0,0 +1,49 @@ +import UIKit + +/// UICollectionView 数据源和自定义布局代理实现 +/// 处理水平滚动和垂直滚动两种翻页模式 +extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure { + /// 配置 UICollectionViewCell + /// 通过 DataSource 获取页面内容视图,支持 cell 复用和预加载视图复用 + /// RTL 水平模式下翻转 cell 内容使文字方向正常 + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + if let identifer = pageReuseIdentifier(for: indexPath.row) { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifer, for: IndexPath(item: indexPath.row, section: 0)) as! RDReaderContentCell + let preloadedView = preloadController.takePreloadedView(for: indexPath.row) + let reusableView = cell.containerView ?? preloadedView + cell.containerView = contentViewForPage(indexPath.row, reusableView: reusableView) + // RTL 水平模式:翻转 cell 内容使文字方向正常(collectionView 已整体翻转) + if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll { + cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1) + } else { + cell.contentView.transform = .identity + } + return cell + } + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath) + + return cell + } + + /// 返回每组的 item 数量,即总页数 + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return numberOfPages() + } + + /// 布局代理回调:当前可见页码变化时通知 + public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) { + if currentPage >= 0, currentPage != pageIndex { + predictedPageDirection = pageIndex >= currentPage + } + currentPage = pageIndex + primePageCache(around: pageIndex, preferredForward: predictedPageDirection) + } + + /// 布局数据源回调:返回垂直滚动模式下指定页的高度 + /// 当前返回 nil 使用默认高度 + public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? { + return nil + } +} diff --git a/Sources/RDReaderView/ReaderView/RDReaderView+ContentAccess.swift b/Sources/RDReaderView/ReaderView/RDReaderView+ContentAccess.swift new file mode 100644 index 0000000..7ea5d0e --- /dev/null +++ b/Sources/RDReaderView/ReaderView/RDReaderView+ContentAccess.swift @@ -0,0 +1,75 @@ +import UIKit + +/// 内容视图注册和复用扩展 +/// 提供类似 UITableView 的 register/dequeueReusable 机制 +extension RDReaderView { + /// 注册内容视图类型 + public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) { + contentViews[identifier] = contentView + collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier) + } + + /// 获取可复用的内容视图 + /// 优先从当前显示的 cell 中获取,其次创建新实例 + public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView { + if self.currentDisplayType != .pageCurl, let cell = self.collectionView.cellForItem(at: IndexPath(row: pageNum, section: 0)) as? RDReaderContentCell, let containerView = cell.containerView { + return containerView + } + let contentViewClass = contentViews[identifier] + assert(contentViewClass != nil, "请调用register(contentView:contentViewWithReuseIdentifier:)") + var contentView = contentViewClass!.init() + return contentView + } + + /// 获取指定页码的内容视图 + /// pageCurl 模式从 PageViewController 获取,滚动模式从 CollectionView cell 获取 + public func pageContentView(pageNum: Int) -> UIView? { + if currentDisplayType == .pageCurl { + return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView + } else { + let cell = collectionView.cellForItem(at: IndexPath(item: pageNum, section: 0)) as? RDReaderContentCell + return cell?.containerView + } + } + + /// 返回当前阅读模式下单页内容容器的实际尺寸。 + /// 优先使用已经显示出来的内容视图尺寸;若页面尚未装载,则回退到当前模式下的理论单页尺寸。 + public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize { + let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil) + if let targetPage, + let contentView = pageContentView(pageNum: targetPage), + contentView.bounds.width > 0, + contentView.bounds.height > 0 { + return contentView.bounds.size + } + + if currentDisplayType == .pageCurl { + if let childViewController = pageViewController.viewControllers?.first as? RDReaderPageChildViewController, + childViewController.view.bounds.width > 0, + childViewController.view.bounds.height > 0 { + return childViewController.view.bounds.size + } + if pageViewController.view.bounds.width > 0, pageViewController.view.bounds.height > 0 { + return pageViewController.view.bounds.size + } + return bounds.size + } + + let containerBounds = collectionView.bounds.size == .zero ? bounds.size : collectionView.bounds.size + guard containerBounds.width > 0, containerBounds.height > 0 else { + return bounds.size + } + + switch currentDisplayType { + case .horizontalScroll: + return CGSize( + width: containerBounds.width / CGFloat(max(pagesPerScreen, 1)), + height: containerBounds.height + ) + case .verticalScroll: + return containerBounds + case .pageCurl: + return bounds.size + } + } +} diff --git a/Sources/RDReaderView/ReaderView/RDReaderView+PageCurl.swift b/Sources/RDReaderView/ReaderView/RDReaderView+PageCurl.swift new file mode 100644 index 0000000..588187c --- /dev/null +++ b/Sources/RDReaderView/ReaderView/RDReaderView+PageCurl.swift @@ -0,0 +1,138 @@ +import UIKit + +/// UIPageViewController 数据源和代理实现 +/// 处理仿真翻页模式下的页面数据供给和翻页事件 +extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate { + + /// 创建指定页码的子控制器 + /// 空白页返回空视图,正常页从缓存或数据源获取内容 + private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController { + if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum { + return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum) + } + let contentView = pageViewForDisplay(pageNum: pageNum) + return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum) + } + + /// 计算某页的"下一页"页码 + /// 考虑封面页独占和末尾空白页配对的情况 + private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? { + let totalPages = numberOfPages() + + // 末尾空白页之后没有更多页 + if pageNum == RDReaderView.blankEndPageNum { + return nil + } + + if isDualPage, let coverIndex = coverPageIndex { + if pageNum == coverIndex { + return RDReaderView.blankPageNum + } + if pageNum == RDReaderView.blankPageNum { + let firstContent = coverIndex + 1 + return firstContent < totalPages ? firstContent : nil + } + } + + let next = pageNum + 1 + if next < totalPages { + return next + } + + // pageNum 是最后一页,检查在双页模式下是否需要空白页配对 + if isDualPage { + if let coverIndex = coverPageIndex { + let adjustedIndex = pageNum - (coverIndex + 1) + if adjustedIndex >= 0 && adjustedIndex % 2 == 0 { + return RDReaderView.blankEndPageNum + } + } else { + if pageNum % 2 == 0 { + return RDReaderView.blankEndPageNum + } + } + } + return nil + } + + /// 计算某页的"上一页"页码 + /// 考虑封面页独占和末尾空白页配对的情况 + private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? { + if pageNum == RDReaderView.blankEndPageNum { + let totalPages = numberOfPages() + return totalPages > 0 ? totalPages - 1 : nil + } + if isDualPage, let coverIndex = coverPageIndex { + if pageNum == RDReaderView.blankPageNum { + return coverIndex + } + if pageNum == coverIndex + 1 { + return RDReaderView.blankPageNum + } + } + let prev = pageNum - 1 + return prev >= 0 ? prev : nil + } + + /// UIPageViewController 数据源:返回当前页之前(左/上)的页面控制器 + /// RTL 模式下 before/after 语义互换:before(向右翻)= 下一页 + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let vc = viewController as? RDReaderPageChildViewController else { return nil } + let isDualPage = landscapeDualPageEnabled && isLandscape + let isRTL = pageDirection == .rightToLeft + + let targetNum = isRTL + ? nextPageNum(after: vc.pageNum, isDualPage: isDualPage) + : prevPageNum(before: vc.pageNum, isDualPage: isDualPage) + + guard let num = targetNum else { return nil } + let targetVC = makeSinglePageChildVC(for: num) + willPreviousTransitionToViewController = targetVC + return targetVC + } + + /// UIPageViewController 数据源:返回当前页之后(右/下)的页面控制器 + /// RTL 模式下 before/after 语义互换 + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let vc = viewController as? RDReaderPageChildViewController else { return nil } + let isDualPage = landscapeDualPageEnabled && isLandscape + let isRTL = pageDirection == .rightToLeft + + let targetNum = isRTL + ? prevPageNum(before: vc.pageNum, isDualPage: isDualPage) + : nextPageNum(after: vc.pageNum, isDualPage: isDualPage) + + guard let num = targetNum else { return nil } + let targetVC = makeSinglePageChildVC(for: num) + willNextTransitionToViewController = targetVC + return targetVC + } + + /// UIPageViewController 代理:翻页动画完成回调 + /// 更新当前页码,触发预加载,并检测 PageViewController 异常状态 + public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if completed, let firstVC = pageViewController.viewControllers?.first as? RDReaderPageChildViewController { + let pn = firstVC.pageNum + if pn != RDReaderView.blankPageNum && pn != RDReaderView.blankEndPageNum { + currentPage = pn + primePageCache(around: pn, preferredForward: predictedPageDirection) + } + } + + predictedPageDirection = nil + finishPageCurlTransition() + } + + /// UIPageViewController 代理:即将开始转场动画 + /// 记录预测的翻页方向,用于优化预加载策略 + public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + pagingController.isTransitioning = true + willTransitionToViewController = pendingViewControllers.first + if let target = pendingViewControllers.first as? RDReaderPageChildViewController, + target.pageNum != RDReaderView.blankPageNum, + target.pageNum != RDReaderView.blankEndPageNum { + predictedPageDirection = target.pageNum >= currentPage + primePageCache(around: target.pageNum, preferredForward: predictedPageDirection) + } + } +} diff --git a/Sources/RDReaderView/ReaderView/RDReaderView+ToolView.swift b/Sources/RDReaderView/ReaderView/RDReaderView+ToolView.swift new file mode 100644 index 0000000..b9a3ddc --- /dev/null +++ b/Sources/RDReaderView/ReaderView/RDReaderView+ToolView.swift @@ -0,0 +1,115 @@ +import UIKit + +/// 工具栏管理扩展:处理工具栏的显示/隐藏动画、安装、布局 +extension RDReaderView { + + /// 点击屏幕中央区域,切换工具栏的显示/隐藏 + func tapCenter() { + refreshToolViewsFromProviderIfNeeded() + print("[Debug] tapCenter called, isShowToolView was: \(isShowToolView), topToolView: \(String(describing: topToolView)), onBack: \(String(describing: (topToolView as? RDEPUBReaderTopToolView)?.onBack))") + isShowToolView = !isShowToolView + if isShowToolView { + if let topToolView = topToolView { + installToolViewIfNeeded(topToolView, position: .top) + layoutIfNeeded() + topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) + UIView.animate(withDuration: toolViewAnimationDuration) { + topToolView.transform = .identity + } + } + + if let bottomToolView = bottomToolView { + installToolViewIfNeeded(bottomToolView, position: .bottom) + layoutIfNeeded() + bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) + UIView.animate(withDuration: toolViewAnimationDuration) { + bottomToolView.transform = .identity + } + } + + collectionView.isUserInteractionEnabled = false + pageViewController.view.isUserInteractionEnabled = false + } else { + if let topToolView = topToolView { + UIView.animate(withDuration: toolViewAnimationDuration, animations: { + topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) + }) { _ in + topToolView.removeFromSuperview() + } + } + + if let bottomToolView = bottomToolView { + UIView.animate(withDuration: toolViewAnimationDuration, animations: { + bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) + }) { _ in + bottomToolView.removeFromSuperview() + } + } + + collectionView.isUserInteractionEnabled = true + pageViewController.view.isUserInteractionEnabled = true + } + } + + /// 判断点击命中的视图是否在指定工具栏内 + func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool { + if toolView.frame.contains(point) { + return true + } + + guard let hitView else { return false } + return hitView === toolView || hitView.isDescendant(of: toolView) + } + + enum ToolViewPosition { + case top + case bottom + } + + func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) { + guard toolView.superview !== self else { return } + + toolView.removeFromSuperview() + addSubview(toolView) + toolView.translatesAutoresizingMaskIntoConstraints = false + + let heightConstraint: NSLayoutConstraint + switch position { + case .top: + topToolViewHeightConstraint?.isActive = false + heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .top)) + topToolViewHeightConstraint = heightConstraint + NSLayoutConstraint.activate([ + toolView.leadingAnchor.constraint(equalTo: leadingAnchor), + toolView.trailingAnchor.constraint(equalTo: trailingAnchor), + toolView.topAnchor.constraint(equalTo: topAnchor), + heightConstraint + ]) + case .bottom: + bottomToolViewHeightConstraint?.isActive = false + heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .bottom)) + bottomToolViewHeightConstraint = heightConstraint + NSLayoutConstraint.activate([ + toolView.leadingAnchor.constraint(equalTo: leadingAnchor), + toolView.trailingAnchor.constraint(equalTo: trailingAnchor), + toolView.bottomAnchor.constraint(equalTo: bottomAnchor), + heightConstraint + ]) + } + } + + func updateToolViewHeightConstraintsIfNeeded() { + topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top) + bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom) + } + + private func resolvedToolViewHeight(for position: ToolViewPosition) -> CGFloat { + let contentHeight: CGFloat = 52 + switch position { + case .top: + return safeAreaInsets.top + contentHeight + case .bottom: + return safeAreaInsets.bottom + contentHeight + } + } +} diff --git a/Sources/RDReaderView/ReaderView/RDReaderView.swift b/Sources/RDReaderView/ReaderView/RDReaderView.swift index 5d2e766..30bb391 100644 --- a/Sources/RDReaderView/ReaderView/RDReaderView.swift +++ b/Sources/RDReaderView/ReaderView/RDReaderView.swift @@ -20,65 +20,6 @@ import UIKit -/// 阅读器数据源协议 -/// 上层控制器通过实现此协议,向 RDReaderView 提供页面数量、内容视图和工具栏。 -/// 所有方法由 RDReaderView 在需要渲染页面时回调。 -@objc public protocol RDReaderDataSource: NSObjectProtocol { - /// 返回阅读器的总页数 - /// - Parameter readerView: 发起请求的阅读器视图 - /// - Returns: 总页数 - func pageCountOfReaderView(readerView: RDReaderView) -> Int - /// 返回指定页码的内容视图 - /// - Parameters: - /// - readerView: 发起请求的阅读器视图 - /// - pageNum: 页码(从0开始) - /// - containerView: 可复用的容器视图(非 nil 时可复用以减少创建开销) - /// - Returns: 该页的内容视图 - func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView - /// 返回指定页码的唯一标识符,用于 UICollectionViewCell 复用 - /// - Parameters: - /// - readerView: 发起请求的阅读器视图 - /// - pageNum: 页码 - /// - Returns: 页标识符字符串,nil 则使用默认 cell - func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? - /// 返回顶部工具栏视图(可选),点击屏幕中央时会显示/隐藏 - @objc optional func topToolView(readerView: RDReaderView) -> UIView? - /// 返回底部工具栏视图(可选),点击屏幕中央时会显示/隐藏 - @objc optional func bottomToolView(readerView: RDReaderView) -> UIView? -} - -/// 阅读器代理协议 -/// 上层控制器通过实现此协议,接收翻页和横竖屏切换事件 -@objc public protocol RDReaderDelegate: NSObjectProtocol { - /// 翻页回调,当当前显示页码变化时触发 - /// - Parameters: - /// - readerView: 发起回调的阅读器视图 - /// - pageNum: 当前页码 - func pageNum(readerView: RDReaderView, pageNum: Int) - /// 横竖屏切换时回调,可在此重新分页 - @objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) -} - -extension RDReaderView { - /// 翻页模式枚举,决定 RDReaderView 使用哪种底层视图来展示内容 - public enum DisplayType { - /// 仿真翻页:使用 UIPageViewController,模拟真实翻书效果 - case pageCurl - /// 水平滚动:使用 UICollectionView + 自定义布局,每屏一项,水平分页 - case horizontalScroll - /// 垂直滚动:使用 UICollectionView,全宽项目,垂直连续滚动 - case verticalScroll - } - - /// 翻页方向枚举 - public enum PageDirection { - /// 从左往右翻页(默认,适用于中文/英文书籍) - case leftToRight - /// 从右往左翻页(适用于日文漫画等) - case rightToLeft - } -} - /// 核心翻页容器视图 /// RDReaderView 模块的核心类,负责管理四种翻页模式的切换、手势识别、 /// 横屏双页显示、页面缓存预加载等核心功能。 @@ -112,7 +53,7 @@ public class RDReaderView: UIView { /// 仿真翻页模式使用的 UIPageViewController /// 通过 pageCurl 转场样式实现仿真翻书效果 - private lazy var pageViewController: UIPageViewController = { + lazy var pageViewController: UIPageViewController = { let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil) pageVC.delegate = self pageVC.dataSource = self @@ -120,7 +61,7 @@ public class RDReaderView: UIView { }() /// 自定义流式布局,支持水平滚动、垂直滚动和水平覆盖滚动三种布局模式 - private lazy var layout: RDReaderFlowLayout = { + lazy var layout: RDReaderFlowLayout = { let layout = RDReaderFlowLayout(displayType: .horizontalScroll) layout.dataSource = self layout.delegate = self @@ -129,12 +70,16 @@ public class RDReaderView: UIView { /// 滚动模式使用的 UICollectionView /// 用于 horizontalScroll 和 verticalScroll 两种翻页模式 - private lazy var collectionView: UICollectionView = { + lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = UIColor.clear return collectionView }() + private let spreadResolver = RDReaderSpreadResolver() + private let tapRegionHandler = RDReaderTapRegionHandler() + let preloadController = RDReaderPreloadController() + var pagingController = RDReaderPagingController() /// 当前显示的页码 /// 值变化时会通过 delegate 回调通知上层控制器 @@ -151,6 +96,7 @@ public class RDReaderView: UIView { /// 用于检测用户点击屏幕的区域,触发翻页或工具栏切换 private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = { let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:))) + tap.delegate = self return tap }() @@ -179,30 +125,42 @@ public class RDReaderView: UIView { /// 翻到下一页 /// 在双页模式下使用 adjacentDualPage 计算目标页码,单页模式直接 +1 private func goNextPage() { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - if pagesPerScreen > 1 { - if let target = adjacentDualPage(from: currentPage, forward: true) { - transitionToPage(pageNum: target, animated: true) - } - } else if currentPage + 1 < totalPages { - transitionToPage(pageNum: currentPage + 1, animated: true) + let totalPages = numberOfPages() + if let target = spreadResolver.nextPage( + from: currentPage, + totalPages: totalPages, + pagesPerScreen: pagesPerScreen, + coverPageIndex: coverPageIndex, + forward: true + ) { + transitionToPage(pageNum: target, animated: true) } } /// 翻到上一页 /// 在双页模式下使用 adjacentDualPage 计算目标页码,单页模式直接 -1 private func goPreviousPage() { - if pagesPerScreen > 1 { - if let target = adjacentDualPage(from: currentPage, forward: false) { - transitionToPage(pageNum: target, animated: true) - } - } else if currentPage - 1 >= 0 { - transitionToPage(pageNum: currentPage - 1, animated: true) + let totalPages = numberOfPages() + if let target = spreadResolver.nextPage( + from: currentPage, + totalPages: totalPages, + pagesPerScreen: pagesPerScreen, + coverPageIndex: coverPageIndex, + forward: false + ) { + transitionToPage(pageNum: target, animated: true) } } + private let legacyDataSourceAdapter = RDReaderLegacyDataSourceAdapter() /// 数据源代理,提供页面数量、内容视图和工具栏 - public weak var dataSource: RDReaderDataSource? = nil + public weak var dataSource: RDReaderDataSource? { + didSet { + legacyDataSourceAdapter.dataSource = dataSource + } + } + /// 新的通用分页内容提供者,优先级高于 ``dataSource``。 + public weak var pageProvider: RDReaderPageProvider? /// 代理,接收翻页和横竖屏切换事件 public weak var delegate: RDReaderDelegate? = nil /// 当前翻页模式,默认为仿真翻页 @@ -217,13 +175,46 @@ public class RDReaderView: UIView { /// 设为 nil 表示没有封面页(所有页面正常两两配对:0+1, 2+3, 4+5...)。 public var coverPageIndex: Int? = nil + private var resolvedPageProvider: RDReaderPageProvider? { + pageProvider ?? legacyDataSourceAdapter + } + + func numberOfPages() -> Int { + resolvedPageProvider?.numberOfPages(in: self) ?? 0 + } + + func pageReuseIdentifier(for pageNum: Int) -> String? { + resolvedPageProvider?.pageIdentifier?(in: self, index: pageNum) + } + + func contentViewForPage(_ pageNum: Int, reusableView: UIView?) -> UIView { + resolvedPageProvider?.readerView(self, viewForPageAt: pageNum, reusableView: reusableView) ?? UIView() + } + + private func resolvedTopChromeView() -> UIView? { + resolvedPageProvider?.readerViewTopChrome?(self) + } + + private func resolvedBottomChromeView() -> UIView? { + resolvedPageProvider?.readerViewBottomChrome?(self) + } + + func refreshToolViewsFromProviderIfNeeded() { + if topToolView == nil { + topToolView = resolvedTopChromeView() + } + if bottomToolView == nil { + bottomToolView = resolvedBottomChromeView() + } + } + /// 是否启用了封面页独占 private var hasCoverPage: Bool { return coverPageIndex != nil } /// 当前是否横屏(宽度 > 高度) - private var isLandscape: Bool { + var isLandscape: Bool { return bounds.width > bounds.height } @@ -239,8 +230,12 @@ public class RDReaderView: UIView { /// - Parameter pageNum: 页码 /// - Returns: 是否独占一屏 public func isFullScreenPage(_ pageNum: Int) -> Bool { - guard landscapeDualPageEnabled, isLandscape, let coverIndex = coverPageIndex else { return false } - return pageNum == coverIndex + spreadResolver.isFullScreenPage( + pageNum, + landscapeDualPageEnabled: landscapeDualPageEnabled, + isLandscape: isLandscape, + coverPageIndex: coverPageIndex + ) } /// 根据逻辑页码计算横屏双页模式下的配对信息 @@ -248,25 +243,12 @@ public class RDReaderView: UIView { /// - Parameter pageNum: 逻辑页码 /// - Returns: (左页码, 右页码(可选,nil表示右页为空白)) private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - if let coverIndex = coverPageIndex { - if pageNum == coverIndex { - // 封面页独占一屏 - return (coverIndex, nil) - } - // 封面之后的页面:偏移1后两两配对 - // 例如封面是0页,则 1+2, 3+4, 5+6... - let adjustedIndex = pageNum - (coverIndex + 1) // 跳过封面后的偏移 - let pairStart = coverIndex + 1 + (adjustedIndex / 2) * 2 - let left = pairStart - let right = pairStart + 1 < totalPages ? pairStart + 1 : nil - return (left, right) - } else { - // 无封面页:正常两两配对 0+1, 2+3, 4+5... - let left = (pageNum / 2) * 2 - let right = left + 1 < totalPages ? left + 1 : nil - return (left, right) - } + let totalPages = numberOfPages() + return spreadResolver.dualPagePair( + for: pageNum, + totalPages: totalPages, + coverPageIndex: coverPageIndex + ) } /// 计算横屏双页下,某页往前/后翻一屏后的起始页码 @@ -275,21 +257,18 @@ public class RDReaderView: UIView { /// - forward: 是否向后翻(true=下一页,false=上一页) /// - Returns: 目标页码,nil 表示到头了 private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - let pair = dualPagePair(for: pageNum) - if forward { - let nextStart = (pair.right ?? pair.left) + 1 - return nextStart < totalPages ? nextStart : nil - } else { - let prevEnd = pair.left - 1 - guard prevEnd >= 0 else { return nil } - return dualPagePair(for: prevEnd).left - } + let totalPages = numberOfPages() + return spreadResolver.adjacentDualPage( + from: pageNum, + totalPages: totalPages, + coverPageIndex: coverPageIndex, + forward: forward + ) } /// 将页码钳制到当前书籍的合法范围内 private func clampedPageNumber(_ pageNum: Int) -> Int? { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 + let totalPages = numberOfPages() guard totalPages > 0 else { return nil } return min(max(pageNum, 0), totalPages - 1) } @@ -298,65 +277,53 @@ public class RDReaderView: UIView { private var previousIsLandscape: Bool? /// 已注册的内容视图类型字典,key 为重用标识符 - private var contentViews = [String : UIView.Type]() + var contentViews = [String : UIView.Type]() /// pageCurl 模式下,即将向前翻转到的子控制器 - private var willPreviousTransitionToViewController: UIViewController? = nil + var willPreviousTransitionToViewController: UIViewController? = nil /// pageCurl 模式下,即将向后翻转到的子控制器 - private var willNextTransitionToViewController: UIViewController? = nil + var willNextTransitionToViewController: UIViewController? = nil /// pageCurl 模式下,正在转场的目标控制器 - private var willTransitionToViewController: UIViewController? = nil + var willTransitionToViewController: UIViewController? = nil /// 顶部工具栏视图 - private var topToolView: UIView? + var topToolView: UIView? /// 底部工具栏视图 - private var bottomToolView: UIView? + var bottomToolView: UIView? /// 顶部工具栏高度约束 - private var topToolViewHeightConstraint: NSLayoutConstraint? + var topToolViewHeightConstraint: NSLayoutConstraint? /// 底部工具栏高度约束 - private var bottomToolViewHeightConstraint: NSLayoutConstraint? + var bottomToolViewHeightConstraint: NSLayoutConstraint? /// 工具栏是否正在显示 - private var isShowToolView: Bool = false + var isShowToolView: Bool = false /// 是否正在执行页面转场动画 - private var isTransitioning: Bool = false + private var isTransitioning: Bool { + get { pagingController.isTransitioning } + set { pagingController.isTransitioning = newValue } + } /// UI 是否已经构建完成(避免重复构建) - private var didBuildUI = false - /// 预加载宿主视图(隐藏不可见),用于存放预加载的页面内容 - private let preloadHostView = UIView() - /// 预加载的页面视图缓存,key 为页码 - private var preloadedPageViews: [Int: UIView] = [:] - /// pageCurl 模式下的页面视图缓存,key 为页码 - private var pageCurlCachedViews: [Int: UIView] = [:] + private var didBuildUI: Bool { + get { pagingController.didBuildUI } + set { pagingController.didBuildUI = newValue } + } /// 预测的翻页方向(true=向前,false=向后),用于优化预加载方向 - private var predictedPageDirection: Bool? + var predictedPageDirection: Bool? /// 预加载半径:当前页前后各预加载几屏,默认 1 屏 - public var preloadRadius: Int = 1 - /// 缓存签名:用于检测布局环境是否变化,变化时需要清空缓存 - private var cacheSignature: CacheSignature? + public var preloadRadius: Int { + get { preloadController.radius } + set { preloadController.radius = newValue } + } /// 用于 pageCurl 双页模式下封面页旁边的空白页标识 static let blankPageNum = Int.max /// 用于 pageCurl 双页模式下末尾不成对页旁边的空白页标识 static let blankEndPageNum = Int.max - 1 - /// 缓存签名结构体 - /// 当这些参数中的任何一个发生变化时,缓存需要被清空并重建 - private struct CacheSignature: Equatable { - /// 当前翻页模式 - let displayType: DisplayType - /// 是否横屏 - let isLandscape: Bool - /// 每屏页数 - let pagesPerScreen: Int - /// 视图尺寸 - let boundsSize: CGSize - } - /// 当 pageCurl 正在转场时,后续跳转请求会先排队,待当前动画稳定后再执行。 - private struct PageTransitionRequest: Equatable { - let pageNum: Int - let animated: Bool - } + private typealias PageTransitionRequest = RDReaderPagingController.PageTransitionRequest - private var pendingTransitionRequest: PageTransitionRequest? + private var pendingTransitionRequest: PageTransitionRequest? { + get { pagingController.pendingTransitionRequest } + set { pagingController.pendingTransitionRequest = newValue } + } override init(frame: CGRect) { super.init(frame: frame) @@ -367,7 +334,7 @@ public class RDReaderView: UIView { public override func layoutSubviews() { super.layoutSubviews() guard bounds.width > 0, bounds.height > 0 else { return } - preloadHostView.frame = bounds + preloadController.setHostFrame(bounds) updateToolViewHeightConstraintsIfNeeded() let nowLandscape = isLandscape if let prev = previousIsLandscape, prev != nowLandscape { @@ -411,7 +378,7 @@ public class RDReaderView: UIView { collectionView.reloadData() collectionView.layoutIfNeeded() // 3. 根据保存的页码恢复滚动位置 - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 + let totalPages = numberOfPages() let safePage = min(savedPage, max(0, totalPages - 1)) let targetOffset = layout.currentContentOffset(count: safePage) collectionView.setContentOffset(targetOffset, animated: false) @@ -485,8 +452,7 @@ public class RDReaderView: UIView { guard currentPage >= 0 else { return } DispatchQueue.main.async { [weak self] in guard let self else { return } - self.isTransitioning = false - self.pendingTransitionRequest = nil + self.pagingController.resetPendingState() self.willPreviousTransitionToViewController = nil self.willNextTransitionToViewController = nil self.willTransitionToViewController = nil @@ -498,14 +464,11 @@ public class RDReaderView: UIView { /// pageCurl 动画过程中如果收到新的跳转请求,则排队等待当前动画结束。 private func shouldQueuePageTransition(_ request: PageTransitionRequest) -> Bool { - guard currentDisplayType == .pageCurl, isTransitioning else { return false } - pendingTransitionRequest = request - return true + pagingController.shouldQueuePageTransition(request, currentDisplayType: currentDisplayType) } /// 收尾 pageCurl 转场,并尝试执行排队中的后续跳转。 - private func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) { - isTransitioning = false + func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) { willPreviousTransitionToViewController = nil willNextTransitionToViewController = nil willTransitionToViewController = nil @@ -515,222 +478,50 @@ public class RDReaderView: UIView { return } - guard let pendingTransitionRequest else { return } - self.pendingTransitionRequest = nil - transitionToPage(pageNum: pendingTransitionRequest.pageNum, animated: pendingTransitionRequest.animated) + if let pending = pagingController.finishPageCurlTransition() { + transitionToPage(pageNum: pending.pageNum, animated: pending.animated) + } } // MARK: - Preload / Forecast - /// 确保预加载宿主视图已添加到视图层级 - /// 该视图隐藏且不可交互,仅用于存放预加载的页面内容 - private func ensurePreloadHostView() { - guard preloadHostView.superview == nil else { return } - preloadHostView.isHidden = true - preloadHostView.isUserInteractionEnabled = false - preloadHostView.clipsToBounds = true - preloadHostView.frame = bounds - preloadHostView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - insertSubview(preloadHostView, at: 0) - } - - /// 判断指定页码是否可以被缓存 - /// - Parameter pageNum: 页码 - /// - Returns: true 表示可以缓存(有效页码且在范围内) - private func shouldCachePage(_ pageNum: Int) -> Bool { - pageNum >= 0 - && pageNum != RDReaderView.blankPageNum - && pageNum != RDReaderView.blankEndPageNum - && pageNum < (dataSource?.pageCountOfReaderView(readerView: self) ?? 0) - } - - /// 获取当前缓存签名,用于检测布局环境是否发生变化 - private func currentCacheSignature() -> CacheSignature { - CacheSignature( + private var preloadEnvironment: RDReaderPreloadController.Environment { + RDReaderPreloadController.Environment( displayType: currentDisplayType, isLandscape: isLandscape, pagesPerScreen: pagesPerScreen, - boundsSize: bounds.size + boundsSize: bounds.size, + landscapeDualPageEnabled: landscapeDualPageEnabled, + coverPageIndex: coverPageIndex, + totalPages: numberOfPages(), + spreadResolver: spreadResolver ) } - /// 清空所有页面缓存并更新缓存签名 - /// 在翻页模式切换、横竖屏切换时调用 private func invalidatePageCaches() { - pageCurlCachedViews.values.forEach { $0.removeFromSuperview() } - preloadedPageViews.values.forEach { $0.removeFromSuperview() } - pageCurlCachedViews.removeAll() - preloadedPageViews.removeAll() - cacheSignature = currentCacheSignature() + preloadController.invalidate(environment: preloadEnvironment) } - /// 检查缓存签名是否变化,变化则清空缓存 - private func refreshCacheSignatureIfNeeded() { - let signature = currentCacheSignature() - if cacheSignature != signature { - invalidatePageCaches() - } else if cacheSignature == nil { - cacheSignature = signature - } + func pageViewForDisplay(pageNum: Int) -> UIView { + preloadController.pageViewForDisplay( + pageNum: pageNum, + environment: preloadEnvironment, + contentViewProvider: pageContentViewForPreload(pageNum:reusableView:) + ) } - /// 计算指定页码所在"展开对"的起始页码 - /// 单页模式返回自身,双页模式返回配对的左页码 - private func spreadStart(for pageNum: Int) -> Int? { - guard shouldCachePage(pageNum) else { return nil } - let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll - guard isDualPage else { return pageNum } - return dualPagePair(for: pageNum).left + func primePageCache(around pageNum: Int, preferredForward: Bool? = nil) { + preloadController.prime( + around: pageNum, + preferredForward: preferredForward, + parentView: self, + environment: preloadEnvironment, + contentViewProvider: pageContentViewForPreload(pageNum:reusableView:) + ) } - /// 获取指定页码所在"展开对"的所有页码 - /// 单页模式返回自身,双页模式返回左右两页 - private func spreadPageNumbers(startingAt pageNum: Int) -> Set { - guard shouldCachePage(pageNum) else { return [] } - let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll - guard isDualPage else { return [pageNum] } - let pair = dualPagePair(for: pageNum) - var pages: Set = [pair.left] - if let right = pair.right, shouldCachePage(right) { - pages.insert(right) - } - return pages - } - - /// 计算相邻"展开对"的起始页码,用于预加载时计算前后相邻页 - private func adjacentSpreadStart(from pageNum: Int, forward: Bool) -> Int? { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - guard totalPages > 0, - let spreadStart = spreadStart(for: pageNum) else { - return nil - } - - let isDualPage = landscapeDualPageEnabled && isLandscape && currentDisplayType != .verticalScroll - guard isDualPage else { - let target = forward ? spreadStart + 1 : spreadStart - 1 - return shouldCachePage(target) ? target : nil - } - - if forward { - return adjacentDualPage(from: spreadStart, forward: true) - } - - let pair = dualPagePair(for: spreadStart) - let prevEnd = pair.left - 1 - guard prevEnd >= 0 else { return nil } - return dualPagePair(for: prevEnd).left - } - - /// 获取锚定页码对应的可见页码集合 - private func visiblePageNumbers(for anchorPage: Int) -> Set { - spreadPageNumbers(startingAt: anchorPage) - } - - /// 获取用于显示的页面视图 - /// 优先从缓存中获取,其次从预加载缓存中获取,最后新建 - private func pageViewForDisplay(pageNum: Int) -> UIView { - let reusableView = detachedReusablePageView(for: pageNum) - let view = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: reusableView) - ?? reusableView - ?? UIView() - pageCurlCachedViews[pageNum] = view - return view - } - - /// 返回可安全复用的 page view。 - /// 只有已经脱离视图层级的 view 才能复用,避免 pageCurl 转场期间同一个 UIView - /// 被多个 child controller 争用,从而导致前一个页面瞬时空白。 - private func detachedReusablePageView(for pageNum: Int) -> UIView? { - if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) { - preloaded.removeFromSuperview() - return preloaded - } - - guard let cached = pageCurlCachedViews[pageNum], cached.superview == nil else { - return nil - } - - pageCurlCachedViews.removeValue(forKey: pageNum) - return cached - } - - /// 裁剪缓存,只保留指定页码集合中的页面 - /// 不在保留集合中的页面会被从视图层级中移除 - private func trimCachedPageViews(keeping pageNumbers: Set) { - pageCurlCachedViews = pageCurlCachedViews.filter { key, value in - let keep = pageNumbers.contains(key) - if !keep { - value.removeFromSuperview() - } - return keep - } - - preloadedPageViews = preloadedPageViews.filter { key, value in - let keep = pageNumbers.contains(key) - if !keep { - value.removeFromSuperview() - } - return keep - } - } - - /// 预测需要预加载的页码集合 - /// 基于当前页码和 preloadRadius,向前和向后各预测 preloadRadius 屏, - /// 并根据 preferredForward 方向额外多预测一屏 - private func forecastTargets(around pageNum: Int, preferredForward: Bool?) -> [Int] { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - guard totalPages > 0, shouldCachePage(pageNum) else { return [] } - - var targets = Set() - - var previousAnchor = pageNum - for _ in 0.. UIView? { + contentViewForPage(pageNum, reusableView: reusableView) } /// 视图被添加到父视图时调用,触发 UI 构建 @@ -804,8 +595,8 @@ public class RDReaderView: UIView { if currentDisplayType == .pageCurl { attachPageViewControllerIfNeeded() } - ensurePreloadHostView() - cacheSignature = currentCacheSignature() + preloadController.ensureHostView(in: self) + preloadController.initializeSignature(preloadEnvironment) addGestureRecognizer(tapGestureRecognizer) // 不取消底层触摸事件,确保工具栏按钮(返回等)的 touchUpInside 能正常触发 @@ -819,142 +610,16 @@ public class RDReaderView: UIView { let point = tap.location(in: tap.view) if isShowToolView { let hitView = hitTest(point, with: nil) + print("[Debug] tapAction: isShowToolView=true, point=\(point), hitView=\(String(describing: hitView)), top=\(String(describing: topToolView))") if let top = topToolView, isHitView(hitView, inside: top, point: point) { return } if let bottom = bottomToolView, isHitView(hitView, inside: bottom, point: point) { return } } - let viewFrame = tap.view!.frame - let leftFrame = CGRect(x: 0, y: 0, width: viewFrame.width / 3, height: viewFrame.height) - let centerFrame = CGRect(x: viewFrame.width / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) - let rightFrame = CGRect(x: viewFrame.width * 2 / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) - if leftFrame.contains(point) { - if isShowToolView { - tapEvent = .center - } else { - tapEvent = .left - } - } - - if centerFrame.contains(point) { - tapEvent = .center - } - - if rightFrame.contains(point) { - if isShowToolView { - tapEvent = .center - } else { - tapEvent = .right - } - } - } - - /// 点击屏幕中央区域,切换工具栏的显示/隐藏 - /// 显示时:工具栏从屏幕外滑入,禁用翻页交互 - /// 隐藏时:工具栏滑出屏幕,恢复翻页交互 - private func tapCenter() { - isShowToolView = !isShowToolView - if isShowToolView { - if let topToolView = topToolView { - installToolViewIfNeeded(topToolView, position: .top) - layoutIfNeeded() - topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) - UIView.animate(withDuration: toolViewAnimationDuration) { - topToolView.transform = .identity - } - } - - if let bottomToolView = bottomToolView { - installToolViewIfNeeded(bottomToolView, position: .bottom) - layoutIfNeeded() - bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) - UIView.animate(withDuration: toolViewAnimationDuration) { - bottomToolView.transform = .identity - } - } - - collectionView.isUserInteractionEnabled = false - pageViewController.view.isUserInteractionEnabled = false - } else { - if let topToolView = topToolView { - UIView.animate(withDuration: toolViewAnimationDuration, animations: { - topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) - }) { _ in - topToolView.removeFromSuperview() - } - } - - if let bottomToolView = bottomToolView { - UIView.animate(withDuration: toolViewAnimationDuration, animations: { - bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) - }) { _ in - bottomToolView.removeFromSuperview() - } - } - - collectionView.isUserInteractionEnabled = true - pageViewController.view.isUserInteractionEnabled = true - } - } - - /// 判断点击命中的视图是否在指定工具栏内 - private func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool { - if toolView.frame.contains(point) { - return true - } - - guard let hitView else { return false } - return hitView === toolView || hitView.isDescendant(of: toolView) - } - - private enum ToolViewPosition { - case top - case bottom - } - - private func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) { - guard toolView.superview !== self else { return } - - toolView.removeFromSuperview() - addSubview(toolView) - toolView.translatesAutoresizingMaskIntoConstraints = false - - let heightConstraint: NSLayoutConstraint - switch position { - case .top: - topToolViewHeightConstraint?.isActive = false - heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .top)) - topToolViewHeightConstraint = heightConstraint - NSLayoutConstraint.activate([ - toolView.leadingAnchor.constraint(equalTo: leadingAnchor), - toolView.trailingAnchor.constraint(equalTo: trailingAnchor), - toolView.topAnchor.constraint(equalTo: topAnchor), - heightConstraint - ]) - case .bottom: - bottomToolViewHeightConstraint?.isActive = false - heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .bottom)) - bottomToolViewHeightConstraint = heightConstraint - NSLayoutConstraint.activate([ - toolView.leadingAnchor.constraint(equalTo: leadingAnchor), - toolView.trailingAnchor.constraint(equalTo: trailingAnchor), - toolView.bottomAnchor.constraint(equalTo: bottomAnchor), - heightConstraint - ]) - } - } - - private func updateToolViewHeightConstraintsIfNeeded() { - topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top) - bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom) - } - - private func resolvedToolViewHeight(for position: ToolViewPosition) -> CGFloat { - let contentHeight: CGFloat = 52 - switch position { - case .top: - return safeAreaInsets.top + contentHeight - case .bottom: - return safeAreaInsets.bottom + contentHeight - } + guard let viewFrame = tap.view?.frame else { return } + tapEvent = tapRegionHandler.resolveTapEvent( + point: point, + viewFrame: viewFrame, + isToolViewVisible: isShowToolView + ) } /// 切换翻页模式(仿真/水平滚动/上下滚动) @@ -1071,8 +736,8 @@ public class RDReaderView: UIView { /// 重新切换到当前翻页模式,并刷新工具栏 public func reloadData() { switchReaderDisplayType(currentDisplayType) - topToolView = self.dataSource?.topToolView?(readerView: self) - bottomToolView = self.dataSource?.bottomToolView?(readerView: self) + topToolView = resolvedTopChromeView() + bottomToolView = resolvedBottomChromeView() } @@ -1082,289 +747,40 @@ public class RDReaderView: UIView { } - - -/// UIPageViewController 数据源和代理实现 -/// 处理仿真翻页模式下的页面数据供给和翻页事件 -extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate { - - /// 创建指定页码的子控制器 - /// 空白页返回空视图,正常页从缓存或数据源获取内容 - private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController { - if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum { - return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum) - } - let contentView = pageViewForDisplay(pageNum: pageNum) - return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum) +extension RDReaderView: RDReaderPageNavigating { + public func reloadPages() { + reloadData() } - /// 计算某页的"下一页"页码 - /// 考虑封面页独占和末尾空白页配对的情况 - /// - Parameters: - /// - pageNum: 当前页码 - /// - isDualPage: 是否双页模式 - /// - Returns: 下一页页码,nil 表示没有下一页 - private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - - // 末尾空白页之后没有更多页 - if pageNum == RDReaderView.blankEndPageNum { - return nil - } - - if isDualPage, let coverIndex = coverPageIndex { - if pageNum == coverIndex { - return RDReaderView.blankPageNum - } - if pageNum == RDReaderView.blankPageNum { - let firstContent = coverIndex + 1 - return firstContent < totalPages ? firstContent : nil - } - } - - let next = pageNum + 1 - if next < totalPages { - return next - } - - // pageNum 是最后一页,检查在双页模式下是否需要空白页配对 - if isDualPage { - if let coverIndex = coverPageIndex { - // 有封面时:封面之后的页面两两配对,偶数偏移=左页需要配对 - let adjustedIndex = pageNum - (coverIndex + 1) - if adjustedIndex >= 0 && adjustedIndex % 2 == 0 { - return RDReaderView.blankEndPageNum - } - } else { - // 无封面:偶数索引=左页需要配对 - if pageNum % 2 == 0 { - return RDReaderView.blankEndPageNum - } - } - } - return nil - } - - /// 计算某页的"上一页"页码 - /// 考虑封面页独占和末尾空白页配对的情况 - /// - Parameters: - /// - pageNum: 当前页码 - /// - isDualPage: 是否双页模式 - /// - Returns: 上一页页码,nil 表示没有上一页 - private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? { - // 末尾空白页的前一页是最后一个真实页 - if pageNum == RDReaderView.blankEndPageNum { - let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - return totalPages > 0 ? totalPages - 1 : nil - } - if isDualPage, let coverIndex = coverPageIndex { - if pageNum == RDReaderView.blankPageNum { - return coverIndex - } - if pageNum == coverIndex + 1 { - return RDReaderView.blankPageNum - } - } - let prev = pageNum - 1 - return prev >= 0 ? prev : nil - } - - /// UIPageViewController 数据源:返回当前页之前(左/上)的页面控制器 - /// RTL 模式下 before/after 语义互换:before(向右翻)= 下一页 - public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let vc = viewController as? RDReaderPageChildViewController else { return nil } - let isDualPage = landscapeDualPageEnabled && isLandscape - let isRTL = pageDirection == .rightToLeft - - // RTL 时 before/after 语义互换:before(向右翻)= 下一页 - let targetNum = isRTL - ? nextPageNum(after: vc.pageNum, isDualPage: isDualPage) - : prevPageNum(before: vc.pageNum, isDualPage: isDualPage) - - guard let num = targetNum else { return nil } - let targetVC = makeSinglePageChildVC(for: num) - willPreviousTransitionToViewController = targetVC - return targetVC - } - - /// UIPageViewController 数据源:返回当前页之后(右/下)的页面控制器 - /// RTL 模式下 before/after 语义互换 - public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let vc = viewController as? RDReaderPageChildViewController else { return nil } - let isDualPage = landscapeDualPageEnabled && isLandscape - let isRTL = pageDirection == .rightToLeft - - let targetNum = isRTL - ? prevPageNum(before: vc.pageNum, isDualPage: isDualPage) - : nextPageNum(after: vc.pageNum, isDualPage: isDualPage) - - guard let num = targetNum else { return nil } - let targetVC = makeSinglePageChildVC(for: num) - willNextTransitionToViewController = targetVC - return targetVC - } - - /// UIPageViewController 代理:翻页动画完成回调 - /// 更新当前页码,触发预加载,并检测 PageViewController 异常状态 - public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - if completed, let firstVC = pageViewController.viewControllers?.first as? RDReaderPageChildViewController { - let pn = firstVC.pageNum - if pn != RDReaderView.blankPageNum && pn != RDReaderView.blankEndPageNum { - currentPage = pn - primePageCache(around: pn, preferredForward: predictedPageDirection) - } - } - - predictedPageDirection = nil - finishPageCurlTransition() - } - - /// UIPageViewController 代理:即将开始转场动画 - /// 记录预测的翻页方向,用于优化预加载策略 - public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { - isTransitioning = true - willTransitionToViewController = pendingViewControllers.first - if let target = pendingViewControllers.first as? RDReaderPageChildViewController, - target.pageNum != RDReaderView.blankPageNum, - target.pageNum != RDReaderView.blankEndPageNum { - predictedPageDirection = target.pageNum >= currentPage - primePageCache(around: target.pageNum, preferredForward: predictedPageDirection) - } - } - -} - -/// UICollectionView 数据源和自定义布局代理实现 -/// 处理水平滚动和垂直滚动两种翻页模式 -extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure { - /// 配置 UICollectionViewCell - /// 通过 DataSource 获取页面内容视图,支持 cell 复用和预加载视图复用 - /// RTL 水平模式下翻转 cell 内容使文字方向正常 - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - - if let identifer = self.dataSource?.pageIdentifier(readerView: self, pageNum: indexPath.row) { - - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifer, for: IndexPath(item: indexPath.row, section: 0)) as! RDReaderContentCell - let preloadedView = preloadedPageViews.removeValue(forKey: indexPath.row) - preloadedView?.removeFromSuperview() - let reusableView = cell.containerView ?? preloadedView - let conttainerView = self.dataSource?.pageContentView(readerView: self, pageNum: indexPath.row, containerView: reusableView) - if let conttainerView = conttainerView { - cell.containerView = conttainerView - } - // RTL 水平模式:翻转 cell 内容使文字方向正常(collectionView 已整体翻转) - if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll { - cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1) - } else { - cell.contentView.transform = .identity - } - return cell - } - - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath) - - return cell - } - - /// 返回每组的 item 数量,即总页数 - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return self.dataSource?.pageCountOfReaderView(readerView: self) ?? 0 - } - - /// 布局代理回调:当前可见页码变化时通知 - public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) { - if currentPage >= 0, currentPage != pageIndex { - predictedPageDirection = pageIndex >= currentPage - } - currentPage = pageIndex - primePageCache(around: pageIndex, preferredForward: predictedPageDirection) - } - - /// 布局数据源回调:返回垂直滚动模式下指定页的高度 - /// 当前返回 nil 使用默认高度 - public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? { - return nil + public func transition(to page: Int, animated: Bool) { + transitionToPage(pageNum: page, animated: animated) } } -/// 内容视图注册和复用扩展 -/// 提供类似 UITableView 的 register/dequeueReusable 机制 -extension RDReaderView { - /// 注册内容视图类型 - /// - Parameters: - /// - contentView: 内容视图的类型 - /// - identifier: 重用标识符 - public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) { - contentViews[identifier] = contentView - collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier) +extension RDReaderView: UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + guard gestureRecognizer === tapGestureRecognizer else { return true } + + let point = touch.location(in: self) + if let topToolView, isHitView(touch.view, inside: topToolView, point: point) { + return false + } + if let bottomToolView, isHitView(touch.view, inside: bottomToolView, point: point) { + return false + } + return true } - /// 获取可复用的内容视图 - /// 优先从当前显示的 cell 中获取,其次创建新实例 - public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView { - if self.currentDisplayType != .pageCurl, let cell = self.collectionView.cellForItem(at: IndexPath(row: pageNum, section: 0)) as? RDReaderContentCell, let containerView = cell.containerView { - return containerView - } - let contentViewClass = contentViews[identifier] - assert(contentViewClass != nil, "请调用register(contentView:contentViewWithReuseIdentifier:)") - var contentView = contentViewClass!.init() - return contentView - } - - /// 获取指定页码的内容视图 - /// pageCurl 模式从 PageViewController 获取,滚动模式从 CollectionView cell 获取 - public func pageContentView(pageNum: Int) -> UIView? { - if currentDisplayType == .pageCurl { - return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView - } else { - let cell = collectionView.cellForItem(at: IndexPath(item: pageNum, section: 0)) as? RDReaderContentCell - return cell?.containerView - } - } - - /// 返回当前阅读模式下单页内容容器的实际尺寸。 - /// 优先使用已经显示出来的内容视图尺寸;若页面尚未装载,则回退到当前模式下的理论单页尺寸。 - public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize { - let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil) - if let targetPage, - let contentView = pageContentView(pageNum: targetPage), - contentView.bounds.width > 0, - contentView.bounds.height > 0 { - return contentView.bounds.size - } - - if currentDisplayType == .pageCurl { - if let childViewController = pageViewController.viewControllers?.first as? RDReaderPageChildViewController, - childViewController.view.bounds.width > 0, - childViewController.view.bounds.height > 0 { - return childViewController.view.bounds.size - } - if pageViewController.view.bounds.width > 0, pageViewController.view.bounds.height > 0 { - return pageViewController.view.bounds.size - } - return bounds.size - } - - let containerBounds = collectionView.bounds.size == .zero ? bounds.size : collectionView.bounds.size - guard containerBounds.width > 0, containerBounds.height > 0 else { - return bounds.size - } - - switch currentDisplayType { - case .horizontalScroll: - return CGSize( - width: containerBounds.width / CGFloat(max(pagesPerScreen, 1)), - height: containerBounds.height - ) - case .verticalScroll: - return containerBounds - case .pageCurl: - return bounds.size - } + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + gestureRecognizer === tapGestureRecognizer } } + + private var cellViewKey: Int8 = 0 /// UIView 扩展:通过响应链查找最近的父 UIViewController diff --git a/Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift b/Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift new file mode 100644 index 0000000..382a1c0 --- /dev/null +++ b/Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift @@ -0,0 +1,90 @@ +import UIKit + +/// 阅读器数据源协议 +/// 上层控制器通过实现此协议,向 RDReaderView 提供页面数量、内容视图和工具栏。 +/// 所有方法由 RDReaderView 在需要渲染页面时回调。 +@objc public protocol RDReaderDataSource: NSObjectProtocol { + /// 返回阅读器的总页数 + func pageCountOfReaderView(readerView: RDReaderView) -> Int + /// 返回指定页码的内容视图 + func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView + /// 返回指定页码的唯一标识符,用于 UICollectionViewCell 复用 + func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? + /// 返回顶部工具栏视图(可选),点击屏幕中央时会显示/隐藏 + @objc optional func topToolView(readerView: RDReaderView) -> UIView? + /// 返回底部工具栏视图(可选),点击屏幕中央时会显示/隐藏 + @objc optional func bottomToolView(readerView: RDReaderView) -> UIView? +} + +/// 与内容格式无关的统一分页提供者协议。 +/// 逐步替代面向 EPUB 命名的 ``RDReaderDataSource``,方便后续复用到 PDF 等其他阅读场景。 +@objc public protocol RDReaderPageProvider: NSObjectProtocol { + func numberOfPages(in readerView: RDReaderView) -> Int + func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView + @objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String? + @objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView? + @objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView? +} + +/// 阅读器代理协议 +/// 上层控制器通过实现此协议,接收翻页和横竖屏切换事件 +@objc public protocol RDReaderDelegate: NSObjectProtocol { + /// 翻页回调,当当前显示页码变化时触发 + func pageNum(readerView: RDReaderView, pageNum: Int) + /// 横竖屏切换时回调,可在此重新分页 + @objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) +} + +public protocol RDReaderPageNavigating: AnyObject { + var currentPage: Int { get } + func reloadPages() + func transition(to page: Int, animated: Bool) +} + +// MARK: - 枚举 + +extension RDReaderView { + /// 翻页模式枚举,决定 RDReaderView 使用哪种底层视图来展示内容 + public enum DisplayType { + /// 仿真翻页:使用 UIPageViewController,模拟真实翻书效果 + case pageCurl + /// 水平滚动:使用 UICollectionView + 自定义布局,每屏一项,水平分页 + case horizontalScroll + /// 垂直滚动:使用 UICollectionView,全宽项目,垂直连续滚动 + case verticalScroll + } + + /// 翻页方向枚举 + public enum PageDirection { + /// 从左往右翻页(默认,适用于中文/英文书籍) + case leftToRight + /// 从右往左翻页(适用于日文漫画等) + case rightToLeft + } +} + +// MARK: - Legacy 适配器 + +final class RDReaderLegacyDataSourceAdapter: NSObject, RDReaderPageProvider { + weak var dataSource: RDReaderDataSource? + + func numberOfPages(in readerView: RDReaderView) -> Int { + dataSource?.pageCountOfReaderView(readerView: readerView) ?? 0 + } + + func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView { + dataSource?.pageContentView(readerView: readerView, pageNum: index, containerView: reusableView) ?? UIView() + } + + func pageIdentifier(in readerView: RDReaderView, index: Int) -> String? { + dataSource?.pageIdentifier(readerView: readerView, pageNum: index) + } + + func readerViewTopChrome(_ readerView: RDReaderView) -> UIView? { + dataSource?.topToolView?(readerView: readerView) + } + + func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView? { + dataSource?.bottomToolView?(readerView: readerView) + } +}