diff --git a/Cartfile b/Cartfile new file mode 100644 index 0000000..c8bea08 --- /dev/null +++ b/Cartfile @@ -0,0 +1,3 @@ +github "tid-kijyun/Kanna" ~> 1.1.0 +github "kodlian/Eki" +github "Thomvis/BrightFutures" \ No newline at end of file diff --git a/Erik.podspec b/Erik.podspec index 20dc20d..efe7301 100644 --- a/Erik.podspec +++ b/Erik.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # s.name = "Erik" - s.version = "1.0.3" + s.version = "1.1.0" s.summary = "A headless browser written in Swift" s.description = <<-DESC Erik is an headless browser based on WebKit and HTML parser Kanna. diff --git a/Erik/Document.swift b/Erik/Document.swift index c6f232c..df63ab8 100644 --- a/Erik/Document.swift +++ b/Erik/Document.swift @@ -50,14 +50,14 @@ public class Document : Node { super.init(rawValue: rawValue, selectors: []) } - var title: String? { return (rawValue as? HTMLDocument)?.title } - var head: Element? { + public var title: String? { return (rawValue as? HTMLDocument)?.title } + public var head: Element? { guard let doc = self.rawValue as? HTMLDocument, element = doc.head else { return nil } return Element(rawValue: element, selectors: ["head"]) } - var body: Element? { + public var body: Element? { guard let doc = self.rawValue as? HTMLDocument, element = doc.body else { return nil } @@ -65,6 +65,7 @@ public class Document : Node { } } +// HTML Node public class Node { var layoutEngine: LayoutEngine? @@ -82,6 +83,7 @@ public class Node { self.rawValue = rawValue } + // Select elements using css selector public func querySelectorAll(selector: String) -> [Element] { let selectors = self.selectors + [selector] return rawValue.css(selector).map { @@ -91,6 +93,7 @@ public class Node { } } + // Select an element using css selector public func querySelector(selector: String) -> Element? { guard let element = rawValue.at_css(selector) else { return nil @@ -101,20 +104,45 @@ public class Node { return elem } + // Get all children element public var elements: [Element] { return querySelectorAll("*") } + // Get first child element public var firstChild: Element? { return querySelectorAll(":first-child").first } + // Get last child element public var lastChild: Element? { return querySelectorAll(":last-child").first } } +extension Node { + + // Fill value of selected child + public func type(selector: String, value: String, key: String = "value") -> Element? { + if let element = self.querySelector(selector) { + element[key] = value + return element + } + return nil + } + + // Click on selected child + public func click(selector: String) -> Element? { + if let element = self.querySelector(selector) { + element.click() + return element + } + return nil + } + +} + extension Node: CustomStringConvertible { public var description: String { return self.toHTML ?? "" diff --git a/Erik/Erik.swift b/Erik/Erik.swift index 3c5357d..6d60521 100644 --- a/Erik/Erik.swift +++ b/Erik/Erik.swift @@ -24,11 +24,29 @@ SOFTWARE. import Foundation import WebKit +// MARK: Error +public enum ErikError: ErrorType { + // Error provided by javascript + case JavaScriptError(message: String) + // A timeout occurs + case TimeOutError + // No content returned + case NoContent + // HTML is not parsable + case HTMLNotParsable(html: String) + // Invalid url submited (NSURL init failed) + case InvalidURL(urlString: String) +} + +// MARK: Erik class + +// Instance of headless browser public class Erik { public var layoutEngine: LayoutEngine public var htmlParser: HTMLParser + // Init the headless browser public init(webView: WKWebView? = nil) { if let view = webView { self.layoutEngine = WebKitLayoutEngine(webView: view) @@ -38,22 +56,77 @@ public class Erik { self.htmlParser = KanaParser.instance } + @available(*, deprecated=1.1, obsoleted=2.0, message="Use url") + public var currentURL: NSURL? { + return layoutEngine.url + } + + // Get current url + public var url: NSURL? { + return layoutEngine.url + } + + // Get current title + public var title: String? { + return layoutEngine.title + } + + // Go to specific url public func visitURL(URL: NSURL, completionHandler: ((Document?, ErrorType?) -> Void)?) { layoutEngine.browseURL(URL) {[unowned self] (object, error) -> Void in self.publishContent(object, error: error, completionHandler: completionHandler) } } - public var currentURL: NSURL? { - return layoutEngine.currentURL + // Go to specific url + public func visitURL(urlString: String, completionHandler: ((Document?, ErrorType?) -> Void)?) { + if let url = NSURL(string: urlString) { + visitURL(url, completionHandler: completionHandler) + } else { + completionHandler?(nil, ErikError.InvalidURL(urlString: urlString)) + } } + // Go to specific url using url request + public func loadURLRequest(URLRequest: NSURLRequest, completionHandler: ((Document?, ErrorType?) -> Void)?) { + layoutEngine.browseURL(URLRequest) {[unowned self] (object, error) -> Void in + self.publishContent(object, error: error, completionHandler: completionHandler) + } + } + + // Get current content public func currentContent(completionHandler: ((Document?, ErrorType?) -> Void)?) { layoutEngine.currentContent {[unowned self] (object, error) -> Void in self.publishContent(object, error: error, completionHandler: completionHandler) } } + + // Navigates to the previous loaded page. + public func goBack() { + layoutEngine.goBack() + } + + // Navigates to the next page ie. the one loaded before `goBack` + public func goForward() { + layoutEngine.goForward() + } + + // A Boolean value indicating whether browser can go back + public var canGoBack: Bool { + return layoutEngine.canGoBack + } + + // A Boolean value indicating whether browser can go forward + public var canGoForward: Bool { + return layoutEngine.canGoForward + } + + // Reloads the current page + public func reload() { + layoutEngine.reload() + } + // MARK: private private func publishContent(object: AnyObject?, error: ErrorType?, completionHandler: ((Document?, ErrorType?) -> Void)?) { guard let html = object as? String else { completionHandler?(nil, ErikError.NoContent) @@ -74,8 +147,9 @@ public class Erik { completionHandler?(doc, error) } - } + +// MARK: javascript extension Erik: JavaScriptEvaluator { public func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) { @@ -83,23 +157,35 @@ extension Erik: JavaScriptEvaluator { } } -public enum ErikError: ErrorType { - case JavaScriptError(message: String) - case TimeOutError - case NoContent - case HTMLNotParsable(html: String) -} -// MARK: static +// MARK: Erik static extension Erik { + // Shared instance used for static functions public static let sharedInstance = Erik() public static func visitURL(URL: NSURL, completionHandler: ((Document?, ErrorType?) -> Void)?) { Erik.sharedInstance.visitURL(URL, completionHandler: completionHandler) } - + + public static func visitURL(urlString: String, completionHandler: ((Document?, ErrorType?) -> Void)?) { + Erik.sharedInstance.visitURL(urlString, completionHandler: completionHandler) + } + + public static func loadURLRequest(URLRequest: NSURLRequest, completionHandler: ((Document?, ErrorType?) -> Void)?) { + Erik.sharedInstance.loadURLRequest(URLRequest, completionHandler: completionHandler) + } + + @available(*, deprecated=1.1, obsoleted=1.2, message="Use url") public static var currentURL: NSURL? { - return Erik.sharedInstance.currentURL + return Erik.sharedInstance.url + } + + public static var url: NSURL? { + return Erik.sharedInstance.url + } + + public static var title: String? { + return Erik.sharedInstance.title } public static func currentContent(completionHandler: ((Document?, ErrorType?) -> Void)?) { @@ -109,6 +195,25 @@ extension Erik { public static func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) { Erik.sharedInstance.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) } + + public static func goBack() { + Erik.sharedInstance.goBack() + } + public static func goForward() { + Erik.sharedInstance.goForward() + } + + public static var canGoBack: Bool { + return Erik.sharedInstance.canGoBack + } + + public static var canGoForward: Bool { + return Erik.sharedInstance.canGoForward + } + + public static func reload() { + Erik.sharedInstance.reload() + } } \ No newline at end of file diff --git a/Erik/Info.plist b/Erik/Info.plist index d3de8ee..a6f720e 100644 --- a/Erik/Info.plist +++ b/Erik/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 1.1 CFBundleSignature ???? CFBundleVersion diff --git a/Erik/LayoutEngine.swift b/Erik/LayoutEngine.swift index 067b059..8f04851 100644 --- a/Erik/LayoutEngine.swift +++ b/Erik/LayoutEngine.swift @@ -28,9 +28,18 @@ public protocol JavaScriptEvaluator { public protocol URLBrowser { func browseURL(URL: NSURL, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) - var currentURL: NSURL? {get} + func browseURL(URLRequest: NSURLRequest, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) + var url: NSURL? {get} + var title: String? {get} func currentContent(completionHandler: ((AnyObject?, ErrorType?) -> Void)?) + func goBack() + func goForward() + + var canGoBack: Bool { get } + var canGoForward: Bool { get } + func reload() + func clear() } public typealias LayoutEngine = protocol @@ -66,14 +75,46 @@ extension WebKitLayoutEngine { public func browseURL(URL: NSURL, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) { let request = NSURLRequest(URL: URL) - webView.loadRequest(request) + self.browseURL(request, completionHandler: completionHandler) + } + + public func browseURL(URLRequest: NSURLRequest, completionHandler: ((AnyObject?, ErrorType?) -> Void)?) { + webView.loadRequest(URLRequest) self.currentContent(completionHandler) } - + + @available(*, deprecated=1.1, obsoleted=2.0, message="Use url") public var currentURL: NSURL? { return self.webView.URL } + public var url: NSURL? { + return self.webView.URL + } + + public var title: String? { + return self.webView.title + } + + public func goBack() { + self.webView.goBack() + } + public func goForward() { + self.webView.goForward() + } + + public var canGoBack: Bool { + return self.webView.canGoBack + } + + public var canGoForward: Bool { + return self.webView.canGoForward + } + + public func reload() { + self.webView.reload() + } + public func currentContent(completionHandler: ((AnyObject?, ErrorType?) -> Void)?) { handleLoadRequestCompletion { self.handleHTML(completionHandler) diff --git a/ErikOSX/Info.plist b/ErikOSX/Info.plist index 68c1c75..2b5f3f4 100644 --- a/ErikOSX/Info.plist +++ b/ErikOSX/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 1.1 CFBundleSignature ???? CFBundleVersion diff --git a/ErikTests/ErikTests.swift b/ErikTests/ErikTests.swift index f15e6fc..cbaa572 100644 --- a/ErikTests/ErikTests.swift +++ b/ErikTests/ErikTests.swift @@ -15,10 +15,10 @@ import BrightFutures +let url = NSURL(string:"https://www.google.com")! class ErikTests: XCTestCase { - let url = NSURL(string:"http://www.google.com")! #if os(OSX) let googleFormSelector = "f" #elseif os(iOS) @@ -33,6 +33,27 @@ class ErikTests: XCTestCase { super.tearDown() } + func testVisit() { + + let visitExpectation = self.expectationWithDescription("visit") + + Erik.visitURL(url) { (obj, err) -> Void in + if let error = err { + print(error) + + XCTFail("\(error)") + } + else if let _ = obj { + XCTAssertNotNil(Erik.title) + visitExpectation.fulfill() + + // XCTAssertEqual(Erik.url?.host ?? "dummy", url.host) // failed is redirected to google.XXX TODO how to force lang (request header?) + XCTAssertEqual(Erik.url?.scheme ?? "dummy", url.scheme) + } + } + self.waitForExpectationsWithTimeout(5, handler: nil) + } + func testAsync() { let expt = self.expectationWithDescription("Dispatch") @@ -97,7 +118,7 @@ class ErikTests: XCTestCase { } - Erik.currentContent {[unowned self] (obj, err) -> Void in + Erik.currentContent { (obj, err) -> Void in if let error = err { print(error) XCTFail("\(error)") @@ -106,9 +127,9 @@ class ErikTests: XCTestCase { print(doc) currentContentExpectation.fulfill() - XCTAssertNotEqual(self.url, Erik.currentURL) + XCTAssertNotEqual(url, Erik.url) - XCTAssertNotNil("\(Erik.currentURL)".rangeOfString(value!)) + XCTAssertNotNil("\(Erik.url)".rangeOfString(value!)) } } }