added iOS source code
[wl-app.git] / iOS / Pods / FolioReaderKit / Source / Models / Highlight+Helper.swift
1 //
2 //  Highlight+Helper.swift
3 //  FolioReaderKit
4 //
5 //  Created by Heberti Almeida on 06/07/16.
6 //  Copyright (c) 2015 Folio Reader. All rights reserved.
7 //
8
9 import Foundation
10 import RealmSwift
11
12 /**
13  HighlightStyle type, default is .Yellow.
14  */
15 public enum HighlightStyle: Int {
16     case yellow
17     case green
18     case blue
19     case pink
20     case underline
21
22     public init () {
23         // Default style is `.yellow`
24         self = .yellow
25     }
26
27     /**
28      Return HighlightStyle for CSS class.
29      */
30     public static func styleForClass(_ className: String) -> HighlightStyle {
31         switch className {
32         case "highlight-yellow":    return .yellow
33         case "highlight-green":     return .green
34         case "highlight-blue":      return .blue
35         case "highlight-pink":      return .pink
36         case "highlight-underline": return .underline
37         default:                    return .yellow
38         }
39     }
40
41     /**
42      Return CSS class for HighlightStyle.
43      */
44     public static func classForStyle(_ style: Int) -> String {
45
46         let enumStyle = (HighlightStyle(rawValue: style) ?? HighlightStyle())
47         switch enumStyle {
48         case .yellow:       return "highlight-yellow"
49         case .green:        return "highlight-green"
50         case .blue:         return "highlight-blue"
51         case .pink:         return "highlight-pink"
52         case .underline:    return "highlight-underline"
53         }
54     }
55
56     /// Color components for the style
57     ///
58     /// - Returns: Tuple of all color compnonents.
59     private func colorComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
60         switch self {
61         case .yellow:       return (red: 255, green: 235, blue: 107, alpha: 0.9)
62         case .green:        return (red: 192, green: 237, blue: 114, alpha: 0.9)
63         case .blue:         return (red: 173, green: 216, blue: 255, alpha: 0.9)
64         case .pink:         return (red: 255, green: 176, blue: 202, alpha: 0.9)
65         case .underline:    return (red: 240, green: 40, blue: 20, alpha: 0.6)
66         }
67     }
68
69     /**
70      Return CSS class for HighlightStyle.
71      */
72     public static func colorForStyle(_ style: Int, nightMode: Bool = false) -> UIColor {
73         let enumStyle = (HighlightStyle(rawValue: style) ?? HighlightStyle())
74         let colors = enumStyle.colorComponents()
75         return UIColor(red: colors.red/255, green: colors.green/255, blue: colors.blue/255, alpha: (nightMode ? colors.alpha : 1))
76     }
77 }
78
79 /// Completion block
80 public typealias Completion = (_ error: NSError?) -> ()
81
82 extension Highlight {
83
84     /// Save a Highlight with completion block
85     ///
86     /// - Parameters:
87     ///   - readerConfig: Current folio reader configuration.
88     ///   - completion: Completion block.
89     public func persist(withConfiguration readerConfig: FolioReaderConfig, completion: Completion? = nil) {
90         do {
91             let realm = try Realm(configuration: readerConfig.realmConfiguration)
92             realm.beginWrite()
93             realm.add(self, update: true)
94             try realm.commitWrite()
95             completion?(nil)
96         } catch let error as NSError {
97             print("Error on persist highlight: \(error)")
98             completion?(error)
99         }
100     }
101
102     /// Remove a Highlight
103     ///
104     /// - Parameter readerConfig: Current folio reader configuration.
105     public func remove(withConfiguration readerConfig: FolioReaderConfig) {
106         do {
107             guard let realm = try? Realm(configuration: readerConfig.realmConfiguration) else {
108                 return
109             }
110             try realm.write {
111                 realm.delete(self)
112                 try realm.commitWrite()
113             }
114         } catch let error as NSError {
115             print("Error on remove highlight: \(error)")
116         }
117     }
118
119     /// Remove a Highlight by ID
120     ///
121     /// - Parameters:
122     ///   - readerConfig: Current folio reader configuration.
123     ///   - highlightId: The ID to be removed
124     public static func removeById(withConfiguration readerConfig: FolioReaderConfig, highlightId: String) {
125         var highlight: Highlight?
126         let predicate = NSPredicate(format:"highlightId = %@", highlightId)
127
128         do {
129             let realm = try Realm(configuration: readerConfig.realmConfiguration)
130             highlight = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self).first
131             highlight?.remove(withConfiguration: readerConfig)
132         } catch let error as NSError {
133             print("Error on remove highlight by id: \(error)")
134         }
135     }
136
137     /// Update a Highlight by ID
138     ///
139     /// - Parameters:
140     ///   - readerConfig: Current folio reader configuration.
141     ///   - highlightId: The ID to be removed
142     ///   - type: The `HighlightStyle`
143     public static func updateById(withConfiguration readerConfig: FolioReaderConfig, highlightId: String, type: HighlightStyle) {
144         var highlight: Highlight?
145         let predicate = NSPredicate(format:"highlightId = %@", highlightId)
146         do {
147             let realm = try Realm(configuration: readerConfig.realmConfiguration)
148             highlight = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self).first
149             realm.beginWrite()
150
151             highlight?.type = type.hashValue
152
153             try realm.commitWrite()
154             
155         } catch let error as NSError {
156             print("Error on updateById: \(error)")
157         }
158
159     }
160
161     /// Return a list of Highlights with a given ID
162     ///
163     /// - Parameters:
164     ///   - readerConfig: Current folio reader configuration.
165     ///   - bookId: Book ID
166     ///   - page: Page number
167     /// - Returns: Return a list of Highlights
168     public static func allByBookId(withConfiguration readerConfig: FolioReaderConfig, bookId: String, andPage page: NSNumber? = nil) -> [Highlight] {
169         var highlights: [Highlight]?
170         var predicate = NSPredicate(format: "bookId = %@", bookId)
171         if let page = page {
172             predicate = NSPredicate(format: "bookId = %@ && page = %@", bookId, page)
173         }
174
175         do {
176             let realm = try Realm(configuration: readerConfig.realmConfiguration)
177             highlights = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self)
178             return (highlights ?? [])
179         } catch let error as NSError {
180             print("Error on fetch all by book Id: \(error)")
181             return []
182         }
183     }
184
185     /// Return all Highlights
186     ///
187     /// - Parameter readerConfig: - readerConfig: Current folio reader configuration.
188     /// - Returns: Return all Highlights
189     public static func all(withConfiguration readerConfig: FolioReaderConfig) -> [Highlight] {
190         var highlights: [Highlight]?
191         do {
192             let realm = try Realm(configuration: readerConfig.realmConfiguration)
193             highlights = realm.objects(Highlight.self).toArray(Highlight.self)
194             return (highlights ?? [])
195         } catch let error as NSError {
196             print("Error on fetch all: \(error)")
197             return []
198         }
199     }
200 }
201
202 // MARK: - HTML Methods
203
204 extension Highlight {
205
206     public struct MatchingHighlight {
207         var text: String
208         var id: String
209         var startOffset: String
210         var endOffset: String
211         var bookId: String
212         var currentPage: Int
213     }
214
215     /**
216      Match a highlight on string.
217      */
218     public static func matchHighlight(_ matchingHighlight: MatchingHighlight) -> Highlight? {
219         let pattern = "<highlight id=\"\(matchingHighlight.id)\" onclick=\".*?\" class=\"(.*?)\">((.|\\s)*?)</highlight>"
220         let regex = try? NSRegularExpression(pattern: pattern, options: [])
221         let matches = regex?.matches(in: matchingHighlight.text, options: [], range: NSRange(location: 0, length: matchingHighlight.text.utf16.count))
222         let str = (matchingHighlight.text as NSString)
223
224         let mapped = matches?.map { (match) -> Highlight in
225             var contentPre = str.substring(with: NSRange(location: match.range.location-kHighlightRange, length: kHighlightRange))
226             var contentPost = str.substring(with: NSRange(location: match.range.location + match.range.length, length: kHighlightRange))
227
228             // Normalize string before save
229             contentPre = Highlight.subString(ofContent: contentPre, fromRangeOfString: ">", withPattern: "((?=[^>]*$)(.|\\s)*$)")
230             contentPost = Highlight.subString(ofContent: contentPost, fromRangeOfString: "<", withPattern: "^((.|\\s)*?)(?=<)")
231
232             let highlight = Highlight()
233             highlight.highlightId = matchingHighlight.id
234             highlight.type = HighlightStyle.styleForClass(str.substring(with: match.range(at: 1))).rawValue
235             highlight.date = Date()
236             highlight.content = Highlight.removeSentenceSpam(str.substring(with: match.range(at: 2)))
237             highlight.contentPre = Highlight.removeSentenceSpam(contentPre)
238             highlight.contentPost = Highlight.removeSentenceSpam(contentPost)
239             highlight.page = matchingHighlight.currentPage
240             highlight.bookId = matchingHighlight.bookId
241             highlight.startOffset = (Int(matchingHighlight.startOffset) ?? -1)
242             highlight.endOffset = (Int(matchingHighlight.endOffset) ?? -1)
243
244             return highlight
245         }
246
247         return mapped?.first
248     }
249
250     private static func subString(ofContent content: String, fromRangeOfString rangeString: String, withPattern pattern: String) -> String {
251         var updatedContent = content
252         if updatedContent.range(of: rangeString) != nil {
253             let regex = try? NSRegularExpression(pattern: pattern, options: [])
254             let searchString = regex?.firstMatch(in: updatedContent, options: .reportProgress, range: NSRange(location: 0, length: updatedContent.count))
255
256             if let string = searchString, (string.range.location != NSNotFound) {
257                 updatedContent = (updatedContent as NSString).substring(with: string.range)
258             }
259         }
260
261         return updatedContent
262     }
263
264     /// Remove a Highlight from HTML by ID
265     ///
266     /// - Parameters:
267     ///   - page: The page containing the HTML.
268     ///   - highlightId: The ID to be removed
269     /// - Returns: The removed id
270     @discardableResult public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String) -> String? {
271         guard let currentPage = page else { return nil }
272         
273         if let removedId = currentPage.webView?.js("removeHighlightById('\(highlightId)')") {
274             return removedId
275         } else {
276             print("Error removing Highlight from page")
277             return nil
278         }
279     }
280     
281     /**
282      Remove span tag before store the highlight, this span is added on JavaScript.
283      <span class=\"sentence\"></span>
284      
285      - parameter text: Text to analise
286      - returns: Striped text
287      */
288     public static func removeSentenceSpam(_ text: String) -> String {
289         
290         // Remove from text
291         func removeFrom(_ text: String, withPattern pattern: String) -> String {
292             var locator = text
293             let regex = try? NSRegularExpression(pattern: pattern, options: [])
294             let matches = regex?.matches(in: locator, options: [], range: NSRange(location: 0, length: locator.utf16.count))
295             let str = (locator as NSString)
296             
297             var newLocator = ""
298             matches?.forEach({ (match: NSTextCheckingResult) in
299                 newLocator += str.substring(with: match.range(at: 1))
300             })
301             
302             if (matches?.count > 0 && newLocator.isEmpty == false) {
303                 locator = newLocator
304             }
305             
306             return locator
307         }
308         
309         let pattern = "<span class=\"sentence\">((.|\\s)*?)</span>"
310         let cleanText = removeFrom(text, withPattern: pattern)
311         return cleanText
312     }
313 }