2 // Highlight+Helper.swift
5 // Created by Heberti Almeida on 06/07/16.
6 // Copyright (c) 2015 Folio Reader. All rights reserved.
13 HighlightStyle type, default is .Yellow.
15 public enum HighlightStyle: Int {
23 // Default style is `.yellow`
28 Return HighlightStyle for CSS class.
30 public static func styleForClass(_ className: String) -> HighlightStyle {
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
42 Return CSS class for HighlightStyle.
44 public static func classForStyle(_ style: Int) -> String {
46 let enumStyle = (HighlightStyle(rawValue: style) ?? HighlightStyle())
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"
56 /// Color components for the style
58 /// - Returns: Tuple of all color compnonents.
59 private func colorComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
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)
70 Return CSS class for HighlightStyle.
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))
80 public typealias Completion = (_ error: NSError?) -> ()
84 /// Save a Highlight with completion block
87 /// - readerConfig: Current folio reader configuration.
88 /// - completion: Completion block.
89 public func persist(withConfiguration readerConfig: FolioReaderConfig, completion: Completion? = nil) {
91 let realm = try Realm(configuration: readerConfig.realmConfiguration)
93 realm.add(self, update: true)
94 try realm.commitWrite()
96 } catch let error as NSError {
97 print("Error on persist highlight: \(error)")
102 /// Remove a Highlight
104 /// - Parameter readerConfig: Current folio reader configuration.
105 public func remove(withConfiguration readerConfig: FolioReaderConfig) {
107 guard let realm = try? Realm(configuration: readerConfig.realmConfiguration) else {
112 try realm.commitWrite()
114 } catch let error as NSError {
115 print("Error on remove highlight: \(error)")
119 /// Remove a Highlight by ID
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)
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)")
137 /// Update a Highlight by ID
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)
147 let realm = try Realm(configuration: readerConfig.realmConfiguration)
148 highlight = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self).first
151 highlight?.type = type.hashValue
153 try realm.commitWrite()
155 } catch let error as NSError {
156 print("Error on updateById: \(error)")
161 /// Return a list of Highlights with a given ID
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)
172 predicate = NSPredicate(format: "bookId = %@ && page = %@", bookId, page)
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)")
185 /// Return all Highlights
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]?
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)")
202 // MARK: - HTML Methods
204 extension Highlight {
206 public struct MatchingHighlight {
209 var startOffset: String
210 var endOffset: String
216 Match a highlight on string.
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)
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))
228 // Normalize string before save
229 contentPre = Highlight.subString(ofContent: contentPre, fromRangeOfString: ">", withPattern: "((?=[^>]*$)(.|\\s)*$)")
230 contentPost = Highlight.subString(ofContent: contentPost, fromRangeOfString: "<", withPattern: "^((.|\\s)*?)(?=<)")
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)
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))
256 if let string = searchString, (string.range.location != NSNotFound) {
257 updatedContent = (updatedContent as NSString).substring(with: string.range)
261 return updatedContent
264 /// Remove a Highlight from HTML by ID
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 }
273 if let removedId = currentPage.webView?.js("removeHighlightById('\(highlightId)')") {
276 print("Error removing Highlight from page")
282 Remove span tag before store the highlight, this span is added on JavaScript.
283 <span class=\"sentence\"></span>
285 - parameter text: Text to analise
286 - returns: Striped text
288 public static func removeSentenceSpam(_ text: String) -> String {
291 func removeFrom(_ text: String, withPattern pattern: String) -> String {
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)
298 matches?.forEach({ (match: NSTextCheckingResult) in
299 newLocator += str.substring(with: match.range(at: 1))
302 if (matches?.count > 0 && newLocator.isEmpty == false) {
309 let pattern = "<span class=\"sentence\">((.|\\s)*?)</span>"
310 let cleanText = removeFrom(text, withPattern: pattern)