feat: add searchable emoji picker
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
public struct Emoji: Sendable {
|
||||
public let value: String
|
||||
public let name: String
|
||||
public let category: String
|
||||
}
|
||||
|
||||
public enum EmojiCatalog {
|
||||
private struct Entry: Sendable {
|
||||
let emoji: Emoji
|
||||
let searchText: String
|
||||
}
|
||||
|
||||
private static let entries: [Entry] = {
|
||||
// SwiftPM's generated accessor does not search Contents/Resources in a packaged app.
|
||||
let packagedBundle = Bundle.main.resourceURL.flatMap {
|
||||
Bundle(url: $0.appendingPathComponent("Conjure_ConjureKit.bundle", isDirectory: true))
|
||||
}
|
||||
let bundle = packagedBundle ?? Bundle.module
|
||||
guard let url = bundle.url(forResource: "emoji", withExtension: "tsv"),
|
||||
let contents = try? String(contentsOf: url, encoding: .utf8) else {
|
||||
preconditionFailure("The bundled emoji catalog is missing or unreadable")
|
||||
}
|
||||
|
||||
// Pinned github/gemoji v4.1.0; attribution and license accompany the TSV.
|
||||
return contents.split(whereSeparator: \.isNewline).map { line in
|
||||
let fields = line.split(separator: "\t", omittingEmptySubsequences: false)
|
||||
precondition(fields.count == 5, "Invalid bundled emoji catalog row")
|
||||
return Entry(
|
||||
emoji: Emoji(value: String(fields[0]), name: String(fields[1]), category: String(fields[2])),
|
||||
searchText: normalized([fields[0], fields[1], fields[3], fields[4]].joined(separator: " "))
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
public static let all: [Emoji] = entries.map(\.emoji)
|
||||
|
||||
public static let categories: [String] = {
|
||||
var seen = Set<String>()
|
||||
return all.compactMap { seen.insert($0.category).inserted ? $0.category : nil }
|
||||
}()
|
||||
|
||||
/// Matches every query word against names, GitHub aliases, tags, or literal emoji.
|
||||
/// Category names use the exact values supplied by `categories`; results retain catalog order.
|
||||
public static func search(_ query: String, category: String? = nil) -> [Emoji] {
|
||||
let words = normalized(query).split(whereSeparator: \.isWhitespace)
|
||||
if words.isEmpty && category == nil { return all }
|
||||
return entries.compactMap { entry in
|
||||
guard category == nil || entry.emoji.category == category,
|
||||
words.allSatisfy({ entry.searchText.contains($0) }) else { return nil }
|
||||
return entry.emoji
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalized(_ text: String) -> String {
|
||||
text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: Locale(identifier: "en_US_POSIX"))
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.replacingOccurrences(of: ":", with: " ")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user