diff --git a/Package.resolved b/Package.resolved index 60392aa..f244e32 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", + "version" : "1.1.1" } }, { diff --git a/Package.swift b/Package.swift index 595c92e..e3c3cc9 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.0")), + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.1")), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), ], diff --git a/Sources/EmojiKit/AppleEmojiCategory.swift b/Sources/EmojiKit/AppleEmojiCategory.swift index b396fca..2823b34 100644 --- a/Sources/EmojiKit/AppleEmojiCategory.swift +++ b/Sources/EmojiKit/AppleEmojiCategory.swift @@ -16,6 +16,7 @@ public class AppleEmojiCategory: Codable, Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(name) hasher.combine(emojis) + hasher.combine(variations) } @@ -66,10 +67,12 @@ public class AppleEmojiCategory: Codable, Hashable { public let name: Name public var emojis: OrderedDictionary + public var variations: [String: [Emoji]] - public init(name: Name, emojis: OrderedDictionary) { + public init(name: Name, emojis: OrderedDictionary, variations: [String: [Emoji]]) { self.name = name self.emojis = emojis + self.variations = variations } } diff --git a/Sources/EmojiKit/EmojiManager.swift b/Sources/EmojiKit/EmojiManager.swift index 6f94515..64f6d09 100644 --- a/Sources/EmojiKit/EmojiManager.swift +++ b/Sources/EmojiKit/EmojiManager.swift @@ -6,6 +6,7 @@ // import Foundation +import OrderedCollections public typealias EmojiCategory = AppleEmojiCategory @@ -47,6 +48,32 @@ public enum EmojiManager { } } + // When skin tone modifiers are stripped from some emojis, + // they don't have the same Unicode scalar values as the neutral version. + // We need to maintain a manual mapping so that the lists of variations are accurate. + private static let emojiSpecialMapping: [[UInt32]: UInt32] = [ + [0x1FAF1, 0x200D, 0x1FAF2]: 0x1F91D, // 🤝 handshake + [0x1F469, 0x200D, 0x1F91D, 0x200D, 0x1F469]: 0x1F46D, // 👭 women holding hands + [0x1F469, 0x200D, 0x1F91D, 0x200D, 0x1F468]: 0x1F46B, // 👫 woman and man holding hands + [0x1F468, 0x200D, 0x1F91D, 0x200D, 0x1F468]: 0x1F46C, // 👬 men holding hands + [0x1F9D1, 0x200D, 0x2764, 0x200D, 0x1F48B, 0x200D, 0x1F9D1]: 0x1F48F, // 💏 kiss: person, person + [0x1F9D1, 0x200D, 0x2764, 0x200D, 0x1F9D1]: 0x1F491, // 💑 couple with heart: person, person + ] + + private static func uint32ToEmoji(_ value: UInt32) -> String? { + // Create a Unicode scalar from the UInt32 value + guard let scalar = UnicodeScalar(value) else { + print("Invalid Unicode scalar value") + return nil + } + + // Create a Character from the Unicode scalar + let character = Character(scalar) + + // Convert the Character to a String and return it + return String(character) + } + /// Returns all emojis for a specific version /// - Parameters: /// - version: The specific version you want to fetch (default: the highest supported version for a device's iOS version) @@ -59,25 +86,49 @@ public enum EmojiManager { var filteredEmojis: [UnicodeEmojiCategory] = [] var appleCategories: [AppleEmojiCategory] = [] for category in result { - let supportedEmojis = category.emojis.filter({ - showAllVariations ? true : isNeutralEmoji(for: $0.key) - }) + var variations = [String: [Emoji]]() + var supportedEmojis = OrderedDictionary() + category.emojis.forEach { + if isNeutralEmoji(for: $0.key) { + supportedEmojis[$0.key] = $0.value + } else if showAllVariations { + var unqualifiedNeutralEmoji = unqualifiedNeutralEmoji(for: $0.key) + + let unicodeScalars = unqualifiedNeutralEmoji.unicodeScalars.map { $0.value } + if let actualUnqualifiedNeutralScalar = emojiSpecialMapping[unicodeScalars], + let actualUnqualifiedNeutralEmoji = uint32ToEmoji(actualUnqualifiedNeutralScalar) { + unqualifiedNeutralEmoji = String(actualUnqualifiedNeutralEmoji) + } + + if let variationsForEmoji = variations[unqualifiedNeutralEmoji] { + variations[unqualifiedNeutralEmoji] = variationsForEmoji + [$0.value] + } else { + variations[unqualifiedNeutralEmoji] = [$0.value] + } + } + } + let unicodeCategory = UnicodeEmojiCategory(name: category.name, emojis: supportedEmojis) filteredEmojis.append(unicodeCategory) if shouldMergeCategory(category), let index = appleCategories.firstIndex(where: { $0.name == .smileysAndPeople }) { if category.name == .smileysAndEmotions { - let oldValues = appleCategories[index].emojis + let oldEmojis = appleCategories[index].emojis appleCategories[index].emojis = supportedEmojis - appleCategories[index].emojis.merge(oldValues) { (current, _) in current } + appleCategories[index].emojis.merge(oldEmojis) { (current, _) in current } + + let oldVariations = appleCategories[index].variations + appleCategories[index].variations = variations + appleCategories[index].variations.merge(oldVariations) { (current, _) in current } } else { appleCategories[index].emojis.merge(supportedEmojis) { (current, _) in current } + appleCategories[index].variations.merge(variations) { (current, _) in current } } } else { guard let appleCategory = unicodeCategory.appleCategory else { continue } - appleCategories.append(AppleEmojiCategory(name: appleCategory, emojis: supportedEmojis)) + appleCategories.append(AppleEmojiCategory(name: appleCategory, emojis: supportedEmojis, variations: variations)) } } return appleCategories.sorted(by: { $0.name.order < $1.name.order }) @@ -89,19 +140,32 @@ public enum EmojiManager { return category.name == .smileysAndEmotions || category.name == .peopleAndBody } - private static func isNeutralEmoji(for emoji: String) -> Bool { - let unicodes = getUnicodes(emoji: emoji) - let colors = ["1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"] + private static let skinToneRange: ClosedRange = 0x1F3FB...0x1F3FF - for color in colors where unicodes.contains(color) { - return false - } - return true + public static func isNeutralEmoji(for emojiValue: String) -> Bool { + return emojiValue.unicodeScalars.allSatisfy { !skinToneRange.contains($0.value) } } - private static func getUnicodes(emoji: String) -> [String] { - let unicodeScalars = emoji.unicodeScalars - let unicodes = unicodeScalars.map { $0.value } - return unicodes.map { String($0, radix: 16, uppercase: true) } + public static func isSkinToneModifier(scalar: Unicode.Scalar) -> Bool { + return skinToneRange.contains(scalar.value) + } + + public static func neutralEmoji(for emojiValue: String) -> String { + let filteredScalars = emojiValue.unicodeScalars.filter { !isSkinToneModifier(scalar: $0) } + return String(String.UnicodeScalarView(filteredScalars)) + } + + public static func unqualifiedNeutralEmoji(for emoji: String) -> String { + let variationSelector: Character = "\u{FE0F}" + var unqualifiedEmoji = "" + + for scalar in neutralEmoji(for: emoji).unicodeScalars { + let character = Character(scalar) + if character != variationSelector { + unqualifiedEmoji.append(character) + } + } + + return unqualifiedEmoji } }