-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from swift-tweets/dev
Develop 0.1.0
- Loading branch information
Showing
36 changed files
with
2,866 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "SwiftTweetsKit" | ||
name: "TweetupKit", | ||
dependencies: [ | ||
.Package(url: "https://github.com/swift-tweets/OAuthSwift.git", "2.0.0-beta") | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
# swift-tweets-kit | ||
# TweetupKit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import Foundation | ||
|
||
public struct APIError: Error { | ||
public let response: HTTPURLResponse | ||
public let json: Any | ||
|
||
public init(response: HTTPURLResponse, json: Any) { | ||
self.response = response | ||
self.json = json | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
extension Array where Element: Equatable { | ||
internal func separated(by separator: Element) -> [[Element]] { | ||
var separated: [[Element]] = [[]] | ||
for element in self { | ||
if element == separator { | ||
separated.append([]) | ||
} else { | ||
separated[separated.endIndex - 1].append(element) | ||
} | ||
} | ||
|
||
return separated | ||
} | ||
} | ||
|
||
extension Array where Element: Hashable { | ||
internal func trimmingElements(in set: Set<Element>) -> [Element] { | ||
var trimmed = [Element]() | ||
var elements = [Element]() | ||
|
||
for element in self { | ||
if set.contains(element) { | ||
if !trimmed.isEmpty { | ||
elements.append(element) | ||
} | ||
} else { | ||
elements.forEach { trimmed.append($0) } | ||
elements = [] | ||
trimmed.append(element) | ||
} | ||
} | ||
|
||
return trimmed | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
extension ArraySlice { | ||
internal var headAndTail: (Element?, ArraySlice<Element>) { | ||
guard count > 0 else { | ||
return (nil, []) | ||
} | ||
return (first, self[(startIndex + 1) ..< endIndex]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import Foundation | ||
|
||
public func sync<T, R>(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ())) -> (T) throws -> R { | ||
return { value in | ||
var resultValue: R! | ||
var resultError: Error? | ||
var waiting = true | ||
|
||
operation(value) { getValue in | ||
defer { | ||
waiting = false | ||
} | ||
do { | ||
resultValue = try getValue() | ||
} catch let error { | ||
resultError = error | ||
} | ||
} | ||
let runLoop = RunLoop.current | ||
while waiting && runLoop.run(mode: .defaultRunLoopMode, before: .distantFuture) { } | ||
|
||
if let error = resultError { | ||
throw error | ||
} | ||
|
||
return resultValue | ||
} | ||
} | ||
|
||
internal func repeated<T, R>(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ()), interval: TimeInterval? = nil) -> ([T], @escaping (() throws -> [R]) -> ()) -> () { | ||
return { values, callback in | ||
_repeat(operation: operation, for: values[0..<values.count], interval: interval, callback: callback) | ||
} | ||
} | ||
|
||
private func _repeat<T, R>(operation: @escaping (T, @escaping (() throws -> R) -> ()) -> (), for values: ArraySlice<T>, interval: TimeInterval?, results: [R] = [], callback: @escaping (() throws -> [R]) -> ()) { | ||
let (headOrNil, tail) = values.headAndTail | ||
guard let head = headOrNil else { | ||
callback { results } | ||
return | ||
} | ||
|
||
let waitingOperation: (T, @escaping (() throws -> R) -> ()) -> () | ||
if let interval = interval, values.count > 1 { | ||
waitingOperation = waiting(operation: operation, with: interval) | ||
} else { | ||
waitingOperation = operation | ||
} | ||
|
||
waitingOperation(head) { result in | ||
do { | ||
_repeat(operation: operation, for: tail, interval: interval, results: results + [try result()], callback: callback) | ||
} catch let error { | ||
callback { throw error } | ||
} | ||
} | ||
} | ||
|
||
internal func flatten<T, U, R>(_ operation1: @escaping (T, @escaping (() throws -> U) -> ()) -> (), _ operation2: @escaping (U, @escaping (() throws -> R) -> ()) -> ()) -> (T, @escaping (() throws -> R) -> ()) -> () { | ||
return { value, callback in | ||
operation1(value) { getValue in | ||
do { | ||
let value = try getValue() | ||
operation2(value) { getValue in | ||
callback { | ||
try getValue() | ||
} | ||
} | ||
} catch let error { | ||
callback { | ||
throw error | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
internal func waiting<T, R>(operation: @escaping (T, @escaping (() throws -> R) -> ()) -> (), with interval: TimeInterval) -> (T, @escaping (() throws -> R) -> ()) -> () { | ||
let wait: ((), @escaping (() throws -> ()) -> ()) -> () = { _, completion in | ||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { | ||
completion { | ||
() | ||
} | ||
} | ||
} | ||
return { value, completion in | ||
join(operation, wait)((value, ())) { getValue in | ||
completion { | ||
let (value, _) = try getValue() | ||
return value | ||
} | ||
} | ||
} | ||
} | ||
|
||
internal func join<T1, R1, T2, R2>(_ operation1: @escaping (T1, @escaping (() throws -> R1) -> ()) -> (), _ operation2: @escaping (T2, @escaping (() throws -> R2) -> ()) -> ()) -> ((T1, T2), @escaping (() throws -> (R1, R2)) -> ()) -> () { | ||
return { values, completion in | ||
let (value1, value2) = values | ||
var result1: R1? | ||
var result2: R2? | ||
var hasThrownError = false | ||
|
||
operation1(value1) { getValue in | ||
do { | ||
let result = try getValue() | ||
DispatchQueue.main.async { | ||
guard let result2 = result2 else { | ||
result1 = result | ||
return | ||
} | ||
completion { | ||
(result, result2) | ||
} | ||
} | ||
} catch let error { | ||
DispatchQueue.main.async { | ||
if hasThrownError { | ||
return | ||
} | ||
hasThrownError = true | ||
completion { | ||
throw error | ||
} | ||
} | ||
} | ||
} | ||
|
||
operation2(value2) { getValue in | ||
do { | ||
let result = try getValue() | ||
DispatchQueue.main.async { | ||
guard let result1 = result1 else { | ||
result2 = result | ||
return | ||
} | ||
completion { | ||
(result1, result) | ||
} | ||
} | ||
} catch let error { | ||
DispatchQueue.main.async { | ||
if hasThrownError { | ||
return | ||
} | ||
hasThrownError = true | ||
completion { | ||
throw error | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
public struct Code { | ||
public var language: Language | ||
public var fileName: String | ||
public var body: String | ||
} | ||
|
||
extension Code: CustomStringConvertible { | ||
public var description: String { | ||
return "```\(language.identifier):\(fileName)\n\(body)\n```" | ||
} | ||
} | ||
|
||
extension Code: Equatable { | ||
public static func ==(lhs: Code, rhs: Code) -> Bool { | ||
return lhs.language == rhs.language && lhs.fileName == rhs.fileName && lhs.body == rhs.body | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import WebKit | ||
import Foundation | ||
|
||
internal class CodeRenderer: NSObject { | ||
private var webView: WebView! | ||
fileprivate var loading = true | ||
private var getImage: (() throws -> CGImage)? = nil | ||
private var completions: [(() throws -> CGImage) -> ()] = [] | ||
private var zelf: CodeRenderer? // not to released during the async operation | ||
|
||
fileprivate static let height: CGFloat = 736 | ||
|
||
init(url: String) { | ||
super.init() | ||
zelf = self | ||
|
||
DispatchQueue.main.async { | ||
self.webView = WebView(frame: NSRect(x: 0, y: 0, width: 414, height: CodeRenderer.height)) | ||
self.webView.frameLoadDelegate = self | ||
self.webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" | ||
self.webView.mainFrameURL = url | ||
} | ||
} | ||
|
||
func image(completion: @escaping (() throws -> CGImage) -> ()) { | ||
DispatchQueue.main.async { | ||
if let getImage = self.getImage { | ||
completion { | ||
try getImage() | ||
} | ||
} | ||
|
||
self.completions.append(completion) | ||
} | ||
} | ||
|
||
func writeImage(to path: String, completion: @escaping (() throws -> ()) -> ()) { | ||
image { getImage in | ||
completion { | ||
let image = try getImage() | ||
let url = URL(fileURLWithPath: path) | ||
guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { | ||
throw CodeRendererError.writingFailed | ||
} | ||
|
||
CGImageDestinationAddImage(destination, image, nil) | ||
|
||
guard CGImageDestinationFinalize(destination) else { | ||
throw CodeRendererError.writingFailed | ||
} | ||
} | ||
} | ||
} | ||
|
||
fileprivate func resolve(getImage: @escaping (() throws -> CGImage)) { | ||
for completion in completions { | ||
completion(getImage) | ||
} | ||
completions.removeAll() | ||
self.getImage = getImage | ||
self.zelf = nil | ||
} | ||
} | ||
|
||
extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread | ||
func webView(_ sender: WebView, didFinishLoadFor frame: WebFrame) { | ||
let document = frame.domDocument! | ||
let body = document.getElementsByTagName("body").item(0)! | ||
let bodyBox = body.boundingBox() | ||
let pageBox = CGRect(origin: bodyBox.origin, size: CGSize(width: bodyBox.width, height: max(bodyBox.size.height, CodeRenderer.height))) | ||
|
||
let files = document.getElementsByClassName("blob-file-content")! | ||
guard files.length > 0 else { | ||
resolve(getImage: { throw CodeRendererError.illegalResponse } ) | ||
return | ||
} | ||
let code = files.item(0) as! DOMElement | ||
let codeBox = code.boundingBox() | ||
|
||
let view = frame.frameView.documentView! | ||
let imageRep = view.bitmapImageRepForCachingDisplay(in: CGRect(origin: .zero, size: pageBox.size))! | ||
|
||
view.cacheDisplay(in: pageBox, to: imageRep) | ||
|
||
let scale: CGFloat = 2.0 | ||
let codeBox2 = codeBox * scale | ||
let pageBox2 = pageBox * scale | ||
|
||
let width = Int(codeBox2.size.width) | ||
let height = Int(codeBox2.size.height) | ||
var pixels = [UInt8](repeating: 0, count: width * height * 4) | ||
let colorSpace = CGColorSpaceCreateDeviceRGB() | ||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) | ||
let context = CGContext(data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! | ||
let targetRect = CGRect(x: -codeBox2.origin.x, y: codeBox2.origin.y - CGFloat(pageBox2.size.height - codeBox2.size.height), width: pageBox2.size.width, height: pageBox2.size.height) | ||
context.draw(imageRep.cgImage!, in: targetRect) | ||
|
||
let provider: CGDataProvider = CGDataProvider(data: Data(bytes: pixels) as CFData)! | ||
resolve(getImage: { | ||
CGImage( | ||
width: width, | ||
height: height, | ||
bitsPerComponent: 8, | ||
bitsPerPixel: 32, | ||
bytesPerRow: width * 4, | ||
space: colorSpace, | ||
bitmapInfo: bitmapInfo, | ||
provider: provider, | ||
decode: nil, | ||
shouldInterpolate: false, | ||
intent: .defaultIntent | ||
)! | ||
}) | ||
|
||
loading = false | ||
} | ||
|
||
func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { | ||
resolve(getImage: { throw error }) | ||
loading = false | ||
} | ||
} | ||
|
||
public enum CodeRendererError: Error { | ||
case writingFailed | ||
case illegalResponse | ||
} | ||
|
||
internal func *(rect: CGRect, k: CGFloat) -> CGRect { | ||
return CGRect(origin: rect.origin * k, size: rect.size * k) | ||
} | ||
|
||
internal func *(point: CGPoint, k: CGFloat) -> CGPoint { | ||
return CGPoint(x: point.x * k, y: point.y * k) | ||
} | ||
|
||
internal func *(size: CGSize, k: CGFloat) -> CGSize { | ||
return CGSize(width: size.width * k, height: size.height * k) | ||
} |
Oops, something went wrong.