Signal-iOS/Scripts/EmojiGenerator.swift

262 lines
9.6 KiB
Swift
Executable File

#!/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 {
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"
}
static let outputCategories: [EmojiCategory] = [
.smileysAndPeople,
.animals,
.food,
.activities,
.travel,
.objects,
.symbols,
.flags
]
struct EmojiData: Codable {
let name: String?
let shortName: String
let unified: String
let sortOrder: UInt
let category: EmojiCategory
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"
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()
}
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 {")
for emojiData in sortedEmojiData {
fileHandle.writeLine(" case \(emojiData.enumName) = \"\(emojiData.emoji)\"")
}
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 {")
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()
}