session-ios/Signal/src/util/DisplayableText.swift

226 lines
7.7 KiB
Swift

//
// 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
}
}