2 Commits
0.1.0 ... 0.1.2

4 changed files with 109 additions and 39 deletions

View File

@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git", "location" : "https://github.com/apple/swift-collections.git",
"state" : { "state" : {
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", "revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.0" "version" : "1.1.1"
} }
}, },
{ {

View File

@@ -14,7 +14,7 @@ let package = Package(
) )
], ],
dependencies: [ 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/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
], ],

View File

@@ -16,18 +16,20 @@ public class AppleEmojiCategory: Codable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(name) hasher.combine(name)
hasher.combine(emojis) hasher.combine(emojis)
hasher.combine(variations)
} }
public enum Name: String, CaseIterable, Codable { public enum Name: String, CaseIterable, Codable {
case flags = "flags" case frequentlyUsed = "frequentlyUsed"
case activity = "activity" case smileysAndPeople = "smileysAndPeople"
case objects = "objects"
case travelAndPlaces = "travelAndPlaces"
case symbols = "symbols"
case animalsAndNature = "animalsAndNature" case animalsAndNature = "animalsAndNature"
case foodAndDrink = "foodAndDrink" case foodAndDrink = "foodAndDrink"
case smileysAndPeople = "smileysAndPeople" case activity = "activity"
case travelAndPlaces = "travelAndPlaces"
case objects = "objects"
case symbols = "symbols"
case flags = "flags"
public static var orderedCases: [Name] { public static var orderedCases: [Name] {
return allCases.sorted(by: { $0.order < $1.order }) return allCases.sorted(by: { $0.order < $1.order })
@@ -35,22 +37,24 @@ public class AppleEmojiCategory: Codable, Hashable {
public var order: Int { public var order: Int {
switch self { switch self {
case .flags: case .frequentlyUsed:
return 8 return 0
case .activity: case .smileysAndPeople:
return 4 return 1
case .objects:
return 6
case .travelAndPlaces:
return 5
case .symbols:
return 7
case .animalsAndNature: case .animalsAndNature:
return 2 return 2
case .foodAndDrink: case .foodAndDrink:
return 3 return 3
case .smileysAndPeople: case .activity:
return 1 return 4
case .travelAndPlaces:
return 5
case .objects:
return 6
case .symbols:
return 7
case .flags:
return 8
} }
} }
@@ -66,10 +70,12 @@ public class AppleEmojiCategory: Codable, Hashable {
public let name: Name public let name: Name
public var emojis: OrderedDictionary<String, Emoji> public var emojis: OrderedDictionary<String, Emoji>
public var variations: [String: [Emoji]]
public init(name: Name, emojis: OrderedDictionary<String, Emoji>) { public init(name: Name, emojis: OrderedDictionary<String, Emoji>, variations: [String: [Emoji]]) {
self.name = name self.name = name
self.emojis = emojis self.emojis = emojis
self.variations = variations
} }
} }

View File

@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import OrderedCollections
public typealias EmojiCategory = AppleEmojiCategory 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 /// Returns all emojis for a specific version
/// - Parameters: /// - Parameters:
/// - version: The specific version you want to fetch (default: the highest supported version for a device's iOS version) /// - version: The specific version you want to fetch (default: the highest supported version for a device's iOS version)
@@ -59,25 +86,43 @@ public enum EmojiManager {
var filteredEmojis: [UnicodeEmojiCategory] = [] var filteredEmojis: [UnicodeEmojiCategory] = []
var appleCategories: [AppleEmojiCategory] = [] var appleCategories: [AppleEmojiCategory] = []
for category in result { for category in result {
let supportedEmojis = category.emojis.filter({ var variations = [String: [Emoji]]()
showAllVariations ? true : isNeutralEmoji(for: $0.key) var supportedEmojis = OrderedDictionary<String, Emoji>()
}) category.emojis.forEach {
if isNeutralEmoji(for: $0.key) {
supportedEmojis[$0.key] = $0.value
} else if showAllVariations {
let unqualifiedNeutralEmoji = unqualifiedNeutralEmoji(for: $0.key)
if let variationsForEmoji = variations[unqualifiedNeutralEmoji] {
variations[unqualifiedNeutralEmoji] = variationsForEmoji + [$0.value]
} else {
variations[unqualifiedNeutralEmoji] = [$0.value]
}
}
}
let unicodeCategory = UnicodeEmojiCategory(name: category.name, emojis: supportedEmojis) let unicodeCategory = UnicodeEmojiCategory(name: category.name, emojis: supportedEmojis)
filteredEmojis.append(unicodeCategory) filteredEmojis.append(unicodeCategory)
if shouldMergeCategory(category), let index = appleCategories.firstIndex(where: { $0.name == .smileysAndPeople }) { if shouldMergeCategory(category), let index = appleCategories.firstIndex(where: { $0.name == .smileysAndPeople }) {
if category.name == .smileysAndEmotions { if category.name == .smileysAndEmotions {
let oldValues = appleCategories[index].emojis let oldEmojis = appleCategories[index].emojis
appleCategories[index].emojis = supportedEmojis 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 { } else {
appleCategories[index].emojis.merge(supportedEmojis) { (current, _) in current } appleCategories[index].emojis.merge(supportedEmojis) { (current, _) in current }
appleCategories[index].variations.merge(variations) { (current, _) in current }
} }
} else { } else {
guard let appleCategory = unicodeCategory.appleCategory else { guard let appleCategory = unicodeCategory.appleCategory else {
continue 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 }) return appleCategories.sorted(by: { $0.name.order < $1.name.order })
@@ -89,19 +134,38 @@ public enum EmojiManager {
return category.name == .smileysAndEmotions || category.name == .peopleAndBody return category.name == .smileysAndEmotions || category.name == .peopleAndBody
} }
private static func isNeutralEmoji(for emoji: String) -> Bool { private static let skinToneRange: ClosedRange<UInt32> = 0x1F3FB...0x1F3FF
let unicodes = getUnicodes(emoji: emoji)
let colors = ["1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"]
for color in colors where unicodes.contains(color) { public static func isNeutralEmoji(for emojiValue: String) -> Bool {
return false return emojiValue.unicodeScalars.allSatisfy { !skinToneRange.contains($0.value) }
}
return true
} }
private static func getUnicodes(emoji: String) -> [String] { public static func isSkinToneModifier(scalar: Unicode.Scalar) -> Bool {
let unicodeScalars = emoji.unicodeScalars return skinToneRange.contains(scalar.value)
let unicodes = unicodeScalars.map { $0.value } }
return unicodes.map { String($0, radix: 16, uppercase: true) }
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)
}
}
let unicodeScalars = unqualifiedEmoji.unicodeScalars.map { $0.value }
if let actualUnqualifiedNeutralScalar = emojiSpecialMapping[unicodeScalars],
let actualUnqualifiedNeutralEmoji = uint32ToEmoji(actualUnqualifiedNeutralScalar) {
return String(actualUnqualifiedNeutralEmoji)
}
return unqualifiedEmoji
} }
} }