// // Highlight+Helper.swift // FolioReaderKit // // Created by Heberti Almeida on 06/07/16. // Copyright (c) 2015 Folio Reader. All rights reserved. // import Foundation import RealmSwift /** HighlightStyle type, default is .Yellow. */ public enum HighlightStyle: Int { case yellow case green case blue case pink case underline public init () { // Default style is `.yellow` self = .yellow } /** Return HighlightStyle for CSS class. */ public static func styleForClass(_ className: String) -> HighlightStyle { switch className { case "highlight-yellow": return .yellow case "highlight-green": return .green case "highlight-blue": return .blue case "highlight-pink": return .pink case "highlight-underline": return .underline default: return .yellow } } /** Return CSS class for HighlightStyle. */ public static func classForStyle(_ style: Int) -> String { let enumStyle = (HighlightStyle(rawValue: style) ?? HighlightStyle()) switch enumStyle { case .yellow: return "highlight-yellow" case .green: return "highlight-green" case .blue: return "highlight-blue" case .pink: return "highlight-pink" case .underline: return "highlight-underline" } } /// Color components for the style /// /// - Returns: Tuple of all color compnonents. private func colorComponents() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { switch self { case .yellow: return (red: 255, green: 235, blue: 107, alpha: 0.9) case .green: return (red: 192, green: 237, blue: 114, alpha: 0.9) case .blue: return (red: 173, green: 216, blue: 255, alpha: 0.9) case .pink: return (red: 255, green: 176, blue: 202, alpha: 0.9) case .underline: return (red: 240, green: 40, blue: 20, alpha: 0.6) } } /** Return CSS class for HighlightStyle. */ public static func colorForStyle(_ style: Int, nightMode: Bool = false) -> UIColor { let enumStyle = (HighlightStyle(rawValue: style) ?? HighlightStyle()) let colors = enumStyle.colorComponents() return UIColor(red: colors.red/255, green: colors.green/255, blue: colors.blue/255, alpha: (nightMode ? colors.alpha : 1)) } } /// Completion block public typealias Completion = (_ error: NSError?) -> () extension Highlight { /// Save a Highlight with completion block /// /// - Parameters: /// - readerConfig: Current folio reader configuration. /// - completion: Completion block. public func persist(withConfiguration readerConfig: FolioReaderConfig, completion: Completion? = nil) { do { let realm = try Realm(configuration: readerConfig.realmConfiguration) realm.beginWrite() realm.add(self, update: true) try realm.commitWrite() completion?(nil) } catch let error as NSError { print("Error on persist highlight: \(error)") completion?(error) } } /// Remove a Highlight /// /// - Parameter readerConfig: Current folio reader configuration. public func remove(withConfiguration readerConfig: FolioReaderConfig) { do { guard let realm = try? Realm(configuration: readerConfig.realmConfiguration) else { return } try realm.write { realm.delete(self) try realm.commitWrite() } } catch let error as NSError { print("Error on remove highlight: \(error)") } } /// Remove a Highlight by ID /// /// - Parameters: /// - readerConfig: Current folio reader configuration. /// - highlightId: The ID to be removed public static func removeById(withConfiguration readerConfig: FolioReaderConfig, highlightId: String) { var highlight: Highlight? let predicate = NSPredicate(format:"highlightId = %@", highlightId) do { let realm = try Realm(configuration: readerConfig.realmConfiguration) highlight = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self).first highlight?.remove(withConfiguration: readerConfig) } catch let error as NSError { print("Error on remove highlight by id: \(error)") } } /// Update a Highlight by ID /// /// - Parameters: /// - readerConfig: Current folio reader configuration. /// - highlightId: The ID to be removed /// - type: The `HighlightStyle` public static func updateById(withConfiguration readerConfig: FolioReaderConfig, highlightId: String, type: HighlightStyle) { var highlight: Highlight? let predicate = NSPredicate(format:"highlightId = %@", highlightId) do { let realm = try Realm(configuration: readerConfig.realmConfiguration) highlight = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self).first realm.beginWrite() highlight?.type = type.hashValue try realm.commitWrite() } catch let error as NSError { print("Error on updateById: \(error)") } } /// Return a list of Highlights with a given ID /// /// - Parameters: /// - readerConfig: Current folio reader configuration. /// - bookId: Book ID /// - page: Page number /// - Returns: Return a list of Highlights public static func allByBookId(withConfiguration readerConfig: FolioReaderConfig, bookId: String, andPage page: NSNumber? = nil) -> [Highlight] { var highlights: [Highlight]? var predicate = NSPredicate(format: "bookId = %@", bookId) if let page = page { predicate = NSPredicate(format: "bookId = %@ && page = %@", bookId, page) } do { let realm = try Realm(configuration: readerConfig.realmConfiguration) highlights = realm.objects(Highlight.self).filter(predicate).toArray(Highlight.self) return (highlights ?? []) } catch let error as NSError { print("Error on fetch all by book Id: \(error)") return [] } } /// Return all Highlights /// /// - Parameter readerConfig: - readerConfig: Current folio reader configuration. /// - Returns: Return all Highlights public static func all(withConfiguration readerConfig: FolioReaderConfig) -> [Highlight] { var highlights: [Highlight]? do { let realm = try Realm(configuration: readerConfig.realmConfiguration) highlights = realm.objects(Highlight.self).toArray(Highlight.self) return (highlights ?? []) } catch let error as NSError { print("Error on fetch all: \(error)") return [] } } } // MARK: - HTML Methods extension Highlight { public struct MatchingHighlight { var text: String var id: String var startOffset: String var endOffset: String var bookId: String var currentPage: Int } /** Match a highlight on string. */ public static func matchHighlight(_ matchingHighlight: MatchingHighlight) -> Highlight? { let pattern = "((.|\\s)*?)" let regex = try? NSRegularExpression(pattern: pattern, options: []) let matches = regex?.matches(in: matchingHighlight.text, options: [], range: NSRange(location: 0, length: matchingHighlight.text.utf16.count)) let str = (matchingHighlight.text as NSString) let mapped = matches?.map { (match) -> Highlight in var contentPre = str.substring(with: NSRange(location: match.range.location-kHighlightRange, length: kHighlightRange)) var contentPost = str.substring(with: NSRange(location: match.range.location + match.range.length, length: kHighlightRange)) // Normalize string before save contentPre = Highlight.subString(ofContent: contentPre, fromRangeOfString: ">", withPattern: "((?=[^>]*$)(.|\\s)*$)") contentPost = Highlight.subString(ofContent: contentPost, fromRangeOfString: "<", withPattern: "^((.|\\s)*?)(?=<)") let highlight = Highlight() highlight.highlightId = matchingHighlight.id highlight.type = HighlightStyle.styleForClass(str.substring(with: match.range(at: 1))).rawValue highlight.date = Date() highlight.content = Highlight.removeSentenceSpam(str.substring(with: match.range(at: 2))) highlight.contentPre = Highlight.removeSentenceSpam(contentPre) highlight.contentPost = Highlight.removeSentenceSpam(contentPost) highlight.page = matchingHighlight.currentPage highlight.bookId = matchingHighlight.bookId highlight.startOffset = (Int(matchingHighlight.startOffset) ?? -1) highlight.endOffset = (Int(matchingHighlight.endOffset) ?? -1) return highlight } return mapped?.first } private static func subString(ofContent content: String, fromRangeOfString rangeString: String, withPattern pattern: String) -> String { var updatedContent = content if updatedContent.range(of: rangeString) != nil { let regex = try? NSRegularExpression(pattern: pattern, options: []) let searchString = regex?.firstMatch(in: updatedContent, options: .reportProgress, range: NSRange(location: 0, length: updatedContent.count)) if let string = searchString, (string.range.location != NSNotFound) { updatedContent = (updatedContent as NSString).substring(with: string.range) } } return updatedContent } /// Remove a Highlight from HTML by ID /// /// - Parameters: /// - page: The page containing the HTML. /// - highlightId: The ID to be removed /// - Returns: The removed id @discardableResult public static func removeFromHTMLById(withinPage page: FolioReaderPage?, highlightId: String) -> String? { guard let currentPage = page else { return nil } if let removedId = currentPage.webView?.js("removeHighlightById('\(highlightId)')") { return removedId } else { print("Error removing Highlight from page") return nil } } /** Remove span tag before store the highlight, this span is added on JavaScript. - parameter text: Text to analise - returns: Striped text */ public static func removeSentenceSpam(_ text: String) -> String { // Remove from text func removeFrom(_ text: String, withPattern pattern: String) -> String { var locator = text let regex = try? NSRegularExpression(pattern: pattern, options: []) let matches = regex?.matches(in: locator, options: [], range: NSRange(location: 0, length: locator.utf16.count)) let str = (locator as NSString) var newLocator = "" matches?.forEach({ (match: NSTextCheckingResult) in newLocator += str.substring(with: match.range(at: 1)) }) if (matches?.count > 0 && newLocator.isEmpty == false) { locator = newLocator } return locator } let pattern = "((.|\\s)*?)" let cleanText = removeFrom(text, withPattern: pattern) return cleanText } }