├── Kanna ├── CSS.swift ├── Deprecated.swift ├── Kanna.swift ├── libxmlHTMLDocument.swift ├── libxmlHTMLNode.swift └── libxmlParserOption.swift ├── LICENSE ├── Modules ├── Kanna.h ├── libxml2-kanna.h └── module.modulemap ├── README.md ├── SRTableRowView.swift ├── SpeedReader.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ ├── blue.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ ├── bright.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── kay.yin.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── blue.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── bright.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ ├── SpeedReader.xcscheme │ │ └── xcschememanagement.plist │ └── kay.yin.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── SpeedReader.xcscheme │ └── xcschememanagement.plist ├── SpeedReader ├── AppDelegate.swift ├── AppIcon.icns ├── ArticleViewController.swift ├── Base.lproj │ └── Main.storyboard ├── Images.xcassets │ ├── Contents.json │ ├── NSHistoryItem.imageset │ │ ├── Contents.json │ │ ├── NSHistoryItem-1.pdf │ │ ├── NSHistoryItem-2.pdf │ │ └── NSHistoryItem.pdf │ ├── createNew.imageset │ │ ├── Contents.json │ │ └── file-2.png │ ├── openLocal.imageset │ │ ├── Contents.json │ │ └── file.png │ ├── openWeb.imageset │ │ ├── Contents.json │ │ └── compass.png │ ├── paper.imageset │ │ ├── Contents.json │ │ ├── paper texture@2x_55A54008AD1BA589AA210D2629C1DF41_0.png │ │ └── paper texture_55A54008AD1BA589AA210D2629C1DF41_0.png │ ├── pauseButtonArtwork.imageset │ │ ├── Contents.json │ │ └── pauseButtonArtwork.png │ └── playButtonArtwork.imageset │ │ ├── Contents.json │ │ └── playButtonArtwork.png ├── Info.plist ├── MainWindowController.swift ├── ReadDetailWindow.swift ├── ReadView.swift ├── ReadViewController.swift ├── SRAppearanceCellView.swift ├── SRArticleSnippetCellView.swift ├── SRFontCellView.swift ├── SRGeneralPrefCellView.swift ├── SRHistoryViewController.swift ├── SRImportWebViewController.swift ├── SRLanguageCellView.swift ├── SRModalWindowController.swift ├── SRNewArticleViewController.swift ├── SRPreferencesViewController.swift ├── SRReadCellView.swift ├── SRSpeedCellView.swift ├── SRSplitViewController.swift ├── SRWordsCellView.swift ├── SpeedReader-Bridging-Header.h ├── SpeedReader.entitlements └── SpeedReader.xcdatamodeld │ └── SpeedReader.xcdatamodel │ └── contents ├── speedreader.gif └── speedreader_dark.gif /Kanna/CSS.swift: -------------------------------------------------------------------------------- 1 | /**@file CSS.swift 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | import Foundation 26 | 27 | #if SWIFT_PACKAGE 28 | import SwiftClibxml2 29 | #else 30 | import libxmlKanna 31 | #endif 32 | 33 | typealias AKRegularExpression = NSRegularExpression 34 | #if os(Linux) && swift(>=4) 35 | typealias AKTextCheckingResult = NSTextCheckingResult 36 | #elseif os(Linux) && swift(>=3) 37 | typealias AKTextCheckingResult = TextCheckingResult 38 | #else 39 | typealias AKTextCheckingResult = NSTextCheckingResult 40 | #endif 41 | 42 | public enum CSSError: Error { 43 | case UnsupportSyntax(String) 44 | } 45 | 46 | /** 47 | CSS 48 | */ 49 | public enum CSS { 50 | /** 51 | CSS3 selector to XPath 52 | 53 | @param selector CSS3 selector 54 | 55 | @return XPath 56 | */ 57 | public static func toXPath(_ css: String) throws -> String { 58 | let selectorGroups = css.components(separatedBy: ",") 59 | return try selectorGroups 60 | .map { try toXPath(selector: $0) } 61 | .joined(separator: " | ") 62 | } 63 | 64 | private static func toXPath(selector: String) throws -> String { 65 | var xpath = "//" 66 | var str = selector 67 | var prev = str 68 | 69 | while !str.isEmpty { 70 | var attributes: [String] = [] 71 | var combinator: String = "" 72 | 73 | str = str.trimmingCharacters(in: .whitespaces) 74 | 75 | // element 76 | let element = getElement(&str) 77 | 78 | // class / id 79 | while let attr = getClassId(&str) { 80 | attributes.append(attr) 81 | } 82 | 83 | // attribute 84 | while let attr = getAttribute(&str) { 85 | attributes.append(attr) 86 | } 87 | 88 | // matchCombinator 89 | if let combi = genCombinator(&str) { 90 | combinator = combi 91 | } 92 | 93 | // generate xpath phrase 94 | let attr = attributes.joined(separator: " and ") 95 | if attr.isEmpty { 96 | xpath += "\(element)\(combinator)" 97 | } else { 98 | xpath += "\(element)[\(attr)]\(combinator)" 99 | } 100 | 101 | if str == prev { 102 | throw CSSError.UnsupportSyntax(selector) 103 | } 104 | prev = str 105 | } 106 | return xpath 107 | } 108 | } 109 | 110 | private func firstMatch(_ pattern: String) -> (String) -> AKTextCheckingResult? { 111 | return { str in 112 | let length = str.utf16.count 113 | do { 114 | let regex = try AKRegularExpression(pattern: pattern, options: .caseInsensitive) 115 | if let result = regex.firstMatch(in: str, options: .reportProgress, range: NSRange(location: 0, length: length)) { 116 | return result 117 | } 118 | } catch _ { 119 | 120 | } 121 | return nil 122 | } 123 | } 124 | 125 | private func nth(prefix: String, a: Int, b: Int) -> String { 126 | let sibling = "\(prefix)-sibling::*" 127 | if a == 0 { 128 | return "count(\(sibling)) = \(b-1)" 129 | } else if a > 0 { 130 | if b != 0 { 131 | return "((count(\(sibling)) + 1) >= \(b)) and ((((count(\(sibling)) + 1)-\(b)) mod \(a)) = 0)" 132 | } 133 | return "((count(\(sibling)) + 1) mod \(a)) = 0" 134 | } 135 | let a = abs(a) 136 | return "(count(\(sibling)) + 1) <= \(b)" + ((a != 1) ? " and ((((count(\(sibling)) + 1)-\(b)) mod \(a) = 0)" : "") 137 | } 138 | 139 | // a(n) + b | a(n) - b 140 | private func nth_child(a: Int, b: Int) -> String { 141 | return nth(prefix: "preceding", a: a, b: b) 142 | } 143 | 144 | private func nth_last_child(a: Int, b: Int) -> String { 145 | return nth(prefix: "following", a: a, b: b) 146 | } 147 | 148 | private let escapePattern = "(?:\\\\([!\"#\\$%&\'\\(\\)\\*\\+,\\./:;<=>\\?@\\[\\\\\\]\\^`\\{\\|\\}~]))" 149 | private let escapeRepeatPattern = "\(escapePattern)*" 150 | private let matchElement = firstMatch("^((?:[a-z0-9\\*_-]+\(escapeRepeatPattern))+)((\\|)((?:[a-z0-9\\*_-]+\(escapeRepeatPattern))+))?") 151 | private let matchClassId = firstMatch("^([#.])((?:[a-z0-9\\*_-]+\(escapeRepeatPattern))+)") 152 | private let matchAttr1 = firstMatch("^\\[([^\\]]*)\\]") 153 | private let matchAttr2 = firstMatch("^\\[\\s*([^~\\|\\^\\$\\*=\\s]+)\\s*([~\\|\\^\\$\\*]?=)\\s*(.*)\\s*\\]") 154 | private let matchAttrN = firstMatch("^:not\\((.*?\\)?)\\)") 155 | private let matchPseudo = firstMatch("^:([\'\"()a-z0-9_+-]+)") 156 | private let matchCombinator = firstMatch("^\\s*([\\s>+~,])\\s*") 157 | private let matchSubNthChild = firstMatch("^(nth-child|nth-last-child)\\(\\s*(odd|even|\\d+)\\s*\\)") 158 | private let matchSubNthChildN = firstMatch("^(nth-child|nth-last-child)\\(\\s*(-?\\d*)n(\\+\\d+)?\\s*\\)") 159 | private let matchSubNthOfType = firstMatch("nth-of-type\\((odd|even|\\d+)\\)") 160 | private let matchSubContains = firstMatch("contains\\([\"\'](.*?)[\"\']\\)") 161 | 162 | private func substringWithRangeAtIndex(_ result: AKTextCheckingResult, str: String, at: Int) -> String { 163 | if result.numberOfRanges > at { 164 | #if swift(>=4.0) || os(Linux) 165 | let range = result.range(at: at) 166 | #else 167 | let range = result.rangeAt(at) 168 | #endif 169 | if range.length > 0 { 170 | let startIndex = str.index(str.startIndex, offsetBy: range.location) 171 | let endIndex = str.index(startIndex, offsetBy: range.length) 172 | return String(str[startIndex.. String { 179 | return text.replacingOccurrences(of: escapePattern, with: "$1", options: .regularExpression, range: nil) 180 | } 181 | 182 | private func getElement(_ str: inout String, skip: Bool = true) -> String { 183 | if let result = matchElement(str) { 184 | let (text, text2) = (escapeCSS(substringWithRangeAtIndex(result, str: str, at: 1)), 185 | escapeCSS(substringWithRangeAtIndex(result, str: str, at: 5))) 186 | 187 | if skip { 188 | str = String(str[str.index(str.startIndex, offsetBy: result.range.length).. String? { 205 | if let result = matchClassId(str) { 206 | let (attr, text) = (escapeCSS(substringWithRangeAtIndex(result, str: str, at: 1)), 207 | escapeCSS(substringWithRangeAtIndex(result, str: str, at: 2))) 208 | if skip { 209 | str = String(str[str.index(str.startIndex, offsetBy: result.range.length).. String? { 222 | if let result = matchAttr2(str) { 223 | let (attr, expr, text) = (escapeCSS(substringWithRangeAtIndex(result, str: str, at: 1)), 224 | substringWithRangeAtIndex(result, str: str, at: 2), 225 | escapeCSS(substringWithRangeAtIndex(result, str: str, at: 3).replacingOccurrences(of: "[\'\"](.*)[\'\"]", with: "$1", options: .regularExpression, range: nil))) 226 | 227 | if skip { 228 | str = String(str[str.index(str.startIndex, offsetBy: result.range.length)..= 1) and (((position()-1) mod 2) = 0)" 308 | } else if arg1 == "even" { 309 | return "(position() mod 2) = 0" 310 | } else { 311 | return "position() = \(arg1)" 312 | } 313 | } else if let sub = matchSubContains(one) { 314 | let text = substringWithRangeAtIndex(sub, str: one, at: 1) 315 | return "contains(., '\(text)')" 316 | } else { 317 | return nil 318 | } 319 | } 320 | } 321 | return nil 322 | } 323 | 324 | private func getAttrNot(_ str: inout String, skip: Bool = true) -> String? { 325 | if let result = matchAttrN(str) { 326 | var one = substringWithRangeAtIndex(result, str: str, at: 1) 327 | if skip { 328 | str = String(str[str.index(str.startIndex, offsetBy: result.range.length)..=4.0) || os(Linux) 335 | let range = sub.range(at: 1) 336 | #else 337 | let range = sub.rangeAt(1) 338 | #endif 339 | let startIndex = one.index(one.startIndex, offsetBy: range.location) 340 | let endIndex = one.index(startIndex, offsetBy: range.length) 341 | 342 | let elem = one[startIndex ..< endIndex] 343 | return "self::\(elem)" 344 | } else if let attr = getClassId(&one) { 345 | return attr 346 | } 347 | } 348 | return nil 349 | } 350 | 351 | private func genCombinator(_ str: inout String, skip: Bool = true) -> String? { 352 | if let result = matchCombinator(str) { 353 | let one = substringWithRangeAtIndex(result, str: str, at: 1) 354 | if skip { 355 | str = String(str[str.index(str.startIndex, offsetBy: result.range.length)..": 360 | return "/" 361 | case "+": 362 | return "/following-sibling::*[1]/self::" 363 | case "~": 364 | return "/following-sibling::" 365 | default: 366 | return "//" 367 | } 368 | } 369 | return nil 370 | } 371 | -------------------------------------------------------------------------------- /Kanna/Deprecated.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecated.swift 3 | // Kanna 4 | // 5 | // Created by Atsushi Kiwaki on 2017/10/27. 6 | // Copyright © 2017 Atsushi Kiwaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //------------------------------------------------------------- 12 | // XML 13 | //------------------------------------------------------------- 14 | @available(*, unavailable, message: "Use XML(xml: String, url: String?, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 15 | public func XML(xml: String, url: String?, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 16 | return nil 17 | } 18 | 19 | @available(*, unavailable, message: "Use XML(xml: String, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 20 | public func XML(xml: String, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 21 | return nil 22 | } 23 | 24 | @available(*, unavailable, message: "Use XML(xml: Data, url: String?, encoding: String.Encoding, option: ParseOption). The type of the first argument has been changed to Data and the type of the second argument has been changed to String.Encoding from UInt.") 25 | public func XML(xml: NSData, url: String?, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 26 | return nil 27 | } 28 | 29 | @available(*, unavailable, message: "Use XML(xml: Data, encoding: String.Encoding, option: ParseOption). The type of the first argument has been changed to Data and the type of the second argument has been changed to String.Encoding from UInt.") 30 | public func XML(xml: NSData, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 31 | return nil 32 | } 33 | 34 | @available(*, unavailable, message: "Use XML(url: URL, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 35 | public func XML(url: URL, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 36 | return nil 37 | } 38 | 39 | //------------------------------------------------------------- 40 | // HTML 41 | //------------------------------------------------------------- 42 | @available(*, unavailable, message: "Use HTML(html: String, url: String?, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 43 | public func HTML(html: String, url: String?, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 44 | return nil 45 | } 46 | 47 | @available(*, unavailable, message: "Use HTML(html: String, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 48 | public func HTML(html: String, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 49 | return nil 50 | } 51 | 52 | @available(*, unavailable, message: "Use HTML(html: Data, url: String?, encoding: String.Encoding, option: ParseOption). The type of the first argument has been changed to Data and the type of the second argument has been changed to String.Encoding from UInt.") 53 | public func HTML(html: NSData, url: String?, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 54 | return nil 55 | } 56 | 57 | @available(*, unavailable, message: "Use HTML(html: Data, encoding: String.Encoding, option: ParseOption). The type of the first argument has been changed to Data and the type of the second argument has been changed to String.Encoding from UInt.") 58 | public func HTML(html: NSData, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 59 | return nil 60 | } 61 | 62 | @available(*, unavailable, message: "Use HTML(url: URL, encoding: String.Encoding, option: ParseOption). The type of the second argument has been changed to String.Encoding from UInt.") 63 | public func HTML(url: URL, encoding: UInt, option: ParseOption = kDefaultXmlParseOption) -> XMLDocument? { 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /Kanna/Kanna.swift: -------------------------------------------------------------------------------- 1 | /**@file Kanna.swift 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | import Foundation 26 | 27 | #if SWIFT_PACKAGE 28 | import SwiftClibxml2 29 | #else 30 | import libxmlKanna 31 | #endif 32 | 33 | /* 34 | ParseOption 35 | */ 36 | public enum ParseOption { 37 | // libxml2 38 | case xmlParseUseLibxml(Libxml2XMLParserOptions) 39 | case htmlParseUseLibxml(Libxml2HTMLParserOptions) 40 | } 41 | 42 | public let kDefaultXmlParseOption = ParseOption.xmlParseUseLibxml([.RECOVER, .NOERROR, .NOWARNING]) 43 | public let kDefaultHtmlParseOption = ParseOption.htmlParseUseLibxml([.RECOVER, .NOERROR, .NOWARNING]) 44 | 45 | public enum ParseError: Error { 46 | case Empty 47 | case EncodingMismatch 48 | case InvalidOptions 49 | } 50 | 51 | /** 52 | Parse XML 53 | 54 | @param xml an XML string 55 | @param url the base URL to use for the document 56 | @param encoding the document encoding 57 | @param options a ParserOption 58 | */ 59 | public func XML(xml: String, url: String?, encoding: String.Encoding, option: ParseOption = kDefaultXmlParseOption) throws -> XMLDocument { 60 | switch option { 61 | case .xmlParseUseLibxml(let opt): 62 | return try libxmlXMLDocument(xml: xml, url: url, encoding: encoding, option: opt.rawValue) 63 | default: 64 | throw ParseError.InvalidOptions 65 | } 66 | } 67 | 68 | public func XML(xml: String, encoding: String.Encoding, option: ParseOption = kDefaultXmlParseOption) throws -> XMLDocument { 69 | return try XML(xml: xml, url: nil, encoding: encoding, option: option) 70 | } 71 | 72 | // NSData 73 | public func XML(xml: Data, url: String?, encoding: String.Encoding, option: ParseOption = kDefaultXmlParseOption) throws -> XMLDocument { 74 | guard let xmlStr = String(data: xml, encoding: encoding) else { 75 | throw ParseError.EncodingMismatch 76 | } 77 | return try XML(xml: xmlStr, url: url, encoding: encoding, option: option) 78 | } 79 | 80 | public func XML(xml: Data, encoding: String.Encoding, option: ParseOption = kDefaultXmlParseOption) throws -> XMLDocument { 81 | return try XML(xml: xml, url: nil, encoding: encoding, option: option) 82 | } 83 | 84 | // NSURL 85 | public func XML(url: URL, encoding: String.Encoding, option: ParseOption = kDefaultXmlParseOption) throws -> XMLDocument { 86 | guard let data = try? Data(contentsOf: url) else { 87 | throw ParseError.EncodingMismatch 88 | } 89 | return try XML(xml: data, url: url.absoluteString, encoding: encoding, option: option) 90 | } 91 | 92 | /** 93 | Parse HTML 94 | 95 | @param html an HTML string 96 | @param url the base URL to use for the document 97 | @param encoding the document encoding 98 | @param options a ParserOption 99 | */ 100 | public func HTML(html: String, url: String?, encoding: String.Encoding, option: ParseOption = kDefaultHtmlParseOption) throws -> HTMLDocument { 101 | switch option { 102 | case .htmlParseUseLibxml(let opt): 103 | return try libxmlHTMLDocument(html: html, url: url, encoding: encoding, option: opt.rawValue) 104 | default: 105 | throw ParseError.InvalidOptions 106 | } 107 | } 108 | 109 | public func HTML(html: String, encoding: String.Encoding, option: ParseOption = kDefaultHtmlParseOption) throws -> HTMLDocument { 110 | return try HTML(html: html, url: nil, encoding: encoding, option: option) 111 | } 112 | 113 | // NSData 114 | public func HTML(html: Data, url: String?, encoding: String.Encoding, option: ParseOption = kDefaultHtmlParseOption) throws -> HTMLDocument { 115 | guard let htmlStr = String(data: html, encoding: encoding) else { 116 | throw ParseError.EncodingMismatch 117 | } 118 | return try HTML(html: htmlStr, url: url, encoding: encoding, option: option) 119 | } 120 | 121 | public func HTML(html: Data, encoding: String.Encoding, option: ParseOption = kDefaultHtmlParseOption) throws -> HTMLDocument { 122 | return try HTML(html: html, url: nil, encoding: encoding, option: option) 123 | } 124 | 125 | // NSURL 126 | public func HTML(url: URL, encoding: String.Encoding, option: ParseOption = kDefaultHtmlParseOption) throws -> HTMLDocument { 127 | guard let data = try? Data(contentsOf: url) else { 128 | throw ParseError.EncodingMismatch 129 | } 130 | return try HTML(html: data, url: url.absoluteString, encoding: encoding, option: option) 131 | } 132 | 133 | /** 134 | Searchable 135 | */ 136 | public protocol Searchable { 137 | /** 138 | Search for node from current node by XPath. 139 | 140 | @param xpath 141 | */ 142 | func xpath(_ xpath: String, namespaces: [String:String]?) -> XPathObject 143 | func xpath(_ xpath: String) -> XPathObject 144 | func at_xpath(_ xpath: String, namespaces: [String:String]?) -> XMLElement? 145 | func at_xpath(_ xpath: String) -> XMLElement? 146 | 147 | /** 148 | Search for node from current node by CSS selector. 149 | 150 | @param selector a CSS selector 151 | */ 152 | func css(_ selector: String, namespaces: [String:String]?) -> XPathObject 153 | func css(_ selector: String) -> XPathObject 154 | func at_css(_ selector: String, namespaces: [String:String]?) -> XMLElement? 155 | func at_css(_ selector: String) -> XMLElement? 156 | } 157 | 158 | /** 159 | SearchableNode 160 | */ 161 | public protocol SearchableNode: Searchable { 162 | var text: String? { get } 163 | var toHTML: String? { get } 164 | var toXML: String? { get } 165 | var innerHTML: String? { get } 166 | var className: String? { get } 167 | var tagName: String? { get set } 168 | var content: String? { get set } 169 | } 170 | 171 | /** 172 | XMLElement 173 | */ 174 | public protocol XMLElement: SearchableNode { 175 | var parent: XMLElement? { get set } 176 | subscript(attr: String) -> String? { get set } 177 | 178 | func addPrevSibling(_ node: XMLElement) 179 | func addNextSibling(_ node: XMLElement) 180 | func removeChild(_ node: XMLElement) 181 | var nextSibling: XMLElement? { get } 182 | var previousSibling: XMLElement? { get } 183 | } 184 | 185 | /** 186 | XMLDocument 187 | */ 188 | public protocol XMLDocument: class, SearchableNode { 189 | } 190 | 191 | /** 192 | HTMLDocument 193 | */ 194 | public protocol HTMLDocument: XMLDocument { 195 | var title: String? { get } 196 | var head: XMLElement? { get } 197 | var body: XMLElement? { get } 198 | } 199 | 200 | /** 201 | XMLNodeSet 202 | */ 203 | public final class XMLNodeSet { 204 | fileprivate var nodes: [XMLElement] = [] 205 | 206 | public var toHTML: String? { 207 | let html = nodes.reduce("") { 208 | if let text = $1.toHTML { 209 | return $0 + text 210 | } 211 | return $0 212 | } 213 | return html.isEmpty == false ? html : nil 214 | } 215 | 216 | public var innerHTML: String? { 217 | let html = nodes.reduce("") { 218 | if let text = $1.innerHTML { 219 | return $0 + text 220 | } 221 | return $0 222 | } 223 | return html.isEmpty == false ? html : nil 224 | } 225 | 226 | public var text: String? { 227 | let html = nodes.reduce("") { 228 | if let text = $1.text { 229 | return $0 + text 230 | } 231 | return $0 232 | } 233 | return html 234 | } 235 | 236 | public subscript(index: Int) -> XMLElement { 237 | return nodes[index] 238 | } 239 | 240 | public var count: Int { 241 | return nodes.count 242 | } 243 | 244 | internal init() { 245 | } 246 | 247 | internal init(nodes: [XMLElement]) { 248 | self.nodes = nodes 249 | } 250 | 251 | public func at(_ index: Int) -> XMLElement? { 252 | return count > index ? nodes[index] : nil 253 | } 254 | 255 | public var first: XMLElement? { 256 | return at(0) 257 | } 258 | 259 | public var last: XMLElement? { 260 | return at(count-1) 261 | } 262 | } 263 | 264 | extension XMLNodeSet: Sequence { 265 | public typealias Iterator = AnyIterator 266 | public func makeIterator() -> Iterator { 267 | var index = 0 268 | return AnyIterator { 269 | if index < self.nodes.count { 270 | let n = self.nodes[index] 271 | index += 1 272 | return n 273 | } 274 | return nil 275 | } 276 | } 277 | } 278 | 279 | /** 280 | XPathObject 281 | */ 282 | 283 | public enum XPathObject { 284 | case none 285 | case NodeSet(nodeset: XMLNodeSet) 286 | case Bool(bool: Swift.Bool) 287 | case Number(num: Double) 288 | case String(text: Swift.String) 289 | } 290 | 291 | extension XPathObject { 292 | internal init(document: XMLDocument?, docPtr: xmlDocPtr, object: xmlXPathObject) { 293 | switch object.type { 294 | case XPATH_NODESET: 295 | let nodeSet = object.nodesetval 296 | if nodeSet == nil || nodeSet?.pointee.nodeNr == 0 || nodeSet?.pointee.nodeTab == nil { 297 | self = .none 298 | return 299 | } 300 | 301 | var nodes : [XMLElement] = [] 302 | let size = Int((nodeSet?.pointee.nodeNr)!) 303 | for i in 0 ..< size { 304 | let node: xmlNodePtr = nodeSet!.pointee.nodeTab[i]! 305 | let htmlNode = libxmlHTMLNode(document: document, docPtr: docPtr, node: node) 306 | nodes.append(htmlNode) 307 | } 308 | self = .NodeSet(nodeset: XMLNodeSet(nodes: nodes)) 309 | return 310 | case XPATH_BOOLEAN: 311 | self = .Bool(bool: object.boolval != 0) 312 | return 313 | case XPATH_NUMBER: 314 | self = .Number(num: object.floatval) 315 | case XPATH_STRING: 316 | guard let str = UnsafeRawPointer(object.stringval)?.assumingMemoryBound(to: CChar.self) else { 317 | self = .String(text: "") 318 | return 319 | } 320 | self = .String(text: Swift.String(cString: str)) 321 | return 322 | default: 323 | self = .none 324 | return 325 | } 326 | } 327 | 328 | public subscript(index: Int) -> XMLElement { 329 | return nodeSet![index] 330 | } 331 | 332 | public var first: XMLElement? { 333 | return nodeSet?.first 334 | } 335 | 336 | public var count: Int { 337 | guard let nodeset = nodeSet else { 338 | return 0 339 | } 340 | return nodeset.count 341 | } 342 | 343 | var nodeSet: XMLNodeSet? { 344 | if case let .NodeSet(nodeset) = self { 345 | return nodeset 346 | } 347 | return nil 348 | } 349 | 350 | var bool: Swift.Bool? { 351 | if case let .Bool(value) = self { 352 | return value 353 | } 354 | return nil 355 | } 356 | 357 | var number: Double? { 358 | if case let .Number(value) = self { 359 | return value 360 | } 361 | return nil 362 | } 363 | 364 | var string: Swift.String? { 365 | if case let .String(value) = self { 366 | return value 367 | } 368 | return nil 369 | } 370 | 371 | var nodeSetValue: XMLNodeSet { 372 | return nodeSet ?? XMLNodeSet() 373 | } 374 | 375 | var boolValue: Swift.Bool { 376 | return bool ?? false 377 | } 378 | 379 | var numberValue: Double { 380 | return number ?? 0.0 381 | } 382 | 383 | var stringValue: Swift.String { 384 | return string ?? "" 385 | } 386 | } 387 | 388 | extension XPathObject: Sequence { 389 | public typealias Iterator = AnyIterator 390 | public func makeIterator() -> Iterator { 391 | var index = 0 392 | return AnyIterator { 393 | if index < self.nodeSetValue.count { 394 | let obj = self.nodeSetValue[index] 395 | index += 1 396 | return obj 397 | } 398 | return nil 399 | } 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /Kanna/libxmlHTMLDocument.swift: -------------------------------------------------------------------------------- 1 | /**@file libxmlHTMLDocument.swift 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | import Foundation 26 | import CoreFoundation 27 | 28 | #if SWIFT_PACKAGE 29 | import SwiftClibxml2 30 | #else 31 | import libxmlKanna 32 | #endif 33 | 34 | extension String.Encoding { 35 | var IANACharSetName: String? { 36 | #if os(Linux) && swift(>=4) 37 | switch self { 38 | case .ascii: 39 | return "us-ascii" 40 | case .iso2022JP: 41 | return "iso-2022-jp" 42 | case .isoLatin1: 43 | return "iso-8859-1" 44 | case .isoLatin2: 45 | return "iso-8859-2" 46 | case .japaneseEUC: 47 | return "euc-jp" 48 | case .macOSRoman: 49 | return "macintosh" 50 | case .nextstep: 51 | return "x-nextstep" 52 | case .nonLossyASCII: 53 | return nil 54 | case .shiftJIS: 55 | return "cp932" 56 | case .symbol: 57 | return "x-mac-symbol" 58 | case .unicode: 59 | return "utf-16" 60 | case .utf16: 61 | return "utf-16" 62 | case .utf16BigEndian: 63 | return "utf-16be" 64 | case .utf32: 65 | return "utf-32" 66 | case .utf32BigEndian: 67 | return "utf-32be" 68 | case .utf32LittleEndian: 69 | return "utf-32le" 70 | case .utf8: 71 | return "utf-8" 72 | case .windowsCP1250: 73 | return "windows-1250" 74 | case .windowsCP1251: 75 | return "windows-1251" 76 | case .windowsCP1252: 77 | return "windows-1252" 78 | case .windowsCP1253: 79 | return "windows-1253" 80 | case .windowsCP1254: 81 | return "windows-1254" 82 | default: 83 | return nil 84 | } 85 | #elseif os(Linux) && swift(>=3) 86 | switch self { 87 | case String.Encoding.ascii: 88 | return "us-ascii" 89 | case String.Encoding.iso2022JP: 90 | return "iso-2022-jp" 91 | case String.Encoding.isoLatin1: 92 | return "iso-8859-1" 93 | case String.Encoding.isoLatin2: 94 | return "iso-8859-2" 95 | case String.Encoding.japaneseEUC: 96 | return "euc-jp" 97 | case String.Encoding.macOSRoman: 98 | return "macintosh" 99 | case String.Encoding.nextstep: 100 | return "x-nextstep" 101 | case String.Encoding.nonLossyASCII: 102 | return nil 103 | case String.Encoding.shiftJIS: 104 | return "cp932" 105 | case String.Encoding.symbol: 106 | return "x-mac-symbol" 107 | case String.Encoding.unicode: 108 | return "utf-16" 109 | case String.Encoding.utf16: 110 | return "utf-16" 111 | case String.Encoding.utf16BigEndian: 112 | return "utf-16be" 113 | case String.Encoding.utf32: 114 | return "utf-32" 115 | case String.Encoding.utf32BigEndian: 116 | return "utf-32be" 117 | case String.Encoding.utf32LittleEndian: 118 | return "utf-32le" 119 | case String.Encoding.utf8: 120 | return "utf-8" 121 | case String.Encoding.windowsCP1250: 122 | return "windows-1250" 123 | case String.Encoding.windowsCP1251: 124 | return "windows-1251" 125 | case String.Encoding.windowsCP1252: 126 | return "windows-1252" 127 | case String.Encoding.windowsCP1253: 128 | return "windows-1253" 129 | case String.Encoding.windowsCP1254: 130 | return "windows-1254" 131 | default: 132 | return nil 133 | } 134 | #else 135 | let cfenc = CFStringConvertNSStringEncodingToEncoding(self.rawValue) 136 | guard let cfencstr = CFStringConvertEncodingToIANACharSetName(cfenc) else { 137 | return nil 138 | } 139 | return String(describing: cfencstr) 140 | #endif 141 | } 142 | } 143 | 144 | /* 145 | libxmlHTMLDocument 146 | */ 147 | internal final class libxmlHTMLDocument: HTMLDocument { 148 | fileprivate var docPtr: htmlDocPtr? = nil 149 | fileprivate var rootNode: XMLElement? 150 | fileprivate var html: String 151 | fileprivate var url: String? 152 | fileprivate var encoding: String.Encoding 153 | 154 | var text: String? { 155 | return rootNode?.text 156 | } 157 | 158 | var toHTML: String? { 159 | let buf = xmlBufferCreate() 160 | let outputBuf = xmlOutputBufferCreateBuffer(buf, nil) 161 | defer { 162 | xmlOutputBufferClose(outputBuf) 163 | xmlBufferFree(buf) 164 | } 165 | 166 | htmlDocContentDumpOutput(outputBuf, docPtr, nil) 167 | let html = String(cString: UnsafePointer(xmlOutputBufferGetContent(outputBuf))) 168 | return html 169 | } 170 | 171 | var toXML: String? { 172 | var buf: UnsafeMutablePointer? = nil 173 | let size: UnsafeMutablePointer? = nil 174 | defer { 175 | xmlFree(buf) 176 | } 177 | 178 | xmlDocDumpMemory(docPtr, &buf, size) 179 | let html = String(cString: UnsafePointer(buf!)) 180 | return html 181 | } 182 | 183 | var innerHTML: String? { 184 | return rootNode?.innerHTML 185 | } 186 | 187 | var className: String? { 188 | return nil 189 | } 190 | 191 | var tagName: String? { 192 | get { 193 | return nil 194 | } 195 | 196 | set { 197 | 198 | } 199 | } 200 | 201 | var content: String? { 202 | get { 203 | return text 204 | } 205 | 206 | set { 207 | rootNode?.content = newValue 208 | } 209 | } 210 | 211 | init(html: String, url: String?, encoding: String.Encoding, option: UInt) throws { 212 | self.html = html 213 | self.url = url 214 | self.encoding = encoding 215 | 216 | guard html.lengthOfBytes(using: encoding) > 0 else { 217 | throw ParseError.Empty 218 | } 219 | 220 | guard let charsetName = encoding.IANACharSetName, 221 | let cur = html.cString(using: encoding) else { 222 | throw ParseError.EncodingMismatch 223 | } 224 | 225 | let url : String = "" 226 | docPtr = htmlReadDoc(UnsafeRawPointer(cur).assumingMemoryBound(to: xmlChar.self), url, charsetName, CInt(option)) 227 | 228 | guard let docPtr = docPtr else { 229 | throw ParseError.EncodingMismatch 230 | } 231 | 232 | rootNode = libxmlHTMLNode(document: self, docPtr: docPtr) 233 | } 234 | 235 | deinit { 236 | xmlFreeDoc(self.docPtr) 237 | } 238 | 239 | var title: String? { return at_xpath("//title")?.text } 240 | var head: XMLElement? { return at_xpath("//head") } 241 | var body: XMLElement? { return at_xpath("//body") } 242 | 243 | func xpath(_ xpath: String, namespaces: [String:String]?) -> XPathObject { 244 | return rootNode?.xpath(xpath, namespaces: namespaces) ?? XPathObject.none 245 | } 246 | 247 | func xpath(_ xpath: String) -> XPathObject { 248 | return self.xpath(xpath, namespaces: nil) 249 | } 250 | 251 | func at_xpath(_ xpath: String, namespaces: [String:String]?) -> XMLElement? { 252 | return rootNode?.at_xpath(xpath, namespaces: namespaces) 253 | } 254 | 255 | func at_xpath(_ xpath: String) -> XMLElement? { 256 | return self.at_xpath(xpath, namespaces: nil) 257 | } 258 | 259 | func css(_ selector: String, namespaces: [String:String]?) -> XPathObject { 260 | return rootNode?.css(selector, namespaces: namespaces) ?? XPathObject.none 261 | } 262 | 263 | func css(_ selector: String) -> XPathObject { 264 | return self.css(selector, namespaces: nil) 265 | } 266 | 267 | func at_css(_ selector: String, namespaces: [String:String]?) -> XMLElement? { 268 | return rootNode?.at_css(selector, namespaces: namespaces) 269 | } 270 | 271 | func at_css(_ selector: String) -> XMLElement? { 272 | return self.at_css(selector, namespaces: nil) 273 | } 274 | } 275 | 276 | /* 277 | libxmlXMLDocument 278 | */ 279 | internal final class libxmlXMLDocument: XMLDocument { 280 | fileprivate var docPtr: xmlDocPtr? = nil 281 | fileprivate var rootNode: XMLElement? 282 | fileprivate var xml: String 283 | fileprivate var url: String? 284 | fileprivate var encoding: String.Encoding 285 | 286 | var text: String? { 287 | return rootNode?.text 288 | } 289 | 290 | var toHTML: String? { 291 | let buf = xmlBufferCreate() 292 | let outputBuf = xmlOutputBufferCreateBuffer(buf, nil) 293 | defer { 294 | xmlOutputBufferClose(outputBuf) 295 | xmlBufferFree(buf) 296 | } 297 | 298 | htmlDocContentDumpOutput(outputBuf, docPtr, nil) 299 | let html = String(cString: UnsafePointer(xmlOutputBufferGetContent(outputBuf))) 300 | return html 301 | } 302 | 303 | var toXML: String? { 304 | var buf: UnsafeMutablePointer? = nil 305 | let size: UnsafeMutablePointer? = nil 306 | defer { 307 | xmlFree(buf) 308 | } 309 | 310 | xmlDocDumpMemory(docPtr, &buf, size) 311 | let html = String(cString: UnsafePointer(buf!)) 312 | return html 313 | } 314 | 315 | var innerHTML: String? { 316 | return rootNode?.innerHTML 317 | } 318 | 319 | var className: String? { 320 | return nil 321 | } 322 | 323 | var tagName: String? { 324 | get { 325 | return nil 326 | } 327 | 328 | set { 329 | 330 | } 331 | } 332 | 333 | var content: String? { 334 | get { 335 | return text 336 | } 337 | 338 | set { 339 | rootNode?.content = newValue 340 | } 341 | } 342 | 343 | init(xml: String, url: String?, encoding: String.Encoding, option: UInt) throws { 344 | self.xml = xml 345 | self.url = url 346 | self.encoding = encoding 347 | 348 | if xml.isEmpty { 349 | throw ParseError.Empty 350 | } 351 | 352 | 353 | guard let charsetName = encoding.IANACharSetName, 354 | let cur = xml.cString(using: encoding) else { 355 | throw ParseError.EncodingMismatch 356 | } 357 | let url : String = "" 358 | docPtr = xmlReadDoc(UnsafeRawPointer(cur).assumingMemoryBound(to: xmlChar.self), url, charsetName, CInt(option)) 359 | rootNode = libxmlHTMLNode(document: self, docPtr: docPtr!) 360 | } 361 | 362 | deinit { 363 | xmlFreeDoc(self.docPtr) 364 | } 365 | 366 | func xpath(_ xpath: String, namespaces: [String:String]?) -> XPathObject { 367 | return rootNode?.xpath(xpath, namespaces: namespaces) ?? XPathObject.none 368 | } 369 | 370 | func xpath(_ xpath: String) -> XPathObject { 371 | return self.xpath(xpath, namespaces: nil) 372 | } 373 | 374 | func at_xpath(_ xpath: String, namespaces: [String:String]?) -> XMLElement? { 375 | return rootNode?.at_xpath(xpath, namespaces: namespaces) 376 | } 377 | 378 | func at_xpath(_ xpath: String) -> XMLElement? { 379 | return self.at_xpath(xpath, namespaces: nil) 380 | } 381 | 382 | func css(_ selector: String, namespaces: [String:String]?) -> XPathObject { 383 | return rootNode?.css(selector, namespaces: namespaces) ?? XPathObject.none 384 | } 385 | 386 | func css(_ selector: String) -> XPathObject { 387 | return self.css(selector, namespaces: nil) 388 | } 389 | 390 | func at_css(_ selector: String, namespaces: [String:String]?) -> XMLElement? { 391 | return rootNode?.at_css(selector, namespaces: namespaces) 392 | } 393 | 394 | func at_css(_ selector: String) -> XMLElement? { 395 | return self.at_css(selector, namespaces: nil) 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /Kanna/libxmlHTMLNode.swift: -------------------------------------------------------------------------------- 1 | /**@file libxmlHTMLNode.swift 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | import Foundation 26 | 27 | #if SWIFT_PACKAGE 28 | import SwiftClibxml2 29 | #else 30 | import libxmlKanna 31 | #endif 32 | 33 | /** 34 | libxmlHTMLNode 35 | */ 36 | internal final class libxmlHTMLNode: XMLElement { 37 | var text: String? { 38 | if nodePtr != nil { 39 | return libxmlGetNodeContent(nodePtr!) 40 | } 41 | return nil 42 | } 43 | 44 | var toHTML: String? { 45 | let buf = xmlBufferCreate() 46 | htmlNodeDump(buf, docPtr, nodePtr) 47 | let html = String(cString: UnsafePointer((buf?.pointee.content)!)) 48 | xmlBufferFree(buf) 49 | return html 50 | } 51 | 52 | var toXML: String? { 53 | let buf = xmlBufferCreate() 54 | xmlNodeDump(buf, docPtr, nodePtr, 0, 0) 55 | let html = String(cString: UnsafePointer((buf?.pointee.content)!)) 56 | xmlBufferFree(buf) 57 | return html 58 | } 59 | 60 | var innerHTML: String? { 61 | if let html = self.toHTML { 62 | let inner = html.replacingOccurrences(of: "]*>$", with: "", options: .regularExpression, range: nil) 63 | .replacingOccurrences(of: "^<[^>]*>", with: "", options: .regularExpression, range: nil) 64 | return inner 65 | } 66 | return nil 67 | } 68 | 69 | var className: String? { 70 | return self["class"] 71 | } 72 | 73 | var tagName: String? { 74 | get { 75 | guard let name = nodePtr?.pointee.name else { 76 | return nil 77 | } 78 | return String(cString: name) 79 | } 80 | 81 | set { 82 | if let newValue = newValue { 83 | xmlNodeSetName(nodePtr, newValue) 84 | } 85 | } 86 | } 87 | 88 | var content: String? { 89 | get { 90 | return text 91 | } 92 | 93 | set { 94 | if let newValue = newValue { 95 | let v = escape(newValue) 96 | xmlNodeSetContent(nodePtr, v) 97 | } 98 | } 99 | } 100 | 101 | var parent: XMLElement? { 102 | get { 103 | return libxmlHTMLNode(document: doc, docPtr: docPtr!, node: (nodePtr?.pointee.parent)!) 104 | } 105 | 106 | set { 107 | if let node = newValue as? libxmlHTMLNode { 108 | node.addChild(self) 109 | } 110 | } 111 | } 112 | 113 | var nextSibling: XMLElement? { 114 | let val = xmlNextElementSibling(self.nodePtr) 115 | return self.node(from: val) 116 | } 117 | 118 | var previousSibling: XMLElement? { 119 | let val = xmlPreviousElementSibling(self.nodePtr) 120 | return self.node(from: val) 121 | } 122 | 123 | fileprivate weak var weakDocument: XMLDocument? 124 | fileprivate var document: XMLDocument? 125 | fileprivate var docPtr: htmlDocPtr? = nil 126 | fileprivate var nodePtr: xmlNodePtr? = nil 127 | fileprivate var isRoot: Bool = false 128 | fileprivate var doc: XMLDocument? { 129 | return weakDocument ?? document 130 | } 131 | 132 | subscript(attributeName: String) -> String? 133 | { 134 | get { 135 | var attr = nodePtr?.pointee.properties 136 | while attr != nil { 137 | let mem = attr?.pointee 138 | if let tagName = String(validatingUTF8: UnsafeRawPointer((mem?.name)!).assumingMemoryBound(to: CChar.self)) { 139 | if attributeName == tagName { 140 | if let children = mem?.children { 141 | return libxmlGetNodeContent(children) 142 | } else { 143 | return "" 144 | } 145 | } 146 | } 147 | attr = attr?.pointee.next 148 | } 149 | return nil 150 | } 151 | 152 | set(newValue) { 153 | if let newValue = newValue { 154 | xmlSetProp(nodePtr, attributeName, newValue) 155 | } else { 156 | xmlUnsetProp(nodePtr, attributeName) 157 | } 158 | } 159 | } 160 | 161 | init(document: XMLDocument?, docPtr: xmlDocPtr) { 162 | self.weakDocument = document 163 | self.docPtr = docPtr 164 | self.nodePtr = xmlDocGetRootElement(docPtr) 165 | self.isRoot = true 166 | } 167 | 168 | init(document: XMLDocument?, docPtr: xmlDocPtr, node: xmlNodePtr) { 169 | self.document = document 170 | self.docPtr = docPtr 171 | self.nodePtr = node 172 | } 173 | 174 | // MARK: Searchable 175 | func xpath(_ xpath: String, namespaces: [String:String]?) -> XPathObject { 176 | let ctxt = xmlXPathNewContext(docPtr) 177 | if ctxt == nil { 178 | return XPathObject.none 179 | } 180 | ctxt?.pointee.node = nodePtr 181 | 182 | if let nsDictionary = namespaces { 183 | for (ns, name) in nsDictionary { 184 | xmlXPathRegisterNs(ctxt, ns, name) 185 | } 186 | } 187 | 188 | let result = xmlXPathEvalExpression(xpath, ctxt) 189 | defer { 190 | xmlXPathFreeObject(result) 191 | } 192 | xmlXPathFreeContext(ctxt) 193 | if result == nil { 194 | return XPathObject.none 195 | } 196 | 197 | return XPathObject(document: doc, docPtr: docPtr!, object: result!.pointee) 198 | } 199 | 200 | func xpath(_ xpath: String) -> XPathObject { 201 | return self.xpath(xpath, namespaces: nil) 202 | } 203 | 204 | func at_xpath(_ xpath: String, namespaces: [String:String]?) -> XMLElement? { 205 | return self.xpath(xpath, namespaces: namespaces).nodeSetValue.first 206 | } 207 | 208 | func at_xpath(_ xpath: String) -> XMLElement? { 209 | return self.at_xpath(xpath, namespaces: nil) 210 | } 211 | 212 | func css(_ selector: String, namespaces: [String:String]?) -> XPathObject { 213 | if let xpath = try? CSS.toXPath(selector) { 214 | if isRoot { 215 | return self.xpath(xpath, namespaces: namespaces) 216 | } else { 217 | return self.xpath("." + xpath, namespaces: namespaces) 218 | } 219 | } 220 | return XPathObject.none 221 | } 222 | 223 | func css(_ selector: String) -> XPathObject { 224 | return self.css(selector, namespaces: nil) 225 | } 226 | 227 | func at_css(_ selector: String, namespaces: [String:String]?) -> XMLElement? { 228 | return self.css(selector, namespaces: namespaces).nodeSetValue.first 229 | } 230 | 231 | func at_css(_ selector: String) -> XMLElement? { 232 | return self.css(selector, namespaces: nil).nodeSetValue.first 233 | } 234 | 235 | func addPrevSibling(_ node: XMLElement) { 236 | guard let node = node as? libxmlHTMLNode else { 237 | return 238 | } 239 | xmlAddPrevSibling(nodePtr, node.nodePtr) 240 | } 241 | 242 | func addNextSibling(_ node: XMLElement) { 243 | guard let node = node as? libxmlHTMLNode else { 244 | return 245 | } 246 | xmlAddNextSibling(nodePtr, node.nodePtr) 247 | } 248 | 249 | func addChild(_ node: XMLElement) { 250 | guard let node = node as? libxmlHTMLNode else { 251 | return 252 | } 253 | xmlUnlinkNode(node.nodePtr) 254 | xmlAddChild(nodePtr, node.nodePtr) 255 | } 256 | 257 | func removeChild(_ node: XMLElement) { 258 | 259 | guard let node = node as? libxmlHTMLNode else { 260 | return 261 | } 262 | xmlUnlinkNode(node.nodePtr) 263 | xmlFree(node.nodePtr) 264 | } 265 | 266 | private func node(from ptr: xmlNodePtr?) -> XMLElement? { 267 | guard let doc = self.doc, let docPtr = self.docPtr, let nodePtr = ptr else { 268 | return nil 269 | } 270 | 271 | let element = libxmlHTMLNode(document: doc, docPtr: docPtr, node: nodePtr) 272 | return element 273 | } 274 | } 275 | 276 | private func libxmlGetNodeContent(_ nodePtr: xmlNodePtr) -> String? { 277 | let content = xmlNodeGetContent(nodePtr) 278 | defer { 279 | #if swift(>=4.1) 280 | content?.deallocate() 281 | #else 282 | content?.deallocate(capacity: 1) 283 | #endif 284 | } 285 | if let result = String(validatingUTF8: UnsafeRawPointer(content!).assumingMemoryBound(to: CChar.self)) { 286 | return result 287 | } 288 | return nil 289 | } 290 | 291 | let entities = [ 292 | "&": "&", 293 | "<" : "<", 294 | ">" : ">", 295 | ] 296 | 297 | private func escape(_ str: String) -> String { 298 | var newStr = str 299 | for (unesc, esc) in entities { 300 | newStr = newStr.replacingOccurrences(of: unesc, with: esc, options: .regularExpression, range: nil) 301 | } 302 | return newStr 303 | } 304 | 305 | -------------------------------------------------------------------------------- /Kanna/libxmlParserOption.swift: -------------------------------------------------------------------------------- 1 | /**@file libxmlParserOption.swift 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | import Foundation 26 | 27 | #if SWIFT_PACKAGE 28 | import SwiftClibxml2 29 | #else 30 | import libxmlKanna 31 | #endif 32 | 33 | /* 34 | Libxml2HTMLParserOptions 35 | */ 36 | public struct Libxml2HTMLParserOptions : OptionSet { 37 | public typealias RawValue = UInt 38 | private var value: UInt = 0 39 | init(_ value: UInt) { self.value = value } 40 | private init(_ opt: htmlParserOption) { self.value = UInt(opt.rawValue) } 41 | public init(rawValue value: UInt) { self.value = value } 42 | public init(nilLiteral: ()) { self.value = 0 } 43 | public static var allZeros: Libxml2HTMLParserOptions { return .init(0) } 44 | static func fromMask(raw: UInt) -> Libxml2HTMLParserOptions { return .init(raw) } 45 | public var rawValue: UInt { return self.value } 46 | 47 | public static let STRICT = Libxml2HTMLParserOptions(0) 48 | public static let RECOVER = Libxml2HTMLParserOptions(HTML_PARSE_RECOVER) 49 | public static let NODEFDTD = Libxml2HTMLParserOptions(HTML_PARSE_NODEFDTD) 50 | public static let NOERROR = Libxml2HTMLParserOptions(HTML_PARSE_NOERROR) 51 | public static let NOWARNING = Libxml2HTMLParserOptions(HTML_PARSE_NOWARNING) 52 | public static let PEDANTIC = Libxml2HTMLParserOptions(HTML_PARSE_PEDANTIC) 53 | public static let NOBLANKS = Libxml2HTMLParserOptions(HTML_PARSE_NOBLANKS) 54 | public static let NONET = Libxml2HTMLParserOptions(HTML_PARSE_NONET) 55 | public static let NOIMPLIED = Libxml2HTMLParserOptions(HTML_PARSE_NOIMPLIED) 56 | public static let COMPACT = Libxml2HTMLParserOptions(HTML_PARSE_COMPACT) 57 | public static let IGNORE_ENC = Libxml2HTMLParserOptions(HTML_PARSE_IGNORE_ENC) 58 | } 59 | 60 | /* 61 | Libxml2XMLParserOptions 62 | */ 63 | public struct Libxml2XMLParserOptions: OptionSet { 64 | public typealias RawValue = UInt 65 | private var value: UInt = 0 66 | init(_ value: UInt) { self.value = value } 67 | private init(_ opt: xmlParserOption) { self.value = UInt(opt.rawValue) } 68 | public init(rawValue value: UInt) { self.value = value } 69 | public init(nilLiteral: ()) { self.value = 0 } 70 | public static var allZeros: Libxml2XMLParserOptions { return .init(0) } 71 | static func fromMask(raw: UInt) -> Libxml2XMLParserOptions { return .init(raw) } 72 | public var rawValue: UInt { return self.value } 73 | 74 | public static let STRICT = Libxml2XMLParserOptions(0) 75 | public static let RECOVER = Libxml2XMLParserOptions(XML_PARSE_RECOVER) 76 | public static let NOENT = Libxml2XMLParserOptions(XML_PARSE_NOENT) 77 | public static let DTDLOAD = Libxml2XMLParserOptions(XML_PARSE_DTDLOAD) 78 | public static let DTDATTR = Libxml2XMLParserOptions(XML_PARSE_DTDATTR) 79 | public static let DTDVALID = Libxml2XMLParserOptions(XML_PARSE_DTDVALID) 80 | public static let NOERROR = Libxml2XMLParserOptions(XML_PARSE_NOERROR) 81 | public static let NOWARNING = Libxml2XMLParserOptions(XML_PARSE_NOWARNING) 82 | public static let PEDANTIC = Libxml2XMLParserOptions(XML_PARSE_PEDANTIC) 83 | public static let NOBLANKS = Libxml2XMLParserOptions(XML_PARSE_NOBLANKS) 84 | public static let SAX1 = Libxml2XMLParserOptions(XML_PARSE_SAX1) 85 | public static let XINCLUDE = Libxml2XMLParserOptions(XML_PARSE_XINCLUDE) 86 | public static let NONET = Libxml2XMLParserOptions(XML_PARSE_NONET) 87 | public static let NODICT = Libxml2XMLParserOptions(XML_PARSE_NODICT) 88 | public static let NSCLEAN = Libxml2XMLParserOptions(XML_PARSE_NSCLEAN) 89 | public static let NOCDATA = Libxml2XMLParserOptions(XML_PARSE_NOCDATA) 90 | public static let NOXINCNODE = Libxml2XMLParserOptions(XML_PARSE_NOXINCNODE) 91 | public static let COMPACT = Libxml2XMLParserOptions(XML_PARSE_COMPACT) 92 | public static let OLD10 = Libxml2XMLParserOptions(XML_PARSE_OLD10) 93 | public static let NOBASEFIX = Libxml2XMLParserOptions(XML_PARSE_NOBASEFIX) 94 | public static let HUGE = Libxml2XMLParserOptions(XML_PARSE_HUGE) 95 | public static let OLDSAX = Libxml2XMLParserOptions(XML_PARSE_OLDSAX) 96 | public static let IGNORE_ENC = Libxml2XMLParserOptions(XML_PARSE_IGNORE_ENC) 97 | public static let BIG_LINES = Libxml2XMLParserOptions(XML_PARSE_BIG_LINES) 98 | } 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luming Yin 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 | -------------------------------------------------------------------------------- /Modules/Kanna.h: -------------------------------------------------------------------------------- 1 | /**@file Kanna.h 2 | 3 | Kanna 4 | 5 | Copyright (c) 2015 Atsushi Kiwaki (@_tid_) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | #import 26 | 27 | //! Project version number for Kanna. 28 | FOUNDATION_EXPORT double KannaVersionNumber; 29 | 30 | //! Project version string for Kanna. 31 | FOUNDATION_EXPORT const unsigned char KannaVersionString[]; 32 | 33 | // In this header, you should import all the public headers of your framework using statements like #import 34 | -------------------------------------------------------------------------------- /Modules/libxml2-kanna.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | -------------------------------------------------------------------------------- /Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | module libxmlKanna [system] { 2 | link "xml2" 3 | umbrella header "libxml2-kanna.h" 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpeedReader 2 | 3 | Ever wondered if you can read faster? Try Speed Reader! 4 | 5 | Now updated for Dark Mode under macOS Mojave, simply paste in an article you would like to read, adjust the speed reading rate and choose your favorite font, then Speed Read it with the power of silencing vocalization! 6 | 7 | ![Speed Reader](speedreader_dark.gif?raw=true "Speed Reader") 8 | 9 | Article Library: 10 | - Organize, preview and open all articles you have speed read at a glance 11 | - Easily import any local article or webpages to speed read 12 | 13 | Focus View: 14 | - Speed reader automatically detects the language of your article to better split out phrases for speed reading 15 | - Speed reader's focus view spots a distraction-free UI with an optional dark theme, alongside with accessibility options to improve legibility 16 | - Speed reader's focus view supports customizing type face, font weight, font size and word-number-per-screen adjustments 17 | 18 | Preferences: 19 | - All preferences are located in a single pane, allowing you to tune the speed, font, focus appearance, content language, words per roll and quickly start your speed read session 20 | 21 | --- 22 | 23 | [Download from Github](https://github.com/LumingYin/SpeedReader/releases/download/1.6/SpeedReader.1.6.dmg) 24 | -------------------------------------------------------------------------------- /SRTableRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRTableRowView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRTableRowView: NSTableRowView { 12 | 13 | override func draw(_ dirtyRect: NSRect) { 14 | super.draw(dirtyRect) 15 | } 16 | 17 | override func drawSelection(in dirtyRect: NSRect) { 18 | if self.selectionHighlightStyle != .none { 19 | let selectionRect = NSInsetRect(self.bounds, 0, 0) 20 | NSColor(calibratedWhite: 1, alpha: 0).setStroke() 21 | if #available(OSX 10.14, *) { 22 | if (NSApp.isActive) { 23 | NSColor.selectedContentBackgroundColor.setFill() 24 | } else { 25 | NSColor.unemphasizedSelectedContentBackgroundColor.setFill() 26 | } 27 | } else { 28 | NSColor(calibratedRed: 0.9882, green: 0.8941, blue: 0.607, alpha: 1).setFill() 29 | } 30 | let selectionPath = NSBezierPath.init(roundedRect: selectionRect, xRadius: 0, yRadius: 0) 31 | selectionPath.fill() 32 | selectionPath.stroke() 33 | } 34 | } 35 | 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 32768ECE214785B5006004CB /* libxmlHTMLNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768EC621478505006004CB /* libxmlHTMLNode.swift */; }; 11 | 32768ECF214785B7006004CB /* CSS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768EC721478505006004CB /* CSS.swift */; }; 12 | 32768ED0214785B9006004CB /* libxmlHTMLDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768EC821478505006004CB /* libxmlHTMLDocument.swift */; }; 13 | 32768ED1214785BA006004CB /* libxmlParserOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768EC921478505006004CB /* libxmlParserOption.swift */; }; 14 | 32768ED2214785BC006004CB /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768ECA21478505006004CB /* Deprecated.swift */; }; 15 | 32768ED3214785BD006004CB /* Kanna.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32768ECC21478505006004CB /* Kanna.swift */; }; 16 | 32EAA8AF2276B4FA0041EB71 /* ReadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32EAA8AE2276B4FA0041EB71 /* ReadView.swift */; }; 17 | 3741C1B91F0A873F004E9BE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741C1B81F0A873F004E9BE0 /* AppDelegate.swift */; }; 18 | 3741C1BB1F0A873F004E9BE0 /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741C1BA1F0A873F004E9BE0 /* ArticleViewController.swift */; }; 19 | 3741C1C01F0A873F004E9BE0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3741C1BE1F0A873F004E9BE0 /* Main.storyboard */; }; 20 | 3741C1C81F0A89E0004E9BE0 /* ReadDetailWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741C1C71F0A89E0004E9BE0 /* ReadDetailWindow.swift */; }; 21 | 3741C1CA1F0A8AA4004E9BE0 /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741C1C91F0A8AA4004E9BE0 /* MainWindowController.swift */; }; 22 | 3741C1CC1F0A8B25004E9BE0 /* ReadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741C1CB1F0A8B25004E9BE0 /* ReadViewController.swift */; }; 23 | 569724201F1BCD1500D9E7C7 /* SRArticleSnippetCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5697241F1F1BCD1500D9E7C7 /* SRArticleSnippetCellView.swift */; }; 24 | 569724241F1BE4F900D9E7C7 /* SRTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569724231F1BE4F900D9E7C7 /* SRTableRowView.swift */; }; 25 | 569724261F1BEBC200D9E7C7 /* SRModalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569724251F1BEBC200D9E7C7 /* SRModalWindowController.swift */; }; 26 | 569724281F1BEC4500D9E7C7 /* SRImportWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569724271F1BEC4500D9E7C7 /* SRImportWebViewController.swift */; }; 27 | 5697271D1F15EA56007749AA /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 5697271C1F15EA56007749AA /* AppIcon.icns */; }; 28 | 569B21BD1F1B3C0700D5552C /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 569B21BC1F1B3C0700D5552C /* Images.xcassets */; }; 29 | 569B21BF1F1B3EF500D5552C /* SRSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21BE1F1B3EF500D5552C /* SRSplitViewController.swift */; }; 30 | 569B21C11F1B489C00D5552C /* SRPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21C01F1B489C00D5552C /* SRPreferencesViewController.swift */; }; 31 | 569B21C51F1B5B6400D5552C /* SRHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21C41F1B5B6400D5552C /* SRHistoryViewController.swift */; }; 32 | 569B21C71F1B65F300D5552C /* SRNewArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21C61F1B65F300D5552C /* SRNewArticleViewController.swift */; }; 33 | 569B21C91F1B67F300D5552C /* SRSpeedCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21C81F1B67F300D5552C /* SRSpeedCellView.swift */; }; 34 | 569B21CB1F1B680200D5552C /* SRFontCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21CA1F1B680200D5552C /* SRFontCellView.swift */; }; 35 | 569B21CD1F1B681100D5552C /* SRAppearanceCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21CC1F1B681100D5552C /* SRAppearanceCellView.swift */; }; 36 | 569B21CF1F1B682200D5552C /* SRLanguageCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21CE1F1B682200D5552C /* SRLanguageCellView.swift */; }; 37 | 569B21D11F1B682F00D5552C /* SRWordsCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21D01F1B682F00D5552C /* SRWordsCellView.swift */; }; 38 | 569B21D31F1B684300D5552C /* SRReadCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21D21F1B684300D5552C /* SRReadCellView.swift */; }; 39 | 569B21D51F1B6A3C00D5552C /* SRGeneralPrefCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569B21D41F1B6A3C00D5552C /* SRGeneralPrefCellView.swift */; }; 40 | 569B21DA1F1BC5D600D5552C /* SpeedReader.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 569B21D71F1BC4A600D5552C /* SpeedReader.xcdatamodeld */; }; 41 | /* End PBXBuildFile section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 3206308A21477A4500D27B73 /* SpeedReader-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SpeedReader-Bridging-Header.h"; sourceTree = ""; }; 45 | 32768EC621478505006004CB /* libxmlHTMLNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = libxmlHTMLNode.swift; sourceTree = ""; }; 46 | 32768EC721478505006004CB /* CSS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSS.swift; sourceTree = ""; }; 47 | 32768EC821478505006004CB /* libxmlHTMLDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = libxmlHTMLDocument.swift; sourceTree = ""; }; 48 | 32768EC921478505006004CB /* libxmlParserOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = libxmlParserOption.swift; sourceTree = ""; }; 49 | 32768ECA21478505006004CB /* Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecated.swift; sourceTree = ""; }; 50 | 32768ECC21478505006004CB /* Kanna.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Kanna.swift; sourceTree = ""; }; 51 | 32EAA8AE2276B4FA0041EB71 /* ReadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadView.swift; sourceTree = ""; }; 52 | 3741C1B51F0A873F004E9BE0 /* SpeedReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpeedReader.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 3741C1B81F0A873F004E9BE0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 3741C1BA1F0A873F004E9BE0 /* ArticleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = ""; }; 55 | 3741C1BF1F0A873F004E9BE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 3741C1C11F0A873F004E9BE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | 3741C1C71F0A89E0004E9BE0 /* ReadDetailWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadDetailWindow.swift; sourceTree = ""; }; 58 | 3741C1C91F0A8AA4004E9BE0 /* MainWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowController.swift; sourceTree = ""; }; 59 | 3741C1CB1F0A8B25004E9BE0 /* ReadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadViewController.swift; sourceTree = ""; }; 60 | 5697241F1F1BCD1500D9E7C7 /* SRArticleSnippetCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRArticleSnippetCellView.swift; sourceTree = ""; }; 61 | 569724231F1BE4F900D9E7C7 /* SRTableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SRTableRowView.swift; path = ../SRTableRowView.swift; sourceTree = ""; }; 62 | 569724251F1BEBC200D9E7C7 /* SRModalWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRModalWindowController.swift; sourceTree = ""; }; 63 | 569724271F1BEC4500D9E7C7 /* SRImportWebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRImportWebViewController.swift; sourceTree = ""; }; 64 | 5697271B1F15E655007749AA /* SpeedReader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpeedReader.entitlements; sourceTree = ""; }; 65 | 5697271C1F15EA56007749AA /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = ""; }; 66 | 569B21BC1F1B3C0700D5552C /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 67 | 569B21BE1F1B3EF500D5552C /* SRSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRSplitViewController.swift; sourceTree = ""; }; 68 | 569B21C01F1B489C00D5552C /* SRPreferencesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRPreferencesViewController.swift; sourceTree = ""; }; 69 | 569B21C41F1B5B6400D5552C /* SRHistoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRHistoryViewController.swift; sourceTree = ""; }; 70 | 569B21C61F1B65F300D5552C /* SRNewArticleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRNewArticleViewController.swift; sourceTree = ""; }; 71 | 569B21C81F1B67F300D5552C /* SRSpeedCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRSpeedCellView.swift; sourceTree = ""; }; 72 | 569B21CA1F1B680200D5552C /* SRFontCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRFontCellView.swift; sourceTree = ""; }; 73 | 569B21CC1F1B681100D5552C /* SRAppearanceCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRAppearanceCellView.swift; sourceTree = ""; }; 74 | 569B21CE1F1B682200D5552C /* SRLanguageCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRLanguageCellView.swift; sourceTree = ""; }; 75 | 569B21D01F1B682F00D5552C /* SRWordsCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRWordsCellView.swift; sourceTree = ""; }; 76 | 569B21D21F1B684300D5552C /* SRReadCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRReadCellView.swift; sourceTree = ""; }; 77 | 569B21D41F1B6A3C00D5552C /* SRGeneralPrefCellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SRGeneralPrefCellView.swift; sourceTree = ""; }; 78 | 569B21D81F1BC4A600D5552C /* SpeedReader.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SpeedReader.xcdatamodel; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 3741C1B21F0A873F004E9BE0 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXFrameworksBuildPhase section */ 90 | 91 | /* Begin PBXGroup section */ 92 | 1259B537378C67F201272C9D /* Frameworks */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | ); 96 | name = Frameworks; 97 | sourceTree = ""; 98 | }; 99 | 32768EC521478505006004CB /* Kanna */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 32768EC621478505006004CB /* libxmlHTMLNode.swift */, 103 | 32768EC721478505006004CB /* CSS.swift */, 104 | 32768EC821478505006004CB /* libxmlHTMLDocument.swift */, 105 | 32768EC921478505006004CB /* libxmlParserOption.swift */, 106 | 32768ECA21478505006004CB /* Deprecated.swift */, 107 | 32768ECC21478505006004CB /* Kanna.swift */, 108 | ); 109 | path = Kanna; 110 | sourceTree = ""; 111 | }; 112 | 3741C1AC1F0A873F004E9BE0 = { 113 | isa = PBXGroup; 114 | children = ( 115 | 32768EC521478505006004CB /* Kanna */, 116 | 3741C1B71F0A873F004E9BE0 /* SpeedReader */, 117 | 3741C1B61F0A873F004E9BE0 /* Products */, 118 | 1259B537378C67F201272C9D /* Frameworks */, 119 | ); 120 | sourceTree = ""; 121 | }; 122 | 3741C1B61F0A873F004E9BE0 /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 3741C1B51F0A873F004E9BE0 /* SpeedReader.app */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 3741C1B71F0A873F004E9BE0 /* SpeedReader */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 569724231F1BE4F900D9E7C7 /* SRTableRowView.swift */, 134 | 3741C1BE1F0A873F004E9BE0 /* Main.storyboard */, 135 | 32EAA8AE2276B4FA0041EB71 /* ReadView.swift */, 136 | 569724271F1BEC4500D9E7C7 /* SRImportWebViewController.swift */, 137 | 569724251F1BEBC200D9E7C7 /* SRModalWindowController.swift */, 138 | 5697241F1F1BCD1500D9E7C7 /* SRArticleSnippetCellView.swift */, 139 | 3741C1C91F0A8AA4004E9BE0 /* MainWindowController.swift */, 140 | 569B21BE1F1B3EF500D5552C /* SRSplitViewController.swift */, 141 | 569B21C41F1B5B6400D5552C /* SRHistoryViewController.swift */, 142 | 3741C1CB1F0A8B25004E9BE0 /* ReadViewController.swift */, 143 | 569B21C01F1B489C00D5552C /* SRPreferencesViewController.swift */, 144 | 569B21D61F1B6A4300D5552C /* TableCellViews */, 145 | 569B21C61F1B65F300D5552C /* SRNewArticleViewController.swift */, 146 | 3741C1BA1F0A873F004E9BE0 /* ArticleViewController.swift */, 147 | 3741C1C71F0A89E0004E9BE0 /* ReadDetailWindow.swift */, 148 | 3741C1C11F0A873F004E9BE0 /* Info.plist */, 149 | 569B21BC1F1B3C0700D5552C /* Images.xcassets */, 150 | 5697271C1F15EA56007749AA /* AppIcon.icns */, 151 | 5697271B1F15E655007749AA /* SpeedReader.entitlements */, 152 | 3741C1B81F0A873F004E9BE0 /* AppDelegate.swift */, 153 | 569B21D71F1BC4A600D5552C /* SpeedReader.xcdatamodeld */, 154 | 3206308A21477A4500D27B73 /* SpeedReader-Bridging-Header.h */, 155 | ); 156 | path = SpeedReader; 157 | sourceTree = ""; 158 | }; 159 | 569B21D61F1B6A4300D5552C /* TableCellViews */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 569B21D41F1B6A3C00D5552C /* SRGeneralPrefCellView.swift */, 163 | 569B21C81F1B67F300D5552C /* SRSpeedCellView.swift */, 164 | 569B21CA1F1B680200D5552C /* SRFontCellView.swift */, 165 | 569B21CC1F1B681100D5552C /* SRAppearanceCellView.swift */, 166 | 569B21CE1F1B682200D5552C /* SRLanguageCellView.swift */, 167 | 569B21D01F1B682F00D5552C /* SRWordsCellView.swift */, 168 | 569B21D21F1B684300D5552C /* SRReadCellView.swift */, 169 | ); 170 | name = TableCellViews; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXGroup section */ 174 | 175 | /* Begin PBXNativeTarget section */ 176 | 3741C1B41F0A873F004E9BE0 /* SpeedReader */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = 3741C1C41F0A873F004E9BE0 /* Build configuration list for PBXNativeTarget "SpeedReader" */; 179 | buildPhases = ( 180 | 3741C1B11F0A873F004E9BE0 /* Sources */, 181 | 3741C1B21F0A873F004E9BE0 /* Frameworks */, 182 | 3741C1B31F0A873F004E9BE0 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = SpeedReader; 189 | productName = SpeedReader; 190 | productReference = 3741C1B51F0A873F004E9BE0 /* SpeedReader.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | /* End PBXNativeTarget section */ 194 | 195 | /* Begin PBXProject section */ 196 | 3741C1AD1F0A873F004E9BE0 /* Project object */ = { 197 | isa = PBXProject; 198 | attributes = { 199 | LastSwiftUpdateCheck = 0830; 200 | LastUpgradeCheck = 1000; 201 | ORGANIZATIONNAME = "Kay Yin"; 202 | TargetAttributes = { 203 | 3741C1B41F0A873F004E9BE0 = { 204 | CreatedOnToolsVersion = 8.3.3; 205 | DevelopmentTeam = 3Q7XKEDZ36; 206 | LastSwiftMigration = 1000; 207 | ProvisioningStyle = Automatic; 208 | SystemCapabilities = { 209 | com.apple.HardenedRuntime = { 210 | enabled = 1; 211 | }; 212 | com.apple.Sandbox = { 213 | enabled = 1; 214 | }; 215 | }; 216 | }; 217 | }; 218 | }; 219 | buildConfigurationList = 3741C1B01F0A873F004E9BE0 /* Build configuration list for PBXProject "SpeedReader" */; 220 | compatibilityVersion = "Xcode 3.2"; 221 | developmentRegion = English; 222 | hasScannedForEncodings = 0; 223 | knownRegions = ( 224 | English, 225 | en, 226 | Base, 227 | ); 228 | mainGroup = 3741C1AC1F0A873F004E9BE0; 229 | productRefGroup = 3741C1B61F0A873F004E9BE0 /* Products */; 230 | projectDirPath = ""; 231 | projectRoot = ""; 232 | targets = ( 233 | 3741C1B41F0A873F004E9BE0 /* SpeedReader */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | 3741C1B31F0A873F004E9BE0 /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | 569B21BD1F1B3C0700D5552C /* Images.xcassets in Resources */, 244 | 3741C1C01F0A873F004E9BE0 /* Main.storyboard in Resources */, 245 | 5697271D1F15EA56007749AA /* AppIcon.icns in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXResourcesBuildPhase section */ 250 | 251 | /* Begin PBXSourcesBuildPhase section */ 252 | 3741C1B11F0A873F004E9BE0 /* Sources */ = { 253 | isa = PBXSourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | 3741C1BB1F0A873F004E9BE0 /* ArticleViewController.swift in Sources */, 257 | 569B21DA1F1BC5D600D5552C /* SpeedReader.xcdatamodeld in Sources */, 258 | 569B21D51F1B6A3C00D5552C /* SRGeneralPrefCellView.swift in Sources */, 259 | 569B21C71F1B65F300D5552C /* SRNewArticleViewController.swift in Sources */, 260 | 32768ED2214785BC006004CB /* Deprecated.swift in Sources */, 261 | 569724201F1BCD1500D9E7C7 /* SRArticleSnippetCellView.swift in Sources */, 262 | 3741C1B91F0A873F004E9BE0 /* AppDelegate.swift in Sources */, 263 | 569724261F1BEBC200D9E7C7 /* SRModalWindowController.swift in Sources */, 264 | 569B21CF1F1B682200D5552C /* SRLanguageCellView.swift in Sources */, 265 | 569B21CB1F1B680200D5552C /* SRFontCellView.swift in Sources */, 266 | 569B21BF1F1B3EF500D5552C /* SRSplitViewController.swift in Sources */, 267 | 32768ED1214785BA006004CB /* libxmlParserOption.swift in Sources */, 268 | 3741C1CA1F0A8AA4004E9BE0 /* MainWindowController.swift in Sources */, 269 | 569B21D11F1B682F00D5552C /* SRWordsCellView.swift in Sources */, 270 | 569B21D31F1B684300D5552C /* SRReadCellView.swift in Sources */, 271 | 569B21C91F1B67F300D5552C /* SRSpeedCellView.swift in Sources */, 272 | 32768ECF214785B7006004CB /* CSS.swift in Sources */, 273 | 32768ED3214785BD006004CB /* Kanna.swift in Sources */, 274 | 32768ED0214785B9006004CB /* libxmlHTMLDocument.swift in Sources */, 275 | 569724241F1BE4F900D9E7C7 /* SRTableRowView.swift in Sources */, 276 | 569B21C11F1B489C00D5552C /* SRPreferencesViewController.swift in Sources */, 277 | 3741C1C81F0A89E0004E9BE0 /* ReadDetailWindow.swift in Sources */, 278 | 569B21CD1F1B681100D5552C /* SRAppearanceCellView.swift in Sources */, 279 | 569B21C51F1B5B6400D5552C /* SRHistoryViewController.swift in Sources */, 280 | 32EAA8AF2276B4FA0041EB71 /* ReadView.swift in Sources */, 281 | 3741C1CC1F0A8B25004E9BE0 /* ReadViewController.swift in Sources */, 282 | 32768ECE214785B5006004CB /* libxmlHTMLNode.swift in Sources */, 283 | 569724281F1BEC4500D9E7C7 /* SRImportWebViewController.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | /* End PBXSourcesBuildPhase section */ 288 | 289 | /* Begin PBXVariantGroup section */ 290 | 3741C1BE1F0A873F004E9BE0 /* Main.storyboard */ = { 291 | isa = PBXVariantGroup; 292 | children = ( 293 | 3741C1BF1F0A873F004E9BE0 /* Base */, 294 | ); 295 | name = Main.storyboard; 296 | sourceTree = ""; 297 | }; 298 | /* End PBXVariantGroup section */ 299 | 300 | /* Begin XCBuildConfiguration section */ 301 | 3741C1C21F0A873F004E9BE0 /* Debug */ = { 302 | isa = XCBuildConfiguration; 303 | buildSettings = { 304 | ALWAYS_SEARCH_USER_PATHS = NO; 305 | CLANG_ANALYZER_NONNULL = YES; 306 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 318 | CLANG_WARN_EMPTY_BODY = YES; 319 | CLANG_WARN_ENUM_CONVERSION = YES; 320 | CLANG_WARN_INFINITE_RECURSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 326 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 327 | CLANG_WARN_STRICT_PROTOTYPES = YES; 328 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 329 | CLANG_WARN_UNREACHABLE_CODE = YES; 330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 331 | CODE_SIGN_IDENTITY = "-"; 332 | COPY_PHASE_STRIP = NO; 333 | DEBUG_INFORMATION_FORMAT = dwarf; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | GCC_C_LANGUAGE_STANDARD = gnu99; 337 | GCC_DYNAMIC_NO_PIC = NO; 338 | GCC_NO_COMMON_BLOCKS = YES; 339 | GCC_OPTIMIZATION_LEVEL = 0; 340 | GCC_PREPROCESSOR_DEFINITIONS = ( 341 | "DEBUG=1", 342 | "$(inherited)", 343 | ); 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | MACOSX_DEPLOYMENT_TARGET = 10.12; 351 | MTL_ENABLE_DEBUG_INFO = YES; 352 | ONLY_ACTIVE_ARCH = YES; 353 | SDKROOT = macosx; 354 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 355 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 356 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 357 | }; 358 | name = Debug; 359 | }; 360 | 3741C1C31F0A873F004E9BE0 /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_NONNULL = YES; 365 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 366 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 367 | CLANG_CXX_LIBRARY = "libc++"; 368 | CLANG_ENABLE_MODULES = YES; 369 | CLANG_ENABLE_OBJC_ARC = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 386 | CLANG_WARN_STRICT_PROTOTYPES = YES; 387 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 388 | CLANG_WARN_UNREACHABLE_CODE = YES; 389 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 390 | CODE_SIGN_IDENTITY = "-"; 391 | COPY_PHASE_STRIP = NO; 392 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 393 | ENABLE_NS_ASSERTIONS = NO; 394 | ENABLE_STRICT_OBJC_MSGSEND = YES; 395 | GCC_C_LANGUAGE_STANDARD = gnu99; 396 | GCC_NO_COMMON_BLOCKS = YES; 397 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 398 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 399 | GCC_WARN_UNDECLARED_SELECTOR = YES; 400 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 401 | GCC_WARN_UNUSED_FUNCTION = YES; 402 | GCC_WARN_UNUSED_VARIABLE = YES; 403 | MACOSX_DEPLOYMENT_TARGET = 10.12; 404 | MTL_ENABLE_DEBUG_INFO = NO; 405 | SDKROOT = macosx; 406 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 407 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 408 | }; 409 | name = Release; 410 | }; 411 | 3741C1C51F0A873F004E9BE0 /* Debug */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | CLANG_ENABLE_MODULES = YES; 415 | CODE_SIGN_ENTITLEMENTS = SpeedReader/SpeedReader.entitlements; 416 | CODE_SIGN_IDENTITY = "Mac Developer"; 417 | COMBINE_HIDPI_IMAGES = YES; 418 | DEVELOPMENT_TEAM = 3Q7XKEDZ36; 419 | ENABLE_HARDENED_RUNTIME = YES; 420 | HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2"; 421 | INFOPLIST_FILE = SpeedReader/Info.plist; 422 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 423 | MACOSX_DEPLOYMENT_TARGET = 10.12; 424 | OTHER_LDFLAGS = ""; 425 | PRODUCT_BUNDLE_IDENTIFIER = com.luming.SpeedReader; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | PROVISIONING_PROFILE_SPECIFIER = ""; 428 | SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Modules"; 429 | SWIFT_OBJC_BRIDGING_HEADER = "SpeedReader/SpeedReader-Bridging-Header.h"; 430 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 431 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 432 | SWIFT_VERSION = 4.2; 433 | }; 434 | name = Debug; 435 | }; 436 | 3741C1C61F0A873F004E9BE0 /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | CLANG_ENABLE_MODULES = YES; 440 | CODE_SIGN_ENTITLEMENTS = SpeedReader/SpeedReader.entitlements; 441 | CODE_SIGN_IDENTITY = "Mac Developer"; 442 | COMBINE_HIDPI_IMAGES = YES; 443 | DEVELOPMENT_TEAM = 3Q7XKEDZ36; 444 | ENABLE_HARDENED_RUNTIME = YES; 445 | HEADER_SEARCH_PATHS = "$(SDKROOT)/usr/include/libxml2"; 446 | INFOPLIST_FILE = SpeedReader/Info.plist; 447 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 448 | MACOSX_DEPLOYMENT_TARGET = 10.12; 449 | OTHER_LDFLAGS = ""; 450 | PRODUCT_BUNDLE_IDENTIFIER = com.luming.SpeedReader; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | PROVISIONING_PROFILE_SPECIFIER = ""; 453 | SWIFT_INCLUDE_PATHS = "$(SRCROOT)/Modules"; 454 | SWIFT_OBJC_BRIDGING_HEADER = "SpeedReader/SpeedReader-Bridging-Header.h"; 455 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 456 | SWIFT_VERSION = 4.2; 457 | }; 458 | name = Release; 459 | }; 460 | /* End XCBuildConfiguration section */ 461 | 462 | /* Begin XCConfigurationList section */ 463 | 3741C1B01F0A873F004E9BE0 /* Build configuration list for PBXProject "SpeedReader" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 3741C1C21F0A873F004E9BE0 /* Debug */, 467 | 3741C1C31F0A873F004E9BE0 /* Release */, 468 | ); 469 | defaultConfigurationIsVisible = 0; 470 | defaultConfigurationName = Release; 471 | }; 472 | 3741C1C41F0A873F004E9BE0 /* Build configuration list for PBXNativeTarget "SpeedReader" */ = { 473 | isa = XCConfigurationList; 474 | buildConfigurations = ( 475 | 3741C1C51F0A873F004E9BE0 /* Debug */, 476 | 3741C1C61F0A873F004E9BE0 /* Release */, 477 | ); 478 | defaultConfigurationIsVisible = 0; 479 | defaultConfigurationName = Release; 480 | }; 481 | /* End XCConfigurationList section */ 482 | 483 | /* Begin XCVersionGroup section */ 484 | 569B21D71F1BC4A600D5552C /* SpeedReader.xcdatamodeld */ = { 485 | isa = XCVersionGroup; 486 | children = ( 487 | 569B21D81F1BC4A600D5552C /* SpeedReader.xcdatamodel */, 488 | ); 489 | currentVersion = 569B21D81F1BC4A600D5552C /* SpeedReader.xcdatamodel */; 490 | path = SpeedReader.xcdatamodeld; 491 | sourceTree = ""; 492 | versionGroupType = wrapper.xcdatamodel; 493 | }; 494 | /* End XCVersionGroup section */ 495 | }; 496 | rootObject = 3741C1AD1F0A873F004E9BE0 /* Project object */; 497 | } 498 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/blue.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/blue.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/bright.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/bright.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/kay.yin.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader.xcodeproj/project.xcworkspace/xcuserdata/kay.yin.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/blue.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/blue.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SpeedReader.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | SpeedReader.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/bright.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 35 | 36 | 50 | 51 | 65 | 66 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/bright.xcuserdatad/xcschemes/SpeedReader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/bright.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SpeedReader.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3741C1B41F0A873F004E9BE0 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/kay.yin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/kay.yin.xcuserdatad/xcschemes/SpeedReader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SpeedReader.xcodeproj/xcuserdata/kay.yin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SpeedReader.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3741C1B41F0A873F004E9BE0 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SpeedReader/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SpeedReader 4 | // 5 | // Created by Kay Yin on 7/3/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | func applicationDidFinishLaunching(_ aNotification: Notification) { 16 | 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | // MARK: - Core Data stack 25 | 26 | lazy var persistentContainer: NSPersistentContainer = { 27 | /* 28 | The persistent container for the application. This implementation 29 | creates and returns a container, having loaded the store for the 30 | application to it. This property is optional since there are legitimate 31 | error conditions that could cause the creation of the store to fail. 32 | */ 33 | let container = NSPersistentContainer(name: "SpeedReader") 34 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 35 | if let error = error { 36 | // Replace this implementation with code to handle the error appropriately. 37 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 38 | 39 | /* 40 | Typical reasons for an error here include: 41 | * The parent directory does not exist, cannot be created, or disallows writing. 42 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 43 | * The device is out of space. 44 | * The store could not be migrated to the current model version. 45 | Check the error message to determine what the actual problem was. 46 | */ 47 | fatalError("Unresolved error \(error)") 48 | } 49 | }) 50 | return container 51 | }() 52 | 53 | // MARK: - Core Data Saving and Undo support 54 | 55 | @IBAction func saveAction(_ sender: AnyObject?) { 56 | // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. 57 | let context = persistentContainer.viewContext 58 | 59 | // if !context.commitEditing() { 60 | // NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving") 61 | // } 62 | if context.hasChanges { 63 | do { 64 | try context.save() 65 | } catch { 66 | // Customize this code block to include application-specific recovery steps. 67 | let nserror = error as NSError 68 | NSApplication.shared.presentError(nserror) 69 | } 70 | } 71 | } 72 | 73 | func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? { 74 | // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. 75 | return persistentContainer.viewContext.undoManager 76 | } 77 | 78 | func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { 79 | // Save changes in the application's managed object context before the application terminates. 80 | let context = persistentContainer.viewContext 81 | 82 | // if !context.commitEditing() { 83 | // NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate") 84 | // return .terminateCancel 85 | // } 86 | 87 | if !context.hasChanges { 88 | return .terminateNow 89 | } 90 | 91 | do { 92 | try context.save() 93 | } catch { 94 | let nserror = error as NSError 95 | 96 | // Customize this code block to include application-specific recovery steps. 97 | let result = sender.presentError(nserror) 98 | if (result) { 99 | return .terminateCancel 100 | } 101 | 102 | let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message") 103 | let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info"); 104 | let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title") 105 | let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title") 106 | let alert = NSAlert() 107 | alert.messageText = question 108 | alert.informativeText = info 109 | alert.addButton(withTitle: quitButton) 110 | alert.addButton(withTitle: cancelButton) 111 | 112 | let answer = alert.runModal() 113 | if answer == NSApplication.ModalResponse.alertSecondButtonReturn { 114 | return .terminateCancel 115 | } 116 | } 117 | // If we got here, it is time to quit. 118 | return .terminateNow 119 | } 120 | 121 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 122 | return true 123 | } 124 | 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /SpeedReader/AppIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/AppIcon.icns -------------------------------------------------------------------------------- /SpeedReader/ArticleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Kay Yin on 7/3/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ArticleViewController: NSViewController, NSTextViewDelegate { 12 | var article: Article? 13 | var observer: NSKeyValueObservation? 14 | 15 | @IBOutlet var contentTextView: NSTextView! 16 | @IBOutlet weak var guidanceView: NSView! 17 | @IBOutlet weak var outerTextScrollView: NSScrollView! 18 | 19 | 20 | var detailWindow: ReadDetailWindow? 21 | var allFontNames: [String] = [] 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | self.view.wantsLayer = true 26 | 27 | allFontNames = NSFontManager.shared.availableFontFamilies 28 | 29 | observer = view.observe(\.effectiveAppearance) { [weak self] _, _ in 30 | self?.updateAppearanceRelatedChanges() 31 | } 32 | 33 | contentTextView.textContainerInset = NSSize(width: 20.0, height: 20.0) 34 | contentTextView.delegate = self 35 | } 36 | 37 | private func updateAppearanceRelatedChanges() { 38 | if #available(OSX 10.14, *) { 39 | switch view.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) { 40 | case .aqua?: contentTextView.drawsBackground = true 41 | case .darkAqua?: contentTextView.drawsBackground = false 42 | default: contentTextView.drawsBackground = true 43 | } 44 | } 45 | } 46 | 47 | override var representedObject: Any? { 48 | didSet { 49 | } 50 | } 51 | 52 | func updateToReflectArticle() { 53 | if article != nil { 54 | self.contentTextView.string = article?.content ?? "" 55 | self.contentTextView.scroll(CGPoint(x: 0, y: -50)) 56 | } 57 | } 58 | 59 | func textDidChange(_ notification: Notification) { 60 | article?.content = contentTextView.string 61 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 62 | } 63 | 64 | @IBAction func createArticle(_ sender: NSButton) { 65 | if let vc = storyboard?.instantiateController(withIdentifier: "BlankDocument") as? NSViewController { 66 | self.present(vc, asPopoverRelativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.maxY, behavior: .transient) 67 | } 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/NSHistoryItem.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "NSHistoryItem.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "NSHistoryItem-1.pdf", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "NSHistoryItem-2.pdf", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem-1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem-1.pdf -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem-2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem-2.pdf -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/NSHistoryItem.imageset/NSHistoryItem.pdf -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/createNew.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "file-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/createNew.imageset/file-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/createNew.imageset/file-2.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/openLocal.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "file.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/openLocal.imageset/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/openLocal.imageset/file.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/openWeb.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "compass.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/openWeb.imageset/compass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/openWeb.imageset/compass.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/paper.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "paper texture_55A54008AD1BA589AA210D2629C1DF41_0.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "paper texture@2x_55A54008AD1BA589AA210D2629C1DF41_0.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/paper.imageset/paper texture@2x_55A54008AD1BA589AA210D2629C1DF41_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/paper.imageset/paper texture@2x_55A54008AD1BA589AA210D2629C1DF41_0.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/paper.imageset/paper texture_55A54008AD1BA589AA210D2629C1DF41_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/paper.imageset/paper texture_55A54008AD1BA589AA210D2629C1DF41_0.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/pauseButtonArtwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pauseButtonArtwork.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/pauseButtonArtwork.imageset/pauseButtonArtwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/pauseButtonArtwork.imageset/pauseButtonArtwork.png -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/playButtonArtwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "playButtonArtwork.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SpeedReader/Images.xcassets/playButtonArtwork.imageset/playButtonArtwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/SpeedReader/Images.xcassets/playButtonArtwork.imageset/playButtonArtwork.png -------------------------------------------------------------------------------- /SpeedReader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | AppIcon.icns 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.8 21 | CFBundleVersion 22 | 7 23 | ITSAppUsesNonExemptEncryption 24 | 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | NSHumanReadableCopyright 35 | Icon made by Freepik from www.flaticon.com
Copyright © 2017 Kay Yin. All rights reserved. 36 | NSMainStoryboardFile 37 | Main 38 | NSPrincipalClass 39 | NSApplication 40 | 41 | 42 | -------------------------------------------------------------------------------- /SpeedReader/MainWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindowController.swift 3 | // SpeedReader 4 | // 5 | // Created by Kay Yin on 7/3/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class MainWindowController: NSWindowController, NSSharingServicePickerDelegate { 12 | 13 | @IBOutlet weak var shareButton: NSButton! 14 | var detailWindow: ReadDetailWindow? 15 | var prefVC: SRPreferencesViewController? 16 | 17 | @IBOutlet weak var readButton: NSButton! 18 | override func windowDidLoad() { 19 | super.windowDidLoad() 20 | 21 | self.window?.titleVisibility = NSWindow.TitleVisibility.hidden; 22 | self.window?.styleMask.insert(.fullSizeContentView) 23 | shareButton.sendAction(on: .leftMouseDown) 24 | } 25 | 26 | @IBAction func historyClicked(_ sender: Any) { 27 | if let spvc = self.contentViewController as? SRSplitViewController { 28 | spvc.splitViewItems[0].isCollapsed = !spvc.splitViewItems[0].isCollapsed 29 | } 30 | } 31 | 32 | @IBAction func addClicked(_ sender: NSView) { 33 | if let vc = storyboard?.instantiateController(withIdentifier: "BlankDocument") as? NSViewController { 34 | self.contentViewController?.present(vc, asPopoverRelativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY, behavior: .transient) 35 | } 36 | } 37 | 38 | @IBAction func readClicked(_ sender: Any) { 39 | openReaderWindow() 40 | } 41 | 42 | func openReaderWindow() { 43 | detailWindow = nil 44 | if let contentVC = self.contentViewController as? SRSplitViewController { 45 | if let textVC = contentVC.splitViewItems[1].viewController as? ArticleViewController { 46 | detailWindow = storyboard?.instantiateController(withIdentifier: "ReadDetailWindow") as? ReadDetailWindow 47 | if let readVC = detailWindow?.contentViewController as? ReadViewController { 48 | if let article = textVC.article { 49 | readVC.article = article 50 | readVC.articlePreference = article.preference 51 | if let speed = article.preference?.speed { 52 | readVC.readingSliderValue = speed 53 | } else { 54 | readVC.readingSliderValue = 0.7 55 | } 56 | readVC.textToRead = textVC.contentTextView.string 57 | if let font = article.preference?.font as? NSFont { 58 | readVC.font = font 59 | } 60 | if let isDark = article.preference?.isDark { 61 | readVC.enableDark = isDark 62 | } 63 | } 64 | } 65 | detailWindow?.showWindow(self) 66 | } 67 | } 68 | } 69 | 70 | 71 | @IBAction func shareClicked(_ sender: NSView) { 72 | var shareArray: [String] = [] 73 | if let contentVC = self.contentViewController as? SRSplitViewController { 74 | if let textVC = contentVC.splitViewItems[1].viewController as? ArticleViewController { 75 | let article = textVC.contentTextView.string 76 | shareArray.append(article) 77 | } 78 | } 79 | let sharePicker = NSSharingServicePicker.init(items: shareArray) 80 | sharePicker.delegate = self 81 | sharePicker.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY) 82 | 83 | } 84 | 85 | @IBAction func preferenceSwapped(_ sender: NSSegmentedControl) { 86 | if let spvc = self.contentViewController as? SRSplitViewController { 87 | spvc.splitViewItems[2].isCollapsed = !spvc.splitViewItems[2].isCollapsed 88 | sender.setSelected(!spvc.splitViewItems[2].isCollapsed, forSegment: 0) 89 | } 90 | } 91 | 92 | @IBAction func historySwapped(_ sender: NSSegmentedControl) { 93 | if let spvc = self.contentViewController as? SRSplitViewController { 94 | spvc.splitViewItems[0].isCollapsed = !spvc.splitViewItems[0].isCollapsed 95 | sender.setSelected(!spvc.splitViewItems[0].isCollapsed, forSegment: 0) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SpeedReader/ReadDetailWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadDetailWindow.swift 3 | // SpeedReader 4 | // 5 | // Created by Kay Yin on 7/3/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ReadDetailWindow: NSWindowController { 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | 16 | self.window?.titleVisibility = NSWindow.TitleVisibility.hidden; 17 | self.window?.titlebarAppearsTransparent = true; 18 | self.window?.styleMask.insert(.fullSizeContentView) 19 | self.window?.isMovableByWindowBackground = true 20 | 21 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /SpeedReader/ReadView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadView.swift 3 | // SpeedReader 4 | // 5 | // Created by Blue on 4/29/19. 6 | // Copyright © 2019 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ReadView: NSView { 12 | var trackingArea: NSTrackingArea? 13 | weak var delegate: ReadViewController? 14 | 15 | override func draw(_ dirtyRect: NSRect) { 16 | super.draw(dirtyRect) 17 | } 18 | 19 | override func updateTrackingAreas() { 20 | super.updateTrackingAreas() 21 | if let area = trackingArea { 22 | self.removeTrackingArea(area) 23 | } 24 | trackingArea = NSTrackingArea.init(rect: self.bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil) 25 | if let area = trackingArea { 26 | self.addTrackingArea(area) 27 | } 28 | } 29 | 30 | override func mouseEntered(with event: NSEvent) { 31 | delegate?.enteredHandler(with: event) 32 | } 33 | 34 | override func mouseExited(with event: NSEvent) { 35 | delegate?.exitedHandler(with: event) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /SpeedReader/ReadViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Kay Yin on 7/3/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ReadViewController: NSViewController { 12 | var textToRead: String? 13 | @IBOutlet weak var displayLabel: NSTextField! 14 | var readingSliderValue: Float = 1.0 15 | var readingSpeed: UInt32 = 60 16 | let ms = 1000 17 | var font: NSFont? 18 | var enableDark: Bool = false 19 | var arrayText: [String] = [] 20 | var currentIndexInArray: Int = 0 21 | var article: Article? 22 | var articlePreference: Preference? 23 | var localWordsPerRoll: Int = 1 24 | @IBOutlet weak var playPauseButton: NSButton! 25 | 26 | @IBOutlet weak var visualEffectView: NSVisualEffectView! 27 | 28 | override func awakeFromNib() { 29 | if let readView = self.view as? ReadView { 30 | readView.delegate = self 31 | } 32 | } 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | self.view.wantsLayer = true 37 | self.displayLabel.stringValue = "" 38 | if let _ = font { 39 | self.displayLabel.font = font 40 | } 41 | playPauseButton.image = NSImage(named: "pauseButtonArtwork") 42 | } 43 | 44 | override func viewWillAppear() { 45 | guard var rect = self.view.window?.frame else { 46 | return 47 | } 48 | if let wordsPerRoll = articlePreference?.wordsPerRoll { 49 | localWordsPerRoll = Int(wordsPerRoll) 50 | } 51 | if (localWordsPerRoll == 1) { 52 | rect.size.width = 480 53 | } else if (localWordsPerRoll == 2) { 54 | rect.size.width = 580 55 | } else if (localWordsPerRoll == 3) { 56 | rect.size.width = 680 57 | } else if (localWordsPerRoll == 4) { 58 | rect.size.width = 780 59 | } else if (localWordsPerRoll == 5) { 60 | rect.size.width = 880 61 | } else { 62 | rect.size.width = 480 63 | } 64 | self.view.window?.setFrame(rect, display: true) 65 | if let hideBlur = articlePreference?.reduceTransparency { 66 | if hideBlur == true { 67 | visualEffectView.blendingMode = .withinWindow 68 | } 69 | } 70 | if let darkenBG = articlePreference?.increaseContrast { 71 | if darkenBG == true { 72 | visualEffectView.isHidden = true 73 | } else { 74 | visualEffectView.isHidden = false 75 | } 76 | } 77 | 78 | if enableDark { 79 | visualEffectView.material = .dark 80 | self.displayLabel.textColor = NSColor.white 81 | if let darkenBG = articlePreference?.increaseContrast { 82 | if darkenBG == true { 83 | self.view.layer?.backgroundColor = NSColor.black.cgColor 84 | } 85 | } 86 | } else { 87 | visualEffectView.material = .light 88 | self.displayLabel.textColor = NSColor.black 89 | if let darkenBG = articlePreference?.increaseContrast { 90 | if darkenBG == true { 91 | self.view.layer?.backgroundColor = NSColor.white.cgColor 92 | } 93 | } 94 | } 95 | } 96 | 97 | override func viewDidAppear() { 98 | if let _ = font { 99 | self.displayLabel.font = font 100 | } 101 | startReading() 102 | } 103 | 104 | func calculateReadingSpeed() { 105 | if (readingSliderValue <= 0) { 106 | readingSliderValue = 0.01 107 | } 108 | readingSpeed = UInt32(10.0/readingSliderValue) * UInt32(ms) 109 | 110 | } 111 | 112 | var timer: Timer? 113 | var isReading = false 114 | 115 | @IBAction func playPauseClicked(_ sender: Any) { 116 | if isReading { 117 | playPauseButton.image = NSImage(named: "playButtonArtwork") 118 | timer?.invalidate() 119 | isReading = false 120 | } else { 121 | isReading = true 122 | playPauseButton.image = NSImage(named: "pauseButtonArtwork") 123 | runTimer() 124 | } 125 | } 126 | 127 | func startReading() { 128 | calculateReadingSpeed() 129 | if let text = textToRead { 130 | arrayText = text.stringTokens(splitMarks: [",","."," ","!",":","/","-","\n"]) 131 | arrayText = arrayText.filter { 132 | $0 != "" 133 | } 134 | currentIndexInArray = 0 135 | runTimer() 136 | } 137 | } 138 | 139 | func timerInsideHandler() { 140 | if (self.currentIndexInArray < self.arrayText.count) { 141 | self.isReading = true 142 | var pendingString = "" 143 | for i in 0..) -> [String] { 188 | 189 | var string = "" 190 | var desiredOutput = [String]() 191 | for ch in self { 192 | if splitMarks.contains(String(ch)) { 193 | if !string.isEmpty { 194 | if ch != " " { 195 | desiredOutput.append("\(string)\(ch)") 196 | } else { 197 | desiredOutput.append("\(string)") 198 | } 199 | } 200 | string = "" 201 | } 202 | else { 203 | string += String(ch) 204 | } 205 | } 206 | if !string.isEmpty { 207 | desiredOutput.append(string) 208 | } 209 | return desiredOutput 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /SpeedReader/SRAppearanceCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRAppearanceCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRAppearanceCellView: SRGeneralPrefCellView { 12 | @IBOutlet weak var disclosureTriangle: NSButton! 13 | @IBOutlet weak var topLabel: NSButton! 14 | @IBOutlet weak var increaseContrastBtn: NSButton! 15 | @IBOutlet weak var reduceTransparencyBtn: NSButton! 16 | 17 | @IBOutlet weak var lightDarkToggle: NSSegmentedControl! 18 | override func draw(_ dirtyRect: NSRect) { 19 | super.draw(dirtyRect) 20 | 21 | // Drawing code here. 22 | } 23 | 24 | override func configure() { 25 | if delegate?.article == nil { 26 | increaseContrastBtn.isEnabled = false 27 | reduceTransparencyBtn.isEnabled = false 28 | lightDarkToggle.isEnabled = false 29 | } else { 30 | increaseContrastBtn.isEnabled = true 31 | reduceTransparencyBtn.isEnabled = true 32 | lightDarkToggle.isEnabled = true 33 | } 34 | if let isDark = self.delegate?.article?.preference?.isDark { 35 | if isDark { 36 | lightDarkToggle.selectSegment(withTag: 1) 37 | } else { 38 | lightDarkToggle.selectSegment(withTag: 0) 39 | } 40 | } 41 | 42 | if let increaseContrast = self.delegate?.article?.preference?.increaseContrast { 43 | if increaseContrast { 44 | increaseContrastBtn.state = .on 45 | } else { 46 | increaseContrastBtn.state = .off 47 | } 48 | } 49 | 50 | if let reduceTransparency = self.delegate?.article?.preference?.reduceTransparency { 51 | if reduceTransparency { 52 | reduceTransparencyBtn.state = .on 53 | } else { 54 | reduceTransparencyBtn.state = .off 55 | } 56 | } 57 | 58 | } 59 | 60 | @IBAction func lightDarkChanged(_ sender: NSSegmentedControl) { 61 | if sender.selectedSegment == 0 { 62 | self.delegate?.article?.preference?.isDark = false 63 | } else { 64 | self.delegate?.article?.preference?.isDark = true 65 | } 66 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 67 | } 68 | 69 | @IBAction func increaseContrastChanged(_ sender: NSButton) { 70 | if sender.state == .on { 71 | self.delegate?.article?.preference?.increaseContrast = true 72 | } else { 73 | self.delegate?.article?.preference?.increaseContrast = false 74 | } 75 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 76 | } 77 | 78 | @IBAction func reduceTransparencyChanged(_ sender: NSButton) { 79 | if sender.state == .on { 80 | self.delegate?.article?.preference?.reduceTransparency = true 81 | } else { 82 | self.delegate?.article?.preference?.reduceTransparency = false 83 | } 84 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 85 | } 86 | 87 | @IBAction func collapse(_ sender: NSButton) { 88 | if (sender != disclosureTriangle) { 89 | if (disclosureTriangle.state == .on) { 90 | disclosureTriangle.state = .on 91 | } else { 92 | disclosureTriangle.state = .off 93 | } 94 | } 95 | 96 | if let delegate = delegate { 97 | delegate.collapseAppearance = !(delegate.collapseAppearance) 98 | delegate.updateHeight() 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /SpeedReader/SRArticleSnippetCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRArticleSnippetCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRArticleSnippetCellView: NSTableCellView { 12 | 13 | @IBOutlet weak var iconView: NSImageView! 14 | @IBOutlet weak var articleSummary: NSTextField! 15 | @IBOutlet weak var articleTime: NSTextField! 16 | 17 | override func draw(_ dirtyRect: NSRect) { 18 | super.draw(dirtyRect) 19 | 20 | // Drawing code here. 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /SpeedReader/SRFontCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRFontCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRFontCellView: SRGeneralPrefCellView { 12 | @IBOutlet weak var disclosureTriangle: NSButton! 13 | @IBOutlet weak var topLabel: NSButton! 14 | 15 | @IBOutlet weak var fontNamePopUp: NSPopUpButton! 16 | @IBOutlet weak var fontSubFamilyPopUp: NSPopUpButton! 17 | @IBOutlet weak var fontSizeComboBox: NSComboBox! 18 | var fontPostScriptArray: [String] = [] 19 | 20 | let allFontNames = NSFontManager.shared.availableFontFamilies 21 | let allFontSizes = [24, 36, 48, 64, 72, 96] 22 | 23 | override func draw(_ dirtyRect: NSRect) { 24 | super.draw(dirtyRect) 25 | 26 | // Drawing code here. 27 | } 28 | 29 | @IBAction func fontNameChanged(_ sender: NSPopUpButton) { 30 | updateSubFamily() 31 | updateFontInDelegate() 32 | } 33 | 34 | @IBAction func fontSubFamilyChanged(_ sender: NSPopUpButton) { 35 | updateFontInDelegate() 36 | } 37 | 38 | @IBAction func fontSizeChanged(_ sender: NSComboBox) { 39 | updateFontInDelegate() 40 | } 41 | 42 | @IBAction func collapse(_ sender: NSButton) { 43 | if (sender != disclosureTriangle) { 44 | if (disclosureTriangle.state == .on) { 45 | disclosureTriangle.state = .on 46 | } else { 47 | disclosureTriangle.state = .on 48 | } 49 | } 50 | 51 | if let delegate = delegate { 52 | delegate.collapseFont = !(delegate.collapseFont) 53 | delegate.updateHeight() 54 | } 55 | } 56 | 57 | override func configure() { 58 | if delegate?.article == nil { 59 | fontNamePopUp.isEnabled = false 60 | fontSizeComboBox.isEnabled = false 61 | fontSubFamilyPopUp.isEnabled = false 62 | } else { 63 | fontNamePopUp.isEnabled = true 64 | fontSizeComboBox.isEnabled = true 65 | fontSubFamilyPopUp.isEnabled = true 66 | } 67 | 68 | fontNamePopUp.removeAllItems() 69 | fontNamePopUp.addItem(withTitle: "System Font") 70 | fontNamePopUp.addItems(withTitles: allFontNames) 71 | 72 | updateSubFamily() 73 | fontSizeComboBox.removeAllItems() 74 | fontSizeComboBox.addItems(withObjectValues: allFontSizes) 75 | fontSizeComboBox.stringValue = "\(Int(self.delegate?.article?.preference?.fontSize ?? 24))" 76 | 77 | if let chosenFont = self.delegate?.article?.preference?.font as? NSFont { 78 | // Swift.print("\(chosenFont)") 79 | if let familyName = chosenFont.familyName { 80 | if familyName.contains(".SF") { 81 | fontNamePopUp.selectItem(withTitle: "System Font") 82 | } else { 83 | fontNamePopUp.selectItem(withTitle: familyName) 84 | } 85 | if let displayName = chosenFont.displayName { 86 | if let range = displayName.range(of: familyName) { 87 | let endPos = displayName.distance(from: displayName.startIndex, to: range.upperBound) 88 | let offsetIndex = endPos.advanced(by: 1) 89 | if displayName.count > endPos { 90 | let index = displayName.index(displayName.startIndex, offsetBy: offsetIndex) 91 | let variant = String(displayName[index...]) 92 | fontSubFamilyPopUp.selectItem(withTitle: variant) 93 | } 94 | 95 | } 96 | } 97 | } 98 | fontSizeComboBox.selectItem(withObjectValue: chosenFont.pointSize) 99 | } 100 | } 101 | 102 | func updateSubFamily() { 103 | fontSubFamilyPopUp.removeAllItems() 104 | if let selectedFamily = fontNamePopUp.titleOfSelectedItem { 105 | if let arrayofSubs = NSFontManager.shared.availableMembers(ofFontFamily: selectedFamily) { 106 | var resultingSub:[String] = [] 107 | fontPostScriptArray = [] 108 | for i in 0.. = Article.fetchRequest() 29 | fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastUpdated", ascending: false)] 30 | articles = try context.fetch(fetchRequest) 31 | if tableView != nil { 32 | tableView.reloadData() 33 | if articles.count > 0 { 34 | let indexSet = NSIndexSet(index: 0) 35 | tableView.selectRowIndexes(indexSet as IndexSet, byExtendingSelection: false) 36 | noContentLabel.isHidden = true 37 | hideOnboardingExperience() 38 | updateViewsBasedOnRow(0) 39 | } else { 40 | noContentLabel.isHidden = false 41 | } 42 | } 43 | } catch { 44 | // print("Fetch article failed") 45 | } 46 | } 47 | } 48 | 49 | override func viewWillAppear() { 50 | if articles.count > 0 { 51 | let indexSet = NSIndexSet(index: 0) 52 | tableView.selectRowIndexes(indexSet as IndexSet, byExtendingSelection: false) 53 | hideOnboardingExperience() 54 | updateViewsBasedOnRow(0) 55 | } 56 | } 57 | 58 | func numberOfRows(in tableView: NSTableView) -> Int { 59 | if articles.count <= 0 { 60 | showOnboardingExperience() 61 | } else { 62 | hideOnboardingExperience() 63 | } 64 | return articles.count 65 | } 66 | 67 | func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { 68 | return SRTableRowView() 69 | } 70 | 71 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 72 | if let view = tableView.makeView(withIdentifier: convertToNSUserInterfaceItemIdentifier("HistoryEntryCell"), owner: self) as? SRArticleSnippetCellView { 73 | let article = articles[row] 74 | if let time = article.lastUpdated { 75 | view.articleTime.stringValue = time.description 76 | } 77 | if let content = article.content { 78 | view.articleSummary.stringValue = content 79 | } 80 | if article.typeOfArticle == 1 { 81 | view.iconView.image = NSImage.init(named: "openLocal") 82 | } else if article.typeOfArticle == 2 { 83 | view.iconView.image = NSImage.init(named: "openWeb") 84 | } else{ 85 | view.iconView.image = NSImage.init(named: "createNew") 86 | } 87 | return view 88 | } 89 | return nil 90 | } 91 | 92 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 93 | return 75 94 | } 95 | 96 | func tableViewSelectionDidChange(_ notification: Notification) { 97 | updateViewsBasedOnRow(tableView.selectedRow) 98 | } 99 | 100 | func updateViewsBasedOnRow(_ selectedRow: Int) { 101 | if selectedRow == -1 { 102 | showOnboardingExperience() 103 | return 104 | } else { 105 | hideOnboardingExperience() 106 | let article = articles[selectedRow] 107 | if let articleVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[1].viewController as? ArticleViewController { 108 | articleVC.article = article 109 | articleVC.updateToReflectArticle() 110 | articleVC.guidanceView.isHidden = true 111 | articleVC.outerTextScrollView.isHidden = false 112 | } 113 | if let preferenceVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[2].viewController as? SRPreferencesViewController { 114 | preferenceVC.article = article 115 | preferenceVC.updateToReflectArticle() 116 | } 117 | } 118 | } 119 | 120 | @IBAction func delete(_ sender: NSMenuItem) { 121 | // print(tableView.clickedRow) 122 | if let context = (NSApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext { 123 | context.delete(articles[tableView.clickedRow]) 124 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 125 | getAllArticles() 126 | } 127 | } 128 | 129 | func showOnboardingExperience() { 130 | if let articleVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[1].viewController as? ArticleViewController { 131 | articleVC.article = nil 132 | articleVC.guidanceView.isHidden = false 133 | articleVC.outerTextScrollView.isHidden = true 134 | } 135 | if let preferenceVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[2].viewController as? SRPreferencesViewController { 136 | preferenceVC.article = nil 137 | preferenceVC.tableView.reloadData() 138 | } 139 | if let mainWindow = NSApplication.shared.mainWindow?.windowController as? MainWindowController { 140 | mainWindow.readButton.isEnabled = false 141 | } 142 | } 143 | 144 | func hideOnboardingExperience() { 145 | if let articleVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[1].viewController as? ArticleViewController { 146 | articleVC.guidanceView.isHidden = true 147 | articleVC.outerTextScrollView.isHidden = false 148 | // articleVC.contentTextView.string = articleVC.article?.content 149 | } 150 | if let preferenceVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[2].viewController as? SRPreferencesViewController { 151 | preferenceVC.tableView.reloadData() 152 | } 153 | if let mainWindow = NSApplication.shared.mainWindow?.windowController as? MainWindowController { 154 | mainWindow.readButton.isEnabled = true 155 | } 156 | } 157 | 158 | 159 | } 160 | 161 | // Helper function inserted by Swift 4.2 migrator. 162 | fileprivate func convertToNSUserInterfaceItemIdentifier(_ input: String) -> NSUserInterfaceItemIdentifier { 163 | return NSUserInterfaceItemIdentifier(rawValue: input) 164 | } 165 | -------------------------------------------------------------------------------- /SpeedReader/SRImportWebViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRImportWebViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRImportWebViewController: NSViewController, NSTextFieldDelegate { 12 | @IBOutlet weak var urlLabel: NSTextField! 13 | @IBOutlet weak var titleLabel: NSTextField! 14 | var dotDotDotTimer: Timer? 15 | var articleContent = "" 16 | var articleTitle = "" 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | getClipboardURL() 21 | urlLabel.delegate = self 22 | } 23 | 24 | func controlTextDidChange(_ obj: Notification) { 25 | parseWebpageAsync() 26 | } 27 | 28 | 29 | @IBAction func cancelPressed(_ sender: NSButton) { 30 | self.view.window?.sheetParent?.endSheet(self.view.window!, returnCode: NSApplication.ModalResponse.cancel) 31 | } 32 | 33 | @IBAction func importPressed(_ sender: NSButton) { 34 | if articleContent == "" || articleTitle == "" { 35 | let alert = NSAlert.init() 36 | alert.messageText = "Invalid URL." 37 | alert.informativeText = "The URL you entered does not seem to be valid." 38 | alert.addButton(withTitle: "OK") 39 | alert.runModal() 40 | return 41 | } 42 | guard let context = (NSApp.delegate as? AppDelegate)?.persistentContainer.viewContext else { 43 | return 44 | } 45 | let newArticle: Article = Article(context: context) 46 | newArticle.typeOfArticle = 2 47 | newArticle.webPageUrl = self.urlLabel.stringValue 48 | newArticle.lastUpdated = Date.init() 49 | newArticle.webPageTitle = titleLabel.stringValue 50 | newArticle.content = "\(articleContent)" 51 | 52 | let newPreference: Preference = Preference(context: context) 53 | newArticle.preference = newPreference 54 | 55 | (NSApp.delegate as? AppDelegate)?.saveAction(nil) 56 | self.view.window?.sheetParent?.endSheet(self.view.window!, returnCode: NSApplication.ModalResponse.OK) 57 | if let leftVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[0].viewController as? SRHistoryViewController { 58 | leftVC.getAllArticles() 59 | } 60 | } 61 | 62 | func parseWebpageAsync() { 63 | articleContent = "" 64 | articleTitle = "" 65 | dotDotDotTimer?.invalidate() 66 | self.titleLabel.stringValue = "Loading..." 67 | dotDotDotTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true, block: { (timer) in 68 | if self.titleLabel.stringValue.contains("...") { 69 | self.titleLabel.stringValue = "Loading" 70 | } else if self.titleLabel.stringValue.contains("..") { 71 | self.titleLabel.stringValue = "Loading..." 72 | } else if self.titleLabel.stringValue.contains(".") { 73 | self.titleLabel.stringValue = "Loading.." 74 | } else { 75 | self.titleLabel.stringValue = "Loading." 76 | } 77 | }) 78 | guard let url = URL(string: urlLabel.stringValue) else { 79 | self.titleLabel.stringValue = "Invalid Web URL" 80 | self.dotDotDotTimer?.invalidate() 81 | return 82 | } 83 | URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in 84 | if error != nil { 85 | print("\(String(describing: error))") 86 | self.setLabelUnavailable() 87 | } else { 88 | guard let data = data, 89 | let html:String = String(data: data, encoding: String.Encoding.utf8) else { 90 | return 91 | } 92 | do { 93 | let doc = try HTML(html: html, encoding: .utf8) 94 | let title = doc.title 95 | for link in doc.xpath("//p | //h1 | //h2 | //code") { 96 | guard let text = link.text else { 97 | return 98 | } 99 | self.articleContent = self.articleContent + text + "\n" 100 | } 101 | DispatchQueue.main.async { 102 | self.articleTitle = title ?? "" 103 | self.titleLabel.stringValue = title ?? "" 104 | self.dotDotDotTimer?.invalidate() 105 | } 106 | } catch { 107 | self.setLabelUnavailable() 108 | } 109 | 110 | } 111 | }.resume() 112 | } 113 | 114 | func setLabelUnavailable() { 115 | DispatchQueue.main.async { 116 | self.titleLabel.stringValue = "Unavailable" 117 | self.dotDotDotTimer?.invalidate() 118 | } 119 | } 120 | 121 | func getClipboardURL() { 122 | if let items = NSPasteboard.general.pasteboardItems { 123 | for item in items { 124 | for type in convertFromNSPasteboardPasteboardTypeArray(item.types) { 125 | if type == "public.utf8-plain-text" { 126 | if let url = item.string(forType: convertToNSPasteboardPasteboardType(type)) { 127 | if url.hasPrefix("http://") || url.hasPrefix("https://") { 128 | urlLabel.stringValue = url 129 | parseWebpageAsync() 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | // Helper function inserted by Swift 4.2 migrator. 140 | fileprivate func convertFromNSPasteboardPasteboardTypeArray(_ input: [NSPasteboard.PasteboardType]) -> [String] { 141 | return input.map { key in key.rawValue } 142 | } 143 | 144 | // Helper function inserted by Swift 4.2 migrator. 145 | fileprivate func convertToNSPasteboardPasteboardType(_ input: String) -> NSPasteboard.PasteboardType { 146 | return NSPasteboard.PasteboardType(rawValue: input) 147 | } 148 | -------------------------------------------------------------------------------- /SpeedReader/SRLanguageCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRLanguageCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRLanguageCellView: SRGeneralPrefCellView { 12 | @IBOutlet weak var disclosureTriangle: NSButton! 13 | @IBOutlet weak var topLabel: NSButton! 14 | 15 | 16 | @IBOutlet weak var contentLanguagePopUp: NSPopUpButton! 17 | 18 | override func draw(_ dirtyRect: NSRect) { 19 | super.draw(dirtyRect) 20 | } 21 | 22 | 23 | 24 | @IBAction func contentLanguageChanged(_ sender: NSPopUpButton) { 25 | } 26 | 27 | @IBAction func collapse(_ sender: NSButton) { 28 | if (sender != disclosureTriangle) { 29 | if (disclosureTriangle.state == .on) { 30 | disclosureTriangle.state = .on 31 | } else { 32 | disclosureTriangle.state = .on 33 | } 34 | } 35 | 36 | if let delegate = delegate { 37 | delegate.collapseLanguage = !(delegate.collapseLanguage) 38 | delegate.updateHeight() 39 | } 40 | } 41 | 42 | override func configure() { 43 | if delegate?.article == nil { 44 | contentLanguagePopUp.isEnabled = false 45 | } else { 46 | contentLanguagePopUp.isEnabled = true 47 | } 48 | if let language = self.delegate?.article?.content?.guessLanguage() { 49 | contentLanguagePopUp.removeAllItems() 50 | contentLanguagePopUp.addItem(withTitle: "Auto Detect (\(language))") 51 | } 52 | } 53 | } 54 | 55 | extension String { 56 | func guessLanguage() -> String { 57 | let length = self.utf16.count 58 | let languageCode = CFStringTokenizerCopyBestStringLanguage(self as CFString, CFRange(location: 0, length: length)) as String? ?? "" 59 | 60 | let locale = Locale(identifier: languageCode) 61 | return locale.localizedString(forLanguageCode: languageCode) ?? "Unknown" 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /SpeedReader/SRModalWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRModalWindowController.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRModalWindowController: NSWindowController { 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | 16 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /SpeedReader/SRNewArticleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRNewArticleViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRNewArticleViewController: NSViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do view setup here. 16 | } 17 | 18 | @IBAction func newArticleClicked(_ sender: NSButton) { 19 | if let context = (NSApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext { 20 | let article:Article = Article(context: context) 21 | article.typeOfArticle = 0 22 | article.content = "Speed reading is the art of silencing subvocalization. Most readers have an average reading speed of 200 wpm, which is about as fast as they can read a passage out loud. This is no coincidence. It is their inner voice that paces through the text that keeps them from achieving higher reading speeds. They can only read as fast as they can speak because that's the way they were taught to read, through reading systems like Hooked on Phonics.\n\nHowever, it is entirely possible to read at a much greater speed, with much better reading comprehension, by silencing this inner voice. The solution is simple - absorb reading material faster than that inner voice can keep up.\n\nIn the real world, this is achieved through methods like reading passages using a finger to point your way. You read through a page of text by following your finger line by line at a speed faster than you can normally read. This works because the eye is very good at tracking movement. Even if at this point full reading comprehension is lost, it's exactly this method of training that will allow you to read faster.\n\nWith the aid of software, it's much easier to achieve this same result with much less effort. Load a passage of text (like this one), and the software will pace through the text at a predefined speed that you can adjust as your reading comprehension increases.\n\nTo train to read faster, you must first find your base rate. Your base rate is the speed that you can read a passage of text with full comprehension. We've defaulted to 180 wpm, showing one word at a time, which is about the average that works best for our users. Now, read that passage at that base rate.\n\nAfter you've finished, double that speed by going to the Settings and changing the Words Per Minute value. Reread the passage. You shouldn't expect to understand everything - in fact, more likely than not you'll only catch a couple words here and there. If you have high comprehension, that probably means that you need to set your base rate higher and rerun this test again. You should be straining to keep up with the speed of the words flashing by. This speed should be faster than your inner voice can 'read'.\n\nNow, reread the passage again at your base rate. It should feel a lot slower – if not, try running the speed test again). Now try moving up a little past your base rate – for example, at 400 wpm – , and see how much you can comprehend at that speed.\n\nThat's basically it - constantly read passages at a rate faster than you can keep up, and keep pushing the edge of what you're capable of. You'll find that when you drop down to lower speeds, you'll be able to pick up much more than you would have thought possible.\n\nOne other setting that's worth mentioning in this introduction is the chunk size – the number of words that are flashed at each interval on the screen. When you read aloud, you can only say one word at a time. However, this limit does not apply to speed reading. Once your inner voice subsides and with constant practice, you can read multiple words at a time. This is the best way to achieve reading speeds of 1000+ wpm. Start small with 2 word chunk sizes and find out that as you increase, 3,4, or even higher chunk sizes are possible.\n\nGood luck!" 23 | article.lastUpdated = Date.init() 24 | 25 | let preference: Preference = Preference(context: context) 26 | preference.fontFamily = "System Font" 27 | preference.fontSize = 24 28 | preference.isDark = false 29 | preference.language = "en_US" 30 | preference.speed = 0.5 31 | preference.wordsPerRoll = 1 32 | 33 | article.preference = preference 34 | 35 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 36 | 37 | if let leftVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[0].viewController as? SRHistoryViewController { 38 | leftVC.getAllArticles() 39 | } 40 | 41 | presentingViewController?.dismiss(self) 42 | } 43 | // print("clicked") 44 | } 45 | 46 | @IBAction func importArticleClicked(_ sender: NSButton) { 47 | // print("clicked") 48 | let openPanel = NSOpenPanel(); 49 | openPanel.allowsMultipleSelection = false; 50 | openPanel.canChooseDirectories = false; 51 | openPanel.canCreateDirectories = false; 52 | openPanel.canChooseFiles = true; 53 | openPanel.allowedFileTypes = ["txt","rtf","rtfd","doc", "docx", "html", "webArchive"] 54 | openPanel.beginSheetModal(for: NSApp.mainWindow!) { (i) in 55 | if(i == NSApplication.ModalResponse.OK){ 56 | guard let fileURL = openPanel.url else { 57 | return 58 | } 59 | self.createArticleBasedOnFileURL(fileURL) 60 | } else { 61 | // print("open cancelled") 62 | } 63 | } 64 | } 65 | 66 | func createArticleBasedOnFileURL(_ fileURL: URL) { 67 | // print("Opened \(String(describing: fileURL))") 68 | let normalizedExtension = fileURL.pathExtension.lowercased() 69 | guard let context = (NSApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext else { 70 | return 71 | } 72 | let article: Article = Article(context: context) 73 | article.typeOfArticle = 1 74 | article.content = "Imported from File" 75 | 76 | var parseAttribute: String = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.plain) 77 | 78 | if normalizedExtension == "rtf" { 79 | parseAttribute = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.rtf) 80 | } else if normalizedExtension == "doc" { 81 | parseAttribute = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.docFormat) 82 | } else if normalizedExtension == "docx" { 83 | parseAttribute = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.wordML) 84 | } else if normalizedExtension == "html"{ 85 | parseAttribute = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.html) 86 | } else if normalizedExtension == "webArchive" { 87 | parseAttribute = convertFromNSAttributedStringDocumentType(NSAttributedString.DocumentType.webArchive) 88 | } 89 | do { 90 | let attributedStringWithRtf:NSAttributedString = try NSAttributedString(url: fileURL, options: convertToNSAttributedStringDocumentReadingOptionKeyDictionary([convertFromNSAttributedStringDocumentAttributeKey(NSAttributedString.DocumentAttributeKey.documentType):parseAttribute]), documentAttributes: nil) 91 | article.content = attributedStringWithRtf.string 92 | } catch {} 93 | article.lastUpdated = Date.init() 94 | 95 | let preference: Preference = Preference(context: context) 96 | article.preference = preference 97 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 98 | if let leftVC = (NSApplication.shared.mainWindow?.contentViewController as? SRSplitViewController)?.splitViewItems[0].viewController as? SRHistoryViewController { 99 | leftVC.getAllArticles() 100 | } 101 | } 102 | 103 | @IBAction func importURLClicked(_ sender: NSButton) { 104 | // print("clicked") 105 | if let webWindow = (storyboard?.instantiateController(withIdentifier: "OpenWebWindow") as? NSWindowController)?.window { 106 | NSApp.mainWindow?.beginSheet(webWindow, completionHandler: { (response: NSApplication.ModalResponse) in 107 | // print("sheet closed") 108 | if (response == NSApplication.ModalResponse.OK) { 109 | if let webImport = webWindow.contentViewController as? SRImportWebViewController { 110 | print("dismissed with url to import: \(webImport.urlLabel.stringValue)") 111 | } 112 | } else if (response == NSApplication.ModalResponse.cancel) { 113 | // print("cancelled") 114 | } 115 | }) 116 | } 117 | presentingViewController?.dismiss(self) 118 | } 119 | } 120 | 121 | extension NSOpenPanel { 122 | var selectUrl: URL? { 123 | title = "Select File" 124 | allowsMultipleSelection = false 125 | canChooseDirectories = false 126 | canChooseFiles = true 127 | canCreateDirectories = false 128 | allowedFileTypes = ["txt","rtf","pdf","doc", "docx", "pages"] 129 | return runModal().rawValue == NSFileHandlingPanelOKButton ? urls.first : nil 130 | } 131 | } 132 | 133 | // Helper function inserted by Swift 4.2 migrator. 134 | fileprivate func convertFromNSAttributedStringDocumentType(_ input: NSAttributedString.DocumentType) -> String { 135 | return input.rawValue 136 | } 137 | 138 | // Helper function inserted by Swift 4.2 migrator. 139 | fileprivate func convertToNSAttributedStringDocumentReadingOptionKeyDictionary(_ input: [String: Any]) -> [NSAttributedString.DocumentReadingOptionKey: Any] { 140 | return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.DocumentReadingOptionKey(rawValue: key), value)}) 141 | } 142 | 143 | // Helper function inserted by Swift 4.2 migrator. 144 | fileprivate func convertFromNSAttributedStringDocumentAttributeKey(_ input: NSAttributedString.DocumentAttributeKey) -> String { 145 | return input.rawValue 146 | } 147 | -------------------------------------------------------------------------------- /SpeedReader/SRPreferencesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRPreferencesViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRPreferencesViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource { 12 | 13 | @IBOutlet weak var tableView: NSTableView! 14 | var article: Article? 15 | 16 | // var speed: Float = 0.1 17 | // var font: NSFont = NSFont.systemFont(ofSize: 24.0) 18 | // var enableDark: Bool = false 19 | // var increaseContrast = false 20 | // var reduceTransparency = false 21 | // var wordsPerRoll = 1 22 | // var contentLanguage = "en_US" 23 | 24 | var collapseSpeed: Bool = false 25 | var collapseFont: Bool = false 26 | var collapseAppearance: Bool = false 27 | var collapseLanguage: Bool = false 28 | var collapseWords: Bool = false 29 | 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | tableView.delegate = self 34 | tableView.dataSource = self 35 | // Do view setup here. 36 | } 37 | 38 | func numberOfRows(in tableView: NSTableView) -> Int { 39 | return 7 40 | } 41 | 42 | func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 43 | var identifier = "SpeedCellView" 44 | switch row { 45 | case 0: 46 | identifier = "EmptyCellView" 47 | case 1: 48 | identifier = "SpeedCellView" 49 | case 2: 50 | identifier = "FontCellView" 51 | case 3: 52 | identifier = "AppearanceCellView" 53 | case 4: 54 | identifier = "LanguageCellView" 55 | case 5: 56 | identifier = "WordsCellView" 57 | case 6: 58 | identifier = "ReadCellView" 59 | default: 60 | identifier = "SpeedCellView" 61 | } 62 | if let cell = tableView.makeView(withIdentifier: convertToNSUserInterfaceItemIdentifier(identifier), owner: self) as? SRGeneralPrefCellView { 63 | cell.delegate = self 64 | cell.configure() 65 | return cell 66 | } 67 | return nil 68 | } 69 | 70 | func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { 71 | if (row == 0) { 72 | return 10; 73 | } 74 | else if (row == 1) { 75 | return collapseSpeed ? 20: 79+10 76 | } else if (row == 2) { 77 | return collapseFont ? 20: 95+10 78 | } else if (row == 3) { 79 | return collapseAppearance ? 20: 105+10 80 | } else if (row == 4) { 81 | return collapseLanguage ? 20: 56+10 82 | } else if (row == 5) { 83 | return collapseWords ? 20: 56+10 84 | } else if (row == 6) { 85 | return 40 86 | } else { 87 | return 95 88 | } 89 | } 90 | 91 | func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { 92 | return false 93 | } 94 | 95 | func updateHeight() { 96 | tableView.reloadData() 97 | } 98 | 99 | func updateToReflectArticle() { 100 | if article != nil { 101 | tableView.reloadData() 102 | } 103 | } 104 | } 105 | 106 | // Helper function inserted by Swift 4.2 migrator. 107 | fileprivate func convertToNSUserInterfaceItemIdentifier(_ input: String) -> NSUserInterfaceItemIdentifier { 108 | return NSUserInterfaceItemIdentifier(rawValue: input) 109 | } 110 | -------------------------------------------------------------------------------- /SpeedReader/SRReadCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRReadCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRReadCellView: SRGeneralPrefCellView { 12 | 13 | @IBOutlet weak var speedReadButton: NSButton! 14 | override func draw(_ dirtyRect: NSRect) { 15 | super.draw(dirtyRect) 16 | 17 | // Drawing code here. 18 | } 19 | 20 | override func configure() { 21 | if (speedReadButton != nil) { 22 | if delegate?.article == nil { 23 | speedReadButton.isEnabled = false 24 | } else { 25 | speedReadButton.isEnabled = true 26 | } 27 | } 28 | } 29 | 30 | @IBAction func readInlineClicked(_ sender: Any) { 31 | if let mainWindow = NSApplication.shared.mainWindow?.windowController as? MainWindowController { 32 | mainWindow.openReaderWindow() 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /SpeedReader/SRSpeedCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRSpeedCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRSpeedCellView: SRGeneralPrefCellView { 12 | 13 | @IBOutlet weak var disclosureTriangle: NSButton! 14 | @IBOutlet weak var topLabel: NSButton! 15 | 16 | @IBOutlet weak var slider: NSSlider! 17 | 18 | override func draw(_ dirtyRect: NSRect) { 19 | super.draw(dirtyRect) 20 | } 21 | 22 | @IBAction func speedChanged(_ sender: NSSlider) { 23 | delegate?.article?.preference?.speed = sender.floatValue 24 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 25 | } 26 | 27 | @IBAction func collapse(_ sender: NSButton) { 28 | if (sender != disclosureTriangle) { 29 | if (disclosureTriangle.state == .on) { 30 | disclosureTriangle.state = .on 31 | } else { 32 | disclosureTriangle.state = .on 33 | } 34 | } 35 | 36 | if let delegate = delegate { 37 | delegate.collapseSpeed = !(delegate.collapseSpeed) 38 | delegate.updateHeight() 39 | } 40 | } 41 | 42 | override func configure() { 43 | if delegate?.article == nil { 44 | slider.isEnabled = false 45 | } else { 46 | slider.isEnabled = true 47 | } 48 | if let speed = delegate?.article?.preference?.speed { 49 | slider.floatValue = speed 50 | } else { 51 | slider.floatValue = 0.82 52 | delegate?.article?.preference?.speed = 0.82 53 | } 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /SpeedReader/SRSplitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRSplitViewController.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRSplitViewController: NSSplitViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | self.splitView.delegate = self 16 | // Do view setup here. 17 | } 18 | 19 | 20 | 21 | // override func splitView(_ splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { 22 | // return proposedMinimumPosition - 50 23 | // } 24 | // 25 | // override func splitView(_ splitView: NSSplitView, constrainMaxCoordinate proposedMaximumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { 26 | // return proposedMaximumPosition - 250 27 | // } 28 | // 29 | // override func splitView(_ splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat { 30 | // return proposedMinimumPosition - 200 31 | // } 32 | 33 | // override func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool { 34 | //// NSView* rightView = [[splitView subviews] objectAtIndex:1]; 35 | //// NSLog(@"%@:%s returning %@",[self class], _cmd, ([subview isEqual:rightView])?@"YES":@"NO"); 36 | //// return ([subview isEqual:rightView]); 37 | // 38 | // 39 | // return true 40 | // } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /SpeedReader/SRWordsCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SRWordsCellView.swift 3 | // SpeedReader 4 | // 5 | // Created by Bright on 7/16/17. 6 | // Copyright © 2017 Kay Yin. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class SRWordsCellView: SRGeneralPrefCellView { 12 | @IBOutlet weak var disclosureTriangle: NSButton! 13 | @IBOutlet weak var topLabel: NSButton! 14 | 15 | 16 | @IBOutlet weak var wordsPerRoll: NSSegmentedControl! 17 | 18 | override func draw(_ dirtyRect: NSRect) { 19 | super.draw(dirtyRect) 20 | 21 | // Drawing code here. 22 | } 23 | 24 | override func configure() { 25 | if delegate?.article == nil { 26 | wordsPerRoll.isEnabled = false 27 | } else { 28 | wordsPerRoll.isEnabled = true 29 | } 30 | if let wordsPref = self.delegate?.article?.preference?.wordsPerRoll { 31 | wordsPerRoll.selectedSegment = Int(wordsPref - 1) 32 | } 33 | } 34 | 35 | @IBAction func wordsPerRollChanged(_ sender: NSSegmentedControl) { 36 | self.delegate?.article?.preference?.wordsPerRoll = Int16(sender.selectedSegment + 1) 37 | (NSApplication.shared.delegate as? AppDelegate)?.saveAction(nil) 38 | } 39 | 40 | @IBAction func collapse(_ sender: NSButton) { 41 | if (sender != disclosureTriangle) { 42 | if (disclosureTriangle.state == .on) { 43 | disclosureTriangle.state = .on 44 | } else { 45 | disclosureTriangle.state = .on 46 | } 47 | } 48 | if let delegate = delegate { 49 | delegate.collapseWords = !(delegate.collapseWords) 50 | delegate.updateHeight() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SpeedReader/SpeedReader-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | //#import 6 | //#import 7 | //#import 8 | -------------------------------------------------------------------------------- /SpeedReader/SpeedReader.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SpeedReader/SpeedReader.xcdatamodeld/SpeedReader.xcdatamodel/contents: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /speedreader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/speedreader.gif -------------------------------------------------------------------------------- /speedreader_dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumingYin/SpeedReader/c41350d24c07b939d2753888869880d5d6e29cf4/speedreader_dark.gif --------------------------------------------------------------------------------