session-ios/Scripts/EmojiGenerator.swift

788 lines
33 KiB
Swift
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env xcrun --sdk macosx swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// This script is used to generate/update the set of Emoji used for reactions
//
// stringlint:disable
import Foundation
// OWSAssertionError but for this script
enum EmojiError: Error {
case assertion(String)
init(_ string: String) {
self = .assertion(string)
}
}
// MARK: - Remote Model
// These definitions are kept fairly lightweight since we don't control their format
// All processing of remote data is done by converting RemoteModel items to EmojiModel items
enum RemoteModel {
struct EmojiItem: Codable {
let name: String
let shortName: String
let unified: String
let sortOrder: UInt
let category: EmojiCategory
let skinVariations: [String: SkinVariation]?
let shortNames: [String]?
}
struct SkinVariation: Codable {
let unified: String
}
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 components = "Component"
}
static func fetchEmojiData() throws -> Data {
// let remoteSourceUrl = URL(string: "https://unicodey.com/emoji-data/emoji.json")!
// This URL has been unavailable the past couple of weeks. If you're seeing failures here, try this other one:
let remoteSourceUrl = URL(string: "https://raw.githubusercontent.com/iamcal/emoji-data/master/emoji.json")!
return try Data(contentsOf: remoteSourceUrl)
}
}
// MARK: - Local Model
struct EmojiModel {
let definitions: [EmojiDefinition]
struct EmojiDefinition {
let category: RemoteModel.EmojiCategory
let rawName: String
let enumName: String
var shortNames: Set<String>
let variants: [Emoji]
var baseEmoji: Character { variants[0].base }
struct Emoji: Comparable {
let emojiChar: Character
let base: Character
let skintoneSequence: SkinToneSequence
static func <(lhs: Self, rhs: Self) -> Bool {
for (leftElement, rightElement) in zip(lhs.skintoneSequence, rhs.skintoneSequence) {
if leftElement.sortId != rightElement.sortId {
return leftElement.sortId < rightElement.sortId
}
}
if lhs.skintoneSequence.count != rhs.skintoneSequence.count {
return lhs.skintoneSequence.count < rhs.skintoneSequence.count
} else {
return false
}
}
}
init(parsingRemoteItem remoteItem: RemoteModel.EmojiItem) throws {
category = remoteItem.category
rawName = remoteItem.name
enumName = Self.parseEnumNameFromRemoteItem(remoteItem)
shortNames = Set((remoteItem.shortNames ?? []))
shortNames.insert(rawName.lowercased())
shortNames.insert(enumName.lowercased())
let baseEmojiChar = try Self.codePointsToCharacter(Self.parseCodePointString(remoteItem.unified))
let baseEmoji = Emoji(emojiChar: baseEmojiChar, base: baseEmojiChar, skintoneSequence: .none)
let toneVariants: [Emoji]
if let skinVariations = remoteItem.skinVariations {
toneVariants = try skinVariations.map { key, value in
let modifier = SkinTone.sequence(from: Self.parseCodePointString(key))
let parsedEmoji = try Self.codePointsToCharacter(Self.parseCodePointString(value.unified))
return Emoji(emojiChar: parsedEmoji, base: baseEmojiChar, skintoneSequence: modifier)
}.sorted()
} else {
toneVariants = []
}
variants = [baseEmoji] + toneVariants
try postInitValidation()
}
func postInitValidation() throws {
guard variants.count > 0 else {
throw EmojiError("Expecting at least one variant")
}
guard variants.allSatisfy({ $0.base == baseEmoji }) else {
// All emoji variants must have a common base emoji
throw EmojiError("Inconsistent base emoji: \(baseEmoji)")
}
let hasMultipleComponents = variants.first(where: { $0.skintoneSequence.count > 1 }) != nil
if hasMultipleComponents, skinToneComponents == nil {
// If you hit this, this means a new emoji was added where a skintone modifier sequence specifies multiple
// skin tones for multiple emoji components: e.g. 👫 -> 🧍+🧍
// These are defined in `skinToneComponents`. You'll need to add a new case.
throw EmojiError("\(baseEmoji):\(enumName) definition has variants with multiple skintone modifiers but no component emojis defined")
}
}
static func parseEnumNameFromRemoteItem(_ item: RemoteModel.EmojiItem) -> String {
// some names don't play nice with swift, so we special case them
switch item.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_with_heart": return "personHeartPerson"
default:
let uppperCamelCase = item.shortName
.replacingOccurrences(of: "-", with: " ")
.replacingOccurrences(of: "_", with: " ")
.titlecase
.replacingOccurrences(of: " ", with: "")
return uppperCamelCase.first!.lowercased() + uppperCamelCase.dropFirst()
}
}
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 enumName {
case "peopleHoldingHands": return "[.standingPerson, .standingPerson]"
case "twoWomenHoldingHands": return "[.womanStanding, .womanStanding]"
case "manAndWomanHoldingHands": return "[.womanStanding, .manStanding]"
case "twoMenHoldingHands": return "[.manStanding, .manStanding]"
case "personKissPerson": return "[.adult, .adult]"
case "womanKissMan": return "[.woman, .man]"
case "manKissMan": return "[.man, .man]"
case "womanKissWoman": return "[.woman, .woman]"
case "personHeartPerson": return "[.adult, .adult]"
case "womanHeartMan": return "[.woman, .man]"
case "manHeartMan": return "[.man, .man]"
case "womanHeartWoman": return "[.woman, .woman]"
case "handshake": return "[.rightwardsHand, .leftwardsHand]"
default:
return nil
}
}
var isNormalized: Bool { enumName == normalizedEnumName }
var normalizedEnumName: String {
switch enumName {
// flagUm (US Minor Outlying Islands) looks identical to the
// US flag. We don't present it as a sendable reaction option
// This matches the iOS keyboard behavior.
case "flagUm": return "us"
default: return enumName
}
}
static func parseCodePointString(_ pointString: String) -> [UnicodeScalar] {
return pointString
.components(separatedBy: "-")
.map { Int($0, radix: 16)! }
.map { UnicodeScalar($0)! }
}
static func codePointsToCharacter(_ codepoints: [UnicodeScalar]) throws -> Character {
let result = codepoints.map { String($0) }.joined()
if result.count != 1 {
throw EmojiError("Invalid number of chars for codepoint sequence: \(codepoints)")
}
return result.first!
}
}
init(rawJSONData jsonData: Data) throws {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
definitions = try jsonDecoder
.decode([RemoteModel.EmojiItem].self, from: jsonData)
.sorted { $0.sortOrder < $1.sortOrder }
.map { try EmojiDefinition(parsingRemoteItem: $0) }
}
typealias SkinToneSequence = [EmojiModel.SkinTone]
enum SkinTone: UnicodeScalar, CaseIterable, Equatable {
case light = "🏻"
case mediumLight = "🏼"
case medium = "🏽"
case mediumDark = "🏾"
case dark = "🏿"
var sortId: Int { return SkinTone.allCases.firstIndex(of: self)! }
static func sequence(from codepoints: [UnicodeScalar]) -> SkinToneSequence {
codepoints
.map { SkinTone(rawValue: $0)! }
.reduce(into: [SkinTone]()) { result, skinTone in
guard !result.contains(skinTone) else { return }
result.append(skinTone)
}
}
}
}
extension EmojiModel.SkinToneSequence {
static var none: EmojiModel.SkinToneSequence = []
}
// MARK: - File Writers
extension EmojiGenerator {
static func writePrimaryFile(from emojiModel: EmojiModel) {
// Main enum: Create a string enum defining our enumNames equal to the baseEmoji string
// e.g. case grinning = "😀"
writeBlock(fileName: "Emoji.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("/// A sorted representation of all available emoji")
fileHandle.writeLine("enum Emoji: String, CaseIterable, Equatable {")
fileHandle.indent {
emojiModel.definitions.forEach {
fileHandle.writeLine("case \($0.enumName) = \"\($0.baseEmoji)\"")
}
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
indirect enum Structure {
enum ChunkType {
case firstScalar
case scalarSum
func chunk(_ character: Character, into size: UInt32) -> UInt32 {
guard size > 0 else { return 0 }
let scalarValues: [UInt32] = character.unicodeScalars.map { $0.value }
switch self {
case .firstScalar: return (scalarValues.first.map { $0 / size } ?? 0)
case .scalarSum: return (scalarValues.reduce(0, +) / size)
}
}
func switchString(with variableName: String = "rawValue", size: UInt32) -> String {
switch self {
case .firstScalar: return "rawValue.unicodeScalars.map({ $0.value }).first.map({ $0 / \(size) })"
case .scalarSum: return "(rawValue.unicodeScalars.map({ $0.value }).reduce(0, +) / \(size))"
}
}
}
case ifElse // XCode 15 taking over 10 min with M1 Pro (gave up)
case switchStatement // XCode 15 taking over 10 min with M1 Pro (gave up)
case directLookup // XCode 15 taking 93 sec with M1 Pro
case chunked(UInt32, Structure, ChunkType) // XCode 15 taking <10 sec with M1 Pro (chunk by 100)
}
typealias ChunkedEmojiInfo = (
variant: EmojiModel.EmojiDefinition.Emoji,
baseName: String
)
static func writeStringConversionsFile(from emojiModel: EmojiModel) {
// This combination seems to have the smallest compile time (~2.2 sec out of all of the combinations)
let desiredStructure: Structure = .chunked(100, .directLookup, .scalarSum)
// Conversion from String: Creates an initializer mapping a single character emoji string to an EmojiWithSkinTones
// e.g.
// if rawValue == "😀" { self.init(baseEmoji: .grinning, skinTones: nil) }
// else if rawValue == "🦻🏻" { self.init(baseEmoji: .earWithHearingAid, skinTones: [.light])
writeBlock(fileName: "EmojiWithSkinTones+String.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension EmojiWithSkinTones {")
fileHandle.indent {
switch desiredStructure {
case .chunked(let chunkSize, let childStructure, let chunkType):
let chunkedEmojiInfo = emojiModel.definitions
.reduce(into: [UInt32: [ChunkedEmojiInfo]]()) { result, next in
next.variants.forEach { emoji in
let chunk: UInt32 = chunkType.chunk(emoji.emojiChar, into: chunkSize)
result[chunk] = ((result[chunk] ?? []) + [(emoji, next.enumName)])
.sorted { lhs, rhs in lhs.variant < rhs.variant }
}
}
.sorted { lhs, rhs in lhs.key < rhs.key }
fileHandle.writeLine("init?(rawValue: String) {")
fileHandle.indent {
fileHandle.writeLine("guard rawValue.isSingleEmoji else { return nil }")
fileHandle.writeLine("switch \(chunkType.switchString(size: chunkSize)) {")
fileHandle.indent {
chunkedEmojiInfo.forEach { chunk, _ in
fileHandle.writeLine("case \(chunk): self = EmojiWithSkinTones.emojiFrom\(chunk)(rawValue)")
}
fileHandle.writeLine("default: self = EmojiWithSkinTones(unsupportedValue: rawValue)")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
chunkedEmojiInfo.forEach { chunk, emojiInfo in
fileHandle.writeLine("")
fileHandle.writeLine("private static func emojiFrom\(chunk)(_ rawValue: String) -> EmojiWithSkinTones {")
fileHandle.indent {
switch emojiInfo.count {
case 0:
fileHandle.writeLine("return EmojiWithSkinTones(unsupportedValue: rawValue)")
default:
writeStructure(
childStructure,
for: emojiInfo,
using: fileHandle,
assignmentPrefix: "return "
)
}
}
fileHandle.writeLine("}")
}
default:
fileHandle.writeLine("init?(rawValue: String) {")
fileHandle.indent {
fileHandle.writeLine("guard rawValue.isSingleEmoji else { return nil }")
writeStructure(
desiredStructure,
for: emojiModel.definitions
.flatMap { definition in
definition.variants.map { ($0, definition.enumName) }
},
using: fileHandle
)
}
fileHandle.writeLine("}")
}
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
private static func writeStructure(
_ structure: Structure,
for emojiInfo: [ChunkedEmojiInfo],
using fileHandle: WriteHandle,
assignmentPrefix: String = "self = "
) {
func initItem(_ info: ChunkedEmojiInfo) -> String {
let skinToneString: String = {
guard !info.variant.skintoneSequence.isEmpty else { return "nil" }
return "[\(info.variant.skintoneSequence.map { ".\($0)" }.joined(separator: ", "))]"
}()
return "EmojiWithSkinTones(baseEmoji: .\(info.baseName), skinTones: \(skinToneString))"
}
switch structure {
case .ifElse:
emojiInfo.enumerated().forEach { index, info in
switch index {
case 0: fileHandle.writeLine("if rawValue == \"\(info.variant.emojiChar)\" {")
default: fileHandle.writeLine("} else if rawValue == \"\(info.variant.emojiChar)\" {")
}
fileHandle.indent {
fileHandle.writeLine("\(assignmentPrefix)\(initItem(info))")
}
}
fileHandle.writeLine("} else {")
fileHandle.indent {
fileHandle.writeLine("\(assignmentPrefix)EmojiWithSkinTones(unsupportedValue: rawValue)")
}
fileHandle.writeLine("}")
case .switchStatement:
fileHandle.writeLine("switch rawValue {")
fileHandle.indent {
emojiInfo.forEach { info in
fileHandle.writeLine("case \"\(info.variant.emojiChar)\": \(assignmentPrefix)\(initItem(info))")
}
fileHandle.writeLine("default: \(assignmentPrefix)EmojiWithSkinTones(unsupportedValue: rawValue)")
}
fileHandle.writeLine("}")
case .directLookup:
fileHandle.writeLine("let lookup: [String: EmojiWithSkinTones] = [")
fileHandle.indent {
emojiInfo.enumerated().forEach { index, info in
let isLast: Bool = (index == (emojiInfo.count - 1))
fileHandle.writeLine("\"\(info.variant.emojiChar)\": \(initItem(info))\(isLast ? "" : ",")")
}
}
fileHandle.writeLine("]")
fileHandle.writeLine("\(assignmentPrefix)(lookup[rawValue] ?? EmojiWithSkinTones(unsupportedValue: rawValue))")
case .chunked: break // Provide one of the other types
}
}
static func writeSkinToneLookupFile(from emojiModel: EmojiModel) {
writeBlock(fileName: "Emoji+SkinTones.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
// SkinTone enum
fileHandle.writeLine("enum SkinTone: String, CaseIterable, Equatable {")
fileHandle.indent {
for skinTone in EmojiModel.SkinTone.allCases {
fileHandle.writeLine("case \(skinTone) = \"\(skinTone.rawValue)\"")
}
}
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.indent {
fileHandle.writeLine("switch self {")
emojiModel.definitions.forEach { emojiDef in
if let components = emojiDef.skinToneComponents {
fileHandle.writeLine("case .\(emojiDef.enumName): return \(components)")
}
}
fileHandle.writeLine("default: return nil")
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("")
// Start emojiPerSkinTonePermutation
fileHandle.writeLine("var emojiPerSkinTonePermutation: [[SkinTone]: String]? {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
emojiModel.definitions.forEach { emojiDef in
let skintoneVariants = emojiDef.variants.filter({ $0.skintoneSequence != .none})
if skintoneVariants.isEmpty {
// None of our variants have a skintone, nothing to do
return
}
fileHandle.writeLine("case .\(emojiDef.enumName):")
fileHandle.indent {
fileHandle.writeLine("return [")
fileHandle.indent {
skintoneVariants.forEach {
let skintoneSequenceKey = $0.skintoneSequence.map({ ".\($0)" }).joined(separator: ", ")
fileHandle.writeLine("[\(skintoneSequenceKey)]: \"\($0.emojiChar)\",")
}
}
fileHandle.writeLine("]")
}
}
fileHandle.writeLine("default: return nil")
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
static func writeCategoryLookupFile(from emojiModel: EmojiModel) {
let outputCategories: [RemoteModel.EmojiCategory] = [
.smileysAndPeople,
.animals,
.food,
.activities,
.travel,
.objects,
.symbols,
.flags
]
writeBlock(fileName: "Emoji+Category.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
// Category enum
fileHandle.writeLine("enum Category: String, CaseIterable, Equatable {")
fileHandle.indent {
// Declare cases
for category in outputCategories {
fileHandle.writeLine("case \(category) = \"\(category.rawValue)\"")
}
fileHandle.writeLine("")
// Localized name for category
fileHandle.writeLine("var localizedName: String {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
for category in outputCategories {
fileHandle.writeLine("case .\(category):")
fileHandle.indent {
let stringKey = "EMOJI_CATEGORY_\("\(category)".uppercased())_NAME"
let stringComment = "The name for the emoji category '\(category.rawValue)'"
fileHandle.writeLine("return NSLocalizedString(\"\(stringKey)\", comment: \"\(stringComment)\")")
}
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("")
// Emoji lookup per category
fileHandle.writeLine("var normalizedEmoji: [Emoji] {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
let normalizedEmojiPerCategory: [RemoteModel.EmojiCategory: [EmojiModel.EmojiDefinition]]
normalizedEmojiPerCategory = emojiModel.definitions.reduce(into: [:]) { result, emojiDef in
if emojiDef.isNormalized {
var categoryList = result[emojiDef.category] ?? []
categoryList.append(emojiDef)
result[emojiDef.category] = categoryList
}
}
for category in outputCategories {
let emoji: [EmojiModel.EmojiDefinition] = {
switch category {
case .smileysAndPeople:
// Merge smileys & people. It's important we initially bucket these separately,
// because we want the emojis to be sorted smileys followed by people
return normalizedEmojiPerCategory[.smileys]! + normalizedEmojiPerCategory[.people]!
default:
return normalizedEmojiPerCategory[category]!
}
}()
fileHandle.writeLine("case .\(category):")
fileHandle.indent {
fileHandle.writeLine("return [")
fileHandle.indent {
emoji.compactMap { $0.enumName }.forEach { name in
fileHandle.writeLine(".\(name),")
}
}
fileHandle.writeLine("]")
}
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("")
// Category lookup per emoji
fileHandle.writeLine("var category: Category {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
for emojiDef in emojiModel.definitions {
let category = [.smileys, .people].contains(emojiDef.category) ? .smileysAndPeople : emojiDef.category
if category != .components {
fileHandle.writeLine("case .\(emojiDef.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("}")
fileHandle.writeLine("")
// Normalized variant mapping
fileHandle.writeLine("var isNormalized: Bool { normalized == self }")
fileHandle.writeLine("var normalized: Emoji {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
emojiModel.definitions.filter { !$0.isNormalized }.forEach {
fileHandle.writeLine("case .\($0.enumName): return .\($0.normalizedEnumName)")
}
fileHandle.writeLine("default: return self")
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
static func writeNameLookupFile(from emojiModel: EmojiModel) {
// Name lookup: Create a computed property mapping an Emoji enum element to the raw Emoji name string
// e.g. case .grinning: return "GRINNING FACE"
writeBlock(fileName: "Emoji+Name.swift") { fileHandle in
fileHandle.writeLine("// swiftlint:disable all")
fileHandle.writeLine("// stringlint:disable")
fileHandle.writeLine("")
fileHandle.writeLine("extension Emoji {")
fileHandle.indent {
fileHandle.writeLine("var name: String {")
fileHandle.indent {
fileHandle.writeLine("switch self {")
emojiModel.definitions.forEach {
fileHandle.writeLine("case .\($0.enumName): return \"\($0.shortNames.sorted().joined(separator:", "))\"")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
}
fileHandle.writeLine("}")
fileHandle.writeLine("// swiftlint:disable all")
}
}
}
// MARK: - File I/O Helpers
class WriteHandle {
static let emojiDirectory = URL(
fileURLWithPath: "../Session/Emoji",
isDirectory: true,
relativeTo: EmojiGenerator.pathToFolderContainingThisScript!)
let handle: FileHandle
var indentDepth: Int = 0
var hasBeenClosed = false
func indent(_ block: () -> Void) {
indentDepth += 1
block()
indentDepth -= 1
}
func writeLine(_ body: String) {
let spaces = indentDepth * 4
let prefix = String(repeating: " ", count: spaces)
let suffix = "\n"
let line = prefix + body + suffix
handle.write(line.data(using: .utf8)!)
}
init(fileName: String) {
// Create directory if necessary
if !FileManager.default.fileExists(atPath: Self.emojiDirectory.path) {
try! FileManager.default.createDirectory(at: Self.emojiDirectory, withIntermediateDirectories: true, attributes: nil)
}
// Delete old file and create anew
let url = URL(fileURLWithPath: fileName, relativeTo: Self.emojiDirectory)
if FileManager.default.fileExists(atPath: url.path) {
try! FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: nil, attributes: nil)
handle = try! FileHandle(forWritingTo: url)
}
deinit {
precondition(hasBeenClosed, "File handle still open at de-init")
}
func close() {
handle.closeFile()
hasBeenClosed = true
}
}
extension EmojiGenerator {
static func writeBlock(fileName: String, block: (WriteHandle) -> Void) {
let fileHandle = WriteHandle(fileName: fileName)
defer { fileHandle.close() }
fileHandle.writeLine("")
fileHandle.writeLine("// This file is generated by EmojiGenerator.swift, do not manually edit it.")
fileHandle.writeLine("")
block(fileHandle)
}
// 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
}()
}
// MARK: - Misc
extension String {
var titlecase: String {
components(separatedBy: " ")
.map { $0.first!.uppercased() + $0.dropFirst().lowercased() }
.joined(separator: " ")
}
}
// MARK: - Lifecycle
class EmojiGenerator {
static func run() throws {
let remoteData = try RemoteModel.fetchEmojiData()
let model = try EmojiModel(rawJSONData: remoteData)
writePrimaryFile(from: model)
writeStringConversionsFile(from: model)
writeSkinToneLookupFile(from: model)
writeCategoryLookupFile(from: model)
writeNameLookupFile(from: model)
}
}
do {
try EmojiGenerator.run()
} catch {
print("Failed to generate emoji data: \(error)")
let errorCode = (error as? CustomNSError)?.errorCode ?? -1
exit(Int32(errorCode))
}