// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // import Foundation extension UnicodeScalar { class EmojiRange { // rangeStart and rangeEnd are inclusive. let rangeStart: UInt32 let rangeEnd: UInt32 // MARK: Initializers init(rangeStart: UInt32, rangeEnd: UInt32) { self.rangeStart = rangeStart self.rangeEnd = rangeEnd } } // From: // https://www.unicode.org/Public/emoji/ // Current Version: // https://www.unicode.org/Public/emoji/6.0/emoji-data.txt // // These ranges can be code-generated using: // // * Scripts/emoji-data.txt // * Scripts/emoji_ranges.py static let kEmojiRanges = [ EmojiRange(rangeStart:0x23, rangeEnd:0x23), EmojiRange(rangeStart:0x2A, rangeEnd:0x2A), EmojiRange(rangeStart:0x30, rangeEnd:0x39), EmojiRange(rangeStart:0xA9, rangeEnd:0xA9), EmojiRange(rangeStart:0xAE, rangeEnd:0xAE), EmojiRange(rangeStart:0x200D, rangeEnd:0x200D), EmojiRange(rangeStart:0x203C, rangeEnd:0x203C), EmojiRange(rangeStart:0x2049, rangeEnd:0x2049), EmojiRange(rangeStart:0x20D0, rangeEnd:0x20FF), EmojiRange(rangeStart:0x2122, rangeEnd:0x2122), EmojiRange(rangeStart:0x2139, rangeEnd:0x2139), EmojiRange(rangeStart:0x2194, rangeEnd:0x2199), EmojiRange(rangeStart:0x21A9, rangeEnd:0x21AA), EmojiRange(rangeStart:0x231A, rangeEnd:0x231B), EmojiRange(rangeStart:0x2328, rangeEnd:0x2328), EmojiRange(rangeStart:0x2388, rangeEnd:0x2388), EmojiRange(rangeStart:0x23CF, rangeEnd:0x23CF), EmojiRange(rangeStart:0x23E9, rangeEnd:0x23F3), EmojiRange(rangeStart:0x23F8, rangeEnd:0x23FA), EmojiRange(rangeStart:0x24C2, rangeEnd:0x24C2), EmojiRange(rangeStart:0x25AA, rangeEnd:0x25AB), EmojiRange(rangeStart:0x25B6, rangeEnd:0x25B6), EmojiRange(rangeStart:0x25C0, rangeEnd:0x25C0), EmojiRange(rangeStart:0x25FB, rangeEnd:0x25FE), EmojiRange(rangeStart:0x2600, rangeEnd:0x27BF), EmojiRange(rangeStart:0x2934, rangeEnd:0x2935), EmojiRange(rangeStart:0x2B05, rangeEnd:0x2B07), EmojiRange(rangeStart:0x2B1B, rangeEnd:0x2B1C), EmojiRange(rangeStart:0x2B50, rangeEnd:0x2B50), EmojiRange(rangeStart:0x2B55, rangeEnd:0x2B55), EmojiRange(rangeStart:0x3030, rangeEnd:0x3030), EmojiRange(rangeStart:0x303D, rangeEnd:0x303D), EmojiRange(rangeStart:0x3297, rangeEnd:0x3297), EmojiRange(rangeStart:0x3299, rangeEnd:0x3299), EmojiRange(rangeStart:0xFE00, rangeEnd:0xFE0F), EmojiRange(rangeStart:0x1F000, rangeEnd:0x1F0FF), EmojiRange(rangeStart:0x1F10D, rangeEnd:0x1F10F), EmojiRange(rangeStart:0x1F12F, rangeEnd:0x1F12F), EmojiRange(rangeStart:0x1F16C, rangeEnd:0x1F171), EmojiRange(rangeStart:0x1F17E, rangeEnd:0x1F17F), EmojiRange(rangeStart:0x1F18E, rangeEnd:0x1F18E), EmojiRange(rangeStart:0x1F191, rangeEnd:0x1F19A), EmojiRange(rangeStart:0x1F1AD, rangeEnd:0x1F1FF), EmojiRange(rangeStart:0x1F201, rangeEnd:0x1F20F), EmojiRange(rangeStart:0x1F21A, rangeEnd:0x1F21A), EmojiRange(rangeStart:0x1F22F, rangeEnd:0x1F22F), EmojiRange(rangeStart:0x1F232, rangeEnd:0x1F23A), EmojiRange(rangeStart:0x1F23C, rangeEnd:0x1F23F), EmojiRange(rangeStart:0x1F249, rangeEnd:0x1F64F), EmojiRange(rangeStart:0x1F680, rangeEnd:0x1F6FF), EmojiRange(rangeStart:0x1F774, rangeEnd:0x1F77F), EmojiRange(rangeStart:0x1F7D5, rangeEnd:0x1F7FF), EmojiRange(rangeStart:0x1F80C, rangeEnd:0x1F80F), EmojiRange(rangeStart:0x1F848, rangeEnd:0x1F84F), EmojiRange(rangeStart:0x1F85A, rangeEnd:0x1F85F), EmojiRange(rangeStart:0x1F888, rangeEnd:0x1F88F), EmojiRange(rangeStart:0x1F8AE, rangeEnd:0x1FFFD), EmojiRange(rangeStart:0xE0020, rangeEnd:0xE007F) ] var isEmoji: Bool { // Binary search. var left: Int = 0 var right = Int(UnicodeScalar.kEmojiRanges.count - 1) while true { let mid = (left + right) / 2 let midRange = UnicodeScalar.kEmojiRanges[mid] if value < midRange.rangeStart { if mid == left { return false } right = mid - 1 } else if value > midRange.rangeEnd { if mid == right { return false } left = mid + 1 } else { return true } } } var isZeroWidthJoiner: Bool { return value == 8205 } } extension String { var glyphCount: Int { let richText = NSAttributedString(string: self) let line = CTLineCreateWithAttributedString(richText) return CTLineGetGlyphCount(line) } var isSingleEmoji: Bool { return glyphCount == 1 && containsEmoji } var containsEmoji: Bool { return unicodeScalars.contains { $0.isEmoji } } var containsOnlyEmoji: Bool { return !isEmpty && !unicodeScalars.contains(where: { !$0.isEmoji && !$0.isZeroWidthJoiner }) } } @objc class DisplayableText: NSObject { static let TAG = "[DisplayableText]" let fullText: String let displayText: String let isTextTruncated: Bool let jumbomojiCount: UInt static let kMaxJumbomojiCount: UInt = 5 // This value is a bit arbitrary since we don't need to be 100% correct about // rendering "Jumbomoji". It allows us to place an upper bound on worst-case // performacne. static let kMaxCharactersPerEmojiCount: UInt = 10 // MARK: Initializers init(fullText: String, displayText: String, isTextTruncated: Bool) { self.fullText = fullText self.displayText = displayText self.isTextTruncated = isTextTruncated self.jumbomojiCount = DisplayableText.jumbomojiCount(in:fullText) } // MARK: Emoji // If the string is... // // * Non-empty // * Only contains emoji // * Contains <= kMaxJumbomojiCount emoji // // ...return the number of emoji (to be treated as "Jumbomoji") in the string. private class func jumbomojiCount(in string: String) -> UInt { if string == "" { return 0 } if string.characters.count > Int(kMaxJumbomojiCount * kMaxCharactersPerEmojiCount) { return 0 } guard string.containsOnlyEmoji else { return 0 } let emojiCount = string.glyphCount if UInt(emojiCount) > kMaxJumbomojiCount { return 0 } return UInt(emojiCount) } // MARK: Filter Methods @objc class func displayableText(_ text: String?) -> String? { guard let text = text?.ows_stripped() else { return nil } if (self.hasExcessiveDiacriticals(text: text)) { Logger.warn("\(TAG) filtering text for excessive diacriticals.") let filteredText = text.folding(options: .diacriticInsensitive, locale: .current) return filteredText.ows_stripped() } return text.ows_stripped() } private class func hasExcessiveDiacriticals(text: String) -> Bool { // discard any zalgo style text, by detecting maximum number of glyphs per character for char in text.characters.enumerated() { let scalarCount = String(char.element).unicodeScalars.count if scalarCount > 4 { Logger.warn("\(TAG) detected excessive diacriticals at \(char.element) scalarCount: \(scalarCount)") return true } } return false } }