From cc62715bffbdca7732ced0c0135031d3e64e6362 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 23 Apr 2020 18:07:12 -0700 Subject: [PATCH 1/6] First iteration of the pop-up footnotes. --- Cartfile | 1 + Cartfile.resolved | 1 + r2-navigator-swift.xcodeproj/project.pbxproj | 67 ++++++++++++++++++- .../EPUB/EPUBNavigatorViewController.swift | 42 ++++++++++++ r2-navigator-swift/EPUB/EPUBSpreadView.swift | 13 ++++ .../EPUB/Resources/Scripts/gestures.js | 20 ++++++ r2-navigator-swift/Navigator.swift | 47 ++++++++++++- .../Toolkit/BarButtonItem.swift | 40 +++++++++++ 8 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 r2-navigator-swift/Toolkit/BarButtonItem.swift diff --git a/Cartfile b/Cartfile index 91354f8c..f3bd16a3 100755 --- a/Cartfile +++ b/Cartfile @@ -1 +1,2 @@ github "readium/r2-shared-swift" == 1.4.3 +github "scinfu/SwiftSoup" == 2.3.1 diff --git a/Cartfile.resolved b/Cartfile.resolved index 8727a2fa..a2b9bf36 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1,2 @@ github "readium/r2-shared-swift" "1.4.3" +github "scinfu/SwiftSoup" "2.3.1" diff --git a/r2-navigator-swift.xcodeproj/project.pbxproj b/r2-navigator-swift.xcodeproj/project.pbxproj index 446ba201..8f679917 100644 --- a/r2-navigator-swift.xcodeproj/project.pbxproj +++ b/r2-navigator-swift.xcodeproj/project.pbxproj @@ -7,7 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C3CC67222DBD8600A01731 /* R2Shared.framework */; }; + 0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D777488244F977E00A5E857 /* R2Shared.framework */; }; + 0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D77747E244F970E00A5E857 /* SwiftSoup.framework */; }; + 0D777495244FEB2200A5E857 /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D777494244FEB2200A5E857 /* BarButtonItem.swift */; }; CA0B3AC3222EE555006D9363 /* PDFNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */; }; CA1E4F4B240037E6009C4DE3 /* CompletionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */; }; CA26EF7E22803FE90011653E /* VisualNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA26EF7D22803FE90011653E /* VisualNavigator.swift */; }; @@ -35,8 +37,30 @@ F3E7D42E1F4EE0FE00DF166D /* CBZNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E7D42D1F4EE0FE00DF166D /* CBZNavigatorViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 0D777487244F977E00A5E857 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F3E7D3F41F4EBE2100DF166D; + remoteInfo = "r2-shared-swift"; + }; + 0D777489244F977E00A5E857 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA6161EA21FB257700D2CFE3; + remoteInfo = "r2-shared-swiftTests"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 03C3CC67222DBD8600A01731 /* R2Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = R2Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0D77747E244F970E00A5E857 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = "../r2-testapp-swift/Carthage/Build/iOS/SwiftSoup.framework"; sourceTree = ""; }; + 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "r2-shared-swift.xcodeproj"; path = "../r2-shared-swift/r2-shared-swift.xcodeproj"; sourceTree = ""; }; + 0D777494244FEB2200A5E857 /* BarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; + 0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = Carthage/Build/iOS/SwiftSoup.framework; sourceTree = ""; }; + 0D82BF11244EAE04006FDB31 /* R2Shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = R2Shared.framework; path = Carthage/Build/iOS/R2Shared.framework; sourceTree = ""; }; CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFNavigatorViewController.swift; sourceTree = ""; }; CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionList.swift; sourceTree = ""; }; CA26EF7D22803FE90011653E /* VisualNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualNavigator.swift; sourceTree = ""; }; @@ -73,13 +97,23 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */, + 0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */, + 0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0D777483244F977E00A5E857 /* Products */ = { + isa = PBXGroup; + children = ( + 0D777488244F977E00A5E857 /* R2Shared.framework */, + 0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; CA0B3AC1222EE530006D9363 /* PDF */ = { isa = PBXGroup; children = ( @@ -98,6 +132,7 @@ CAC2A6D62292E4BA000AA2A7 /* WebView.swift */, CAD178B522B3B553004E6812 /* R2NavigatorLocalizedString.swift */, CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */, + 0D777494244FEB2200A5E857 /* BarButtonItem.swift */, ); path = Toolkit; sourceTree = ""; @@ -191,6 +226,10 @@ F3E7D3E71F4DC40800DF166D /* Frameworks */ = { isa = PBXGroup; children = ( + 0D77747E244F970E00A5E857 /* SwiftSoup.framework */, + 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */, + 0D82BF11244EAE04006FDB31 /* R2Shared.framework */, + 0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */, 03C3CC67222DBD8600A01731 /* R2Shared.framework */, ); name = Frameworks; @@ -255,6 +294,12 @@ mainGroup = F3E7D3B91F4D83B000DF166D; productRefGroup = F3E7D3C41F4D83B000DF166D /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 0D777483244F977E00A5E857 /* Products */; + ProjectRef = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; + }, + ); projectRoot = ""; targets = ( F3E7D3C21F4D83B000DF166D /* r2-navigator-swift */, @@ -262,6 +307,23 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + 0D777488244F977E00A5E857 /* R2Shared.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = R2Shared.framework; + remoteRef = 0D777487244F977E00A5E857 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "r2-shared-swiftTests.xctest"; + remoteRef = 0D777489244F977E00A5E857 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ F3E7D3C11F4D83B000DF166D /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -291,6 +353,7 @@ F3E7D3E61F4D84EF00DF166D /* EPUBSpreadView.swift in Sources */, F3E7D42E1F4EE0FE00DF166D /* CBZNavigatorViewController.swift in Sources */, CA479DC52264AEA20053445E /* UIColor.swift in Sources */, + 0D777495244FEB2200A5E857 /* BarButtonItem.swift in Sources */, CAD178B622B3B553004E6812 /* R2NavigatorLocalizedString.swift in Sources */, CAEACA222272EFBD00476340 /* ImageViewController.swift in Sources */, CACE84FB2254BFEE00E19E8B /* EditingAction.swift in Sources */, diff --git a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift index fe551d2e..eb516455 100644 --- a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift +++ b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift @@ -13,6 +13,7 @@ import UIKit import R2Shared import WebKit import SafariServices +import SwiftSoup public protocol EPUBNavigatorDelegate: VisualNavigatorDelegate { @@ -370,6 +371,47 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { go(to: Link(href: href)) } + func spreadView(_ spreadView: EPUBSpreadView, didTapOnNoterefLink html: String, resource: URL) { + + do { + let doc = try parse(html) + guard let link = try doc.select("a[epub:type=noteref]").first() else { + return log(.error, "Could not find noteref link") + } + + let href = try link.attr("href") + guard let hashIndex = href.lastIndex(of: "#") else { + return log(.error, "Could not find hash in link \(href)") + } + let id = String(href[href.index(hashIndex, offsetBy: 1)...]) + let withoutFragment = String(href[..") + } + + let from = try link.html() + + delegate?.navigator(self, presentNote: safe, at: Link(href: absolute.absoluteString), from: from) + + } catch { + log(.error, "Caught error while displaying noteref: \(error)") + } + + } + func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) { if paginationView.currentView == spreadView { notifyCurrentLocation() diff --git a/r2-navigator-swift/EPUB/EPUBSpreadView.swift b/r2-navigator-swift/EPUB/EPUBSpreadView.swift index 4cfbe7e4..49da763f 100644 --- a/r2-navigator-swift/EPUB/EPUBSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBSpreadView.swift @@ -29,6 +29,9 @@ protocol EPUBSpreadViewDelegate: class { /// Called when the user tapped on an internal link. func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String) + /// Called when the user taps on a noteref link. + func spreadView(_ spreadView: EPUBSpreadView, didTapOnNoterefLink html: String, resource: URL) + /// Called when the pages visible in the spread changed. func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) @@ -203,6 +206,15 @@ class EPUBSpreadView: UIView, Loggable { delegate?.spreadView(self, didTapAt: point) } + /// Called from the JS code when a noteref is tapped. + private func didTapNoteref(_ html: Any) { + guard + let html = html as? String, + let url = self.webView.url + else { return } + delegate?.spreadView(self, didTapOnNoterefLink: html, resource: url) + } + /// Converts the touch data returned by the JavaScript `tap` event into a point in the webview's coordinate space. func pointFromTap(_ data: [String: Any]) -> CGPoint? { // To override in subclasses. @@ -344,6 +356,7 @@ class EPUBSpreadView: UIView, Loggable { registerJSMessage(named: "tap") { [weak self] in self?.didTap($0) } registerJSMessage(named: "spreadLoaded") { [weak self] in self?.spreadDidLoad($0) } registerJSMessage(named: "selectionChanged") { [weak self] in self?.selectionDidChange($0) } + registerJSMessage(named: "tapNoteref") { [weak self] in self?.didTapNoteref($0) } } /// Add the message handlers for incoming javascript events. diff --git a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js index 3244b683..ee17737f 100644 --- a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js +++ b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js @@ -7,6 +7,14 @@ }); function onClick(event) { + if (isNoteref(event.target)) { + event.stopPropagation(); + event.preventDefault(); + + webkit.messageHandlers.tapNoteref.postMessage(event.target.outerHTML); + return; + } + if (event.defaultPrevented || isInteractiveElement(event.target)) { return; } @@ -61,4 +69,16 @@ return false; } + function isNoteref(element) { + if ( + element.nodeName.toLowerCase() === 'a' && + element.getAttributeNS('http://www.idpf.org/2007/ops', 'type') === 'noteref' + ) { + return true; + } + if (element.parentElement) { + return isNoteref(element.parentElement); + } + } + })(); diff --git a/r2-navigator-swift/Navigator.swift b/r2-navigator-swift/Navigator.swift index cb4d6f11..bbbd7561 100644 --- a/r2-navigator-swift/Navigator.swift +++ b/r2-navigator-swift/Navigator.swift @@ -12,9 +12,11 @@ import Foundation import SafariServices import R2Shared +import SwiftSoup +import WebKit -public protocol Navigator { +public protocol Navigator: UIViewController { /// Current position in the publication. /// Can be used to save a bookmark to the current position. @@ -86,6 +88,9 @@ public protocol NavigatorDelegate: AnyObject { /// Called when the user tapped an external URL. The default implementation opens the URL with the default browser. func navigator(_ navigator: Navigator, presentExternalURL url: URL) + /// Called when the user tapped on a noteref such as a footnote. The default implementation opens the target "aside" in a modal. + func navigator(_ navigator: Navigator, presentNote content: String, at link: Link, from source: String) + } @@ -99,6 +104,46 @@ public extension NavigatorDelegate { } +public extension NavigatorDelegate { + + func navigator(_ navigator: Navigator, presentNote content: String, at link: Link, from source: String) { + + var title = try? clean(source, .none()) + if title == "*" { + title = nil + } + + let content = (try? clean(content, .none())) ?? "" + let page = + """ + + + + + + \(content) + + + """ + + + let wk = WKWebView() + wk.loadHTMLString(page, baseURL: nil) + + let vc = UIViewController() + vc.view = wk + vc.navigationItem.title = title + vc.navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done, actionHandler: { (item) in + vc.dismiss(animated: true, completion: nil) + }) + + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .formSheet + navigator.present(nav, animated: true, completion: nil) + } + +} + public enum NavigatorError: LocalizedError { /// The user tried to copy the text selection but the DRM License doesn't allow it. diff --git a/r2-navigator-swift/Toolkit/BarButtonItem.swift b/r2-navigator-swift/Toolkit/BarButtonItem.swift new file mode 100644 index 00000000..d55173a2 --- /dev/null +++ b/r2-navigator-swift/Toolkit/BarButtonItem.swift @@ -0,0 +1,40 @@ +// +// BarButtonItem.swift +// r2-navigator-swift +// +// Created by Matt McCullough on 4/21/20. +// +// Copyright 2020 Readium Foundation. All rights reserved. +// Use of this source code is governed by a BSD-style license which is detailed +// in the LICENSE file present in the project repository where this source code is maintained. +// + +import UIKit + +class BarButtonItem: UIBarButtonItem { + typealias ActionFunc = (UIBarButtonItem) -> Void + + private var actionFunc: ActionFunc? + + convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionFunc?) { + self.init(title: title, style: style, target: nil, action: #selector(handlePress)) + target = self + self.actionFunc = actionHandler + } + + convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionFunc: ActionFunc?) { + self.init(image: image, style: style, target: nil, action: #selector(handlePress)) + target = self + self.actionFunc = actionFunc + } + + convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionFunc?) { + self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(handlePress)) + target = self + self.actionFunc = actionHandler + } + + @objc func handlePress(sender: UIBarButtonItem) { + actionFunc?(sender) + } +} From 7514e0655c48fd598a3b12f4a471d3c83b7ca4bc Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 23 Apr 2020 18:02:02 -0700 Subject: [PATCH 2/6] Based on discussion in #113 removed default popup implementation from navigator, made touch handling always send data across the bridge, noteref delegate method returns bool. --- r2-navigator-swift.xcodeproj/project.pbxproj | 5 -- .../EPUB/EPUBFixedSpreadView.swift | 7 +-- .../EPUB/EPUBNavigatorViewController.swift | 58 +++++++++++------- .../EPUB/EPUBReflowableSpreadView.swift | 7 +-- r2-navigator-swift/EPUB/EPUBSpreadView.swift | 60 ++++++++++++------- .../EPUB/Resources/Scripts/gestures.js | 35 ++++++----- r2-navigator-swift/Navigator.swift | 50 ++-------------- .../Toolkit/BarButtonItem.swift | 40 ------------- 8 files changed, 108 insertions(+), 154 deletions(-) delete mode 100644 r2-navigator-swift/Toolkit/BarButtonItem.swift diff --git a/r2-navigator-swift.xcodeproj/project.pbxproj b/r2-navigator-swift.xcodeproj/project.pbxproj index 8f679917..0ad139b2 100644 --- a/r2-navigator-swift.xcodeproj/project.pbxproj +++ b/r2-navigator-swift.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D777488244F977E00A5E857 /* R2Shared.framework */; }; 0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D77747E244F970E00A5E857 /* SwiftSoup.framework */; }; - 0D777495244FEB2200A5E857 /* BarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D777494244FEB2200A5E857 /* BarButtonItem.swift */; }; CA0B3AC3222EE555006D9363 /* PDFNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */; }; CA1E4F4B240037E6009C4DE3 /* CompletionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */; }; CA26EF7E22803FE90011653E /* VisualNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA26EF7D22803FE90011653E /* VisualNavigator.swift */; }; @@ -58,7 +57,6 @@ 03C3CC67222DBD8600A01731 /* R2Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = R2Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0D77747E244F970E00A5E857 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = "../r2-testapp-swift/Carthage/Build/iOS/SwiftSoup.framework"; sourceTree = ""; }; 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "r2-shared-swift.xcodeproj"; path = "../r2-shared-swift/r2-shared-swift.xcodeproj"; sourceTree = ""; }; - 0D777494244FEB2200A5E857 /* BarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarButtonItem.swift; sourceTree = ""; }; 0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = Carthage/Build/iOS/SwiftSoup.framework; sourceTree = ""; }; 0D82BF11244EAE04006FDB31 /* R2Shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = R2Shared.framework; path = Carthage/Build/iOS/R2Shared.framework; sourceTree = ""; }; CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFNavigatorViewController.swift; sourceTree = ""; }; @@ -131,8 +129,6 @@ CAF1E3F422DF23F400E807EA /* PaginationView.swift */, CAC2A6D62292E4BA000AA2A7 /* WebView.swift */, CAD178B522B3B553004E6812 /* R2NavigatorLocalizedString.swift */, - CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */, - 0D777494244FEB2200A5E857 /* BarButtonItem.swift */, ); path = Toolkit; sourceTree = ""; @@ -353,7 +349,6 @@ F3E7D3E61F4D84EF00DF166D /* EPUBSpreadView.swift in Sources */, F3E7D42E1F4EE0FE00DF166D /* CBZNavigatorViewController.swift in Sources */, CA479DC52264AEA20053445E /* UIColor.swift in Sources */, - 0D777495244FEB2200A5E857 /* BarButtonItem.swift in Sources */, CAD178B622B3B553004E6812 /* R2NavigatorLocalizedString.swift in Sources */, CAEACA222272EFBD00476340 /* ImageViewController.swift in Sources */, CACE84FB2254BFEE00E19E8B /* EditingAction.swift in Sources */, diff --git a/r2-navigator-swift/EPUB/EPUBFixedSpreadView.swift b/r2-navigator-swift/EPUB/EPUBFixedSpreadView.swift index 8e191068..564d402c 100644 --- a/r2-navigator-swift/EPUB/EPUBFixedSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBFixedSpreadView.swift @@ -81,10 +81,9 @@ final class EPUBFixedSpreadView: EPUBSpreadView { """) } - override func pointFromTap(_ data: [String : Any]) -> CGPoint? { - guard let x = data["screenX"] as? Int, let y = data["screenY"] as? Int else { - return nil - } + override func pointFromTap(_ data: TapData) -> CGPoint? { + let x = data.screenX + let y = data.screenY return CGPoint( x: CGFloat(x) * scrollView.zoomScale - scrollView.contentOffset.x + webView.frame.minX, diff --git a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift index eb516455..2af48af2 100644 --- a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift +++ b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift @@ -367,49 +367,67 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { delegate?.navigator(self, presentExternalURL: url) } - func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String) { + func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, anchor: String?) { + + // Check to see if this was a noteref link and give delegate the opportunity to display it. + if let anchor = anchor, let note = getNoteContent(anchor: anchor), let delegate = self.delegate { + if delegate.navigator(self, + shouldNavigateToNoteAt: Link(href: href), + content: note, + source: anchor) == false { + return + } + } + go(to: Link(href: href)) } - func spreadView(_ spreadView: EPUBSpreadView, didTapOnNoterefLink html: String, resource: URL) { - + func getNoteContent(anchor: String) -> String? { do { - let doc = try parse(html) - guard let link = try doc.select("a[epub:type=noteref]").first() else { - return log(.error, "Could not find noteref link") - } + let doc = try parse(anchor) + guard let link = try doc.select("a[epub:type=noteref]").first() else { return nil } let href = try link.attr("href") guard let hashIndex = href.lastIndex(of: "#") else { - return log(.error, "Could not find hash in link \(href)") + log(.error, "Could not find hash in link \(href)") + return nil } let id = String(href[href.index(hashIndex, offsetBy: 1)...]) let withoutFragment = String(href[..") + return nil } - let from = try link.html() - - delegate?.navigator(self, presentNote: safe, at: Link(href: absolute.absoluteString), from: from) + return try aside.html() } catch { - log(.error, "Caught error while displaying noteref: \(error)") + log(.error, "Caught error while getting note content: \(error)") + return nil } - } func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) { diff --git a/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift b/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift index aab2ccc4..c146fb3c 100644 --- a/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBReflowableSpreadView.swift @@ -108,10 +108,9 @@ final class EPUBReflowableSpreadView: EPUBSpreadView { } } - override func pointFromTap(_ data: [String : Any]) -> CGPoint? { - guard let x = data["clientX"] as? Int, let y = data["clientY"] as? Int else { - return nil - } + override func pointFromTap(_ data: TapData) -> CGPoint? { + let x = data.clientX + let y = data.clientY var point = CGPoint(x: x, y: y) if isScrollEnabled { diff --git a/r2-navigator-swift/EPUB/EPUBSpreadView.swift b/r2-navigator-swift/EPUB/EPUBSpreadView.swift index 49da763f..b3805d5f 100644 --- a/r2-navigator-swift/EPUB/EPUBSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBSpreadView.swift @@ -27,10 +27,7 @@ protocol EPUBSpreadViewDelegate: class { func spreadView(_ spreadView: EPUBSpreadView, didTapOnExternalURL url: URL) /// Called when the user tapped on an internal link. - func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String) - - /// Called when the user taps on a noteref link. - func spreadView(_ spreadView: EPUBSpreadView, didTapOnNoterefLink html: String, resource: URL) + func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, anchor: String?) /// Called when the pages visible in the spread changed. func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) @@ -53,6 +50,8 @@ class EPUBSpreadView: UIView, Loggable { let readingProgression: ReadingProgression let userSettings: UserSettings let editingActions: EditingActionsController + + var lastTap: TapData? = nil /// If YES, the content will be faded in once loaded. let animatedLoad: Bool @@ -196,27 +195,20 @@ class EPUBSpreadView: UIView, Loggable { } /// Called from the JS code when a tap is detected. - private func didTap(_ body: Any) { - guard let body = body as? [String: Any], - let point = pointFromTap(body) else - { - return - } - + /// If the JS indicates the tap is being handled within the webview, don't take action, + /// just save the tap data for use by webView(_ webView:decidePolicyFor:decisionHandler:) + private func didTap(_ data: Any) { + let tapData = TapData(data: data) + lastTap = tapData + + guard tapData.shouldHandle else { return } + + guard let point = pointFromTap(tapData) else { return } delegate?.spreadView(self, didTapAt: point) } - /// Called from the JS code when a noteref is tapped. - private func didTapNoteref(_ html: Any) { - guard - let html = html as? String, - let url = self.webView.url - else { return } - delegate?.spreadView(self, didTapOnNoterefLink: html, resource: url) - } - /// Converts the touch data returned by the JavaScript `tap` event into a point in the webview's coordinate space. - func pointFromTap(_ data: [String: Any]) -> CGPoint? { + func pointFromTap(_ data: TapData) -> CGPoint? { // To override in subclasses. return nil } @@ -356,7 +348,6 @@ class EPUBSpreadView: UIView, Loggable { registerJSMessage(named: "tap") { [weak self] in self?.didTap($0) } registerJSMessage(named: "spreadLoaded") { [weak self] in self?.spreadDidLoad($0) } registerJSMessage(named: "selectionChanged") { [weak self] in self?.selectionDidChange($0) } - registerJSMessage(named: "tapNoteref") { [weak self] in self?.didTapNoteref($0) } } /// Add the message handlers for incoming javascript events. @@ -436,7 +427,7 @@ extension EPUBSpreadView: WKNavigationDelegate { // Check if url is internal or external if let baseURL = publication.baseURL, url.host == baseURL.host { let href = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/") - delegate?.spreadView(self, didTapOnInternalLink: href) + delegate?.spreadView(self, didTapOnInternalLink: href, anchor: self.lastTap?.anchor) } else { delegate?.spreadView(self, didTapOnExternalURL: url) } @@ -517,3 +508,26 @@ private extension EPUBSpreadView { } } + +/// Produced by gestures.js +struct TapData { + let shouldHandle: Bool + let screenX: Int + let screenY: Int + let clientX: Int + let clientY: Int + let anchor: String? + + init(dict: [String: Any]) { + self.shouldHandle = dict["shouldHandle"] as? Bool ?? false + self.screenX = dict["screenX"] as? Int ?? 0 + self.screenY = dict["screenY"] as? Int ?? 0 + self.clientX = dict["clientX"] as? Int ?? 0 + self.clientY = dict["clientY"] as? Int ?? 0 + self.anchor = dict["anchor"] as? String + } + + init(data: Any) { + self.init(dict: data as? [String: Any] ?? [String: Any]()) + } +} diff --git a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js index ee17737f..afc53646 100644 --- a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js +++ b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js @@ -7,16 +7,16 @@ }); function onClick(event) { - if (isNoteref(event.target)) { - event.stopPropagation(); - event.preventDefault(); - webkit.messageHandlers.tapNoteref.postMessage(event.target.outerHTML); - return; - } + // If the app should handle the tap. + // Examples of the app handling the tap would be + // navigating left/right, or show/hide the toolbar. + // If false, the tap is being handled within the webview, + // such as with a hyperlink or by an publication's JS handler. + let appShouldHandle = true; if (event.defaultPrevented || isInteractiveElement(event.target)) { - return; + appShouldHandle = false; } if (!window.getSelection().isCollapsed) { @@ -24,11 +24,16 @@ return; } + // Send the tap data over the JS bridge even if it's been handled + // within the webview, so that it can be preserved and used + // by the WKNavigationDelegate if needed. webkit.messageHandlers.tap.postMessage({ + "shouldHandle": appShouldHandle, "screenX": event.screenX, "screenY": event.screenY, "clientX": event.clientX, "clientY": event.clientY, + "anchor": getNearestAnchor(event.target), }); // We don't want to disable the default WebView behavior as it breaks some features without bringing any value. @@ -69,16 +74,18 @@ return false; } - function isNoteref(element) { - if ( - element.nodeName.toLowerCase() === 'a' && - element.getAttributeNS('http://www.idpf.org/2007/ops', 'type') === 'noteref' - ) { - return true; + // Retrieves the markup of ... if the tap was + // anywhere within such an element (i.e. even on an tag within it). + // We return the markup rather than just a boolean as this could be more + // useful further up the line. + function getNearestAnchor(element) { + if (element.nodeName.toLowerCase() === 'a') { + return element.outerHTML; } if (element.parentElement) { - return isNoteref(element.parentElement); + return getNearestAnchor(element.parentElement); } + return null; } })(); diff --git a/r2-navigator-swift/Navigator.swift b/r2-navigator-swift/Navigator.swift index bbbd7561..949f6db9 100644 --- a/r2-navigator-swift/Navigator.swift +++ b/r2-navigator-swift/Navigator.swift @@ -12,11 +12,9 @@ import Foundation import SafariServices import R2Shared -import SwiftSoup -import WebKit -public protocol Navigator: UIViewController { +public protocol Navigator { /// Current position in the publication. /// Can be used to save a bookmark to the current position. @@ -88,8 +86,8 @@ public protocol NavigatorDelegate: AnyObject { /// Called when the user tapped an external URL. The default implementation opens the URL with the default browser. func navigator(_ navigator: Navigator, presentExternalURL url: URL) - /// Called when the user tapped on a noteref such as a footnote. The default implementation opens the target "aside" in a modal. - func navigator(_ navigator: Navigator, presentNote content: String, at link: Link, from source: String) + /// Called when the user taps on a noteref link. + func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, source: String) -> Bool } @@ -101,47 +99,11 @@ public extension NavigatorDelegate { UIApplication.shared.openURL(url) } } - -} - -public extension NavigatorDelegate { - func navigator(_ navigator: Navigator, presentNote content: String, at link: Link, from source: String) { - - var title = try? clean(source, .none()) - if title == "*" { - title = nil - } - - let content = (try? clean(content, .none())) ?? "" - let page = - """ - - - - - - \(content) - - - """ - - - let wk = WKWebView() - wk.loadHTMLString(page, baseURL: nil) - - let vc = UIViewController() - vc.view = wk - vc.navigationItem.title = title - vc.navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done, actionHandler: { (item) in - vc.dismiss(animated: true, completion: nil) - }) - - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = .formSheet - navigator.present(nav, animated: true, completion: nil) + func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, source: String) -> Bool { + return true } - + } diff --git a/r2-navigator-swift/Toolkit/BarButtonItem.swift b/r2-navigator-swift/Toolkit/BarButtonItem.swift deleted file mode 100644 index d55173a2..00000000 --- a/r2-navigator-swift/Toolkit/BarButtonItem.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// BarButtonItem.swift -// r2-navigator-swift -// -// Created by Matt McCullough on 4/21/20. -// -// Copyright 2020 Readium Foundation. All rights reserved. -// Use of this source code is governed by a BSD-style license which is detailed -// in the LICENSE file present in the project repository where this source code is maintained. -// - -import UIKit - -class BarButtonItem: UIBarButtonItem { - typealias ActionFunc = (UIBarButtonItem) -> Void - - private var actionFunc: ActionFunc? - - convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionFunc?) { - self.init(title: title, style: style, target: nil, action: #selector(handlePress)) - target = self - self.actionFunc = actionHandler - } - - convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionFunc: ActionFunc?) { - self.init(image: image, style: style, target: nil, action: #selector(handlePress)) - target = self - self.actionFunc = actionFunc - } - - convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionFunc?) { - self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(handlePress)) - target = self - self.actionFunc = actionHandler - } - - @objc func handlePress(sender: UIBarButtonItem) { - actionFunc?(sender) - } -} From 498728b84bf1f5ce596b7035c9cb03b48550dfc6 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 24 Apr 2020 15:43:23 -0700 Subject: [PATCH 3/6] Changes based on feedback in #118 --- .../EPUB/EPUBNavigatorViewController.swift | 34 +++++++++++------ r2-navigator-swift/EPUB/EPUBSpreadView.swift | 25 ++++++++---- .../EPUB/Resources/Scripts/gestures.js | 38 ++++--------------- r2-navigator-swift/Navigator.swift | 8 +++- 4 files changed, 53 insertions(+), 52 deletions(-) diff --git a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift index 2af48af2..0d5e4441 100644 --- a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift +++ b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift @@ -367,14 +367,21 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { delegate?.navigator(self, presentExternalURL: url) } - func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, anchor: String?) { + func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, tapData: TapData?) { // Check to see if this was a noteref link and give delegate the opportunity to display it. - if let anchor = anchor, let note = getNoteContent(anchor: anchor), let delegate = self.delegate { - if delegate.navigator(self, - shouldNavigateToNoteAt: Link(href: href), - content: note, - source: anchor) == false { + if + let tapData = tapData, + let interactive = tapData.interactiveElement, + let (note, referrer) = getNoteData(anchor: interactive), + let delegate = self.delegate + { + if !delegate.navigator( + self, + shouldNavigateToNoteAt: Link(href: href, type: "text/html"), + content: note, + referrer: referrer + ) { return } } @@ -382,18 +389,19 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { go(to: Link(href: href)) } - func getNoteContent(anchor: String) -> String? { + func getNoteData(anchor: String) -> (String, String)? { do { let doc = try parse(anchor) guard let link = try doc.select("a[epub:type=noteref]").first() else { return nil } let href = try link.attr("href") - guard let hashIndex = href.lastIndex(of: "#") else { + let hashParts = href.split(separator: "#") + guard hashParts.count == 2 else { log(.error, "Could not find hash in link \(href)") return nil } - let id = String(href[href.index(hashIndex, offsetBy: 1)...]) - let withoutFragment = String(href[..` element. + /// See https://idpf.github.io/epub-vocabs/structure/#footnotes + /// and http://kb.daisy.org/publishing/docs/html/epub-type.html#ex guard let aside = try document.select("#\(id)").first() else { log(.error, "Could not find the element '#\(id)' in document \(absolute)") return nil } - return try aside.html() + return (try aside.html(), try link.html()) } catch { log(.error, "Caught error while getting note content: \(error)") diff --git a/r2-navigator-swift/EPUB/EPUBSpreadView.swift b/r2-navigator-swift/EPUB/EPUBSpreadView.swift index b3805d5f..403b7a1c 100644 --- a/r2-navigator-swift/EPUB/EPUBSpreadView.swift +++ b/r2-navigator-swift/EPUB/EPUBSpreadView.swift @@ -11,6 +11,7 @@ import WebKit import R2Shared +import SwiftSoup protocol EPUBSpreadViewDelegate: class { @@ -27,7 +28,7 @@ protocol EPUBSpreadViewDelegate: class { func spreadView(_ spreadView: EPUBSpreadView, didTapOnExternalURL url: URL) /// Called when the user tapped on an internal link. - func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, anchor: String?) + func spreadView(_ spreadView: EPUBSpreadView, didTapOnInternalLink href: String, tapData: TapData?) /// Called when the pages visible in the spread changed. func spreadViewPagesDidChange(_ spreadView: EPUBSpreadView) @@ -51,7 +52,7 @@ class EPUBSpreadView: UIView, Loggable { let userSettings: UserSettings let editingActions: EditingActionsController - var lastTap: TapData? = nil + private var lastTap: TapData? = nil /// If YES, the content will be faded in once loaded. let animatedLoad: Bool @@ -201,7 +202,13 @@ class EPUBSpreadView: UIView, Loggable { let tapData = TapData(data: data) lastTap = tapData - guard tapData.shouldHandle else { return } + guard !tapData.defaultPrevented else { return } + if let interactive = tapData.interactiveElement { + let isNoteref = (try? parse(interactive).select("a[epub:type=noteref]").first()) == nil + if !isNoteref { + return + } + } guard let point = pointFromTap(tapData) else { return } delegate?.spreadView(self, didTapAt: point) @@ -427,7 +434,7 @@ extension EPUBSpreadView: WKNavigationDelegate { // Check if url is internal or external if let baseURL = publication.baseURL, url.host == baseURL.host { let href = url.absoluteString.replacingOccurrences(of: baseURL.absoluteString, with: "/") - delegate?.spreadView(self, didTapOnInternalLink: href, anchor: self.lastTap?.anchor) + delegate?.spreadView(self, didTapOnInternalLink: href, tapData: self.lastTap) } else { delegate?.spreadView(self, didTapOnExternalURL: url) } @@ -511,20 +518,22 @@ private extension EPUBSpreadView { /// Produced by gestures.js struct TapData { - let shouldHandle: Bool + let defaultPrevented: Bool let screenX: Int let screenY: Int let clientX: Int let clientY: Int - let anchor: String? + let targetElement: String + let interactiveElement: String? init(dict: [String: Any]) { - self.shouldHandle = dict["shouldHandle"] as? Bool ?? false + self.defaultPrevented = dict["defaultPrevented"] as? Bool ?? false self.screenX = dict["screenX"] as? Int ?? 0 self.screenY = dict["screenY"] as? Int ?? 0 self.clientX = dict["clientX"] as? Int ?? 0 self.clientY = dict["clientY"] as? Int ?? 0 - self.anchor = dict["anchor"] as? String + self.targetElement = dict["targetElement"] as? String ?? "" + self.interactiveElement = dict["interactiveElement"] as? String } init(data: Any) { diff --git a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js index afc53646..c72fc2e9 100644 --- a/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js +++ b/r2-navigator-swift/EPUB/Resources/Scripts/gestures.js @@ -7,17 +7,6 @@ }); function onClick(event) { - - // If the app should handle the tap. - // Examples of the app handling the tap would be - // navigating left/right, or show/hide the toolbar. - // If false, the tap is being handled within the webview, - // such as with a hyperlink or by an publication's JS handler. - let appShouldHandle = true; - - if (event.defaultPrevented || isInteractiveElement(event.target)) { - appShouldHandle = false; - } if (!window.getSelection().isCollapsed) { // There's an on-going selection, the tap will dismiss it so we don't forward it. @@ -28,12 +17,13 @@ // within the webview, so that it can be preserved and used // by the WKNavigationDelegate if needed. webkit.messageHandlers.tap.postMessage({ - "shouldHandle": appShouldHandle, + "defaultPrevented": event.defaultPrevented, "screenX": event.screenX, "screenY": event.screenY, "clientX": event.clientX, "clientY": event.clientY, - "anchor": getNearestAnchor(event.target), + "targetElement": event.target.outerHTML, + "interactiveElement": nearestInteractiveElement(event.target), }); // We don't want to disable the default WebView behavior as it breaks some features without bringing any value. @@ -42,7 +32,7 @@ } // See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling - function isInteractiveElement(element) { + function nearestInteractiveElement(element) { var interactiveTags = [ 'a', 'audio', @@ -58,33 +48,19 @@ 'video', ] if (interactiveTags.indexOf(element.nodeName.toLowerCase()) != -1) { - return true; + return element.outerHTML; } // Checks whether the element is editable by the user. if (element.hasAttribute('contenteditable') && element.getAttribute('contenteditable').toLowerCase() != 'false') { - return true; + return element.outerHTML; } // Checks parents recursively because the touch might be for example on an inside a . if (element.parentElement) { - return isInteractiveElement(element.parentElement); + return nearestInteractiveElement(element.parentElement); } - return false; - } - - // Retrieves the markup of ... if the tap was - // anywhere within such an element (i.e. even on an tag within it). - // We return the markup rather than just a boolean as this could be more - // useful further up the line. - function getNearestAnchor(element) { - if (element.nodeName.toLowerCase() === 'a') { - return element.outerHTML; - } - if (element.parentElement) { - return getNearestAnchor(element.parentElement); - } return null; } diff --git a/r2-navigator-swift/Navigator.swift b/r2-navigator-swift/Navigator.swift index 949f6db9..d0bf34eb 100644 --- a/r2-navigator-swift/Navigator.swift +++ b/r2-navigator-swift/Navigator.swift @@ -86,8 +86,12 @@ public protocol NavigatorDelegate: AnyObject { /// Called when the user tapped an external URL. The default implementation opens the URL with the default browser. func navigator(_ navigator: Navigator, presentExternalURL url: URL) - /// Called when the user taps on a noteref link. - func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, source: String) -> Bool + /// Called when the user taps on a link referring to a note. + /// + /// Return `true` to navigate to the note, or `false` if you intend to present the + /// note yourself, using its `content`. `link.type` contains information about the + /// format of `content` and `referrer`, such as `text/html`. + func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, referrer: String?) -> Bool } From d77f7e6397a14200e8fdaa614ee88d6faaec6ccf Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 24 Apr 2020 15:58:52 -0700 Subject: [PATCH 4/6] Revert pbxproj to match 6654a5d --- r2-navigator-swift.xcodeproj/project.pbxproj | 64 +------------------- 1 file changed, 3 insertions(+), 61 deletions(-) diff --git a/r2-navigator-swift.xcodeproj/project.pbxproj b/r2-navigator-swift.xcodeproj/project.pbxproj index 0ad139b2..446ba201 100644 --- a/r2-navigator-swift.xcodeproj/project.pbxproj +++ b/r2-navigator-swift.xcodeproj/project.pbxproj @@ -7,8 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D777488244F977E00A5E857 /* R2Shared.framework */; }; - 0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D77747E244F970E00A5E857 /* SwiftSoup.framework */; }; + 03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C3CC67222DBD8600A01731 /* R2Shared.framework */; }; CA0B3AC3222EE555006D9363 /* PDFNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */; }; CA1E4F4B240037E6009C4DE3 /* CompletionList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */; }; CA26EF7E22803FE90011653E /* VisualNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA26EF7D22803FE90011653E /* VisualNavigator.swift */; }; @@ -36,29 +35,8 @@ F3E7D42E1F4EE0FE00DF166D /* CBZNavigatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E7D42D1F4EE0FE00DF166D /* CBZNavigatorViewController.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 0D777487244F977E00A5E857 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = F3E7D3F41F4EBE2100DF166D; - remoteInfo = "r2-shared-swift"; - }; - 0D777489244F977E00A5E857 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA6161EA21FB257700D2CFE3; - remoteInfo = "r2-shared-swiftTests"; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ 03C3CC67222DBD8600A01731 /* R2Shared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = R2Shared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0D77747E244F970E00A5E857 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = "../r2-testapp-swift/Carthage/Build/iOS/SwiftSoup.framework"; sourceTree = ""; }; - 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "r2-shared-swift.xcodeproj"; path = "../r2-shared-swift/r2-shared-swift.xcodeproj"; sourceTree = ""; }; - 0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSoup.framework; path = Carthage/Build/iOS/SwiftSoup.framework; sourceTree = ""; }; - 0D82BF11244EAE04006FDB31 /* R2Shared.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = R2Shared.framework; path = Carthage/Build/iOS/R2Shared.framework; sourceTree = ""; }; CA0B3AC2222EE555006D9363 /* PDFNavigatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFNavigatorViewController.swift; sourceTree = ""; }; CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionList.swift; sourceTree = ""; }; CA26EF7D22803FE90011653E /* VisualNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualNavigator.swift; sourceTree = ""; }; @@ -95,23 +73,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0D77748B244F978A00A5E857 /* R2Shared.framework in Frameworks */, - 0D77748D244F97F200A5E857 /* SwiftSoup.framework in Frameworks */, + 03C3CC68222DBD8600A01731 /* R2Shared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0D777483244F977E00A5E857 /* Products */ = { - isa = PBXGroup; - children = ( - 0D777488244F977E00A5E857 /* R2Shared.framework */, - 0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; CA0B3AC1222EE530006D9363 /* PDF */ = { isa = PBXGroup; children = ( @@ -129,6 +97,7 @@ CAF1E3F422DF23F400E807EA /* PaginationView.swift */, CAC2A6D62292E4BA000AA2A7 /* WebView.swift */, CAD178B522B3B553004E6812 /* R2NavigatorLocalizedString.swift */, + CA1E4F4A240037E6009C4DE3 /* CompletionList.swift */, ); path = Toolkit; sourceTree = ""; @@ -222,10 +191,6 @@ F3E7D3E71F4DC40800DF166D /* Frameworks */ = { isa = PBXGroup; children = ( - 0D77747E244F970E00A5E857 /* SwiftSoup.framework */, - 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */, - 0D82BF11244EAE04006FDB31 /* R2Shared.framework */, - 0D82BF0D244EAC62006FDB31 /* SwiftSoup.framework */, 03C3CC67222DBD8600A01731 /* R2Shared.framework */, ); name = Frameworks; @@ -290,12 +255,6 @@ mainGroup = F3E7D3B91F4D83B000DF166D; productRefGroup = F3E7D3C41F4D83B000DF166D /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 0D777483244F977E00A5E857 /* Products */; - ProjectRef = 0D777482244F977E00A5E857 /* r2-shared-swift.xcodeproj */; - }, - ); projectRoot = ""; targets = ( F3E7D3C21F4D83B000DF166D /* r2-navigator-swift */, @@ -303,23 +262,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - 0D777488244F977E00A5E857 /* R2Shared.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = R2Shared.framework; - remoteRef = 0D777487244F977E00A5E857 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 0D77748A244F977E00A5E857 /* r2-shared-swiftTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = "r2-shared-swiftTests.xctest"; - remoteRef = 0D777489244F977E00A5E857 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ F3E7D3C11F4D83B000DF166D /* Resources */ = { isa = PBXResourcesBuildPhase; From 64c3f479f3b7be12b1868f4d8d9b5d59bf431d46 Mon Sep 17 00:00:00 2001 From: Matt Mc Date: Fri, 24 Apr 2020 16:52:30 -0700 Subject: [PATCH 5/6] Fixed default implementation for shouldNavigateToNoteAt --- r2-navigator-swift/Navigator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/r2-navigator-swift/Navigator.swift b/r2-navigator-swift/Navigator.swift index d0bf34eb..d8620717 100644 --- a/r2-navigator-swift/Navigator.swift +++ b/r2-navigator-swift/Navigator.swift @@ -104,7 +104,7 @@ public extension NavigatorDelegate { } } - func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, source: String) -> Bool { + func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: Link, content: String, referrer: String?) -> Bool { return true } From e4556f5ea0916103f34ef5cd9ca7255140c0bb15 Mon Sep 17 00:00:00 2001 From: Matt Mc Date: Fri, 24 Apr 2020 19:31:28 -0700 Subject: [PATCH 6/6] Upgraded noteref implementation to deal with hrefs that are solely hash fragments. --- .../EPUB/EPUBNavigatorViewController.swift | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift index 0d5e4441..316bcf25 100644 --- a/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift +++ b/r2-navigator-swift/EPUB/EPUBNavigatorViewController.swift @@ -373,7 +373,7 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { if let tapData = tapData, let interactive = tapData.interactiveElement, - let (note, referrer) = getNoteData(anchor: interactive), + let (note, referrer) = getNoteData(anchor: interactive, href: href), let delegate = self.delegate { if !delegate.navigator( @@ -389,26 +389,32 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { go(to: Link(href: href)) } - func getNoteData(anchor: String) -> (String, String)? { + /// Checks if the internal link is a noteref, and retrieves both the referring text of the link and the body of the note. + /// + /// Uses the navigation href from didTapOnInternalLink because it is normalized to a path within the book, + /// whereas the anchor tag may have just a hash fragment like `#abc123` which is hard to work with. + /// We do at least validate to ensure that the two hrefs match. + /// + /// Uses `#id` when retrieving the body of the note, not `aside#id` because it may be a `
`. + /// See https://idpf.github.io/epub-vocabs/structure/#footnotes + /// and http://kb.daisy.org/publishing/docs/html/epub-type.html#ex + func getNoteData(anchor: String, href: String) -> (String, String)? { do { let doc = try parse(anchor) guard let link = try doc.select("a[epub:type=noteref]").first() else { return nil } - let href = try link.attr("href") + let anchorHref = try link.attr("href") + guard href.hasSuffix(anchorHref) else { return nil} + let hashParts = href.split(separator: "#") guard hashParts.count == 2 else { log(.error, "Could not find hash in link \(href)") return nil } let id = String(hashParts[1]) - let withoutFragment = String(hashParts[0]) - - guard var loc = self.currentLocation?.href else { - log(.error, "Couldn't get current location") - return nil - } - if loc.hasPrefix("/") { - loc = String(loc.dropFirst()) + var withoutFragment = String(hashParts[0]) + if withoutFragment.hasPrefix("/") { + withoutFragment = String(withoutFragment.dropFirst()) } guard let base = publication.baseURL else { @@ -416,19 +422,12 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate { return nil } - let resource = base.appendingPathComponent(loc) - guard let absolute = URL(string: withoutFragment, relativeTo: resource) else { - log(.error, "Could not get absolute URL from \(withoutFragment) relative to \(self.resourcesURL?.absoluteString ?? "(no self.resourcesURL)")") - return nil - } + let absolute = base.appendingPathComponent(withoutFragment) log(.debug, "Fetching note contents from \(absolute.absoluteString)") let contents = try String(contentsOf: absolute) let document = try parse(contents) - /// Note that we don't link to `aside#id` because it may not be an `