Add beta version
This commit is contained in:
95
.gitignore
vendored
95
.gitignore
vendored
@@ -1,90 +1,9 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# Accio dependency management
|
||||
Dependencies/
|
||||
.accio/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
|
||||
# Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
84
.swiftpm/xcode/xcshareddata/xcschemes/EmojiKit.xcscheme
Normal file
84
.swiftpm/xcode/xcshareddata/xcschemes/EmojiKit.xcscheme
Normal file
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EmojiKit"
|
||||
BuildableName = "EmojiKit"
|
||||
BlueprintName = "EmojiKit"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
viewDebuggingEnabled = "No">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EmojiKit"
|
||||
BuildableName = "EmojiKit"
|
||||
BlueprintName = "EmojiKit"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "download --path ."
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EmojiKit"
|
||||
BuildableName = "EmojiKit"
|
||||
BlueprintName = "EmojiKit"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EmojiKitLibrary"
|
||||
BuildableName = "EmojiKitLibrary"
|
||||
BlueprintName = "EmojiKitLibrary"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EmojiKitLibrary"
|
||||
BuildableName = "EmojiKitLibrary"
|
||||
BlueprintName = "EmojiKitLibrary"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
23
Package.resolved
Normal file
23
Package.resolved
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||
"state" : {
|
||||
"revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
|
||||
"version" : "1.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "0e96a20ffd37a515c5c963952d4335c89bed50a6",
|
||||
"version" : "2.6.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
33
Package.swift
Normal file
33
Package.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
// swift-tools-version: 5.8
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "EmojiKit",
|
||||
platforms: [.macOS(.v13), .iOS(.v16)],
|
||||
products: [
|
||||
.executable(
|
||||
name: "EmojiKit",
|
||||
targets: ["EmojiKit"]
|
||||
),
|
||||
// .library(
|
||||
// name: "EmojiKitLibrary",
|
||||
// targets: ["EmojiKitLibrary"]
|
||||
// )
|
||||
],
|
||||
dependencies: [
|
||||
.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"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "EmojiKit",
|
||||
dependencies: [
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "SwiftSoup", package: "SwiftSoup"),
|
||||
// .target(name: "EmojiKitLibrary")
|
||||
]),
|
||||
// .target(name: "EmojiKitLibrary")
|
||||
]
|
||||
)
|
||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# EmojiKit
|
||||
|
||||
A lightweight script that queries all available emoji releases from [Unicode.org](unicode.org) and makes them easily available in any product for the Apple ecosystem.
|
||||
|
||||
## Usage
|
||||
|
||||
The main idea behind this script is that there is no full list of supported emojis that can be easily used in Swift. This script fetches the full list of emojis from [Unicode.org](unicode.org), parses them and returns a structured `.json` file where all emojis are sorted in their official category. The `.json` files can be easily read afterwards for further usage in any product.
|
||||
|
||||
1. Clone this repository and `cd` into the repository
|
||||
2. Run `swift run -c release EmojiKit` to build the package
|
||||
3. Move the product into `/usr/local/bin` to make it available systemwide with `cp .build/release/EmojiKit /usr/bin/emojiKit`
|
||||
4. Call the script from anywhere with `emojiKit download <path> -v <version>
|
||||
|
||||
> Alternatively you can simply run `sh build.sh` to do steps 2. and 3. in one go.
|
||||
|
||||
### Arguments:
|
||||
* **path**: Specify the directory where the `emoji_v<version>.json` file should be stored. Only provide the directory like `/Desktop` or `.` for the current directory, not the whole file path.
|
||||
* **version (--version, -v)**: New emojis are released yearly (in general). Currently only version `14` and `15` (latest) are supported. But you can easily extend the script by adding new cases to the `EmojiUnicodeVersion` enum.
|
||||
|
||||
## Usage with Xcode
|
||||
> Swift Package Manager and CocoaPods support is planned for the future. Currently, you need to do some manual steps to use the emojis with Xcode and Swift.
|
||||
|
||||
1. In your Xcode project, add a file called `emoji_v<version>` to your assets, Make sure that they are included under *Build Phases - Copy Bundle Resources* (if not add them manually there). For each version (currently `v14` and `v15`) you need to add a separate file into Xcode.
|
||||
2. Run the script as describe above and specify the directory of the files you added in Xcode as the `<path>` argument.
|
||||
3. The script will query the emojis and saves them to your files in Xcode.
|
||||
4. Afterwards you're ready to use them in Xcode by either reading the data yourself or using the `EmojiManager.swift` included in this repository.
|
||||
5. Simply copy the `EmojiManager.swift` and `EmojiCategory.swift` files into your Xcode project.
|
||||
6. Afterwards you can load the emojis by calling `EmojiManager.getAvailableEmojis()`. The manager automatically fetches the correct unicode version depending on the device's iOS version to only show supported ones *(you need to run the script for all supported versions first to make the automatic loading work)*.
|
||||
|
||||
> **Note:** Version 14 includes all emojis that are supported on devices from `iOS 15.4` to `<iOS 16.4`. Devices with an iOS Version `>iOS 16.5` require version 15.
|
||||
|
||||
## Error Handling
|
||||
* If you see emojis on your device that are displayed as a `?` you're most likely running your App on a device below `iOS 15.4`. Anything below `iOS 15.4` is unfortunately not supported. If you still want to use it simply add a new case in the `EmojiUnicodeVersion`, e.g. `v13 = 13` to make it work (no guarantees though).
|
||||
* If the `EmojiManager` doesn't return any emojis, there are two possible reasons. First, you didn't run the script for the version you're trying to load, so your `.json` files are empty or non-existent. Simply run the script for all supported version and it should work. If not make sure that the `.json` files are included in the *Build Phases - Copy Bundle Resources* for each target that wants to use the `EmojiManager`.
|
||||
|
||||
|
||||
|
||||
85
Sources/EmojiKit/DataProcessor.swift
Normal file
85
Sources/EmojiKit/DataProcessor.swift
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// DataProcessor.swift
|
||||
//
|
||||
//
|
||||
// Created by Niklas Amslgruber on 12.06.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
32
Sources/EmojiKit/EmojiCategory.swift
Normal file
32
Sources/EmojiKit/EmojiCategory.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// 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 let name: Name
|
||||
public let values: [String]
|
||||
|
||||
public init(name: Name, values: [String]) {
|
||||
self.name = name
|
||||
self.values = values
|
||||
}
|
||||
}
|
||||
111
Sources/EmojiKit/EmojiDownloader.swift
Normal file
111
Sources/EmojiKit/EmojiDownloader.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// 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 {}
|
||||
58
Sources/EmojiKit/EmojiManager.swift
Normal file
58
Sources/EmojiKit/EmojiManager.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// EmojiManager.swift
|
||||
// Travely
|
||||
//
|
||||
// Created by Niklas Amslgruber on 13.06.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EmojiManager {
|
||||
|
||||
enum Version: Int {
|
||||
case v14 = 14
|
||||
case v15 = 15
|
||||
|
||||
var fileName: String {
|
||||
return "emojis_v\(rawValue)"
|
||||
}
|
||||
|
||||
static func getSupportedVersion() -> Version {
|
||||
if #available(iOS 16.4, *) {
|
||||
return .v15
|
||||
} else {
|
||||
return .v14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func getAvailableEmojis(version: Version = .getSupportedVersion()) -> [EmojiCategory] {
|
||||
if let url = Bundle.main.url(forResource: version.fileName, withExtension: "json"), 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({
|
||||
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) }
|
||||
}
|
||||
}
|
||||
21
Sources/EmojiKit/EmojiScripts.swift
Normal file
21
Sources/EmojiKit/EmojiScripts.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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,
|
||||
]
|
||||
)
|
||||
}
|
||||
33
Sources/EmojiKit/StringHelper.swift
Normal file
33
Sources/EmojiKit/StringHelper.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
147
Sources/EmojiKit/UnicodeParser.swift
Normal file
147
Sources/EmojiKit/UnicodeParser.swift
Normal file
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user