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() 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: " ") } }