diff --git a/Classes/Definitions.swift b/Classes/Definitions.swift index b87a9b0..81edba1 100644 --- a/Classes/Definitions.swift +++ b/Classes/Definitions.swift @@ -110,7 +110,7 @@ extension Result: CustomDebugStringConvertible { internal struct Response { var data: NSData? - var statusCode: Int = Static.DefaultStatusCodeError + var statusCode: Int = ActionError.Static.DefaultStatusCodeError init(data: NSData?, urlResponse: NSURLResponse) { self.data = data @@ -461,7 +461,13 @@ internal func decodeJSON(json: JSON?) -> Result<[U]> { return Result.Success(result) } -/// Helper methods + + +//======================================== +// MARK: Helper Methods +//======================================== + + private func htmlToData(html: NSString?) -> NSData? { if let html = html { let json = html.stringByReplacingOccurrencesOfString("<[^>]+>", withString: "", options: .RegularExpressionSearch, range: NSMakeRange(0, html.length)) @@ -505,3 +511,14 @@ func dispatch_sync_on_main_thread(block: dispatch_block_t) { dispatch_sync(dispatch_get_main_queue(), block) } } + +internal func delay(time: NSTimeInterval, completion: () -> Void) { + if let currentQueue = NSOperationQueue.currentQueue()?.underlyingQueue { + let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(time * Double(NSEC_PER_SEC))) + dispatch_after(delayTime, currentQueue) { + completion() + } + } else { + completion() + } +} diff --git a/Classes/Error.swift b/Classes/Error.swift index 3608c5b..2918b7d 100644 --- a/Classes/Error.swift +++ b/Classes/Error.swift @@ -23,11 +23,6 @@ import Foundation -internal struct Static { - static let DefaultStatusCodeSuccess : Int = 200 - static let DefaultStatusCodeError : Int = 500 -} - public protocol ErrorType { } public enum NoError: ErrorType { } @@ -39,6 +34,12 @@ public enum ActionError: ErrorType { case NotFound case ParsingFailure case TransformFailure + case SnapshotFailure + + internal struct Static { + static let DefaultStatusCodeSuccess : Int = 200 + static let DefaultStatusCodeError : Int = 500 + } } extension ActionError: CustomDebugStringConvertible { @@ -48,6 +49,7 @@ extension ActionError: CustomDebugStringConvertible { case .NotFound: return "Not Found" case .ParsingFailure: return "Parsing Failure" case .TransformFailure: return "Transform Failure" + case .SnapshotFailure: return "Snapshot Failure" } } } diff --git a/Classes/Functions.swift b/Classes/Functions.swift new file mode 100644 index 0000000..576a380 --- /dev/null +++ b/Classes/Functions.swift @@ -0,0 +1,311 @@ +// +// Functions.swift +// +// Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/** + Convenience functions for accessing the WKZombie shared instance functionality. + */ + +//======================================== +// MARK: Get Page +//======================================== + +/** + The returned WKZombie Action will load and return a HTML or JSON page for the specified URL + __using the shared WKZombie instance__. + - seealso: _open()_ function in _WKZombie_ class for more info. + */ +public func open(url: NSURL) -> Action { + return WKZombie.sharedInstance.open(url) +} + +/** + The returned WKZombie Action will load and return a HTML or JSON page for the specified URL + __using the shared WKZombie instance__. + - seealso: _open()_ function in _WKZombie_ class for more info. + */ +public func open(then postAction: PostAction) -> (url: NSURL) -> Action { + return WKZombie.sharedInstance.open(then: postAction) +} + +/** + The returned WKZombie Action will return the current page __using the shared WKZombie instance__. + - seealso: _inspect()_ function in _WKZombie_ class for more info. + */ +public func inspect() -> Action { + return WKZombie.sharedInstance.inspect() +} + + +//======================================== +// MARK: Submit Form +//======================================== + +/** + Submits the specified HTML form __using the shared WKZombie instance__. + - seealso: _submit()_ function in _WKZombie_ class for more info. + */ +public func submit(form: HTMLForm) -> Action { + return WKZombie.sharedInstance.submit(form) +} + +/** + Submits the specified HTML form __using the shared WKZombie instance__. + - seealso: _submit()_ function in _WKZombie_ class for more info. + */ +public func submit(then postAction: PostAction) -> (form: HTMLForm) -> Action { + return WKZombie.sharedInstance.submit(then: postAction) +} + + +//======================================== +// MARK: Click Event +//======================================== + +/** + Simulates the click of a HTML link __using the shared WKZombie instance__. + - seealso: _click()_ function in _WKZombie_ class for more info. + */ +public func click(link : HTMLLink) -> Action { + return WKZombie.sharedInstance.click(link) +} + +/** + Simulates the click of a HTML link __using the shared WKZombie instance__. + - seealso: _click()_ function in _WKZombie_ class for more info. + */ +public func click(then postAction: PostAction) -> (link : HTMLLink) -> Action { + return WKZombie.sharedInstance.click(then: postAction) +} + +/** + Simulates HTMLButton press __using the shared WKZombie instance__. + - seealso: _press()_ function in _WKZombie_ class for more info. + */ +public func press(button : HTMLButton) -> Action { + return WKZombie.sharedInstance.press(button) +} + +/** + Simulates HTMLButton press __using the shared WKZombie instance__. + - seealso: _press()_ function in _WKZombie_ class for more info. + */ +public func press(then postAction: PostAction) -> (button : HTMLButton) -> Action { + return WKZombie.sharedInstance.press(then: postAction) +} + + +//======================================== +// MARK: DOM Modification Methods +//======================================== + +/** + The returned WKZombie Action will set or update a attribute/value pair on the specified HTMLElement + __using the shared WKZombie instance__. + - seealso: _setAttribute()_ function in _WKZombie_ class for more info. + */ +public func setAttribute(key: String, value: String?) -> (element: T) -> Action { + return WKZombie.sharedInstance.setAttribute(key, value: value) +} + + +//======================================== +// MARK: Find Methods +//======================================== + + +/** + The returned WKZombie Action will search a page and return all elements matching the generic HTML element type and + the passed key/value attributes. __The the shared WKZombie instance will be used__. + - seealso: _getAll()_ function in _WKZombie_ class for more info. + */ +public func getAll(by searchType: SearchType) -> (page: HTMLPage) -> Action<[T]> { + return WKZombie.sharedInstance.getAll(by: searchType) +} + +/** + The returned WKZombie Action will search a page and return the first element matching the generic HTML element type and + the passed key/value attributes. __The shared WKZombie instance will be used__. + - seealso: _get()_ function in _WKZombie_ class for more info. + */ +public func get(by searchType: SearchType) -> (page: HTMLPage) -> Action { + return WKZombie.sharedInstance.get(by: searchType) +} + + +//======================================== +// MARK: JavaScript Methods +//======================================== + + +/** + The returned WKZombie Action will execute a JavaScript string __using the shared WKZombie instance__. + - seealso: _execute()_ function in _WKZombie_ class for more info. + */ +public func execute(script: JavaScript) -> (page: HTMLPage) -> Action { + return WKZombie.sharedInstance.execute(script) +} + + +//======================================== +// MARK: Fetch Actions +//======================================== + + +/** + The returned WKZombie Action will download the linked data of the passed HTMLFetchable object + __using the shared WKZombie instance__. + - seealso: _fetch()_ function in _WKZombie_ class for more info. + */ +public func fetch(fetchable: T) -> Action { + return WKZombie.sharedInstance.fetch(fetchable) +} + + +//======================================== +// MARK: Transform Actions +//======================================== + +/** + The returned WKZombie Action will transform a HTMLElement into another HTMLElement using the specified function. + __The shared WKZombie instance will be used__. + - seealso: _map()_ function in _WKZombie_ class for more info. + */ +public func map(f: T -> A) -> (object: T) -> Action { + return WKZombie.sharedInstance.map(f) +} + + +//======================================== +// MARK: Advanced Actions +//======================================== + + +/** + Executes the specified action (with the result of the previous action execution as input parameter) until + a certain condition is met. Afterwards, it will return the collected action results. + __The shared WKZombie instance will be used__. + - seealso: _collect()_ function in _WKZombie_ class for more info. + */ +public func collect(f: T -> Action, until: T -> Bool) -> (initial: T) -> Action<[T]> { + return WKZombie.sharedInstance.collect(f, until: until) +} + +/** + Makes a bulk execution of the specified action with the provided input values. Once all actions have + finished, the collected results will be returned. + __The shared WKZombie instance will be used__. + - seealso: _batch()_ function in _WKZombie_ class for more info. + */ +public func batch(f: T -> Action) -> (elements: [T]) -> Action<[U]> { + return WKZombie.sharedInstance.batch(f) +} + + +//======================================== +// MARK: JSON Actions +//======================================== + + +/** + The returned WKZombie Action will parse NSData and create a JSON object. + __The shared WKZombie instance will be used__. + - seealso: _parse()_ function in _WKZombie_ class for more info. + */ +public func parse(data: NSData) -> Action { + return WKZombie.sharedInstance.parse(data) +} + +/** + The returned WKZombie Action will take a JSONParsable (Array, Dictionary and JSONPage) and + decode it into a Model object. This particular Model class has to implement the + JSONDecodable protocol. + __The shared WKZombie instance will be used__. + - seealso: _decode()_ function in _WKZombie_ class for more info. + */ +public func decode(element: JSONParsable) -> Action { + return WKZombie.sharedInstance.decode(element) +} + +/** + The returned WKZombie Action will take a JSONParsable (Array, Dictionary and JSONPage) and + decode it into an array of Model objects of the same class. The class has to implement the + JSONDecodable protocol. + __The shared WKZombie instance will be used__. + - seealso: _decode()_ function in _WKZombie_ class for more info. + */ +public func decode(array: JSONParsable) -> Action<[T]> { + return WKZombie.sharedInstance.decode(array) +} + + +#if os(iOS) + + //======================================== + // MARK: Snapshot Methods + //======================================== + + /** + This is a convenience operator for the _snap()_ command. It is equal to the __>>>__ operator with the difference + that a snapshot will be taken after the left Action has been finished. + */ + infix operator >>* { associativity left precedence 150 } + public func >>*(a: Action, f: T -> Action) -> Action { + assert(WKZombie.Static.instance != nil, "The >>* operator can only be used with the WKZombie shared instance.") + return a >>> snap() >>> f + } + + /** + The returned WKZombie Action will make a snapshot of the current page. + Note: This method only works under iOS. Also, a snapshotHandler must be registered. + __The shared WKZombie instance will be used__. + - seealso: _snap()_ function in _WKZombie_ class for more info. + */ + public func snap() -> (element: T) -> Action { + return WKZombie.sharedInstance.snap() + } + +#endif + + +//======================================== +// MARK: Debug Methods +//======================================== + + +/** + Prints the current state of the WKZombie browser to the console. + */ +public func dump() { + WKZombie.sharedInstance.dump() +} + +/** + Clears the cache/cookie data (such as login data, etc). + */ +public func clearCache() { + WKZombie.sharedInstance.clearCache() +} + diff --git a/Classes/Logger.swift b/Classes/Logger.swift index 19add3e..0fae56a 100644 --- a/Classes/Logger.swift +++ b/Classes/Logger.swift @@ -1,10 +1,25 @@ // -// Logger.swift -// WKZombie +// Logger.swift // -// Created by Mathias Köhnke on 08/04/16. -// Copyright © 2016 Mathias Köhnke. All rights reserved. +// Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.com) // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import Foundation diff --git a/Classes/RenderOperation.swift b/Classes/RenderOperation.swift index 78ad162..d621d61 100644 --- a/Classes/RenderOperation.swift +++ b/Classes/RenderOperation.swift @@ -254,17 +254,3 @@ extension RenderOperation { } -//======================================== -// MARK: Helper Methods -//======================================== - -private func delay(time: NSTimeInterval, completion: () -> Void) { - if let currentQueue = NSOperationQueue.currentQueue()?.underlyingQueue { - let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(time * Double(NSEC_PER_SEC))) - dispatch_after(delayTime, currentQueue) { - completion() - } - } else { - completion() - } -} diff --git a/Classes/Renderer.swift b/Classes/Renderer.swift index 00d865b..18e68ce 100644 --- a/Classes/Renderer.swift +++ b/Classes/Renderer.swift @@ -172,10 +172,14 @@ internal class Renderer : NSObject { } } - //======================================== - // MARK: Cache - //======================================== - +} + + +//======================================== +// MARK: Cache +//======================================== + +extension Renderer { internal func clearCache() { let distantPast = NSDate.distantPast() NSHTTPCookieStorage.sharedHTTPCookieStorage().removeCookiesSinceDate(distantPast) @@ -183,3 +187,25 @@ internal class Renderer : NSObject { WKWebsiteDataStore.defaultDataStore().removeDataOfTypes(websiteDataTypes, modifiedSince: distantPast, completionHandler:{ }) } } + + +//======================================== +// MARK: Snapshot +//======================================== + +#if os(iOS) +extension Renderer { + internal func snapshot() -> Snapshot? { + precondition(webView.superview != nil, "WKWebView has no superview. Cannot take snapshot.") + UIGraphicsBeginImageContextWithOptions(webView.bounds.size, true, 0) + webView.scrollView.drawViewHierarchyInRect(webView.bounds, afterScreenUpdates: true) + let snapshot = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + if let data = UIImagePNGRepresentation(snapshot) { + return Snapshot(data: data, page: webView.URL) + } + return nil + } +} +#endif diff --git a/Classes/Snapshot.swift b/Classes/Snapshot.swift new file mode 100644 index 0000000..fb1c1fc --- /dev/null +++ b/Classes/Snapshot.swift @@ -0,0 +1,88 @@ +// +// Snapshot.swift +// +// Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) +import UIKit +public typealias SnapshotImage = UIImage +#elseif os(OSX) +import Cocoa +public typealias SnapshotImage = NSImage +#endif + +public typealias SnapshotHandler = Snapshot -> Void + +/// WKZombie Snapshot Helper Class +public class Snapshot { + public let page : NSURL? + public let file : NSURL + public lazy var image : SnapshotImage? = { + if let path = self.file.path { + #if os(iOS) + return UIImage(contentsOfFile: path) + #elseif os(OSX) + return NSImage(contentsOfFile: path) + #endif + } + return nil + }() + + internal init?(data: NSData, page: NSURL? = nil) { + do { + self.file = try Snapshot.store(data) + self.page = page + } catch let error as NSError { + Logger.log("Could not take snapshot: \(error.localizedDescription)") + return nil + } + } + + private static func store(data: NSData) throws -> NSURL { + let identifier = NSProcessInfo.processInfo().globallyUniqueString + + let fileName = String(format: "wkzombie-snapshot-%@", identifier) + let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).URLByAppendingPathComponent(fileName) + + try data.writeToURL(fileURL, options: .AtomicWrite) + + return fileURL + } + + /** + Moves the snapshot file into the specified directory. + + - parameter directory: A Directory URL. + + - throws: Exception if the moving operation fails. + + - returns: The URL with the new file location. + */ + public func moveTo(directory: NSURL) throws -> NSURL? { + let fileManager = NSFileManager.defaultManager() + if let fileName = file.lastPathComponent { + let destination = directory.URLByAppendingPathComponent(fileName) + try fileManager.moveItemAtURL(file, toURL: destination) + return destination + } + return nil + } +} diff --git a/Classes/WKZombie.swift b/Classes/WKZombie.swift index a1f1f6a..4bd1447 100644 --- a/Classes/WKZombie.swift +++ b/Classes/WKZombie.swift @@ -26,6 +26,18 @@ import WebKit public class WKZombie : NSObject { + /// A shared instance of `Manager`, used by top-level WKZombie methods, + /// and suitable for multiple web sessions. + public class var sharedInstance: WKZombie { + dispatch_once(&Static.token) { Static.instance = WKZombie() } + return Static.instance! + } + + internal struct Static { + static var token : dispatch_once_t = 0 + static var instance : WKZombie? + } + private var _renderer : Renderer! private var _fetcher : ContentFetcher! @@ -51,6 +63,11 @@ public class WKZombie : NSObject { } } + #if os(iOS) + /// Snapshot Handler + public var snapshotHandler : SnapshotHandler? + #endif + /** The designated initializer. @@ -70,7 +87,7 @@ public class WKZombie : NSObject { //======================================== private func _handleResponse(data: NSData?, response: NSURLResponse?, error: NSError?) -> Result { - var statusCode : Int = (error == nil) ? Static.DefaultStatusCodeSuccess : Static.DefaultStatusCodeError + var statusCode : Int = (error == nil) ? ActionError.Static.DefaultStatusCodeSuccess : ActionError.Static.DefaultStatusCodeError if let response = response as? NSHTTPURLResponse { statusCode = response.statusCode } @@ -481,6 +498,40 @@ extension WKZombie { } +#if os(iOS) + +//======================================== +// MARK: Snapshot Methods +//======================================== + +/// Default delay before taking snapshots +private let DefaultSnapshotDelay = 0.1 + +extension WKZombie { + + /** + The returned WKZombie Action will make a snapshot of the current page. + Note: This method only works under iOS. Also, a snapshotHandler must be registered. + + - returns: A snapshot class. + */ + public func snap() -> (element: T) -> Action { + return { (element: T) -> Action in + return Action(operation: { [unowned self] completion in + delay(DefaultSnapshotDelay, completion: { + if let snapshotHandler = self.snapshotHandler, snapshot = self._renderer.snapshot() { + snapshotHandler(snapshot) + completion(Result.Success(element)) + } else { + completion(Result.Error(.SnapshotFailure)) + } + }) + }) + } + } +} + +#endif //======================================== // MARK: Debug Methods diff --git a/Example/Example OSX/ViewController.swift b/Example/Example OSX/ViewController.swift index 1d3bbe0..c2348bc 100644 --- a/Example/Example OSX/ViewController.swift +++ b/Example/Example OSX/ViewController.swift @@ -31,10 +31,6 @@ class ViewController: NSViewController { let url = NSURL(string: "https://github.com/logos")! - lazy var browser : WKZombie = { - return WKZombie(name: "Github Logo") - }() - override func viewDidLoad() { super.viewDidLoad() activityIndicator.startAnimation(nil) @@ -42,9 +38,9 @@ class ViewController: NSViewController { } func getTopTrendingEntry(url: NSURL) { - browser.open(url) - >>> browser.get(by: .XPathQuery("//img[contains(@class, 'gh-octocat')]")) - >>> browser.fetch + open(url) + >>> get(by: .XPathQuery("//img[contains(@class, 'gh-octocat')]")) + >>> fetch === output } diff --git a/Example/Example iOS/Base.lproj/Main.storyboard b/Example/Example iOS/Base.lproj/Main.storyboard index 99fee1c..78f2f57 100644 --- a/Example/Example iOS/Base.lproj/Main.storyboard +++ b/Example/Example iOS/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -8,7 +8,7 @@ - + @@ -43,7 +43,7 @@ -