├── .gitignore ├── .swift-version ├── .travis.yml ├── CONTRIBUTING.md ├── Cartfile ├── Example ├── Example OSX │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── Main.storyboard │ ├── Info.plist │ └── ViewController.swift ├── Example iOS │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── ic_logo.imageset │ │ │ ├── Contents.json │ │ │ └── Logo.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Button.swift │ ├── Info.plist │ ├── LoginViewController.swift │ ├── ProfileViewController.swift │ └── SnapshotViewController.swift ├── Example-Bridging-Header.h ├── Example.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Example.xcworkspace │ └── contents.xcworkspacedata ├── Podfile └── Podfile.lock ├── Framework ├── Info.plist └── WKZombie.h ├── LICENSE ├── Package.swift ├── README.md ├── Resources ├── Documentation │ ├── Logo.png │ ├── WKZombie-Simulator-Demo.gif │ └── WKZombie-Web-Demo.gif └── Tests │ ├── HTML │ ├── HTMLEmbeddedPage.html │ ├── HTMLResultPage.html │ └── HTMLTestPage.html │ ├── HostApplication │ ├── AppDelegate.swift │ ├── Default-568h@2x.png │ └── Info.plist │ └── Info.plist ├── Scripts ├── setup-framework.sh ├── test-framework.sh └── update-carthage.sh ├── Sources ├── Example │ └── main.swift └── WKZombie │ ├── ContentFetcher.swift │ ├── Definitions.swift │ ├── Error.swift │ ├── Functions.swift │ ├── HTMLButton.swift │ ├── HTMLElement.swift │ ├── HTMLFetchable.swift │ ├── HTMLForm.swift │ ├── HTMLFrame.swift │ ├── HTMLImage.swift │ ├── HTMLLink.swift │ ├── HTMLPage.swift │ ├── HTMLRedirectable.swift │ ├── HTMLTable.swift │ ├── JSONPage.swift │ ├── Logger.swift │ ├── Page.swift │ ├── Parser.swift │ ├── RenderOperation.swift │ ├── Renderer.swift │ ├── Snapshot.swift │ └── WKZombie.swift ├── Tests └── WKZombieTests │ └── Tests.swift ├── WKZombie.podspec ├── WKZombie.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── Tests.xcscheme │ ├── WKZombie OSX.xcscheme │ └── WKZombie.xcscheme └── WKZombie.xcworkspace └── contents.xcworkspacedata /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .gitmodules 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | 22 | ## Other 23 | *.xccheckout 24 | *.moved-aside 25 | *.xcuserstate 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 37 | # 38 | Pods/ 39 | 40 | # Carthage 41 | # 42 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 43 | #Carthage/Checkouts 44 | #Carthage/Build 45 | Carthage/ 46 | Cartfile.resolved 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 54 | 55 | fastlane/report.xml 56 | fastlane/screenshots 57 | 58 | # SPM 59 | .build/ 60 | Packages/ 61 | 62 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9 3 | before_install: 4 | - bash Scripts/update-carthage.sh 0.16.2 5 | script: 6 | - Scripts/test-framework.sh 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Fork and create a new branch. *It is best to leave `master` for keeping your fork up to date.* 4 | - Try to keep the code style consistent. 5 | - Test your changes with the demo app. 6 | - Push your code and open a Pull Request! 7 | 8 | # Bugs 9 | 10 | Please be clear and as detailed as possible in your description of the bug. 11 | - version you are using. *just post your Podfile.lock if unsure*. 12 | - Steps to reproduce. 13 | - <3 a demo app on git or zip. 14 | 15 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "mkoehnke/hpple" "0.2.2" 2 | -------------------------------------------------------------------------------- /Example/Example OSX/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Cocoa 25 | 26 | @NSApplicationMain 27 | class AppDelegate: NSObject, NSApplicationDelegate { 28 | 29 | 30 | 31 | func applicationDidFinishLaunching(aNotification: NSNotification) { 32 | // Insert code here to initialize your application 33 | } 34 | 35 | func applicationWillTerminate(aNotification: NSNotification) { 36 | // Insert code here to tear down your application 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Example/Example OSX/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Example/Example OSX/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016 Mathias Köhnke. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Example/Example OSX/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Cocoa 25 | import WKZombie 26 | 27 | class ViewController: NSViewController { 28 | 29 | @IBOutlet weak var imageView : NSImageView! 30 | @IBOutlet weak var activityIndicator : NSProgressIndicator! 31 | 32 | let url = URL(string: "https://github.com/logos")! 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | activityIndicator.startAnimation(nil) 37 | getTopTrendingEntry(url: url) 38 | } 39 | 40 | func getTopTrendingEntry(url: URL) { 41 | open(url) 42 | >>> get(by: .XPathQuery("//img[contains(@alt, 'Github Octocat')]")) 43 | >>> fetch 44 | === output 45 | } 46 | 47 | func output(result: HTMLImage?) { 48 | imageView.image = result?.fetchedContent() 49 | activityIndicator.stopAnimation(nil) 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Example/Example iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | @UIApplicationMain 27 | class AppDelegate: UIResponder, UIApplicationDelegate { 28 | 29 | var window: UIWindow? 30 | 31 | 32 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 33 | // Override point for customization after application launch. 34 | return true 35 | } 36 | 37 | func applicationWillResignActive(_ application: UIApplication) { 38 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 39 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 40 | } 41 | 42 | func applicationDidEnterBackground(_ application: UIApplication) { 43 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 44 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 45 | } 46 | 47 | func applicationWillEnterForeground(_ application: UIApplication) { 48 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 49 | } 50 | 51 | func applicationDidBecomeActive(_ application: UIApplication) { 52 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 53 | } 54 | 55 | func applicationWillTerminate(_ application: UIApplication) { 56 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 57 | } 58 | 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Example/Example iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Example/Example iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example iOS/Assets.xcassets/ic_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Logo.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Example iOS/Assets.xcassets/ic_logo.imageset/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkoehnke/WKZombie/6cf807e42ec120d251844522f2b28e66bbf7ddda/Example/Example iOS/Assets.xcassets/ic_logo.imageset/Logo.png -------------------------------------------------------------------------------- /Example/Example iOS/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Example/Example iOS/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /Example/Example iOS/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | open class Button : UIButton { 27 | 28 | override open var isEnabled: Bool { 29 | didSet { 30 | if isEnabled == true { 31 | backgroundColor = UIColor(red: 0.0/255.9, green: 122.0/255.0, blue: 255.0/255.0, alpha: 1.0) 32 | } else { 33 | backgroundColor = .darkGray 34 | } 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Example/Example iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example iOS/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | import WKZombie 26 | 27 | class LoginViewController : UIViewController { 28 | 29 | @IBOutlet weak var nameTextField: UITextField! 30 | @IBOutlet weak var passwordTextField: UITextField! 31 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 32 | @IBOutlet weak var loginButton : UIButton! 33 | 34 | fileprivate let url = URL(string: "https://developer.apple.com/membercenter/index.action")! 35 | fileprivate var snapshots = [Snapshot]() 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | WKZombie.sharedInstance.snapshotHandler = { [weak self] snapshot in 41 | self?.snapshots.append(snapshot) 42 | } 43 | } 44 | 45 | @IBAction func loginButtonTouched(_ button: UIButton) { 46 | guard let user = nameTextField.text, let password = passwordTextField.text else { return } 47 | setUserInterfaceEnabled(enabled: false) 48 | snapshots.removeAll() 49 | activityIndicator.startAnimating() 50 | getProvisioningProfiles(url, user: user, password: password) 51 | } 52 | 53 | private func setUserInterfaceEnabled(enabled: Bool) { 54 | nameTextField.isEnabled = enabled 55 | passwordTextField.isEnabled = enabled 56 | loginButton.isEnabled = enabled 57 | } 58 | 59 | //======================================== 60 | // MARK: HTML Navigation 61 | //======================================== 62 | 63 | func getProvisioningProfiles(_ url: URL, user: String, password: String) { 64 | open(url) 65 | >>* get(by: .id("accountname")) 66 | >>> setAttribute("value", value: user) 67 | >>* get(by: .id("accountpassword")) 68 | >>> setAttribute("value", value: password) 69 | >>* get(by: .name("form2")) 70 | >>> submit(then: .wait(2.0)) 71 | >>* get(by: .contains("href", "/certificate/")) 72 | >>> click(then: .wait(2.5)) 73 | >>* getAll(by: .contains("class", "row-")) 74 | === handleResult 75 | } 76 | 77 | //======================================== 78 | // MARK: Handle Result 79 | //======================================== 80 | 81 | func handleResult(_ result: Result<[HTMLTableRow]>) { 82 | switch result { 83 | case .success(let value): self.outputResult(value) 84 | case .error(let error): self.handleError(error) 85 | } 86 | } 87 | 88 | func outputResult(_ rows: [HTMLTableRow]) { 89 | let columns = rows.flatMap { $0.columns?.first } 90 | performSegue(withIdentifier: "detailSegue", sender: columns) 91 | } 92 | 93 | func handleError(_ error: ActionError) { 94 | clearCache() 95 | dump() 96 | 97 | inspect >>> execute("document.title") === { [weak self] (result: JavaScriptResult?) in 98 | let title = result ?? "" 99 | let alert = UIAlertController(title: "Error On Page:", message: "\"\(title)\"", preferredStyle: .alert) 100 | alert.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil)) 101 | self?.present(alert, animated: true) { 102 | self?.setUserInterfaceEnabled(enabled: true) 103 | self?.activityIndicator.stopAnimating() 104 | } 105 | } 106 | } 107 | 108 | //======================================== 109 | // MARK: Segue 110 | //======================================== 111 | 112 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 113 | if segue.identifier == "detailSegue" { 114 | if let vc = segue.destination as? ProfileViewController, let items = sender as? [HTMLTableColumn] { 115 | vc.items = items 116 | vc.snapshots = snapshots 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Example/Example iOS/ProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | import WKZombie 26 | 27 | class ProfileViewController: UITableViewController { 28 | 29 | var items : [HTMLTableColumn]? 30 | var snapshots : [Snapshot]? 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | navigationItem.hidesBackButton = true 35 | } 36 | 37 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | return items?.count ?? 0 39 | } 40 | 41 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 43 | let item = items?[indexPath.row].children()?.first as HTMLElement? 44 | cell.textLabel?.text = item?.text 45 | return cell 46 | } 47 | 48 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 49 | if segue.identifier == "snapshotSegue" { 50 | let vc = segue.destination as? SnapshotViewController 51 | vc?.snapshots = snapshots 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Example/Example iOS/SnapshotViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenshotViewController.swift 3 | // Example 4 | // 5 | // Created by Mathias Köhnke on 20/05/16. 6 | // Copyright © 2016 Mathias Köhnke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WKZombie 11 | 12 | class SnapshotCell : UICollectionViewCell { 13 | static let cellIdentifier = "snapshotCell" 14 | @IBOutlet weak var imageView : UIImageView! 15 | } 16 | 17 | class SnapshotViewController: UICollectionViewController { 18 | 19 | var snapshots : [Snapshot]? 20 | 21 | 22 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 23 | return snapshots?.count ?? 0 24 | } 25 | 26 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 27 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SnapshotCell.cellIdentifier, for: indexPath) 28 | if let cell = cell as? SnapshotCell { 29 | cell.imageView.image = snapshots?[indexPath.row].image 30 | } 31 | return cell 32 | } 33 | } 34 | 35 | extension SnapshotViewController : UICollectionViewDelegateFlowLayout { 36 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 37 | let width = (view.bounds.size.width / 2) - 1 38 | let height = (view.bounds.size.height * width) / view.bounds.size.width 39 | return CGSize(width: width, height: height) 40 | } 41 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 42 | return 1.0 43 | } 44 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 45 | return 2.0 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/Example-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Example-Bridging-Header.h 3 | // Example 4 | // 5 | // Created by Mathias Köhnke on 07/04/16. 6 | // Copyright © 2016 Mathias Köhnke. All rights reserved. 7 | // 8 | 9 | #ifndef Example_Bridging_Header_h 10 | #define Example_Bridging_Header_h 11 | 12 | #import 13 | 14 | #endif /* Example_Bridging_Header_h */ 15 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | install! 'cocoapods', :deterministic_uuids => false 3 | inhibit_all_warnings! 4 | use_frameworks! 5 | 6 | def import_pods 7 | pod 'WKZombie', :path => '../' 8 | end 9 | 10 | target 'Example iOS' do 11 | platform :ios, '9.1' 12 | import_pods 13 | end 14 | 15 | target 'Example OSX' do 16 | platform :osx, '10.11' 17 | import_pods 18 | end 19 | 20 | post_install do |installer| 21 | installer.pods_project.targets.each do |target| 22 | target.build_configurations.each do |config| 23 | config.build_settings['SWIFT_VERSION'] = '4.0' 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - hpple (0.2.0) 3 | - WKZombie (1.1.0): 4 | - hpple (= 0.2.0) 5 | 6 | DEPENDENCIES: 7 | - WKZombie (from `../`) 8 | 9 | EXTERNAL SOURCES: 10 | WKZombie: 11 | :path: ../ 12 | 13 | SPEC CHECKSUMS: 14 | hpple: 3b765f96fc2cd56ad1a49aef6f7be5cb2aa64b57 15 | WKZombie: b92d04c503fd505b55f039387a54d741a775cdb6 16 | 17 | PODFILE CHECKSUM: cf374de8993bb9bbb1e9cd3fab90e1a99a15e8f8 18 | 19 | COCOAPODS: 1.3.1 20 | -------------------------------------------------------------------------------- /Framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Framework/WKZombie.h: -------------------------------------------------------------------------------- 1 | // 2 | // WKZombie.h 3 | // 4 | // Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import 25 | 26 | //! Project version number for WKZombie. 27 | FOUNDATION_EXPORT double WKZombieVersionNumber; 28 | 29 | //! Project version string for WKZombie. 30 | FOUNDATION_EXPORT const unsigned char WKZombieVersionString[]; 31 | 32 | // In this header, you should import all the public headers of your framework using statements like #import 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mathias Köhnke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "WKZombie", 5 | targets: [ 6 | Target(name: "WKZombie"), 7 | Target(name: "Example", dependencies:["WKZombie"]) 8 | ], 9 | dependencies: [ 10 | .Package(url: "https://github.com/mkoehnke/hpple.git", Version(0,2,2)) 11 | ] 12 | ) 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WKZombie 2 | [![Twitter: @mkoehnke](https://img.shields.io/badge/contact-@mkoehnke-blue.svg?style=flat)](https://twitter.com/mkoehnke) 3 | [![Version](https://img.shields.io/cocoapods/v/WKZombie.svg?style=flat)](http://cocoadocs.org/docsets/WKZombie) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![SPM compatible](https://img.shields.io/badge/SPM-compatible-orange.svg?style=flat)](https://github.com/apple/swift-package-manager) 6 | [![License](https://img.shields.io/cocoapods/l/WKZombie.svg?style=flat)](http://cocoadocs.org/docsets/WKZombie) 7 | [![Platform](https://img.shields.io/cocoapods/p/WKZombie.svg?style=flat)](http://cocoadocs.org/docsets/WKZombie) 8 | [![Build Status](https://travis-ci.org/mkoehnke/WKZombie.svg?branch=master)](https://travis-ci.org/mkoehnke/WKZombie) 9 | 10 | [](#logo) 11 | 12 | WKZombie is an **iOS/OSX web-browser without a graphical user interface**. It was developed as an experiment in order to familiarize myself with **using functional concepts** written in **Swift 4**. 13 | 14 | It incorporates [WebKit](https://webkit.org) (WKWebView) for rendering and [hpple](https://github.com/topfunky/hpple) (libxml2) for parsing the HTML content. In addition, it can take snapshots and has rudimentary support for parsing/decoding [JSON elements](#json-elements). **Chaining asynchronous actions makes the code compact and easy to use.** 15 | 16 | ## Use Cases 17 | There are many use cases for a Headless Browser. Some of them are: 18 | 19 | * Collect data without an API 20 | * Scraping websites 21 | * Automating interaction of websites 22 | * Manipulation of websites 23 | * Running automated tests / snapshots 24 | * etc. 25 | 26 | ## Example 27 | The following example is supposed to demonstrate the WKZombie functionality. Let's assume that we want to **show all iOS Provisioning Profiles in the Apple Developer Portal**. 28 | 29 | #### Manual Web-Browser Navigation 30 | 31 | When using a common web-browser (e.g. Mobile Safari) on iOS, you would typically type in your credentials, sign in and navigate (via links) to the *Provisioning Profiles* section: 32 | 33 | 34 | 35 | #### Automation with WKZombie 36 | 37 | The same navigation process can be reproduced **automatically** within an iOS/OSX app linking WKZombie *Actions*. In addition, it is now possible to manipulate or display this data in a native way with *UITextfield*, *UIButton* and a *UITableView*. 38 | 39 | 40 | 41 | **Take a look at the iOS/OSX demos in the `Example` directory to see how to use it.** 42 | 43 | # Getting Started 44 | 45 | ## iOS / OSX 46 | 47 | The best way to get started is to look at the sample project. Just run the following commands in your shell and you're good to go: 48 | 49 | ```bash 50 | $ cd Example 51 | $ pod install 52 | $ open Example.xcworkspace 53 | ``` 54 | 55 | __Note:__ You will need CocoaPods 1.0 beta4 or higher. 56 | 57 | ## Command-Line 58 | 59 | For a Command-Line demo, run the following commands inside the `WKZombie` root folder: 60 | 61 | ```ogdl 62 | $ swift build -Xcc -I/usr/include/libxml2 -Xlinker -lxml2 63 | 64 | $ .build/debug/Example 65 | ``` 66 | 67 | 68 | # Usage 69 | A WKZombie instance equates to a web session. Top-level convenience methods like *WKZombie.open()* use a shared instance, which is configured with the default settings. 70 | 71 | As such, the following three statements are equivalent: 72 | 73 | ```ruby 74 | let action : Action = open(url) 75 | ``` 76 | 77 | ```ruby 78 | let action : Action = WKZombie.open(url) 79 | ``` 80 | 81 | ```ruby 82 | let browser = WKZombie.sharedInstance 83 | let action : Action = browser.open(url) 84 | ``` 85 | 86 | Applications can also create their own WKZombie instance: 87 | 88 | ```ruby 89 | self.browser = WKZombie(name: "Demo") 90 | ``` 91 | 92 | Be sure to keep `browser` in a stored property for the time of being used. 93 | 94 | ### a. Chaining Actions 95 | 96 | Web page navigation is based on *Actions*, that can be executed **implicitly** when chaining actions using the [`>>>`](#operators) or [`>>*`](#operators) (for snapshots) operators. All chained actions pass their result to the next action. The [`===`](#operators) operator then starts the execution of the action chain. 97 | 98 | The following snippet demonstrates how you would use WKZombie to **collect all Provisioning Profiles** from the Developer Portal and **take snapshots of every page**: 99 | 100 | ```ruby 101 | open(url) 102 | >>* get(by: .id("accountname")) 103 | >>> setAttribute("value", value: user) 104 | >>* get(by: .id("accountpassword")) 105 | >>> setAttribute("value", value: password) 106 | >>* get(by: .name("form2")) 107 | >>> submit 108 | >>* get(by: .contains("href", "/account/")) 109 | >>> click(then: .wait(2.5)) 110 | >>* getAll(by: .contains("class", "row-")) 111 | === myOutput 112 | ``` 113 | 114 | In order to output or process the collected data, one can either use a closure or implement a custom function taking the result as parameter: 115 | 116 | ```ruby 117 | func myOutput(result: [HTMLTableColumn]?) { 118 | // handle result 119 | } 120 | ``` 121 | 122 | or 123 | 124 | ```ruby 125 | func myOutput(result: Result<[HTMLTableColumn]>) { 126 | switch result { 127 | case .success(let value): // handle success 128 | case .error(let error): // handle error 129 | } 130 | } 131 | ``` 132 | 133 | ### b. Manual Actions 134 | 135 | *Actions* can also be started manually by calling the *start()* method: 136 | 137 | ```ruby 138 | let action : Action = browser.open(url) 139 | 140 | action.start { result in 141 | switch result { 142 | case .success(let page): // process page 143 | case .error(let error): // handle error 144 | } 145 | } 146 | ``` 147 | 148 | This is certainly the less complicated way, but you have to write a lot more code, which might become confusing when you want to execute *Actions* successively. 149 | 150 | 151 | ## Basic Action Functions 152 | There are currently a few *Actions* implemented, helping you visit and navigate within a website: 153 | 154 | ### Open a Website 155 | 156 | The returned WKZombie Action will load and return a HTML or JSON page for the specified URL. 157 | 158 | ```ruby 159 | func open(url: URL) -> Action 160 | ``` 161 | 162 | Optionally, a *PostAction* can be passed. This is a special wait/validation action, that is performed after the page has finished loading. See [PostAction](#special-parameters) for more information. 163 | 164 | ```ruby 165 | func open(then: PostAction) -> (url: URL) -> Action 166 | ``` 167 | 168 | ### Get the current Website 169 | 170 | The returned WKZombie Action will retrieve the current page. 171 | 172 | ```ruby 173 | func inspect() -> Action 174 | ``` 175 | 176 | ### Submit a Form 177 | 178 | The returned WKZombie Action will submit the specified HTML form. 179 | 180 | ```ruby 181 | func submit(form: HTMLForm) -> Action 182 | ``` 183 | 184 | Optionally, a *PostAction* can be passed. See [PostAction](#special-parameters) for more information. 185 | 186 | ```ruby 187 | func submit(then: PostAction) -> (form: HTMLForm) -> Action 188 | ``` 189 | 190 | ### Click a Link / Press a Button 191 | 192 | The returned WKZombie Actions will simulate the interaction with a HTML link/button. 193 | 194 | ```ruby 195 | func click(link : HTMLLink) -> Action 196 | func press(button : HTMLButton) -> Action 197 | ``` 198 | 199 | Optionally, a *PostAction* can be passed. See [PostAction](#Special- Parameters) for more information. 200 | 201 | ```ruby 202 | func click(then: PostAction) -> (link : HTMLLink) -> Action 203 | func press(then: PostAction) -> (button : HTMLButton) -> Action 204 | ``` 205 | 206 | **Note: HTMLButton only works if the "onClick" HTML-Attribute is present. If you want to submit a HTML form, you should use [Submit](#submit-a-form) instead.** 207 | 208 | ### Find HTML Elements 209 | 210 | The returned WKZombie Action will search the specified HTML page and return the first element matching the generic HTML element type and passed [SearchType](#special-parameters). 211 | 212 | ```ruby 213 | func get(by: SearchType) -> (page: HTMLPage) -> Action 214 | ``` 215 | 216 | The returned WKZombie Action will search and return all elements matching. 217 | 218 | ```ruby 219 | func getAll(by: SearchType) -> (page: HTMLPage) -> Action<[T]> 220 | ``` 221 | 222 | 223 | ### Set an Attribute 224 | 225 | The returned WKZombie Action will set or update an existing attribute/value pair on the specified HTMLElement. 226 | ```ruby 227 | func setAttribute(key: String, value: String?) -> (element: T) -> Action 228 | ``` 229 | 230 | ### Execute JavaScript 231 | 232 | The returned WKZombie Actions will execute a JavaScript string. 233 | 234 | ```ruby 235 | func execute(script: JavaScript) -> (page: HTMLPage) -> Action 236 | func execute(script: JavaScript) -> Action 237 | ``` 238 | 239 | For example, the following example shows how retrieve the title of the currently loaded website using the *execute()* method: 240 | 241 | ```ruby 242 | browser.inspect 243 | >>> browser.execute("document.title") 244 | === myOutput 245 | 246 | func myOutput(result: JavaScriptResult?) { 247 | // handle result 248 | } 249 | ``` 250 | 251 | The following code shows another way to execute JavaScript, that is e.g. value of an attribute: 252 | 253 | ```ruby 254 | browser.open(url) 255 | >>> browser.get(by: .id("div")) 256 | >>> browser.map { $0.objectForKey("onClick")! } 257 | >>> browser.execute 258 | >>> browser.inspect 259 | === myOutput 260 | 261 | func myOutput(result: HTMLPage?) { 262 | // handle result 263 | } 264 | ``` 265 | 266 | ### Fetching 267 | 268 | Some HTMLElements, that implement the _HTMLFetchable_ protocol (e.g. _HTMLLink_ or _HTMLImage_), contain attributes like _"src"_ or _"href"_, that link to remote objects or data. 269 | The following method returns a WKZombie Action that can conveniently download this data: 270 | 271 | ```ruby 272 | func fetch(fetchable: T) -> Action 273 | ``` 274 | 275 | Once the _fetch_ method has been executed, the data can be retrieved and __converted__. The following example shows how to convert data, fetched from a link, into an UIImage: 276 | 277 | ```ruby 278 | let image : UIImage? = link.fetchedContent() 279 | ``` 280 | 281 | Fetched data can be converted into types, that implement the _HTMLFetchableContent_ protocol. The following types are currently supported: 282 | 283 | - UIImage / NSImage 284 | - Data 285 | 286 | __Note:__ See the OSX example for more info on how to use this. 287 | 288 | ### Transform 289 | 290 | The returned WKZombie Action will transform a WKZombie object into another object using the specified function *f*. 291 | 292 | ```ruby 293 | func map(f: T -> A) -> (element: T) -> Action 294 | ``` 295 | 296 | This function transforms an object into another object using the specified function *f*. 297 | 298 | ```ruby 299 | func map(f: T -> A) -> (object: T) -> A 300 | ``` 301 | 302 | ## Taking Snapshots 303 | 304 | Taking snapshots is **available for iOS**. First, a *snapshotHandler* must be registered, that will be called each time a snapshot has been taken: 305 | 306 | ```ruby 307 | WKZombie.sharedInstance.snapshotHandler = { snapshot in 308 | let image = snapshot.image 309 | } 310 | ``` 311 | 312 | Secondly, adding the `>>*` operator will trigger the snapshot event: 313 | 314 | ```ruby 315 | open(url) 316 | >>* get(by: .id("element")) 317 | === myOutput 318 | ``` 319 | **Note: This operator only works with the WKZombie shared instance.** 320 | 321 | Alternatively, one can use the *snap* command: 322 | 323 | ```ruby 324 | browser.open(url) 325 | >>> browser.snap 326 | >>> browser.get(by: .id("element")) 327 | === myOutput 328 | ``` 329 | 330 | Take a look at the **iOS example for more information** of how to take snapshots. 331 | 332 | 333 | ## Special Parameters 334 | 335 | ### 1. PostAction 336 | 337 | Some *Actions*, that incorporate a (re-)loading of webpages (e.g. [open](#open-a-website), [submit](#submit-a-form), etc.), have *PostActions* available. A *PostAction* is a wait or validation action, that will be performed after the page has finished loading: 338 | 339 | PostAction | Description 340 | ------------------------- | ------------- 341 | **wait** (Seconds) | The time in seconds that the action will wait (after the page has been loaded) before returning. This is useful in cases where the page loading has been completed, but some JavaScript/Image loading is still in progress. 342 | **validate** (Javascript) | The action will complete if the specified JavaScript expression/script returns 'true' or a timeout occurs. 343 | 344 | ### 2. SearchType 345 | 346 | In order to find certain HTML elements within a page, you have to specify a *SearchType*. The return type of [get()](#find-html-elements) and [getAll()](#find-html-elements) is generic and determines which tag should be searched for. For instance, the following would return all links with the class *book*: 347 | 348 | ```ruby 349 | let books : Action = browser.getAll(by: .class("book"))(page: htmlPage) 350 | ``` 351 | 352 | The following 6 types are currently available and supported: 353 | 354 | SearchType | Description 355 | ------------------------------ | ------------- 356 | **id** (String) | Returns an element that matches the specified id. 357 | **name** (String) | Returns all elements matching the specified value for their *name* attribute. 358 | **text** (String) | Returns all elements with inner content, that *contain* the specified text. 359 | **class** (String) | Returns all elements that match the specified class name. 360 | **attribute** (String, String) | Returns all elements that match the specified attribute name/value combination. 361 | **contains** (String, String) | Returns all elements with an attribute containing the specified value. 362 | **XPathQuery** (String) | Returns all elements that match the specified XPath query. 363 | 364 | ## Operators 365 | 366 | The following Operators can be applied to *Actions*, which makes chained *Actions* easier to read: 367 | 368 | Operator | iOS | OSX | Description 369 | :----------:|:---:|:---:| --------------- 370 | `>>>` | x | x | This Operator equates to the *andThen()* method. Here, the left-hand side *Action* will be started and the result is used as parameter for the right-hand side *Action*. **Note:** If the right-hand side *Action* doesn't take a parameter, the result of the left-hand side *Action* will be ignored and not passed. 371 | `>>*` | x | | 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. **Note: This operator throws an assert if used with any other than the shared instance.** 372 | `===` | x | x | This Operator starts the left-hand side *Action* and passes the result as **Optional** to the function on the right-hand side. 373 | 374 | ## Authentication 375 | 376 | Once in a while you might need to handle authentication challenges e.g. *Basic Authentication* or *Self-signed Certificates*. WKZombie provides an `authenticationHandler`, which is invoked when the internal web view needs to respond to an authentication challenge. 377 | 378 | ### Basic Authentication 379 | 380 | The following example shows how Basic Authentication could be handled: 381 | 382 | ```ruby 383 | browser.authenticationHandler = { (challenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) in 384 | return (.useCredential, URLCredential(user: "user", password: "passwd", persistence: .forSession)) 385 | } 386 | 387 | ``` 388 | 389 | ### Self-signed Certificates 390 | 391 | In case of a self-signed certificate, you could use the authentication handler like this: 392 | 393 | ```ruby 394 | browser.authenticationHandler = { (challenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) in 395 | return (.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 396 | } 397 | 398 | ``` 399 | 400 | 401 | ## Advanced Action Functions 402 | 403 | ### Batch 404 | 405 | The returned WKZombie Action will make a bulk execution of the specified action function *f* with the provided input elements. Once all actions have finished executing, the collected results will be returned. 406 | 407 | ```ruby 408 | func batch(f: T -> Action) -> (elements: [T]) -> Action<[U]> 409 | ``` 410 | 411 | ### Collect 412 | 413 | The returned WKZombie Action will execute 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. 414 | 415 | ```ruby 416 | func collect(f: T -> Action, until: T -> Bool) -> (initial: T) -> Action<[T]> 417 | ``` 418 | 419 | ### Swap 420 | 421 | **Note:** Due to a XPath limitation, WKZombie can't access elements within an `iframe` directly. The swap function can workaround this issue by switching web contexts. 422 | 423 | The returned WKZombie Action will swap the current page context with the context of an embedded ` 14 | 15 |
16 | Option 1
17 | Option 2
18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /Resources/Tests/HostApplication/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppDelegate.swift 3 | // 4 | // Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import UIKit 25 | 26 | @UIApplicationMain 27 | class AppDelegate: UIResponder, UIApplicationDelegate { 28 | 29 | var window: UIWindow? 30 | var viewController : UIViewController? 31 | 32 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 33 | window = UIWindow(frame: UIScreen.main.bounds) 34 | window?.backgroundColor = .white 35 | viewController = UIViewController(nibName: nil, bundle: nil) 36 | window?.rootViewController = viewController 37 | window?.makeKeyAndVisible() 38 | return true 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Resources/Tests/HostApplication/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkoehnke/WKZombie/6cf807e42ec120d251844522f2b28e66bbf7ddda/Resources/Tests/HostApplication/Default-568h@2x.png -------------------------------------------------------------------------------- /Resources/Tests/HostApplication/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | NSAppTransportSecurity 30 | 31 | NSExceptionDomains 32 | 33 | badssl.com 34 | 35 | 36 | NSIncludesSubdomains 37 | 38 | 39 | NSTemporaryExceptionAllowsInsecureHTTPLoads 40 | 41 | 42 | NSTemporaryExceptionMinimumTLSVersion 43 | TLSv1.1 44 | 45 | 46 | 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Resources/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Scripts/setup-framework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v carthage > /dev/null; then 4 | printf 'Carthage is not installed.\n' 5 | printf 'See https://github.com/Carthage/Carthage for install instructions.\n' 6 | exit 1 7 | fi 8 | 9 | #carthage update --platform all --use-submodules --no-use-binaries 10 | carthage update --platform all --no-use-binaries 11 | -------------------------------------------------------------------------------- /Scripts/test-framework.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bash Scripts/setup-framework.sh 4 | xcodebuild -workspace WKZombie.xcworkspace -scheme WKZombie -sdk iphonesimulator11.0 -destination 'platform=iOS Simulator,name=iPhone 8,OS=11.0' build test 5 | -------------------------------------------------------------------------------- /Scripts/update-carthage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $# -eq 0 ]] ; then 4 | echo "Carthage version required (e.g. 0.11)" 5 | exit 1 6 | fi 7 | 8 | curl -OlL "https://github.com/Carthage/Carthage/releases/download/$1/Carthage.pkg" 9 | sudo installer -pkg "Carthage.pkg" -target / 10 | rm "Carthage.pkg" 11 | -------------------------------------------------------------------------------- /Sources/Example/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import WKZombie 3 | 4 | // Parameters 5 | let url = URL(string: "https://developer.apple.com/membercenter/index.action")! 6 | let arguments = CommandLine.arguments 7 | let user = arguments[1] 8 | let password = arguments[2] 9 | var shouldKeepRunning = true 10 | 11 | func handleResult(_ result: Result<[HTMLTableRow]>) { 12 | shouldKeepRunning = false 13 | switch result { 14 | case .success(let value): handleSuccess(result: value) 15 | case .error(let error): handleError(error: error) 16 | } 17 | } 18 | 19 | // Result handling 20 | func handleSuccess(result: [HTMLTableRow]?) { 21 | print("\n") 22 | print("PROVISIONING PROFILES:") 23 | print("======================") 24 | 25 | if let columns = result?.flatMap({ $0.columns?.first }) { 26 | for column in columns { 27 | if let element = column.children()?.first as HTMLElement?, let text = element.text { 28 | print(text) 29 | } 30 | } 31 | } else { 32 | print("Nothing found.") 33 | } 34 | } 35 | 36 | func handleError(error: ActionError) { 37 | print(error) 38 | } 39 | 40 | // WKZombie Actions 41 | open(url) 42 | >>> get(by: .id("accountname")) 43 | >>> setAttribute("value", value: user) 44 | >>> get(by: .id("accountpassword")) 45 | >>> setAttribute("value", value: password) 46 | >>> get(by: .name("form2")) 47 | >>> submit(then: .wait(2.0)) 48 | >>> get(by: .contains("href", "/account/")) 49 | >>> click(then: .wait(2.5)) 50 | >>> getAll(by: .contains("class", "row-")) 51 | === handleResult 52 | 53 | // Keep script running until actions are finished 54 | let theRL = RunLoop.current 55 | while shouldKeepRunning && theRL.run(mode: .defaultRunLoopMode, before: .distantFuture) { } 56 | -------------------------------------------------------------------------------- /Sources/WKZombie/ContentFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentFetcher.swift 3 | // 4 | // Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | typealias FetchCompletion = (_ result : Data?, _ response: URLResponse?, _ error: Error?) -> Void 27 | 28 | internal class ContentFetcher { 29 | 30 | fileprivate let defaultSession = URLSession(configuration: URLSessionConfiguration.default) 31 | 32 | @discardableResult 33 | func fetch(_ url: URL, completion: @escaping FetchCompletion) -> URLSessionTask? { 34 | let request = URLRequest(url: url) 35 | 36 | let task = defaultSession.dataTask(with: request, completionHandler: { (data, urlResponse, error) -> Void in 37 | completion(data, urlResponse, error) 38 | }) 39 | task.resume() 40 | return task 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Sources/WKZombie/Definitions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helper.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | 27 | public enum SearchType { 28 | /** 29 | * Returns an element that matches the specified id. 30 | */ 31 | case id(String) 32 | /** 33 | * Returns all elements matching the specified value for their name attribute. 34 | */ 35 | case name(String) 36 | /** 37 | * Returns all elements with inner content, that contain the specified text. 38 | */ 39 | case text(String) 40 | /** 41 | * Returns all elements that match the specified class name. 42 | */ 43 | case `class`(String) 44 | /** 45 | Returns all elements that match the specified attribute name/value combination. 46 | */ 47 | case attribute(String, String) 48 | /** 49 | Returns all elements with an attribute containing the specified value. 50 | */ 51 | case contains(String, String) 52 | /** 53 | Returns all elements that match the specified XPath query. 54 | */ 55 | case XPathQuery(String) 56 | 57 | func xPathQuery() -> String { 58 | switch self { 59 | case .text(let value): return T.createXPathQuery("[contains(text(),'\(value)')]") 60 | case .id(let id): return T.createXPathQuery("[@id='\(id)']") 61 | case .name(let name): return T.createXPathQuery("[@name='\(name)']") 62 | case .attribute(let key, let value): return T.createXPathQuery("[@\(key)='\(value)']") 63 | case .class(let className): return T.createXPathQuery("[@class='\(className)']") 64 | case .contains(let key, let value): return T.createXPathQuery("[contains(@\(key), '\(value)')]") 65 | case .XPathQuery(let query): return query 66 | } 67 | } 68 | } 69 | 70 | //======================================== 71 | // MARK: Result 72 | //======================================== 73 | 74 | public enum Result { 75 | case success(T) 76 | case error(ActionError) 77 | 78 | init(_ error: ActionError?, _ value: T) { 79 | if let err = error { 80 | self = .error(err) 81 | } else { 82 | self = .success(value) 83 | } 84 | } 85 | } 86 | 87 | public extension Result where T:Collection { 88 | public func first
() -> Result { 89 | switch self { 90 | case .success(let result): return resultFromOptional(result.first as? A, error: .notFound) 91 | case .error(let error): return resultFromOptional(nil, error: error) 92 | } 93 | } 94 | } 95 | 96 | extension Result: CustomDebugStringConvertible { 97 | public var debugDescription: String { 98 | switch self { 99 | case .success(let value): 100 | return "Success: \(String(describing: value))" 101 | case .error(let error): 102 | return "Error: \(String(describing: error))" 103 | } 104 | } 105 | } 106 | 107 | //======================================== 108 | // MARK: Response 109 | //======================================== 110 | 111 | internal struct Response { 112 | var data: Data? 113 | var statusCode: Int = ActionError.Static.DefaultStatusCodeError 114 | 115 | init(data: Data?, urlResponse: URLResponse) { 116 | self.data = data 117 | if let httpResponse = urlResponse as? HTTPURLResponse { 118 | self.statusCode = httpResponse.statusCode 119 | } 120 | } 121 | 122 | init(data: Data?, statusCode: Int) { 123 | self.data = data 124 | self.statusCode = statusCode 125 | } 126 | } 127 | 128 | infix operator >>>: AdditionPrecedence 129 | internal func >>>(a: Result, f: (A) -> Result) -> Result { 130 | switch a { 131 | case let .success(x): return f(x) 132 | case let .error(error): return .error(error) 133 | } 134 | } 135 | 136 | /** 137 | This Operator equates to the andThen() method. Here, the left-hand side Action will be started 138 | and the result is used as parameter for the right-hand side Action. 139 | 140 | - parameter a: An Action. 141 | - parameter f: A Function. 142 | 143 | - returns: An Action. 144 | */ 145 | public func >>>(a: Action, f: @escaping (T) -> Action) -> Action { 146 | return a.andThen(f) 147 | } 148 | 149 | /** 150 | This Operator equates to the andThen() method with the exception, that the result of the left-hand 151 | side Action will be ignored and not passed as paramter to the right-hand side Action. 152 | 153 | - parameter a: An Action. 154 | - parameter b: An Action. 155 | 156 | - returns: An Action. 157 | */ 158 | public func >>>(a: Action, b: Action) -> Action { 159 | let f : ((T) -> Action) = { _ in b } 160 | return a.andThen(f) 161 | } 162 | 163 | 164 | /** 165 | This Operator equates to the andThen() method with the exception, that the result of the left-hand 166 | side Action will be ignored and not passed as paramter to the right-hand side Action. 167 | 168 | *Note:* This a workaround to remove the brackets of functions without any parameters (e.g. **inspect()**) 169 | to provide a consistent API. 170 | */ 171 | public func >>>(a: Action, f: () -> Action) -> Action { 172 | return a >>> f() 173 | } 174 | 175 | /** 176 | This Operator equates to the andThen() method. Here, the left-hand side Action will be started 177 | and the result is used as parameter for the right-hand side Action. 178 | 179 | *Note:* This a workaround to remove the brackets of functions without any parameters (e.g. **inspect()**) 180 | to provide a consistent API. 181 | */ 182 | public func >>>(a: () -> Action, f: @escaping (T) -> Action) -> Action { 183 | return a() >>> f 184 | } 185 | 186 | /** 187 | This Operator starts the left-hand side Action and passes the result as Optional to the 188 | function on the right-hand side. 189 | 190 | - parameter a: An Action. 191 | - parameter completion: A Completion Block. 192 | */ 193 | public func ===(a: Action, completion: @escaping (T?) -> Void) { 194 | return a.start { result in 195 | switch result { 196 | case .success(let value): completion(value) 197 | case .error: completion(nil) 198 | } 199 | } 200 | } 201 | 202 | /** 203 | This operator passes the left-hand side Action and passes the result it to the 204 | function/closure on the right-hand side. 205 | 206 | - parameter a: An Action. 207 | - parameter completion: An output function/closure. 208 | */ 209 | public func ===(a: Action, completion: @escaping (Result) -> Void) { 210 | return a.start { result in 211 | completion(result) 212 | } 213 | } 214 | 215 | internal func parseResponse(_ response: Response) -> Result { 216 | let successRange = 200..<300 217 | if !successRange.contains(response.statusCode) { 218 | return .error(.networkRequestFailure) 219 | } 220 | return Result(nil, response.data ?? Data()) 221 | } 222 | 223 | internal func resultFromOptional(_ optional: A?, error: ActionError) -> Result { 224 | if let a = optional { 225 | return .success(a) 226 | } else { 227 | return .error(error) 228 | } 229 | } 230 | 231 | internal func decodeResult(_ url: URL? = nil) -> (_ data: Data?) -> Result { 232 | return { (data: Data?) -> Result in 233 | return resultFromOptional(T.pageWithData(data, url: url) as? T, error: .networkRequestFailure) 234 | } 235 | } 236 | 237 | internal func decodeString(_ data: Data?) -> Result { 238 | return resultFromOptional(data?.toString(), error: .transformFailure) 239 | } 240 | 241 | //======================================== 242 | // MARK: Actions 243 | // Borrowed from Javier Soto's 'Back to the Futures' Talk 244 | // https://speakerdeck.com/javisoto/back-to-the-futures 245 | //======================================== 246 | 247 | public struct Action { 248 | public typealias ResultType = Result 249 | public typealias Completion = (ResultType) -> () 250 | public typealias AsyncOperation = (@escaping Completion) -> () 251 | 252 | fileprivate let operation: AsyncOperation 253 | 254 | public init(result: ResultType) { 255 | self.init(operation: { completion in 256 | DispatchQueue.main.async(execute: { 257 | completion(result) 258 | }) 259 | }) 260 | } 261 | 262 | public init(value: T) { 263 | self.init(result: .success(value)) 264 | } 265 | 266 | public init(error: ActionError) { 267 | self.init(result: .error(error)) 268 | } 269 | 270 | public init(operation: @escaping AsyncOperation) { 271 | self.operation = operation 272 | } 273 | 274 | public func start(_ completion: @escaping Completion) { 275 | self.operation() { result in 276 | DispatchQueue.main.async(execute: { 277 | completion(result) 278 | }) 279 | } 280 | } 281 | } 282 | 283 | public extension Action { 284 | public func map(_ f: @escaping (T) -> U) -> Action { 285 | return Action(operation: { completion in 286 | self.start { result in 287 | DispatchQueue.main.async(execute: { 288 | switch result { 289 | case .success(let value): completion(Result.success(f(value))) 290 | case .error(let error): completion(Result.error(error)) 291 | } 292 | }) 293 | } 294 | }) 295 | } 296 | 297 | public func flatMap(_ f: @escaping (T) -> U?) -> Action { 298 | return Action(operation: { completion in 299 | self.start { result in 300 | DispatchQueue.main.async(execute: { 301 | switch result { 302 | case .success(let value): 303 | if let result = f(value) { 304 | completion(Result.success(result)) 305 | } else { 306 | completion(Result.error(.transformFailure)) 307 | } 308 | case .error(let error): completion(Result.error(error)) 309 | } 310 | }) 311 | } 312 | }) 313 | } 314 | 315 | public func andThen(_ f: @escaping (T) -> Action) -> Action { 316 | return Action(operation: { completion in 317 | self.start { firstFutureResult in 318 | switch firstFutureResult { 319 | case .success(let value): f(value).start(completion) 320 | case .error(let error): 321 | DispatchQueue.main.async(execute: { 322 | completion(Result.error(error)) 323 | }) 324 | } 325 | } 326 | }) 327 | } 328 | } 329 | 330 | 331 | //======================================== 332 | // MARK: Convenience Methods 333 | //======================================== 334 | 335 | public extension Action { 336 | 337 | /** 338 | Executes the specified action (with the result of the previous action execution as input parameter) until 339 | a certain condition is met. Afterwards, it will return the collected action results. 340 | 341 | - parameter initial: The initial input parameter for the Action. 342 | - parameter f: The Action which will be executed. 343 | - parameter until: If 'true', the execution of the specified Action will stop. 344 | 345 | - returns: The collected Sction results. 346 | */ 347 | internal static func collect(_ initial: T, f: @escaping (T) -> Action, until: @escaping (T) -> Bool) -> Action<[T]> { 348 | var values = [T]() 349 | func loop(_ future: Action) -> Action<[T]> { 350 | return Action<[T]>(operation: { completion in 351 | future.start { result in 352 | switch result { 353 | case .success(let newValue): 354 | values.append(newValue) 355 | if until(newValue) == true { 356 | loop(f(newValue)).start(completion) 357 | } else { 358 | DispatchQueue.main.async(execute: { 359 | completion(Result.success(values)) 360 | }) 361 | } 362 | case .error(let error): 363 | DispatchQueue.main.async(execute: { 364 | completion(Result.error(error)) 365 | }) 366 | } 367 | } 368 | }) 369 | } 370 | return loop(f(initial)) 371 | } 372 | 373 | /** 374 | Makes a bulk execution of the specified action with the provided input values. Once all actions have 375 | finished, the collected results will be returned. 376 | 377 | - parameter elements: An array containing the input value for the Action. 378 | - parameter f: The Action. 379 | 380 | - returns: The collected Action results. 381 | */ 382 | internal static func batch(_ elements: [T], f: @escaping (T) -> Action) -> Action<[U]> { 383 | return Action<[U]>(operation: { completion in 384 | let group = DispatchGroup() 385 | var results = [U]() 386 | for element in elements { 387 | group.enter() 388 | f(element).start({ result in 389 | switch result { 390 | case .success(let value): 391 | results.append(value) 392 | group.leave() 393 | case .error(let error): 394 | DispatchQueue.main.async(execute: { 395 | completion(Result.error(error)) 396 | }) 397 | } 398 | }) 399 | } 400 | group.notify(queue: DispatchQueue.main) { 401 | completion(Result.success(results)) 402 | } 403 | }) 404 | } 405 | } 406 | 407 | 408 | //======================================== 409 | // MARK: Post Action 410 | //======================================== 411 | 412 | /** 413 | An wait/validation action that will be performed after the page has reloaded. 414 | */ 415 | public enum PostAction { 416 | /** 417 | The time in seconds that the action will wait (after the page has been loaded) before returning. 418 | This is useful in cases where the page loading has been completed, but some JavaScript/Image loading 419 | is still in progress. 420 | 421 | - returns: Time in Seconds. 422 | */ 423 | case wait(TimeInterval) 424 | /** 425 | The action will complete if the specified JavaScript expression/script returns 'true' 426 | or a timeout occurs. 427 | 428 | - returns: Validation Script. 429 | */ 430 | case validate(String) 431 | /// No Post Action will be performed. 432 | case none 433 | } 434 | 435 | 436 | //======================================== 437 | // MARK: JSON 438 | // Inspired by Tony DiPasquale's Article 439 | // https://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics 440 | //======================================== 441 | 442 | public typealias JSON = Any 443 | public typealias JSONElement = [String : Any] 444 | 445 | internal func parseJSON(_ data: Data) -> Result { 446 | var jsonOptional: U? 447 | var __error = ActionError.parsingFailure 448 | 449 | do { 450 | if let data = htmlToData(NSString(data: data, encoding: String.Encoding.utf8.rawValue)) { 451 | jsonOptional = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0)) as? U 452 | } 453 | } catch _ { 454 | __error = .parsingFailure 455 | jsonOptional = nil 456 | } 457 | 458 | return resultFromOptional(jsonOptional, error: __error) 459 | } 460 | 461 | internal func decodeJSON(_ json: JSON?) -> Result { 462 | if let element = json as? JSONElement { 463 | return resultFromOptional(U.decode(element), error: .parsingFailure) 464 | } 465 | return Result.error(.parsingFailure) 466 | } 467 | 468 | internal func decodeJSON(_ json: JSON?) -> Result<[U]> { 469 | let result = [U]() 470 | if let elements = json as? [JSONElement] { 471 | var result = [U]() 472 | for element in elements { 473 | let decodable : Result = decodeJSON(element as JSON?) 474 | switch decodable { 475 | case .success(let value): result.append(value) 476 | case .error(let error): return Result.error(error) 477 | } 478 | } 479 | } 480 | return Result.success(result) 481 | } 482 | 483 | 484 | 485 | //======================================== 486 | // MARK: Helper Methods 487 | //======================================== 488 | 489 | 490 | private func htmlToData(_ html: NSString?) -> Data? { 491 | if let html = html { 492 | let json = html.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: NSMakeRange(0, html.length)) 493 | return json.data(using: String.Encoding.utf8) 494 | } 495 | return nil 496 | } 497 | 498 | extension Dictionary : JSONParsable { 499 | public func content() -> JSON? { 500 | return self 501 | } 502 | } 503 | 504 | extension Array : JSONParsable { 505 | public func content() -> JSON? { 506 | return self 507 | } 508 | } 509 | 510 | extension String { 511 | internal func terminate() -> String { 512 | let terminator : Character = ";" 513 | var trimmed = trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 514 | if (trimmed.last != terminator) { trimmed += String(terminator) } 515 | return trimmed 516 | } 517 | } 518 | 519 | extension Data { 520 | internal func toString() -> String? { 521 | return String(data: self, encoding: String.Encoding.utf8) 522 | } 523 | } 524 | 525 | 526 | func dispatch_sync_on_main_thread(_ block: ()->()) { 527 | if Thread.isMainThread { 528 | block() 529 | } else { 530 | DispatchQueue.main.sync(execute: block) 531 | } 532 | } 533 | 534 | internal func delay(_ time: TimeInterval, completion: @escaping () -> Void) { 535 | if let currentQueue = OperationQueue.current?.underlyingQueue { 536 | let delayTime = DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 537 | currentQueue.asyncAfter(deadline: delayTime) { 538 | completion() 539 | } 540 | } else { 541 | completion() 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /Sources/WKZombie/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // 4 | // Copyright (c) 2015 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | public protocol ErrorType { } 27 | 28 | public enum NoError: ErrorType { } 29 | 30 | public enum ActionError: ErrorType { 31 | case networkRequestFailure 32 | case notFound 33 | case parsingFailure 34 | case transformFailure 35 | case snapshotFailure 36 | 37 | internal struct Static { 38 | static let DefaultStatusCodeSuccess : Int = 200 39 | static let DefaultStatusCodeError : Int = 500 40 | } 41 | } 42 | 43 | extension ActionError: CustomDebugStringConvertible { 44 | public var debugDescription: String { 45 | switch self { 46 | case .networkRequestFailure: return "Network Request Failure" 47 | case .notFound: return "Element Not Found" 48 | case .parsingFailure: return "Parsing Failure" 49 | case .transformFailure: return "Transform Failure" 50 | case .snapshotFailure: return "Snapshot Failure" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/WKZombie/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // 4 | // Copyright (c) 2016 Mathias Koehnke (http://www.mathiaskoehnke.de) 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | 27 | /** 28 | Convenience functions for accessing the WKZombie shared instance functionality. 29 | */ 30 | 31 | //======================================== 32 | // MARK: Get Page 33 | //======================================== 34 | 35 | /** 36 | The returned WKZombie Action will load and return a HTML or JSON page for the specified URL 37 | __using the shared WKZombie instance__. 38 | - seealso: _open()_ function in _WKZombie_ class for more info. 39 | */ 40 | public func open(_ url: URL) -> Action { 41 | return WKZombie.sharedInstance.open(url) 42 | } 43 | 44 | /** 45 | The returned WKZombie Action will load and return a HTML or JSON page for the specified URL 46 | __using the shared WKZombie instance__. 47 | - seealso: _open()_ function in _WKZombie_ class for more info. 48 | */ 49 | public func open(then postAction: PostAction) -> (_ url: URL) -> Action { 50 | return WKZombie.sharedInstance.open(then: postAction) 51 | } 52 | 53 | /** 54 | The returned WKZombie Action will return the current page __using the shared WKZombie instance__. 55 | - seealso: _inspect()_ function in _WKZombie_ class for more info. 56 | */ 57 | public func inspect() -> Action { 58 | return WKZombie.sharedInstance.inspect() 59 | } 60 | 61 | 62 | //======================================== 63 | // MARK: Submit Form 64 | //======================================== 65 | 66 | /** 67 | Submits the specified HTML form __using the shared WKZombie instance__. 68 | - seealso: _submit()_ function in _WKZombie_ class for more info. 69 | */ 70 | public func submit(_ form: HTMLForm) -> Action { 71 | return WKZombie.sharedInstance.submit(form) 72 | } 73 | 74 | /** 75 | Submits the specified HTML form __using the shared WKZombie instance__. 76 | - seealso: _submit()_ function in _WKZombie_ class for more info. 77 | */ 78 | public func submit(then postAction: PostAction) -> (_ form: HTMLForm) -> Action { 79 | return WKZombie.sharedInstance.submit(then: postAction) 80 | } 81 | 82 | 83 | //======================================== 84 | // MARK: Click Event 85 | //======================================== 86 | 87 | /** 88 | Simulates the click of a HTML link __using the shared WKZombie instance__. 89 | - seealso: _click()_ function in _WKZombie_ class for more info. 90 | */ 91 | public func click(_ link : HTMLLink) -> Action { 92 | return WKZombie.sharedInstance.click(link) 93 | } 94 | 95 | /** 96 | Simulates the click of a HTML link __using the shared WKZombie instance__. 97 | - seealso: _click()_ function in _WKZombie_ class for more info. 98 | */ 99 | public func click(then postAction: PostAction) -> (_ link : HTMLLink) -> Action { 100 | return WKZombie.sharedInstance.click(then: postAction) 101 | } 102 | 103 | /** 104 | Simulates HTMLButton press __using the shared WKZombie instance__. 105 | - seealso: _press()_ function in _WKZombie_ class for more info. 106 | */ 107 | public func press(_ button : HTMLButton) -> Action { 108 | return WKZombie.sharedInstance.press(button) 109 | } 110 | 111 | /** 112 | Simulates HTMLButton press __using the shared WKZombie instance__. 113 | - seealso: _press()_ function in _WKZombie_ class for more info. 114 | */ 115 | public func press(then postAction: PostAction) -> (_ button : HTMLButton) -> Action { 116 | return WKZombie.sharedInstance.press(then: postAction) 117 | } 118 | 119 | //======================================== 120 | // MARK: Swap Page Context 121 | //======================================== 122 | 123 | /** 124 | The returned WKZombie Action will swap the current page context with the context of an embedded iframe. 125 | - seealso: _swap()_ function in _WKZombie_ class for more info. 126 | */ 127 | public func swap(_ iframe : HTMLFrame) -> Action { 128 | return WKZombie.sharedInstance.swap(iframe) 129 | } 130 | 131 | /** 132 | The returned WKZombie Action will swap the current page context with the context of an embedded iframe. 133 | - seealso: _swap()_ function in _WKZombie_ class for more info. 134 | */ 135 | public func swap(then postAction: PostAction) -> (_ iframe : HTMLFrame) -> Action { 136 | return WKZombie.sharedInstance.swap(then: postAction) 137 | } 138 | 139 | //======================================== 140 | // MARK: DOM Modification Methods 141 | //======================================== 142 | 143 | /** 144 | The returned WKZombie Action will set or update a attribute/value pair on the specified HTMLElement 145 | __using the shared WKZombie instance__. 146 | - seealso: _setAttribute()_ function in _WKZombie_ class for more info. 147 | */ 148 | public func setAttribute(_ key: String, value: String?) -> (_ element: T) -> Action { 149 | return WKZombie.sharedInstance.setAttribute(key, value: value) 150 | } 151 | 152 | 153 | //======================================== 154 | // MARK: Find Methods 155 | //======================================== 156 | 157 | 158 | /** 159 | The returned WKZombie Action will search a page and return all elements matching the generic HTML element type and 160 | the passed key/value attributes. __The the shared WKZombie instance will be used__. 161 | - seealso: _getAll()_ function in _WKZombie_ class for more info. 162 | */ 163 | public func getAll(by searchType: SearchType) -> (_ page: HTMLPage) -> Action<[T]> { 164 | return WKZombie.sharedInstance.getAll(by: searchType) 165 | } 166 | 167 | /** 168 | The returned WKZombie Action will search a page and return the first element matching the generic HTML element type and 169 | the passed key/value attributes. __The shared WKZombie instance will be used__. 170 | - seealso: _get()_ function in _WKZombie_ class for more info. 171 | */ 172 | public func get(by searchType: SearchType) -> (_ page: HTMLPage) -> Action { 173 | return WKZombie.sharedInstance.get(by: searchType) 174 | } 175 | 176 | 177 | //======================================== 178 | // MARK: JavaScript Methods 179 | //======================================== 180 | 181 | 182 | /** 183 | The returned WKZombie Action will execute a JavaScript string __using the shared WKZombie instance__. 184 | - seealso: _execute()_ function in _WKZombie_ class for more info. 185 | */ 186 | public func execute(_ script: JavaScript) -> Action { 187 | return WKZombie.sharedInstance.execute(script) 188 | } 189 | 190 | /** 191 | The returned WKZombie Action will execute a JavaScript string __using the shared WKZombie instance__. 192 | - seealso: _execute()_ function in _WKZombie_ class for more info. 193 | */ 194 | public func execute(_ script: JavaScript) -> (_ page: T) -> Action { 195 | return WKZombie.sharedInstance.execute(script) 196 | } 197 | 198 | 199 | //======================================== 200 | // MARK: Fetch Actions 201 | //======================================== 202 | 203 | 204 | /** 205 | The returned WKZombie Action will download the linked data of the passed HTMLFetchable object 206 | __using the shared WKZombie instance__. 207 | - seealso: _fetch()_ function in _WKZombie_ class for more info. 208 | */ 209 | public func fetch(_ fetchable: T) -> Action { 210 | return WKZombie.sharedInstance.fetch(fetchable) 211 | } 212 | 213 | 214 | //======================================== 215 | // MARK: Transform Actions 216 | //======================================== 217 | 218 | /** 219 | The returned WKZombie Action will transform a HTMLElement into another HTMLElement using the specified function. 220 | __The shared WKZombie instance will be used__. 221 | - seealso: _map()_ function in _WKZombie_ class for more info. 222 | */ 223 | public func map(_ f: @escaping (T) -> A) -> (_ object: T) -> Action { 224 | return WKZombie.sharedInstance.map(f) 225 | } 226 | 227 | /** 228 | This function transforms an object into another object using the specified closure. 229 | - seealso: _map()_ function in _WKZombie_ class for more info. 230 | */ 231 | public func map(_ f: @escaping (T) -> A) -> (_ object: T) -> A { 232 | return WKZombie.sharedInstance.map(f) 233 | } 234 | 235 | //======================================== 236 | // MARK: Advanced Actions 237 | //======================================== 238 | 239 | 240 | /** 241 | Executes the specified action (with the result of the previous action execution as input parameter) until 242 | a certain condition is met. Afterwards, it will return the collected action results. 243 | __The shared WKZombie instance will be used__. 244 | - seealso: _collect()_ function in _WKZombie_ class for more info. 245 | */ 246 | public func collect(_ f: @escaping (T) -> Action, until: @escaping (T) -> Bool) -> (_ initial: T) -> Action<[T]> { 247 | return WKZombie.sharedInstance.collect(f, until: until) 248 | } 249 | 250 | /** 251 | Makes a bulk execution of the specified action with the provided input values. Once all actions have 252 | finished, the collected results will be returned. 253 | __The shared WKZombie instance will be used__. 254 | - seealso: _batch()_ function in _WKZombie_ class for more info. 255 | */ 256 | public func batch(_ f: @escaping (T) -> Action) -> (_ elements: [T]) -> Action<[U]> { 257 | return WKZombie.sharedInstance.batch(f) 258 | } 259 | 260 | 261 | //======================================== 262 | // MARK: JSON Actions 263 | //======================================== 264 | 265 | 266 | /** 267 | The returned WKZombie Action will parse NSData and create a JSON object. 268 | __The shared WKZombie instance will be used__. 269 | - seealso: _parse()_ function in _WKZombie_ class for more info. 270 | */ 271 | public func parse(_ data: Data) -> Action { 272 | return WKZombie.sharedInstance.parse(data) 273 | } 274 | 275 | /** 276 | The returned WKZombie Action will take a JSONParsable (Array, Dictionary and JSONPage) and 277 | decode it into a Model object. This particular Model class has to implement the 278 | JSONDecodable protocol. 279 | __The shared WKZombie instance will be used__. 280 | - seealso: _decode()_ function in _WKZombie_ class for more info. 281 | */ 282 | public func decode(_ element: JSONParsable) -> Action { 283 | return WKZombie.sharedInstance.decode(element) 284 | } 285 | 286 | /** 287 | The returned WKZombie Action will take a JSONParsable (Array, Dictionary and JSONPage) and 288 | decode it into an array of Model objects of the same class. The class has to implement the 289 | JSONDecodable protocol. 290 | __The shared WKZombie instance will be used__. 291 | - seealso: _decode()_ function in _WKZombie_ class for more info. 292 | */ 293 | public func decode(_ array: JSONParsable) -> Action<[T]> { 294 | return WKZombie.sharedInstance.decode(array) 295 | } 296 | 297 | 298 | #if os(iOS) 299 | 300 | //======================================== 301 | // MARK: Snapshot Methods 302 | //======================================== 303 | 304 | /** 305 | This is a convenience operator for the _snap()_ command. It is equal to the __>>>__ operator with the difference 306 | that a snapshot will be taken after the left Action has been finished. 307 | */ 308 | infix operator >>*: AdditionPrecedence 309 | 310 | private func assertIfNotSharedInstance() { 311 | assert(WKZombie.Static.instance != nil, "The >>* operator can only be used with the WKZombie shared instance.") 312 | } 313 | 314 | public func >>*(a: Action, f: @escaping ((T) -> Action)) -> Action { 315 | assertIfNotSharedInstance() 316 | return a >>> snap >>> f 317 | } 318 | 319 | public func >>*(a: Action, b: Action) -> Action { 320 | assertIfNotSharedInstance() 321 | return a >>> snap >>> b 322 | } 323 | 324 | public func >>*(a: Action, f: () -> Action) -> Action { 325 | assertIfNotSharedInstance() 326 | return a >>> snap >>> f 327 | } 328 | 329 | public func >>*(a: () -> Action, f: @escaping ((T) -> Action)) -> Action { 330 | assertIfNotSharedInstance() 331 | return a >>> snap >>> f 332 | } 333 | 334 | /** 335 | The returned WKZombie Action will make a snapshot of the current page. 336 | Note: This method only works under iOS. Also, a snapshotHandler must be registered. 337 | __The shared WKZombie instance will be used__. 338 | - seealso: _snap()_ function in _WKZombie_ class for more info. 339 | */ 340 | public func snap(_ element: T) -> Action { 341 | return WKZombie.sharedInstance.snap(element) 342 | } 343 | 344 | #endif 345 | 346 | 347 | //======================================== 348 | // MARK: Debug Methods 349 | //======================================== 350 | 351 | 352 | /** 353 | Prints the current state of the WKZombie browser to the console. 354 | */ 355 | public func dump() { 356 | WKZombie.sharedInstance.dump() 357 | } 358 | 359 | /** 360 | Clears the cache/cookie data (such as login data, etc). 361 | */ 362 | @available(OSX 10.11, *) 363 | public func clearCache() { 364 | WKZombie.sharedInstance.clearCache() 365 | } 366 | 367 | -------------------------------------------------------------------------------- /Sources/WKZombie/HTMLButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLButton.swift 3 | // WKZombieDemo 4 | // 5 | // Created by Mathias Köhnke on 06/04/16. 6 | // Copyright © 2016 Mathias Köhnke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// HTML Button class, which represents the