Add support for iOS 15 and unicode release v13 (#1)

This commit is contained in:
Niklas Amslgruber
2023-06-16 17:04:40 +02:00
committed by GitHub
parent 6bbb3a1a71
commit 977c01327f
16 changed files with 3777 additions and 146 deletions

View File

@@ -1,86 +0,0 @@
//
// DataProcessor.swift
//
//
// Created by Niklas Amslgruber on 12.06.23.
//
import Foundation
import EmojiKitLibrary
public enum DataProcessor {
public enum EmojiUnicodeVersion: Int {
case v14 = 14
case v15 = 15
public var fileName: String {
return "emojis_v\(rawValue).json"
}
public static func getSupportedVersionForCurrentIOSVersion() -> EmojiUnicodeVersion {
if #available(iOS 16.4, *) {
return .v15
} else {
return .v14
}
}
}
public enum SkinType {
case neutral
case light
case mediumLight
case medium
case mediumDark
case dark
var unicode: String {
switch self {
case .neutral:
return ""
case .light:
return "1F3FB"
case .mediumLight:
return "1F3FC"
case .medium:
return "1F3FD"
case .mediumDark:
return "1F3FE"
case .dark:
return "1F3FF"
}
}
}
public static func getEmojis(at url: URL, for version: EmojiUnicodeVersion, skinTypes: [SkinType] = [.neutral]) -> [EmojiCategory] {
guard let content = try? Data(contentsOf: url), let result = try? JSONDecoder().decode([EmojiCategory].self, from: content) else {
return []
}
var filteredEmojis: [EmojiCategory] = []
for wrapper in result {
let supportedEmojis = wrapper.values.filter({
isMatchingSkinType(of: $0, for: skinTypes)
})
filteredEmojis.append(EmojiCategory(name: wrapper.name, values: supportedEmojis))
}
return filteredEmojis
}
private static func isMatchingSkinType(of emoji: String, for skinTypes: [SkinType]) -> Bool {
let unicodes = getUnicodes(emoji: emoji)
for skinType in skinTypes where unicodes.contains(skinType.unicode) {
return true
}
return false
}
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) }
}
}

View File

@@ -0,0 +1,67 @@
//
// EmojiCategory.swift
//
//
// Created by Niklas Amslgruber on 10.06.23.
//
import Foundation
public class EmojiCategory: Codable {
public enum Name: String, CaseIterable, Codable {
case flags = "Flags"
case activities = "Activities"
case components = "Component"
case objects = "Objects"
case travelAndPlaces = "Travel & Places"
case symbols = "Symbols"
case peopleAndBody = "People & Body"
case animalsAndNature = "Animals & Nature"
case foodAndDrink = "Food & Drink"
case smileysAndEmotions = "Smileys & Emotion"
public static var orderedCases: [EmojiCategory.Name] {
return EmojiCategory.Name.allCases.sorted(by: { $0.order < $1.order })
}
// The component category does not include relevant emojis
public static var relevantCases: [EmojiCategory.Name] {
return EmojiCategory.Name.orderedCases.filter({ $0 != .components })
}
// Order that Apple uses in their emoji picker
public var order: Int {
switch self {
case .flags:
return 10
case .activities:
return 5
case .components:
return 8
case .objects:
return 7
case .travelAndPlaces:
return 6
case .symbols:
return 9
case .peopleAndBody:
return 2
case .animalsAndNature:
return 3
case .foodAndDrink:
return 4
case .smileysAndEmotions:
return 1
}
}
}
public let name: Name
public let values: [String]
public init(name: Name, values: [String]) {
self.name = name
self.values = values
}
}

View File

@@ -1,111 +0,0 @@
//
// File.swift
//
//
// Created by Niklas Amslgruber on 10.06.23.
//
import Foundation
import ArgumentParser
import EmojiKitLibrary
struct EmojiDownloader: ParsableCommand, AsyncParsableCommand {
static let configuration: CommandConfiguration = CommandConfiguration(
commandName: "download",
abstract: "Downloads a list of all available emojis and their counts from unicode.rog for the respective unicode version"
)
@Argument var path: String
@Option(name: .shortAndLong) var version: DataProcessor.EmojiUnicodeVersion = .v15
func run() async throws {
print("⚙️", "Starting to download all emojis for version \(version.rawValue) from unicode.org...\n")
guard let emojiListURL = await getTemporaryURLForEmojiList(version: version), let emojiCountsURL = await getTemporaryURLForEmojiCounts(version: version) else {
print("⚠️", "Could not get content from unicode.org. Either the emoji list or the emoji count file is not available.\n")
return
}
print("🎉", "Successfully retrieved temporary URLs for version \(version.rawValue).\n")
print("⚙️", "Starting to parse content...\n")
let parser = UnicodeParser()
do {
let emojisByCategory: [EmojiCategory] = try await parser.parseEmojiList(for: emojiListURL)
let emojiCounts: [EmojiCategory.Name: Int] = parser.parseCountHTML(for: emojiCountsURL)
for category in emojisByCategory {
assert(emojiCounts[category.name] == category.values.count)
}
print("🎉", "Successfully parsed emojis and matched counts to the count file.\n")
print("⚙️", "Saving emojis to file emojis_v\(version.rawValue).json...\n")
save(data: emojisByCategory, for: version)
print("🎉", "Successfully saved emojis to file.\n")
} catch {
print("⚠️", "Could not parse emoji lists or emoji counts. Process failed with: \(error).\n")
}
}
func getTemporaryURLForEmojiList(version: DataProcessor.EmojiUnicodeVersion) async -> URL? {
return await load(urlString: "https://unicode.org/Public/emoji/\(version.rawValue).0/emoji-test.txt")
}
func getTemporaryURLForEmojiCounts(version: DataProcessor.EmojiUnicodeVersion) async -> URL? {
return await load(urlString: "https://www.unicode.org/emoji/charts-\(version.rawValue).0/emoji-counts.html")
}
private func load(urlString: String) async -> URL? {
guard let url = URL(string: urlString) else {
return nil
}
let session = URLSession(configuration: .default)
do {
let (tmpFileURL, response) = try await session.download(from: url)
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode == 200 else {
print("⚠️", "Failed with a non 200 HTTP status")
return nil
}
return tmpFileURL
} catch {
print("⚠️", error)
return nil
}
}
private func save(data: [EmojiCategory], for: DataProcessor.EmojiUnicodeVersion) {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let result = try? encoder.encode(data) else {
print("⚠️", "Couldn't encode emoji categories.")
return
}
var filePath = URL(filePath: path)
filePath.append(path: version.fileName)
let jsonString = String(data: result, encoding: .utf8)
if FileManager.default.fileExists(atPath: filePath.absoluteString) == false {
FileManager.default.createFile(atPath: filePath.absoluteString, contents: nil)
}
do {
try jsonString?.write(to: filePath, atomically: true, encoding: .utf8)
} catch {
print("⚠️", error)
}
}
}
extension DataProcessor.EmojiUnicodeVersion: ExpressibleByArgument {}

View File

@@ -0,0 +1,79 @@
//
// EmojiManager.swift
// Travely
//
// Created by Niklas Amslgruber on 13.06.23.
//
import Foundation
public enum EmojiManager {
public enum Version: Double {
case v13_1 = 13.1
case v14 = 14
case v15 = 15
public var fileName: String {
return "emojis_v\(versionIdentifier)"
}
public var versionIdentifier: String {
switch self {
case .v13_1:
return "13.1"
case .v14:
return "14.0"
case .v15:
return "15.0"
}
}
public static func getSupportedVersion() -> Version {
if #available(iOS 16.4, *) {
return .v15
} else if #available(iOS 15.4, *) {
return .v14
} else {
return .v13_1
}
}
}
/// 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)
/// - showAllVariations: Some emojis inlcude skin type variations which increases the number of emojis drastically. (default: only the yellow neutral emojis are returned)
/// - url: Specify the location of the `emoji_v<version_number>.json` files if needed (default: bundle resource path)
/// - Returns: Array of categories with all emojis that are assigned to each category
public static func getAvailableEmojis(version: Version = .getSupportedVersion(), showAllVariations: Bool = false, at url: URL? = nil) -> [EmojiCategory] {
let fileUrl = url ?? Bundle.module.url(forResource: version.fileName, withExtension: "json")
if let url = fileUrl, let content = try? Data(contentsOf: url), let result = try? JSONDecoder().decode([EmojiCategory].self, from: content) {
var filteredEmojis: [EmojiCategory] = []
for category in result {
let supportedEmojis = category.values.filter({
showAllVariations ? true : isNeutralEmoji(for: $0)
})
filteredEmojis.append(EmojiCategory(name: category.name, values: supportedEmojis))
}
return filteredEmojis
}
return []
}
private static func isNeutralEmoji(for emoji: String) -> Bool {
let unicodes = getUnicodes(emoji: emoji)
let colors = ["1F3FB", "1F3FC", "1F3FD", "1F3FE", "1F3FF"]
for color in colors where unicodes.contains(color) {
return false
}
return true
}
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) }
}
}

View File

@@ -1,21 +0,0 @@
//
// EmojiScripts.swift
//
//
// Created by Niklas Amslgruber on 12.06.23.
//
import Foundation
import ArgumentParser
@main
struct EmojiScripts: ParsableCommand, AsyncParsableCommand {
static let configuration: CommandConfiguration = CommandConfiguration(
commandName: "emojis",
abstract: "Manage Emojis from Unicode",
subcommands: [
EmojiDownloader.self,
]
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
//
// StringHelper.swift
//
//
// Created by Niklas Amslgruber on 10.06.23.
//
import Foundation
extension String {
func trim() -> String {
return self.trimmingCharacters(in: .whitespacesAndNewlines)
}
func asEmoji() -> String? {
guard let unicodeNumber = Int(self, radix: 16), let unicode = Unicode.Scalar(unicodeNumber) else {
return nil
}
return String(unicode)
}
}
extension String.SubSequence {
func trim() -> String {
return String(self).trim()
}
func asEmoji() -> String? {
return String(self).asEmoji()
}
}

View File

@@ -1,147 +0,0 @@
//
// UnicodeParser.swift
//
//
// Created by Niklas Amslgruber on 12.06.23.
//
import Foundation
import SwiftSoup
import EmojiKitLibrary
class UnicodeParser {
enum Tags: String {
case comment = "#"
case group = "# group:"
case unqualified
case minimallyQualified = "minimally-qualified"
}
func parseEmojiList(for fileUrl: URL) async throws -> [EmojiCategory] {
let handle = try FileHandle(forReadingFrom: fileUrl)
var currentGroup: EmojiCategory.Name = .activities
var emojisByGroup: [EmojiCategory.Name: [String]] = [:]
for try await line in handle.bytes.lines {
/// Skip comments, but keep groups
if isLine(line, ofType: .comment), isLine(line, ofType: .group) == false {
continue
}
/// Get current group
if isLine(line, ofType: .group) {
let name = line.split(separator: ":")
let categoryName = name.last?.trim() ?? ""
guard let category = EmojiCategory.Name(rawValue: categoryName) else {
continue
}
currentGroup = category
emojisByGroup[category] = []
}
/// Split line into list of entries
let lineComponents = line.split(separator: ";")
/// Get hex-string from compenents
guard let hexString = lineComponents.map({ $0.trim() }).first else {
continue
}
/// Check if category exists
guard lineComponents.count > 1 else {
continue
}
let category = lineComponents[1].trim()
/// Remove `unqualified` or `minimally-qualified` entries
guard (isLine(category, ofType: .unqualified) || isLine(category, ofType: .minimallyQualified)) == false else {
continue
}
let hexComponents = hexString.split(separator: " ")
/// Check for multi-hex emojis
if hexComponents.count > 1 {
let multiHexEmoji = hexComponents.compactMap({ $0.asEmoji() }).joined()
if multiHexEmoji.isEmpty == false {
emojisByGroup[currentGroup]?.append(multiHexEmoji)
}
} else {
if let unicode = hexString.asEmoji(), unicode.isEmpty == false {
emojisByGroup[currentGroup]?.append(unicode)
}
}
}
try handle.close()
var result: [EmojiCategory] = []
for category in EmojiCategory.Name.allCases {
result.append(EmojiCategory(name: category, values: emojisByGroup[category] ?? []))
}
return result
}
func parseCountHTML(for url: URL) -> [EmojiCategory.Name: Int] {
do {
let html = try String(contentsOf: url)
let doc: Document = try SwiftSoup.parse(html)
guard let table = try doc.select("table").first() else {
return [:]
}
let rows: Elements = try table.select("tbody tr")
let categories = rows.first
let totals = rows.last
guard let categories, let totals, let categoryEntries = try? categories.select("th"), let countEntries = try? totals.select("th") else {
return [:]
}
var categoryNames: [EmojiCategory.Name] = []
var countNumbers: [Int] = []
for categoryElement in categoryEntries {
if categoryElement == categoryEntries.first || categoryElement == categoryEntries.last {
continue
}
guard let text = try? categoryElement.text(), let category = EmojiCategory.Name(rawValue: text) else {
continue
}
categoryNames.append(category)
}
for countElement in countEntries {
if countElement == countEntries.first || countElement == countEntries.last {
continue
}
guard let text = try? countElement.text(), let number = Int(text) else {
continue
}
countNumbers.append(number)
}
var result: [EmojiCategory.Name: Int] = [:]
for (index, categoryName) in categoryNames.enumerated() {
result[categoryName] = countNumbers[index]
}
return result
} catch {
print("Error parsing HTML: \(error)")
}
return [:]
}
private func isLine(_ line: String, ofType type: Tags) -> Bool {
return line.starts(with: type.rawValue)
}
}