diff --git a/SwiftyMercuryReady.xcodeproj/project.pbxproj b/SwiftyMercuryReady.xcodeproj/project.pbxproj index d572d93..40c24af 100644 --- a/SwiftyMercuryReady.xcodeproj/project.pbxproj +++ b/SwiftyMercuryReady.xcodeproj/project.pbxproj @@ -19,9 +19,21 @@ 97081C931F672DAB00FF7268 /* NavBarTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97081C921F672DAB00FF7268 /* NavBarTitleView.swift */; }; 97081C951F67BDC400FF7268 /* ScrollableNavBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97081C941F67BDC400FF7268 /* ScrollableNavBarViewController.swift */; }; 97578AE11F68160500A24273 /* ReaderToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97578AE01F68160500A24273 /* ReaderToolBar.swift */; }; + 97A2CE2620E12565007AFC3C /* SwiftyMercuryReadyTesty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97A2CE2520E12565007AFC3C /* SwiftyMercuryReadyTesty.swift */; }; + 97A2CE2E20E12D65007AFC3C /* SwiftyMercuryApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9706DC921F07B4580054D133 /* SwiftyMercuryApi.swift */; }; 97D0D88E1F7FB233007DE08A /* keys.plist in Resources */ = {isa = PBXBuildFile; fileRef = 97D0D88D1F7FB233007DE08A /* keys.plist */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 97A2CE2820E12565007AFC3C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9706DC751F07B4070054D133 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9706DC7C1F07B4070054D133; + remoteInfo = SwiftyMercuryReady; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 9706DC7D1F07B4070054D133 /* SwiftyMercuryReady.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftyMercuryReady.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9706DC801F07B4070054D133 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -37,6 +49,9 @@ 97081C921F672DAB00FF7268 /* NavBarTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavBarTitleView.swift; sourceTree = ""; }; 97081C941F67BDC400FF7268 /* ScrollableNavBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollableNavBarViewController.swift; sourceTree = ""; }; 97578AE01F68160500A24273 /* ReaderToolBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderToolBar.swift; sourceTree = ""; }; + 97A2CE2320E12565007AFC3C /* SwiftyMercuryReadyTesty.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftyMercuryReadyTesty.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 97A2CE2520E12565007AFC3C /* SwiftyMercuryReadyTesty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyMercuryReadyTesty.swift; sourceTree = ""; }; + 97A2CE2720E12565007AFC3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 97D0D88D1F7FB233007DE08A /* keys.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = keys.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -48,6 +63,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 97A2CE2020E12565007AFC3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -55,6 +77,7 @@ isa = PBXGroup; children = ( 9706DC7F1F07B4070054D133 /* SwiftyMercuryReady */, + 97A2CE2420E12565007AFC3C /* SwiftyMercuryReadyTesty */, 9706DC7E1F07B4070054D133 /* Products */, ); sourceTree = ""; @@ -63,6 +86,7 @@ isa = PBXGroup; children = ( 9706DC7D1F07B4070054D133 /* SwiftyMercuryReady.app */, + 97A2CE2320E12565007AFC3C /* SwiftyMercuryReadyTesty.xctest */, ); name = Products; sourceTree = ""; @@ -112,6 +136,15 @@ name = ArticleReaderDemo; sourceTree = ""; }; + 97A2CE2420E12565007AFC3C /* SwiftyMercuryReadyTesty */ = { + isa = PBXGroup; + children = ( + 97A2CE2520E12565007AFC3C /* SwiftyMercuryReadyTesty.swift */, + 97A2CE2720E12565007AFC3C /* Info.plist */, + ); + path = SwiftyMercuryReadyTesty; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -132,13 +165,31 @@ productReference = 9706DC7D1F07B4070054D133 /* SwiftyMercuryReady.app */; productType = "com.apple.product-type.application"; }; + 97A2CE2220E12565007AFC3C /* SwiftyMercuryReadyTesty */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97A2CE2C20E12565007AFC3C /* Build configuration list for PBXNativeTarget "SwiftyMercuryReadyTesty" */; + buildPhases = ( + 97A2CE1F20E12565007AFC3C /* Sources */, + 97A2CE2020E12565007AFC3C /* Frameworks */, + 97A2CE2120E12565007AFC3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 97A2CE2920E12565007AFC3C /* PBXTargetDependency */, + ); + name = SwiftyMercuryReadyTesty; + productName = SwiftyMercuryReadyTesty; + productReference = 97A2CE2320E12565007AFC3C /* SwiftyMercuryReadyTesty.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 9706DC751F07B4070054D133 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0830; + LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Stéphane Sercu"; TargetAttributes = { @@ -148,6 +199,12 @@ LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; + 97A2CE2220E12565007AFC3C = { + CreatedOnToolsVersion = 9.2; + DevelopmentTeam = 5P2WT92MAV; + ProvisioningStyle = Automatic; + TestTargetID = 9706DC7C1F07B4070054D133; + }; }; }; buildConfigurationList = 9706DC781F07B4070054D133 /* Build configuration list for PBXProject "SwiftyMercuryReady" */; @@ -164,6 +221,7 @@ projectRoot = ""; targets = ( 9706DC7C1F07B4070054D133 /* SwiftyMercuryReady */, + 97A2CE2220E12565007AFC3C /* SwiftyMercuryReadyTesty */, ); }; /* End PBXProject section */ @@ -180,6 +238,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 97A2CE2120E12565007AFC3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -199,8 +264,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 97A2CE1F20E12565007AFC3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97A2CE2E20E12D65007AFC3C /* SwiftyMercuryApi.swift in Sources */, + 97A2CE2620E12565007AFC3C /* SwiftyMercuryReadyTesty.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 97A2CE2920E12565007AFC3C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9706DC7C1F07B4070054D133 /* SwiftyMercuryReady */; + targetProxy = 97A2CE2820E12565007AFC3C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 9706DC891F07B4070054D133 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -346,6 +428,48 @@ }; name = Release; }; + 97A2CE2A20E12565007AFC3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5P2WT92MAV; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = SwiftyMercuryReadyTesty/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.StephSercu.SwiftyMercuryReadyTesty; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMercuryReady.app/SwiftyMercuryReady"; + }; + name = Debug; + }; + 97A2CE2B20E12565007AFC3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5P2WT92MAV; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = SwiftyMercuryReadyTesty/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.StephSercu.SwiftyMercuryReadyTesty; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftyMercuryReady.app/SwiftyMercuryReady"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -367,6 +491,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 97A2CE2C20E12565007AFC3C /* Build configuration list for PBXNativeTarget "SwiftyMercuryReadyTesty" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97A2CE2A20E12565007AFC3C /* Debug */, + 97A2CE2B20E12565007AFC3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 9706DC751F07B4070054D133 /* Project object */; diff --git a/SwiftyMercuryReady/ArticleReaderController.swift b/SwiftyMercuryReady/ArticleReaderController.swift index 029db17..c1de8c1 100644 --- a/SwiftyMercuryReady/ArticleReaderController.swift +++ b/SwiftyMercuryReady/ArticleReaderController.swift @@ -420,8 +420,10 @@ class ArticleReaderController: ScrollableNavBarViewController, WKNavigationDeleg /// Define what to do if the user clicks on a link inside the reader func navigationRequested(request: URLRequest, navigationType: WKNavigationType) { - self.isReaderEnabled = false - self.webView.load(request) + if navigationType != .other { + self.isReaderEnabled = false + self.webView.load(request) + } } func contentDidLoad(reader: ReaderWebView, content: MercuryResponse) { diff --git a/SwiftyMercuryReady/ArticlesReaderDemo.swift b/SwiftyMercuryReady/ArticlesReaderDemo.swift index f81af0a..9ed393e 100644 --- a/SwiftyMercuryReady/ArticlesReaderDemo.swift +++ b/SwiftyMercuryReady/ArticlesReaderDemo.swift @@ -18,7 +18,7 @@ class Article { get { var dom: String = URL(string: self.url)!.host! if dom.hasPrefix("www.") { - dom = String(dom.characters.dropFirst(4)) + dom = String(dom.dropFirst(4)) } return dom } @@ -55,7 +55,14 @@ class ArticleTableViewController: UITableViewController { Article(title: "WiFi232 – An Internet Hayes Modem for your Retro Computer", url: "http://biosrhythm.com/?page_id=1453"), Article(title: "Why does Heap's algorithm work?", - url: "http://ruslanledesma.com/2016/06/17/why-does-heap-work.html")] + url: "http://ruslanledesma.com/2016/06/17/why-does-heap-work.html"), + Article(title: "How SQL Database Engines Work, by the Creator of SQLite (2008) [video]", + url:"https://www.youtube.com/watch?v=Z_cX3bzkExE"), + Article(title: "OpenAI Five", + url:"https://blog.openai.com/openai-five/"), + Article(title: "“No Man’s Sky” Displayed on the Amiga 1000", + url:"http://www.bytecellar.com/2018/03/14/a-planetary-anachronism-no-mans-sky-beautifully-rendered-on-the-amiga-1000/"), + ] private let cellId = "articleCellid" diff --git a/SwiftyMercuryReady/ReaderWebViewDemo.swift b/SwiftyMercuryReady/ReaderWebViewDemo.swift index c93c5e9..f5b49d1 100644 --- a/SwiftyMercuryReady/ReaderWebViewDemo.swift +++ b/SwiftyMercuryReady/ReaderWebViewDemo.swift @@ -22,8 +22,9 @@ class ReaderWebViewController: UIViewController { readerWebView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true - readerWebView.load(url: URL(string: "https://www.eff.org/alice")!) - + //readerWebView.load(url: URL(string: "https://www.eff.org/alice")!) + //readerWebView.load(url: URL(string: "https://www.flightradar24.com/FIN6KC/f31526e")!) + readerWebView.load(url: URL(string: "https://blog.openai.com/openai-five/")!) let btnTheme = UIButton() btnTheme.setTitle("Theme", for: .normal) diff --git a/SwiftyMercuryReady/SwiftyMercuryApi.swift b/SwiftyMercuryReady/SwiftyMercuryApi.swift index 4e07d6f..2f834fd 100644 --- a/SwiftyMercuryReady/SwiftyMercuryApi.swift +++ b/SwiftyMercuryReady/SwiftyMercuryApi.swift @@ -13,7 +13,7 @@ import Foundation Just send an url and retrieve the parsed content of the page the url points to. */ -class MercuryApi { +open class MercuryApi { private var apiKey = "" private let apiUrl = "https://mercury.postlight.com/parser?url=" @@ -31,7 +31,7 @@ class MercuryApi { public static let shared = MercuryApi() - func parseUrl(url: String, completion: ((MercuryResponse?) -> Void)!) { + public func parseUrl(url: String, completion: ((MercuryResponse?) -> Void)!) { var req = URLRequest(url: URL(string: self.apiUrl + url)!) req.addValue(self.apiKey, forHTTPHeaderField: "x-api-key") req.addValue("application/json", forHTTPHeaderField: "Content-Type") @@ -39,13 +39,18 @@ class MercuryApi { if json == nil { completion(nil) } else { - completion(MercuryResponse(fromJson: json!)) + if (json!.count == 1 && (((json!["message"] as? String) != nil))) || + (json!.count == 2 && (((json!["messages"] as? String) != nil)) && (((json!["error"] as? Int) != nil))) { + completion(nil) // Error + } else { + completion(MercuryResponse(fromJson: json!)) + } } }) } /// Helper function which send HTTP req. and parse the json content in the response (if any) - private func getJson(request: URLRequest, completion: @escaping (Dictionary?) -> Void) { + private func getJson(request: URLRequest, completion: @escaping ([String: Any]?) -> Void) { let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) -> Void in guard let responseData = data else { @@ -55,6 +60,7 @@ class MercuryApi { }) return } + guard let JSON = try? JSONSerialization.jsonObject(with: responseData, options: .mutableContainers) as! [String: Any] else { DispatchQueue.main.async(execute: { ()->() in @@ -71,7 +77,7 @@ class MercuryApi { } /// Abstraction of a typical json response sent by the api. -class MercuryResponse { +open class MercuryResponse { init(fromJson json: [String: Any]) { self.title = json["title"] as? String self.author = json["author"] as? String @@ -89,8 +95,6 @@ class MercuryResponse { self.total_pages = json["total_pages"] as? Int self.rendered_pages = json["rendered_pages"] as? Int self.next_page_url = json["next_page_url"] as? String - - } /// Transform the date returned by the API (string format: 2017-06-13T00:14:56.000Z) to a Date object diff --git a/SwiftyMercuryReadyTesty/Info.plist b/SwiftyMercuryReadyTesty/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/SwiftyMercuryReadyTesty/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/SwiftyMercuryReadyTesty/SwiftyMercuryReadyTesty.swift b/SwiftyMercuryReadyTesty/SwiftyMercuryReadyTesty.swift new file mode 100644 index 0000000..80990e2 --- /dev/null +++ b/SwiftyMercuryReadyTesty/SwiftyMercuryReadyTesty.swift @@ -0,0 +1,79 @@ +// +// SwiftyMercuryReadyTesty.swift +// SwiftyMercuryReadyTesty +// +// Created by Stéphane Sercu on 25/06/18. +// Copyright © 2018 Stéphane Sercu. All rights reserved. +// + +import XCTest + +class SwiftyMercuryReadyTesty: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + /// URLs that should be succesfully parsed by the API and handled by the MercuryApi client + let parsableExamples = [ + "http://www.thetransportpolitic.com/2017/07/01/a-generational-failure-as-the-u-s-fantasizes-the-rest-of-the-world-builds-a-new-transport-system/", + "https://blog.ipinfo.io/api-side-project-to-250-million-requests-with-0-marketing-budget-bb0de01c01f6", + "http://aging.nautil.us/feature/226/how-aging-research-is-changing-our-lives", + "https://www.sec.gov/litigation/litreleases/2017/lr23870.htm", + "https://www.eff.org/alice", + "https://drikerf.com/building-pixels-a-daily-source-of-inspiration/", + "https://open.nytimes.com/react-relay-and-graphql-under-the-hood-of-the-times-website-redesign-22fb62ea9764", + "https://blog.2ndquadrant.com/what-is-select-skip-locked-for-in-postgresql-9-5/", + "http://biosrhythm.com/?page_id=1453", + "http://ruslanledesma.com/2016/06/17/why-does-heap-work.html", + "https://www.youtube.com/watch?v=IuEEEwgdAZs", + ] + + /// URLs that aren't parsable bu Mercury Api but that should be handled by the client + let nonParsableExamples = ["https://www.flightradar24.com/53.84,2.19/4"] + + /// examples of bad urls and other error-causing cases + let failingExamples = ["notAnURL"] + + func testParsableExamples() { + var exps: [String: XCTestExpectation] = [:] + for str_urls in parsableExamples { + exps[str_urls] = expectation(description: str_urls + " should be parsed") + MercuryApi.shared.parseUrl(url: str_urls, completion: {(resp) -> Void in + XCTAssertNotNil(resp) + exps[str_urls]!.fulfill() + }) + } + wait(for: Array(exps.values), timeout: 20) + } + + func testNonParsableExamples() { + var exps: [String: XCTestExpectation] = [:] + for str_urls in nonParsableExamples { + exps[str_urls] = expectation(description: str_urls + " shouldn't be parsed") + MercuryApi.shared.parseUrl(url: str_urls, completion: {(resp) -> Void in + XCTAssertNil(resp) + exps[str_urls]!.fulfill() + }) + } + wait(for: Array(exps.values), timeout: 20) + } + func testFailingExamples() { + var exps: [String: XCTestExpectation] = [:] + for str_urls in failingExamples { + exps[str_urls] = expectation(description: str_urls + " shouldn't be parsed") + MercuryApi.shared.parseUrl(url: str_urls, completion: {(resp) -> Void in + XCTAssertNil(resp) + exps[str_urls]!.fulfill() + }) + } + wait(for: Array(exps.values), timeout: 20) + } + +}