444 lines
17 KiB
Swift
Executable File
444 lines
17 KiB
Swift
Executable File
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
#!/usr/bin/env xcrun --sdk macosx swift
|
|
|
|
import Foundation
|
|
|
|
class EmojiGenerator {
|
|
// from http://stackoverflow.com/a/31480534/255489
|
|
static var pathToFolderContainingThisScript: URL? = {
|
|
let cwd = FileManager.default.currentDirectoryPath
|
|
|
|
let script = CommandLine.arguments[0]
|
|
|
|
if script.hasPrefix("/") { // absolute
|
|
let path = (script as NSString).deletingLastPathComponent
|
|
return URL(fileURLWithPath: path)
|
|
} else { // relative
|
|
let urlCwd = URL(fileURLWithPath: cwd)
|
|
|
|
if let urlPath = URL(string: script, relativeTo: urlCwd) {
|
|
let path = (urlPath.path as NSString).deletingLastPathComponent
|
|
return URL(fileURLWithPath: path)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}()
|
|
|
|
static let emojiDirectory = URL(fileURLWithPath: "../Signal/src/util/Emoji", isDirectory: true, relativeTo: pathToFolderContainingThisScript!)
|
|
|
|
enum EmojiCategory: String, Codable, Equatable {
|
|
case smileys = "Smileys & Emotion"
|
|
case people = "People & Body"
|
|
|
|
// This category is not provided in the data set, but is actually
|
|
// a merger of the categories of `smileys` and `people`
|
|
case smileysAndPeople = "Smileys & People"
|
|
|
|
case animals = "Animals & Nature"
|
|
case food = "Food & Drink"
|
|
case activities = "Activities"
|
|
case travel = "Travel & Places"
|
|
case objects = "Objects"
|
|
case symbols = "Symbols"
|
|
case flags = "Flags"
|
|
case skinTones = "Skin Tones"
|
|
}
|
|
|
|
enum SkinTone: String, CaseIterable, Equatable {
|
|
case light = "1F3FB"
|
|
case mediumLight = "1F3FC"
|
|
case medium = "1F3FD"
|
|
case mediumDark = "1F3FE"
|
|
case dark = "1F3FF"
|
|
|
|
var sortId: Int { return SkinTone.allCases.firstIndex(of: self)! }
|
|
|
|
var unicodeScalar: UnicodeScalar { UnicodeScalar(Int(rawValue, radix: 16)!)! }
|
|
}
|
|
|
|
static let outputCategories: [EmojiCategory] = [
|
|
.smileysAndPeople,
|
|
.animals,
|
|
.food,
|
|
.activities,
|
|
.travel,
|
|
.objects,
|
|
.symbols,
|
|
.flags
|
|
]
|
|
|
|
struct SkinVariation: Codable {
|
|
let unified: String
|
|
|
|
var emoji: String {
|
|
let unicodeComponents = unified.components(separatedBy: "-").map { Int($0, radix: 16)! }
|
|
return unicodeComponents.map { String(UnicodeScalar($0)!) }.joined()
|
|
}
|
|
}
|
|
|
|
struct EmojiData: Codable {
|
|
let name: String?
|
|
let shortName: String
|
|
let unified: String
|
|
let sortOrder: UInt
|
|
let category: EmojiCategory
|
|
let skinVariations: [String: SkinVariation]?
|
|
|
|
var enumName: String {
|
|
// some names don't play nice with swift, so we special case them
|
|
switch shortName {
|
|
case "+1": return "plusOne"
|
|
case "-1": return "negativeOne"
|
|
case "8ball": return "eightBall"
|
|
case "repeat": return "`repeat`"
|
|
case "100": return "oneHundred"
|
|
case "1234": return "oneTwoThreeFour"
|
|
case "couplekiss": return "personKissPerson"
|
|
case "couple": return "womanAndManHoldingHands"
|
|
case "couple_with_heart": return "personHeartPerson"
|
|
case "beetle":
|
|
if name == "LADY BEETLE" { return "ladyBeetle" }
|
|
return "beetle"
|
|
case "man_in_tuxedo":
|
|
if name == nil { return "manInTuxedo" }
|
|
return "personInTuxedo"
|
|
default:
|
|
let uppperCamelCase = shortName.replacingOccurrences(of: "-", with: "_").components(separatedBy: "_").map(titlecase).joined(separator: "")
|
|
return String(uppperCamelCase.unicodeScalars.first!).lowercased() + String(uppperCamelCase.unicodeScalars.dropFirst())
|
|
}
|
|
}
|
|
|
|
var emoji: String {
|
|
let unicodeComponents = unified.components(separatedBy: "-").map { Int($0, radix: 16)! }
|
|
return unicodeComponents.map { String(UnicodeScalar($0)!) }.joined()
|
|
}
|
|
|
|
var hasSkinVariations: Bool { skinVariations?.isEmpty == false }
|
|
var emojiPerSkinTone: [[SkinTone]: String]? {
|
|
guard let skinVariations = skinVariations else { return nil }
|
|
var emojiPerSkinTone = [[SkinTone]: String]()
|
|
for (key, value) in skinVariations {
|
|
let skinTones = key
|
|
.components(separatedBy: "-")
|
|
.map { SkinTone(rawValue: $0)! }
|
|
.reduce(into: [SkinTone]()) { result, skinTone in
|
|
guard !result.contains(skinTone) else { return }
|
|
result.append(skinTone)
|
|
}
|
|
emojiPerSkinTone[skinTones] = value.emoji
|
|
}
|
|
return emojiPerSkinTone
|
|
}
|
|
var sortedEmojiPerSkinTone: [([SkinTone], String)]? {
|
|
guard let emojiPerSkinTone = emojiPerSkinTone else { return nil }
|
|
return emojiPerSkinTone.sorted { lhs, rhs in
|
|
var index = 0
|
|
while true {
|
|
if index >= lhs.key.count {
|
|
return true
|
|
}
|
|
|
|
if index >= rhs.key.count {
|
|
return false
|
|
}
|
|
|
|
let lhsSkinTone = lhs.key[index]
|
|
let rhsSkinTone = rhs.key[index]
|
|
|
|
if lhsSkinTone != rhsSkinTone {
|
|
return lhsSkinTone.sortId < rhsSkinTone.sortId
|
|
}
|
|
|
|
index += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
var allowsMultipleSkinTones: Bool { hasSkinVariations && emojiPerSkinTone!.count > 5 }
|
|
|
|
var skinToneComponents: String? {
|
|
// There's no great way to do this except manually. Some emoji have multiple skin tones.
|
|
// In the picker, we need to use one emoji to represent each person. For now, we manually
|
|
// specify this. Hopefully, in the future, the data set will contain this information.
|
|
switch shortName {
|
|
case "two_women_holding_hands": return "[.womanStanding, .womanStanding]"
|
|
case "two_men_holding_hands": return "[.manStanding, .manStanding]"
|
|
case "people_holding_hands": return "[.standingPerson, .standingPerson]"
|
|
case "couple": return "[.womanStanding, .manStanding]"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func titlecase(_ value: String) -> String {
|
|
guard let first = value.unicodeScalars.first else { return value }
|
|
return String(first).uppercased() + String(value.unicodeScalars.dropFirst())
|
|
}
|
|
}
|
|
|
|
static func generate() {
|
|
guard let jsonData = try? Data(contentsOf: URL(string: "https://unicodey.com/emoji-data/emoji.json")!) else {
|
|
fatalError("Failed to download emoji-data json")
|
|
}
|
|
|
|
let jsonDecoder = JSONDecoder()
|
|
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
|
|
let sortedEmojiData = try! jsonDecoder.decode([EmojiData].self, from: jsonData)
|
|
.sorted { $0.sortOrder < $1.sortOrder }
|
|
.filter { $0.category != .skinTones } // for now, we don't care about skin tones
|
|
|
|
// Main enum
|
|
writeBlock(fileName: "Emoji.swift") { fileHandle in
|
|
fileHandle.writeLine("/// A sorted representation of all available emoji")
|
|
fileHandle.writeLine("enum Emoji: String, CaseIterable, Equatable {")
|
|
|
|
for emojiData in sortedEmojiData {
|
|
fileHandle.writeLine(" case \(emojiData.enumName) = \"\(emojiData.emoji)\"")
|
|
}
|
|
|
|
fileHandle.writeLine("}")
|
|
}
|
|
|
|
// Conversion from String
|
|
writeBlock(fileName: "EmojiWithSkinTones+String.swift") { fileHandle in
|
|
fileHandle.writeLine("extension EmojiWithSkinTones {")
|
|
|
|
fileHandle.writeLine(" init?(rawValue: String) {")
|
|
fileHandle.writeLine(" guard rawValue.isSingleEmoji else { return nil }")
|
|
fileHandle.writeLine(" switch rawValue {")
|
|
|
|
for emojiData in sortedEmojiData {
|
|
fileHandle.writeLine(" case \"\(emojiData.emoji)\": self.init(baseEmoji: .\(emojiData.enumName), skinTones: nil)")
|
|
|
|
if let sortedEmojiPerSkinTone = emojiData.sortedEmojiPerSkinTone {
|
|
for (skinTones, emoji) in sortedEmojiPerSkinTone {
|
|
fileHandle.writeLine(" case \"\(emoji)\": self.init(baseEmoji: .\(emojiData.enumName), skinTones: [\(skinTones.map { ".\($0)" }.joined(separator: ", "))])")
|
|
}
|
|
}
|
|
}
|
|
|
|
fileHandle.writeLine(" default: return nil")
|
|
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine("}")
|
|
}
|
|
|
|
// Skin tones lookup
|
|
writeBlock(fileName: "Emoji+SkinTones.swift") { fileHandle in
|
|
fileHandle.writeLine("extension Emoji {")
|
|
|
|
// Start SkinTone enum
|
|
fileHandle.writeLine(" enum SkinTone: String, CaseIterable, Equatable {")
|
|
for skinTone in SkinTone.allCases {
|
|
fileHandle.writeLine(" case \(skinTone) = \"\(skinTone.unicodeScalar)\"")
|
|
}
|
|
|
|
// End SkinTone Enum
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine("")
|
|
|
|
// skin tone helpers
|
|
fileHandle.writeLine(" var hasSkinTones: Bool { return emojiPerSkinTonePermutation != nil }")
|
|
fileHandle.writeLine(" var allowsMultipleSkinTones: Bool { return hasSkinTones && skinToneComponentEmoji != nil }")
|
|
|
|
fileHandle.writeLine("")
|
|
|
|
// Start skinToneComponentEmoji
|
|
fileHandle.writeLine(" var skinToneComponentEmoji: [Emoji]? {")
|
|
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
for emojiData in sortedEmojiData.filter({ $0.skinToneComponents != nil }) {
|
|
fileHandle.writeLine(" case .\(emojiData.enumName): return \(emojiData.skinToneComponents!)")
|
|
}
|
|
|
|
fileHandle.writeLine(" default: return nil")
|
|
|
|
fileHandle.writeLine(" }")
|
|
|
|
// End skinToneComponentEmoji
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine("")
|
|
|
|
// Start emojiPerSkinTonePermutation
|
|
fileHandle.writeLine(" var emojiPerSkinTonePermutation: [[SkinTone]: String]? {")
|
|
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
for emojiData in sortedEmojiData.filter({ $0.sortedEmojiPerSkinTone != nil }) {
|
|
fileHandle.writeLine(" case .\(emojiData.enumName):")
|
|
fileHandle.writeLine(" return [")
|
|
for (skinTones, emoji) in emojiData.sortedEmojiPerSkinTone! {
|
|
fileHandle.writeLine(" [\(skinTones.map { ".\($0)" }.joined(separator: ", "))]: \"\(emoji)\",")
|
|
}
|
|
fileHandle.writeLine(" ]")
|
|
}
|
|
|
|
fileHandle.writeLine(" default: return nil")
|
|
|
|
fileHandle.writeLine(" }")
|
|
|
|
// End emojiPerSkinTonePermutation
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine("}")
|
|
}
|
|
|
|
// Category lookup
|
|
writeBlock(fileName: "Emoji+Category.swift") { fileHandle in
|
|
// Start Extension
|
|
fileHandle.writeLine("extension Emoji {")
|
|
|
|
// Start Category enum
|
|
fileHandle.writeLine(" enum Category: String, CaseIterable, Equatable {")
|
|
for category in outputCategories {
|
|
fileHandle.writeLine(" case \(category) = \"\(category.rawValue)\"")
|
|
}
|
|
|
|
fileHandle.writeLine("")
|
|
|
|
// Localized name for category
|
|
fileHandle.writeLine(" var localizedName: String {")
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
for category in outputCategories {
|
|
fileHandle.writeLine(" case .\(category):")
|
|
fileHandle.writeLine(" return NSLocalizedString(\"EMOJI_CATEGORY_\("\(category)".uppercased())_NAME\",")
|
|
fileHandle.writeLine(" comment: \"The name for the emoji category '\(category.rawValue)'\")")
|
|
}
|
|
|
|
fileHandle.writeLine(" }")
|
|
fileHandle.writeLine(" }")
|
|
fileHandle.writeLine("")
|
|
|
|
// Emoji lookup per category
|
|
fileHandle.writeLine(" var emoji: [Emoji] {")
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
let emojiPerCategory = sortedEmojiData.reduce(into: [EmojiCategory: [EmojiData]]()) { result, emojiData in
|
|
var categoryList = result[emojiData.category] ?? []
|
|
categoryList.append(emojiData)
|
|
result[emojiData.category] = categoryList
|
|
}
|
|
|
|
for category in outputCategories {
|
|
let emoji: [EmojiData] = {
|
|
switch category {
|
|
case .smileysAndPeople:
|
|
// Merge smileys & people. It's important we initially bucket these seperately,
|
|
// because we want the emojis to be sorted smileys followed by people
|
|
return emojiPerCategory[.smileys]! + emojiPerCategory[.people]!
|
|
default:
|
|
return emojiPerCategory[category]!
|
|
}
|
|
}()
|
|
|
|
fileHandle.writeLine(" case .\(category):")
|
|
|
|
fileHandle.writeLine(" return [")
|
|
|
|
emoji.compactMap { $0.enumName }.forEach { name in
|
|
fileHandle.writeLine(" .\(name),")
|
|
}
|
|
|
|
fileHandle.writeLine(" ]")
|
|
}
|
|
|
|
fileHandle.writeLine(" }")
|
|
fileHandle.writeLine(" }")
|
|
|
|
// End Category Enum
|
|
fileHandle.writeLine(" }")
|
|
|
|
fileHandle.writeLine("")
|
|
|
|
// Category lookup per emoji
|
|
fileHandle.writeLine(" var category: Category {")
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
for emojiData in sortedEmojiData {
|
|
let category = [.smileys, .people].contains(emojiData.category) ? .smileysAndPeople : emojiData.category
|
|
fileHandle.writeLine(" case .\(emojiData.enumName): return .\(category)")
|
|
}
|
|
|
|
// Write a default case, because this enum is too long for the compiler to validate it's exhaustive
|
|
fileHandle.writeLine(" default: fatalError(\"Unexpected case \\(self)\")")
|
|
|
|
fileHandle.writeLine(" }")
|
|
fileHandle.writeLine(" }")
|
|
|
|
// End Extension
|
|
fileHandle.writeLine("}")
|
|
}
|
|
|
|
// Name lookup
|
|
writeBlock(fileName: "Emoji+Name.swift") { fileHandle in
|
|
// Start Extension
|
|
fileHandle.writeLine("extension Emoji {")
|
|
|
|
// Value lookup per emoji
|
|
fileHandle.writeLine(" var name: String? {")
|
|
fileHandle.writeLine(" switch self {")
|
|
|
|
for emojiData in sortedEmojiData {
|
|
guard let name = emojiData.name else { continue }
|
|
fileHandle.writeLine(" case .\(emojiData.enumName): return \"\(name)\"")
|
|
}
|
|
|
|
fileHandle.writeLine(" default: return nil")
|
|
|
|
fileHandle.writeLine(" }")
|
|
fileHandle.writeLine(" }")
|
|
|
|
// End Extension
|
|
fileHandle.writeLine("}")
|
|
}
|
|
}
|
|
|
|
static func writeBlock(fileName: String, block: (FileHandle) -> Void) {
|
|
if !FileManager.default.fileExists(atPath: emojiDirectory.path) {
|
|
try! FileManager.default.createDirectory(at: emojiDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
|
|
let url = URL(fileURLWithPath: fileName, relativeTo: emojiDirectory)
|
|
|
|
if FileManager.default.fileExists(atPath: url.path) {
|
|
try! FileManager.default.removeItem(at: url)
|
|
}
|
|
FileManager.default.createFile(atPath: url.path, contents: nil, attributes: nil)
|
|
|
|
let fileHandle = try! FileHandle(forWritingTo: url)
|
|
defer { fileHandle.closeFile() }
|
|
|
|
fileHandle.writeLine("//")
|
|
fileHandle.writeLine("// Copyright (c) 2020 Open Whisper Systems. All rights reserved.")
|
|
fileHandle.writeLine("//")
|
|
|
|
fileHandle.writeLine("")
|
|
fileHandle.writeLine("// This file is generated by EmojiGenerator.swift, do not manually edit it.")
|
|
fileHandle.writeLine("")
|
|
|
|
block(fileHandle)
|
|
}
|
|
}
|
|
|
|
extension FileHandle {
|
|
func writeLine(_ string: String) {
|
|
write((string + "\n").data(using: .utf8)!)
|
|
}
|
|
}
|
|
|
|
do {
|
|
EmojiGenerator.generate()
|
|
}
|