-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
ReaderMode.swift
296 lines (255 loc) · 9.76 KB
/
ReaderMode.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import WebKit
import SwiftyJSON
let ReaderModeProfileKeyStyle = "readermode.style"
enum ReaderModeMessageType: String {
case stateChange = "ReaderModeStateChange"
case pageEvent = "ReaderPageEvent"
}
enum ReaderPageEvent: String {
case pageShow = "PageShow"
}
enum ReaderModeState: String {
case available = "Available"
case unavailable = "Unavailable"
case active = "Active"
}
enum ReaderModeTheme: String {
case light = "light"
case dark = "dark"
case sepia = "sepia"
}
enum ReaderModeFontType: String {
case serif = "serif"
case sansSerif = "sans-serif"
}
enum ReaderModeFontSize: Int {
case size1 = 1
case size2 = 2
case size3 = 3
case size4 = 4
case size5 = 5
case size6 = 6
case size7 = 7
case size8 = 8
case size9 = 9
case size10 = 10
case size11 = 11
case size12 = 12
case size13 = 13
func isSmallest() -> Bool {
return self == ReaderModeFontSize.size1
}
func smaller() -> ReaderModeFontSize {
if isSmallest() {
return self
} else {
return ReaderModeFontSize(rawValue: self.rawValue - 1)!
}
}
func isLargest() -> Bool {
return self == ReaderModeFontSize.size13
}
static var defaultSize: ReaderModeFontSize {
switch UIApplication.shared.preferredContentSizeCategory {
case UIContentSizeCategory.extraSmall:
return .size1
case UIContentSizeCategory.small:
return .size3
case UIContentSizeCategory.medium:
return .size5
case UIContentSizeCategory.large:
return .size7
case UIContentSizeCategory.extraLarge:
return .size9
case UIContentSizeCategory.extraExtraLarge:
return .size11
case UIContentSizeCategory.extraExtraExtraLarge:
return .size13
default:
return .size5
}
}
func bigger() -> ReaderModeFontSize {
if isLargest() {
return self
} else {
return ReaderModeFontSize(rawValue: self.rawValue + 1)!
}
}
}
struct ReaderModeStyle {
var theme: ReaderModeTheme
var fontType: ReaderModeFontType
var fontSize: ReaderModeFontSize
/// Encode the style to a JSON dictionary that can be passed to ReaderMode.js
func encode() -> String {
return JSON(["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue]).stringValue() ?? ""
}
/// Encode the style to a dictionary that can be stored in the profile
func encodeAsDictionary() -> [String:Any] {
return ["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue]
}
init(theme: ReaderModeTheme, fontType: ReaderModeFontType, fontSize: ReaderModeFontSize) {
self.theme = theme
self.fontType = fontType
self.fontSize = fontSize
}
/// Initialize the style from a dictionary, taken from the profile. Returns nil if the object cannot be decoded.
init?(dict: [String:Any]) {
let themeRawValue = dict["theme"] as? String
let fontTypeRawValue = dict["fontType"] as? String
let fontSizeRawValue = dict["fontSize"] as? Int
if themeRawValue == nil || fontTypeRawValue == nil || fontSizeRawValue == nil {
return nil
}
let theme = ReaderModeTheme(rawValue: themeRawValue!)
let fontType = ReaderModeFontType(rawValue: fontTypeRawValue!)
let fontSize = ReaderModeFontSize(rawValue: fontSizeRawValue!)
if theme == nil || fontType == nil || fontSize == nil {
return nil
}
self.theme = theme!
self.fontType = fontType!
self.fontSize = fontSize!
}
}
let DefaultReaderModeStyle = ReaderModeStyle(theme: .light, fontType: .sansSerif, fontSize: ReaderModeFontSize.defaultSize)
/// This struct captures the response from the Readability.js code.
struct ReadabilityResult {
var domain = ""
var url = ""
var content = ""
var title = ""
var credits = ""
init?(object: AnyObject?) {
if let dict = object as? NSDictionary {
if let uri = dict["uri"] as? NSDictionary {
if let url = uri["spec"] as? String {
self.url = url
}
if let host = uri["host"] as? String {
self.domain = host
}
}
if let content = dict["content"] as? String {
self.content = content
}
if let title = dict["title"] as? String {
self.title = title
}
if let credits = dict["byline"] as? String {
self.credits = credits
}
} else {
return nil
}
}
/// Initialize from a JSON encoded string
init?(string: String) {
let object = JSON(parseJSON: string)
let domain = object["domain"].string
let url = object["url"].string
let content = object["content"].string
let title = object["title"].string
let credits = object["credits"].string
if domain == nil || url == nil || content == nil || title == nil || credits == nil {
return nil
}
self.domain = domain!
self.url = url!
self.content = content!
self.title = title!
self.credits = credits!
}
/// Encode to a dictionary, which can then for example be json encoded
func encode() -> [String:Any] {
return ["domain": domain, "url": url, "content": content, "title": title, "credits": credits]
}
/// Encode to a JSON encoded string
func encode() -> String {
let dict: [String: Any] = self.encode()
return JSON(object: dict).stringValue()!
}
}
/// Delegate that contains callbacks that we have added on top of the built-in WKWebViewDelegate
protocol ReaderModeDelegate {
func readerMode(_ readerMode: ReaderMode, didChangeReaderModeState state: ReaderModeState, forTab tab: Tab)
func readerMode(_ readerMode: ReaderMode, didDisplayReaderizedContentForTab tab: Tab)
}
let ReaderModeNamespace = "window.__firefox__.reader"
class ReaderMode: TabHelper {
var delegate: ReaderModeDelegate?
fileprivate weak var tab: Tab?
var state: ReaderModeState = ReaderModeState.unavailable
fileprivate var originalURL: URL?
class func name() -> String {
return "ReaderMode"
}
required init(tab: Tab) {
self.tab = tab
// This is a WKUserScript at the moment because webView.evaluateJavaScript() fails with an unspecified error. Possibly script size related.
if let path = Bundle.main.path(forResource: "Readability", ofType: "js") {
if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
tab.webView!.configuration.userContentController.addUserScript(userScript)
}
}
// This is executed after a page has been loaded. It executes Readability and then fires a script message to let us know if the page is compatible with reader mode.
if let path = Bundle.main.path(forResource: "ReaderMode", ofType: "js") {
if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
tab.webView!.configuration.userContentController.addUserScript(userScript)
}
}
}
func scriptMessageHandlerName() -> String? {
return "readerModeMessageHandler"
}
fileprivate func handleReaderPageEvent(_ readerPageEvent: ReaderPageEvent) {
switch readerPageEvent {
case .pageShow:
if let tab = tab {
delegate?.readerMode(self, didDisplayReaderizedContentForTab: tab)
}
}
}
fileprivate func handleReaderModeStateChange(_ state: ReaderModeState) {
self.state = state
guard let tab = tab else {
return
}
delegate?.readerMode(self, didChangeReaderModeState: state, forTab: tab)
}
func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if let msg = message.body as? Dictionary<String, String> {
if let messageType = ReaderModeMessageType(rawValue: msg["Type"] ?? "") {
switch messageType {
case .pageEvent:
if let readerPageEvent = ReaderPageEvent(rawValue: msg["Value"] ?? "Invalid") {
handleReaderPageEvent(readerPageEvent)
}
break
case .stateChange:
if let readerModeState = ReaderModeState(rawValue: msg["Value"] ?? "Invalid") {
handleReaderModeStateChange(readerModeState)
}
break
}
}
}
}
var style: ReaderModeStyle = DefaultReaderModeStyle {
didSet {
if state == ReaderModeState.active {
tab?.webView?.evaluateJavaScript("\(ReaderModeNamespace).setStyle(\(style.encode()))", completionHandler: { (object, error) -> Void in
return
})
}
}
}
}