Skip to content

Commit

Permalink
Merge pull request #7 from swift-tweets/dev
Browse files Browse the repository at this point in the history
Develop 0.1.0
  • Loading branch information
koher authored Mar 29, 2017
2 parents 23de949 + 7e5ff83 commit 0420a91
Show file tree
Hide file tree
Showing 36 changed files with 2,866 additions and 23 deletions.
5 changes: 4 additions & 1 deletion Package.swift
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")
]
)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# swift-tweets-kit
# TweetupKit
11 changes: 11 additions & 0 deletions Sources/APIError.swift
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
}
}
35 changes: 35 additions & 0 deletions Sources/ArrayExtensions.swift
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
}
}
8 changes: 8 additions & 0 deletions Sources/ArraySliceExtensions.swift
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])
}
}
153 changes: 153 additions & 0 deletions Sources/Async.swift
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
}
}
}
}
}
}
17 changes: 17 additions & 0 deletions Sources/Code.swift
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
}
}
139 changes: 139 additions & 0 deletions Sources/CodeRenderer.swift
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)
}
Loading

0 comments on commit 0420a91

Please sign in to comment.