@parseapi/sdk 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Response types for the ParseAPI public API.
3
+ * Core answers the common task; optional deep reveals the reviewed detail.
4
+ * Every field inside a deep object is optional and nullable: the API ships
5
+ * `deep: {}` when deep was requested but is not unlocked on the plan.
6
+ */
7
+ /** The `deep` key is omitted unless the request asked for it. */
8
+ type Deep<T> = Partial<{
9
+ [K in keyof T]: T[K] | null;
10
+ }>;
11
+ interface EmojiSkin {
12
+ emoji: string;
13
+ tone: string;
14
+ unicode: string | null;
15
+ hex: string | null;
16
+ }
17
+ interface Emoji {
18
+ emoji: string;
19
+ name: string;
20
+ shortcodes: string[];
21
+ category: string | null;
22
+ deep?: Deep<EmojiDeep>;
23
+ }
24
+ interface EmojiSearch {
25
+ q: string;
26
+ emojis: Emoji[];
27
+ total?: number;
28
+ next_offset?: number | null;
29
+ }
30
+ interface EmojiBrowse {
31
+ emojis: Emoji[];
32
+ total: number;
33
+ next_offset: number | null;
34
+ }
35
+ interface EmojiVariant {
36
+ emoji: string;
37
+ tones: string[];
38
+ }
39
+ interface EmojiArtwork {
40
+ svg?: string;
41
+ png?: Record<string, string>;
42
+ /** Only present when this asset has different rights from its provider. */
43
+ license?: string;
44
+ license_url?: string;
45
+ attribution?: string;
46
+ }
47
+ interface EmojiSource {
48
+ id: string;
49
+ version: string | null;
50
+ source_url: string;
51
+ license: string;
52
+ license_url: string;
53
+ }
54
+ interface EmojiNotice {
55
+ id: string;
56
+ license: string;
57
+ notice: string;
58
+ }
59
+ interface EmojiArtworkSource {
60
+ style: string;
61
+ version: string | null;
62
+ unicode_version: string | null;
63
+ source_url: string;
64
+ license: string;
65
+ license_url: string;
66
+ attribution: string;
67
+ notices: string[];
68
+ loaded_coverage?: {
69
+ svg: number;
70
+ png: number;
71
+ };
72
+ flag_license?: {
73
+ license: string;
74
+ license_url: string;
75
+ description: string;
76
+ };
77
+ }
78
+ interface EmojiMetadata {
79
+ edition: string;
80
+ max_version: string | null;
81
+ source_version: string;
82
+ total: number;
83
+ sources: EmojiSource[];
84
+ artwork_sources: EmojiArtworkSource[];
85
+ notices: EmojiNotice[];
86
+ completeness: {
87
+ missing_count: number;
88
+ unexpected_count: number;
89
+ };
90
+ shortcode_sources?: Record<string, EmojiShortcodeSourceMetadata>;
91
+ [field: string]: unknown;
92
+ }
93
+ interface EmojiCatalogScope {
94
+ filters: Record<string, string>;
95
+ total: number;
96
+ complete_inventory: boolean;
97
+ }
98
+ interface EmojiNativeSearchFacts {
99
+ name: string | null;
100
+ keywords: string[];
101
+ order?: number | null;
102
+ }
103
+ interface EmojiCatalogSearchFacts {
104
+ native: Record<string, EmojiNativeSearchFacts>;
105
+ localized_keywords?: string[];
106
+ }
107
+ interface EmojiCatalogData {
108
+ profile?: 'full' | 'compact';
109
+ scope?: Omit<EmojiCatalogScope, 'total'>;
110
+ search?: EmojiCatalogSearchFacts;
111
+ localization?: EmojiCatalogLocalization;
112
+ metadata: EmojiMetadata;
113
+ total: number;
114
+ emojis: Emoji[];
115
+ forms: Record<string, string>;
116
+ shortcodes: Record<'cldr' | 'github' | 'slack' | 'discord', Record<string, string[]>>;
117
+ legacy: Record<string, string>;
118
+ }
119
+ interface EmojiCatalogLocalization {
120
+ requested_locale: string | null;
121
+ resolved_locale: string | null;
122
+ translated_names: number;
123
+ translated_keywords: number;
124
+ fallback_names: number;
125
+ fallback_keywords: number;
126
+ }
127
+ interface EmojiShortcodeSourceMetadata {
128
+ records: number;
129
+ missing_records: number;
130
+ ambiguous_aliases: number;
131
+ }
132
+ interface EmojiCategory {
133
+ category: string;
134
+ subcategories: string[];
135
+ }
136
+ interface EmojiCategories {
137
+ categories: EmojiCategory[];
138
+ }
139
+ /** UTF-16 code-unit positions. End is exclusive. */
140
+ interface EmojiPosition {
141
+ start: number;
142
+ end: number;
143
+ }
144
+ /** Original input UTF-16 positions; end is exclusive. */
145
+ interface EmojiUnresolved extends EmojiPosition {
146
+ emoji?: string;
147
+ shortcode?: string;
148
+ reason: 'unknown' | 'ambiguous' | 'unavailable';
149
+ }
150
+ interface EmojiDeep {
151
+ subcategory?: string | null;
152
+ base?: string | null;
153
+ variants?: EmojiVariant[];
154
+ codepoints: string[];
155
+ hex: string;
156
+ status: string | null;
157
+ version: string | null;
158
+ keywords: string[];
159
+ skins: EmojiSkin[];
160
+ artwork?: Record<string, EmojiArtwork>;
161
+ }
162
+ interface EmojiParse {
163
+ emojis: (Emoji & {
164
+ positions: EmojiPosition[];
165
+ })[];
166
+ }
167
+ interface EmojiConversion {
168
+ text: string;
169
+ unresolved: EmojiUnresolved[];
170
+ }
171
+
172
+ interface EmojiCatalogLookupOptions {
173
+ deep?: boolean;
174
+ shortcodeSource?: string;
175
+ }
176
+ interface EmojiCatalogSearchOptions {
177
+ deep?: boolean;
178
+ limit?: number;
179
+ offset?: number;
180
+ category?: string;
181
+ subcategory?: string;
182
+ version?: string;
183
+ maxVersion?: string;
184
+ includeSkinTones?: boolean;
185
+ }
186
+ interface EmojiCatalogParseOptions {
187
+ deep?: boolean;
188
+ }
189
+ interface EmojiCatalogConvertOptions {
190
+ direction: 'to_shortcodes' | 'to_emoji';
191
+ shortcodeSource: 'cldr' | 'github' | 'slack' | 'discord';
192
+ }
193
+ interface EmojiCatalogRegexOptions {
194
+ qualifiedOnly?: boolean;
195
+ components?: boolean;
196
+ }
197
+ /** A local validation or resolution failure. No HTTP request was made. */
198
+ declare class EmojiCatalogError extends Error {
199
+ readonly code: 'validation_error' | 'not_found' | 'invalid_catalog';
200
+ constructor(code: 'validation_error' | 'not_found' | 'invalid_catalog', message: string);
201
+ }
202
+ /** Immutable local snapshot. All operations are synchronous and use no network. */
203
+ declare class EmojiCatalog {
204
+ private readonly data;
205
+ private readonly byGlyph;
206
+ private readonly names;
207
+ private readonly aliases;
208
+ private readonly sourceAliases;
209
+ private readonly encodings;
210
+ private readonly searchFacts;
211
+ private readonly categoryKeys;
212
+ private readonly subcategoryKeys;
213
+ private readonly filterCache;
214
+ private searchReady;
215
+ private readonly compareNames;
216
+ private segmenter?;
217
+ constructor(data: EmojiCatalogData);
218
+ /** Build once on first search; shared normalized strings avoid repeated annotation copies. */
219
+ private ensureSearch;
220
+ private encoding;
221
+ /** Evidence for the full server inventory; scope() identifies this downloaded subset. */
222
+ metadata(): EmojiMetadata;
223
+ /** Filters, local record count and whether this download contains the full server inventory. */
224
+ scope(): EmojiCatalogScope;
225
+ lookup(input: string, options?: EmojiCatalogLookupOptions): Emoji;
226
+ search(query: string, options?: EmojiCatalogSearchOptions): EmojiSearch;
227
+ browse(options?: EmojiCatalogSearchOptions): EmojiBrowse;
228
+ /** Unknown and ambiguous inputs remain literal; positions refer to untouched input. */
229
+ convert(text: string, options: EmojiCatalogConvertOptions): EmojiConversion;
230
+ /** Whole-grapheme predicate. Use parse() to extract emoji from prose with positions. */
231
+ regex(options?: EmojiCatalogRegexOptions): RegExp;
232
+ categories(): EmojiCategories;
233
+ parse(text: string, options?: EmojiCatalogParseOptions): EmojiParse;
234
+ private project;
235
+ private page;
236
+ private filtered;
237
+ }
238
+
239
+ export { type Emoji, type EmojiBrowse, EmojiCatalog, type EmojiCatalogConvertOptions, type EmojiCatalogData, EmojiCatalogError, type EmojiCatalogLookupOptions, type EmojiCatalogParseOptions, type EmojiCatalogRegexOptions, type EmojiCatalogScope, type EmojiCatalogSearchOptions, type EmojiConversion, type EmojiDeep, type EmojiMetadata, type EmojiParse, type EmojiSearch };
@@ -0,0 +1,260 @@
1
+ // src/emoji-catalog.ts
2
+ var EmojiCatalogError = class extends Error {
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "EmojiCatalogError";
7
+ }
8
+ };
9
+ var sources = ["cldr", "github", "slack", "discord"];
10
+ var stripColons = (value) => value.trim().replace(/^:+|:+$/g, "");
11
+ var normalizeName = (value) => stripColons(value).toLowerCase().replace(/[\s_-]+/g, "_");
12
+ var normalizeSearch = (value) => normalizeName(value).normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").replace(/œ/g, "oe").replace(/æ/g, "ae").replace(/ß/g, "ss");
13
+ var legacyName = (value) => stripColons(value).toLowerCase().replace(/\s+/g, "_");
14
+ var filterKey = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
15
+ var versionCompare = (a, b) => {
16
+ const left = a.split(".").map(Number), right = b.split(".").map(Number);
17
+ return (left[0] ?? 0) - (right[0] ?? 0) || (left[1] ?? 0) - (right[1] ?? 0);
18
+ };
19
+ var invalid = (message) => {
20
+ throw new EmojiCatalogError("validation_error", message);
21
+ };
22
+ var EmojiCatalog = class {
23
+ data;
24
+ byGlyph = /* @__PURE__ */ new Map();
25
+ names = /* @__PURE__ */ new Map();
26
+ aliases = /* @__PURE__ */ new Map();
27
+ sourceAliases = /* @__PURE__ */ new Map();
28
+ encodings = /* @__PURE__ */ new Map();
29
+ searchFacts = /* @__PURE__ */ new Map();
30
+ categoryKeys = /* @__PURE__ */ new Set();
31
+ subcategoryKeys = /* @__PURE__ */ new Set();
32
+ filterCache = /* @__PURE__ */ new Map();
33
+ searchReady = false;
34
+ compareNames;
35
+ segmenter;
36
+ constructor(data) {
37
+ if (!data || !Array.isArray(data.emojis) || !data.forms || !data.shortcodes || !data.legacy || !data.metadata?.source_version) {
38
+ throw new EmojiCatalogError("invalid_catalog", "Use the JSON response from GET /emoji/catalog.");
39
+ }
40
+ this.data = structuredClone(data);
41
+ this.compareNames = new Intl.Collator(this.data.localization?.resolved_locale ?? void 0).compare;
42
+ const add = (map, key, glyph) => {
43
+ if (!key) return;
44
+ if (!map.has(key)) map.set(key, /* @__PURE__ */ new Set());
45
+ map.get(key).add(glyph);
46
+ };
47
+ for (const row of this.data.emojis) {
48
+ if (!row?.emoji || !row.deep?.version || !Array.isArray(row.deep.keywords)) {
49
+ throw new EmojiCatalogError("invalid_catalog", "Offline catalogs require the default deep=true export.");
50
+ }
51
+ this.byGlyph.set(row.emoji, row);
52
+ add(this.names, normalizeName(row.name), row.emoji);
53
+ for (const alias of row.shortcodes) add(this.aliases, normalizeName(alias), row.emoji);
54
+ this.categoryKeys.add(filterKey(row.category ?? ""));
55
+ this.subcategoryKeys.add(filterKey(row.deep.subcategory ?? ""));
56
+ }
57
+ for (const source of sources) {
58
+ if (!this.data.shortcodes[source]) throw new EmojiCatalogError("invalid_catalog", `Catalog is missing the ${source} shortcode map.`);
59
+ }
60
+ }
61
+ /** Build once on first search; shared normalized strings avoid repeated annotation copies. */
62
+ ensureSearch() {
63
+ if (this.searchReady) return;
64
+ const cache = /* @__PURE__ */ new Map(), normal = (value) => {
65
+ let result = cache.get(value);
66
+ if (result === void 0) {
67
+ result = normalizeSearch(value);
68
+ cache.set(value, result);
69
+ }
70
+ return result;
71
+ };
72
+ for (const source of sources) for (const [alias, glyphs] of Object.entries(this.data.shortcodes[source])) {
73
+ const value = normal(alias);
74
+ for (const glyph of glyphs) if (this.byGlyph.has(glyph)) {
75
+ if (!this.sourceAliases.has(glyph)) this.sourceAliases.set(glyph, /* @__PURE__ */ new Set());
76
+ this.sourceAliases.get(glyph).add(value);
77
+ }
78
+ }
79
+ const localizedKeywords = new Set(this.data.search?.localized_keywords ?? []);
80
+ for (const row of this.data.emojis) {
81
+ const native = this.data.search?.native[row.emoji];
82
+ const names = [...new Set([row.name, native?.name].filter((name) => typeof name === "string").map(normal))];
83
+ const primaryShorts = [...new Set(row.shortcodes.map(normal))];
84
+ const shorts = [.../* @__PURE__ */ new Set([...primaryShorts, ...this.sourceAliases.get(row.emoji) ?? []])];
85
+ const keywords = [...new Set([...row.deep?.keywords ?? [], ...native?.keywords ?? []].map(normal))];
86
+ const allNames = [...names, ...shorts];
87
+ this.searchFacts.set(row.emoji, { names, primaryShorts, shorts, keywords, translatedKeywords: this.data.localization?.resolved_locale && localizedKeywords.has(row.emoji) ? (row.deep?.keywords ?? []).map(normal) : [], allNames, blob: [.../* @__PURE__ */ new Set([...allNames, ...keywords])].join(" ") });
88
+ }
89
+ this.sourceAliases.clear();
90
+ this.searchReady = true;
91
+ }
92
+ encoding(source) {
93
+ let map = this.encodings.get(source);
94
+ if (!map) {
95
+ map = /* @__PURE__ */ new Map();
96
+ for (const [alias, glyphs] of Object.entries(this.data.shortcodes[source])) if (glyphs.length === 1 && this.byGlyph.has(glyphs[0]) && !map.has(glyphs[0])) map.set(glyphs[0], `:${stripColons(alias)}:`);
97
+ this.encodings.set(source, map);
98
+ }
99
+ return map;
100
+ }
101
+ /** Evidence for the full server inventory; scope() identifies this downloaded subset. */
102
+ metadata() {
103
+ return structuredClone(this.data.metadata);
104
+ }
105
+ /** Filters, local record count and whether this download contains the full server inventory. */
106
+ scope() {
107
+ return structuredClone({ ...this.data.scope, filters: this.data.scope?.filters ?? {}, total: this.data.total, complete_inventory: this.data.scope?.complete_inventory ?? this.data.total === this.data.metadata.total });
108
+ }
109
+ lookup(input, options = {}) {
110
+ if (typeof input !== "string" || !input.trim() || input.trim().length > 512) invalid("Emoji input must contain 1\u2013512 characters.");
111
+ const value = input.trim(), source = options.shortcodeSource;
112
+ if (source && !sources.includes(source)) invalid("shortcodeSource must be cldr, github, slack or discord.");
113
+ let glyph = this.byGlyph.has(value) ? value : void 0;
114
+ if (!glyph && !source && Object.hasOwn(this.data.legacy, legacyName(value))) glyph = this.data.legacy[legacyName(value)];
115
+ if (!glyph) {
116
+ let form = value;
117
+ if (/^(?:U\+)?[0-9A-F]{4,6}(?:[\s,-]+(?:U\+)?[0-9A-F]{4,6})*$/i.test(value)) {
118
+ const points = value.split(/[\s,-]+/).map((part) => Number.parseInt(part.replace(/^U\+/i, ""), 16));
119
+ if (points.length > 32 || points.some((cp) => cp > 1114111 || cp >= 55296 && cp <= 57343)) invalid("Unicode codepoints must be valid scalar values.");
120
+ form = String.fromCodePoint(...points);
121
+ } else if (/^U\+/i.test(value)) invalid("Use Unicode codepoints such as U+1F680 or U+2764 U+FE0F.");
122
+ const canonical = Object.hasOwn(this.data.forms, form) ? this.data.forms[form] : form;
123
+ if (canonical && this.byGlyph.has(canonical)) glyph = canonical;
124
+ }
125
+ if (!glyph) {
126
+ const candidates = /* @__PURE__ */ new Set();
127
+ for (const key of /* @__PURE__ */ new Set([stripColons(value).toLowerCase(), normalizeName(value)])) {
128
+ for (const name of source ? [source] : sources) {
129
+ const dictionary = this.data.shortcodes[name];
130
+ if (Object.hasOwn(dictionary, key)) {
131
+ for (const candidate of dictionary[key] ?? []) if (this.byGlyph.has(candidate)) candidates.add(candidate);
132
+ }
133
+ }
134
+ }
135
+ if (!source) for (const candidate of this.aliases.get(normalizeName(value)) ?? []) candidates.add(candidate);
136
+ if (!source || !candidates.size && !/^:.*:$/.test(value)) for (const candidate of this.names.get(normalizeName(value)) ?? []) candidates.add(candidate);
137
+ if (candidates.size > 1) invalid("Emoji shortcode is ambiguous. Set shortcodeSource to cldr, github, slack or discord.");
138
+ glyph = candidates.values().next().value;
139
+ }
140
+ const row = glyph ? this.byGlyph.get(glyph) : void 0;
141
+ if (!row) throw new EmojiCatalogError("not_found", "Emoji not found in this downloaded catalog.");
142
+ return this.project(row, options.deep === true);
143
+ }
144
+ search(query, options = {}) {
145
+ if (typeof query !== "string" || !query.trim() || query.trim().length > 256) invalid("Search query must contain 1\u2013256 characters.");
146
+ return this.page(query.trim(), options);
147
+ }
148
+ browse(options = {}) {
149
+ const { q, ...result } = this.page("", options);
150
+ return result;
151
+ }
152
+ /** Unknown and ambiguous inputs remain literal; positions refer to untouched input. */
153
+ convert(text, options) {
154
+ if (!options || !["to_shortcodes", "to_emoji"].includes(options.direction)) invalid("Direction must be to_shortcodes or to_emoji.");
155
+ if (!sources.includes(options.shortcodeSource)) invalid("shortcodeSource must be cldr, github, slack or discord.");
156
+ const parsed = this.parse(text), replacements = [], unresolved = [];
157
+ if (options.direction === "to_shortcodes") {
158
+ const encodings = this.encoding(options.shortcodeSource);
159
+ for (const row of parsed.emojis) {
160
+ const shortcode = encodings.get(row.emoji);
161
+ for (const position of row.positions) if (shortcode) replacements.push({ emoji: row.emoji, shortcode, ...position });
162
+ else unresolved.push({ emoji: row.emoji, ...position, reason: "unavailable" });
163
+ }
164
+ } else for (const match of text.matchAll(/:[^:\s]{1,128}:/gu)) {
165
+ const shortcode = match[0], start = match.index, end = start + shortcode.length;
166
+ try {
167
+ const row = this.lookup(shortcode, { shortcodeSource: options.shortcodeSource });
168
+ replacements.push({ emoji: row.emoji, shortcode, start, end });
169
+ } catch (error) {
170
+ if (!(error instanceof EmojiCatalogError)) throw error;
171
+ unresolved.push({ shortcode, start, end, reason: error.code === "not_found" ? "unknown" : "ambiguous" });
172
+ }
173
+ }
174
+ replacements.sort((a, b) => a.start - b.start);
175
+ unresolved.sort((a, b) => a.start - b.start);
176
+ let cursor = 0;
177
+ const chunks = [];
178
+ for (const row of replacements) {
179
+ chunks.push(text.slice(cursor, row.start), options.direction === "to_shortcodes" ? row.shortcode : row.emoji);
180
+ cursor = row.end;
181
+ }
182
+ chunks.push(text.slice(cursor));
183
+ return { text: chunks.join(""), unresolved };
184
+ }
185
+ /** Whole-grapheme predicate. Use parse() to extract emoji from prose with positions. */
186
+ regex(options = {}) {
187
+ const forms = options.qualifiedOnly ? [...this.byGlyph.keys()] : Object.keys(this.data.forms).filter((form) => this.byGlyph.has(this.data.forms[form]));
188
+ if (options.components) forms.push(...["\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}", "\u{1F9B0}", "\u{1F9B1}", "\u{1F9B2}", "\u{1F9B3}"]);
189
+ const escaped = [...new Set(forms)].sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).map((form) => form.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
190
+ return new RegExp(`^(?:${escaped.length ? escaped.join("|") : "(?!)"})$`, "u");
191
+ }
192
+ categories() {
193
+ const groups = /* @__PURE__ */ new Map();
194
+ for (const row of this.data.emojis) {
195
+ if (!row.category) continue;
196
+ if (!groups.has(row.category)) groups.set(row.category, /* @__PURE__ */ new Set());
197
+ if (row.deep?.subcategory) groups.get(row.category).add(row.deep.subcategory);
198
+ }
199
+ return { categories: [...groups].map(([category, subcategories]) => ({ category, subcategories: [...subcategories] })) };
200
+ }
201
+ parse(text, options = {}) {
202
+ if (typeof text !== "string" || text.length > 16e3) invalid("Text must be a string of at most 16000 UTF-16 code units.");
203
+ const found = /* @__PURE__ */ new Map();
204
+ this.segmenter ??= new Intl.Segmenter("en", { granularity: "grapheme" });
205
+ for (const { segment, index } of this.segmenter.segment(text)) {
206
+ const canonical = Object.hasOwn(this.data.forms, segment) ? this.data.forms[segment] : segment;
207
+ const row = canonical ? this.byGlyph.get(canonical) : void 0;
208
+ if (!row) continue;
209
+ if (!found.has(row.emoji)) found.set(row.emoji, { ...this.project(row, options.deep === true), positions: [] });
210
+ const result = found.get(row.emoji);
211
+ result.positions.push({ start: index, end: index + segment.length });
212
+ }
213
+ return { emojis: [...found.values()] };
214
+ }
215
+ project(row, deep) {
216
+ const { emoji, name, shortcodes, category } = row;
217
+ return { emoji, name, shortcodes: [...shortcodes], category, ...deep ? { deep: structuredClone(row.deep) } : {} };
218
+ }
219
+ page(query, options) {
220
+ if (options.limit !== void 0 && (!Number.isSafeInteger(options.limit) || options.limit < 1)) invalid("Limit must be a positive integer.");
221
+ const limit = Math.min(options.limit ?? 20, 50), offset = options.offset ?? 0;
222
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1e5) invalid("Offset must be an integer between 0 and 100000.");
223
+ for (const [key, known] of [["category", this.categoryKeys], ["subcategory", this.subcategoryKeys]]) {
224
+ if (options[key] !== void 0 && (!options[key].trim() || !known.has(filterKey(options[key])))) invalid(`Unknown emoji ${key}. Use categories() to find supported values.`);
225
+ }
226
+ for (const key of ["version", "maxVersion"]) if (options[key] !== void 0 && (!/^\d{1,2}\.\d{1,2}$/.test(options[key]) || versionCompare(options[key], this.data.metadata.source_version) > 0)) invalid(`${key} must be an Emoji version no newer than ${this.data.metadata.source_version}.`);
227
+ let rows = this.filtered(options);
228
+ if (query) {
229
+ this.ensureSearch();
230
+ const needle = normalizeSearch(query), words = needle.split("_").filter(Boolean);
231
+ const scored = rows.flatMap((row) => {
232
+ const { names, primaryShorts, shorts, keywords, translatedKeywords, allNames, blob } = this.searchFacts.get(row.emoji);
233
+ if (!blob.includes(needle) && !(words.length > 1 && words.every((word) => blob.includes(word)))) return [];
234
+ const score = primaryShorts.includes(needle) || names.includes(needle) ? 0 : shorts.includes(needle) ? 0.5 : allNames.some((value) => value.startsWith(needle)) ? 1 : translatedKeywords.includes(needle) ? 1.5 : allNames.some((value) => value.includes(needle)) ? 2 : words.length > 1 && allNames.some((value) => words.every((word) => value.includes(word))) ? 2.5 : keywords.some((value) => value.includes(needle)) ? 3 : words.length > 1 && words.every((word) => blob.includes(word)) ? 4 : 100;
235
+ return score === 100 ? [] : [{ row, score }];
236
+ });
237
+ rows = scored.sort((a, b) => a.score - b.score || (this.data.localization?.resolved_locale ? this.compareNames(a.row.name, b.row.name) || a.row.emoji.localeCompare(b.row.emoji) : a.row.name.localeCompare(b.row.name))).map(({ row }) => row);
238
+ }
239
+ const page = rows.slice(offset, offset + limit), paged = !query || ["category", "subcategory", "version", "maxVersion", "includeSkinTones", "offset"].some((key) => options[key] !== void 0);
240
+ return { q: query, emojis: page.map((row) => this.project(row, options.deep === true)), ...paged ? { total: rows.length, next_offset: offset + page.length < rows.length ? offset + page.length : null } : {} };
241
+ }
242
+ filtered(options) {
243
+ for (const [key2, known] of [["category", this.categoryKeys], ["subcategory", this.subcategoryKeys]]) {
244
+ if (options[key2] !== void 0 && (!options[key2].trim() || !known.has(filterKey(options[key2])))) invalid(`Unknown emoji ${key2}. Use categories() to find supported values.`);
245
+ }
246
+ for (const key2 of ["version", "maxVersion"]) if (options[key2] !== void 0 && (!/^\d{1,2}\.\d{1,2}$/.test(options[key2]) || versionCompare(options[key2], this.data.metadata.source_version) > 0)) invalid(`${key2} must be an Emoji version no newer than ${this.data.metadata.source_version}.`);
247
+ if (options.includeSkinTones !== false && options.category === void 0 && options.subcategory === void 0 && options.version === void 0 && options.maxVersion === void 0) return this.data.emojis;
248
+ const key = JSON.stringify([options.includeSkinTones === false, options.category === void 0 ? null : filterKey(options.category), options.subcategory === void 0 ? null : filterKey(options.subcategory), options.version, options.maxVersion]);
249
+ const cached = this.filterCache.get(key);
250
+ if (cached) return cached;
251
+ const rows = this.data.emojis.filter((row) => (options.includeSkinTones !== false || !/[\u{1F3FB}-\u{1F3FF}]/u.test(row.emoji)) && (options.category === void 0 || filterKey(row.category ?? "") === filterKey(options.category)) && (options.subcategory === void 0 || filterKey(row.deep?.subcategory ?? "") === filterKey(options.subcategory)) && (options.version === void 0 || versionCompare(row.deep.version, options.version) === 0) && (options.maxVersion === void 0 || versionCompare(row.deep.version, options.maxVersion) <= 0));
252
+ if (this.filterCache.size >= 32) this.filterCache.delete(this.filterCache.keys().next().value);
253
+ this.filterCache.set(key, rows);
254
+ return rows;
255
+ }
256
+ };
257
+ export {
258
+ EmojiCatalog,
259
+ EmojiCatalogError
260
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parseapi/sdk",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "Official ParseAPI client for Node and TypeScript. One key, minimal JSON, fast.",
5
5
  "keywords": [
6
6
  "parseapi",
@@ -37,6 +37,16 @@
37
37
  "types": "./dist/index.d.cts",
38
38
  "default": "./dist/index.cjs"
39
39
  }
40
+ },
41
+ "./offline": {
42
+ "import": {
43
+ "types": "./dist/offline.d.ts",
44
+ "default": "./dist/offline.js"
45
+ },
46
+ "require": {
47
+ "types": "./dist/offline.d.cts",
48
+ "default": "./dist/offline.cjs"
49
+ }
40
50
  }
41
51
  },
42
52
  "files": [
@@ -47,7 +57,7 @@
47
57
  "node": ">=18"
48
58
  },
49
59
  "scripts": {
50
- "build": "tsup src/index.ts --format esm,cjs --dts --target node18 --clean",
60
+ "build": "tsup src/index.ts --format esm,cjs --dts --target node18 --clean && tsup src/offline.ts --format esm,cjs --dts --target node18",
51
61
  "prepublishOnly": "npm test && npm run typecheck && npm run api:check",
52
62
  "test": "vitest run",
53
63
  "typecheck": "tsc --noEmit",