cizgile 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -249,6 +249,24 @@ If you are an assistant writing code with this library, these are the facts that
249
249
  - Use `resolveUri`, `normalizeUri` and `equivalentUris` instead of string concatenation or `new URL()` when you need RFC behaviour (strict scheme handling, no special-scheme rewriting, no host IDNA unless you ask for it).
250
250
  - Every exported function has an explicit TypeScript signature; the `.d.mts` files in `dist/` are the authoritative API.
251
251
 
252
+ ## Performance
253
+
254
+ Measured with `bun run bench` (vitest bench, Node 24, one core of a desktop CPU). Higher is better.
255
+
256
+ | input | cizgile | `@sindresorhus/slugify` | `slugify` (simov) |
257
+ | ------------------------------------- | -------------- | ----------------------- | ----------------- |
258
+ | ASCII title (60 chars) | **396k ops/s** | 136k | 147k |
259
+ | Latin with diacritics | **235k ops/s** | 96k | 179k |
260
+ | Turkish, `locale: "tr"` | 212k ops/s | 99k | **216k** |
261
+ | Cyrillic, `transliterate: [cyrillic]` | 179k ops/s | 86k | **188k** |
262
+ | 2.5 KB of mixed text | **6.4k ops/s** | 4.3k | 3.4k |
263
+ | `isSlug` | 3.7M ops/s | — | — |
264
+
265
+ `resolveUri` runs at ~0.8M ops/s (the built-in `URL` parser: ~1M), `removeDotSegments` at 2M,
266
+ `percentEncode` at 0.5M (`encodeURIComponent`: 3.3M — it is native), `normalizeUri` at 0.3M.
267
+ Options objects are resolved once and cached structurally, so inline `{ locale: "tr" }` literals cost
268
+ nothing after the first call.
269
+
252
270
  ## How it compares
253
271
 
254
272
  | input | cizgile | Django `slugify` | Rails `parameterize` | `@sindresorhus/slugify` |
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { S as symbols, _ as fold, g as DEFAULT_TABLES, l as latinLocales } from "./shared/locales-latin-D8enWKzw.mjs";
1
+ import { C as symbols, _ as compileTables, g as DEFAULT_TABLES, l as latinLocales, v as fold } from "./shared/locales-latin-DshBH0E6.mjs";
2
2
  import { N as isSegmentNzNc, n as iriToUri } from "./shared/iri-CzbwHYgr.mjs";
3
3
 
4
4
  //#region src/slug/bidi.ts
@@ -36,7 +36,7 @@ function decamelize(input) {
36
36
 
37
37
  //#endregion
38
38
  //#region src/slug/charset.ts
39
- function escapeRegExp(text) {
39
+ function escapeRegExp$1(text) {
40
40
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
41
  }
42
42
  function escapeClassChar(text) {
@@ -142,6 +142,19 @@ function checkScripts(text, level = "moderately-restrictive") {
142
142
 
143
143
  //#endregion
144
144
  //#region src/slug/options.ts
145
+ let symbolOnlyCache;
146
+ function symbolEntries(table) {
147
+ symbolOnlyCache ??= /* @__PURE__ */ new WeakMap();
148
+ const cached = symbolOnlyCache.get(table);
149
+ if (cached !== void 0) return cached;
150
+ const out = {};
151
+ for (const [key, value] of Object.entries(table)) if (!/[\p{L}\p{M}\p{N}]/u.test(key)) out[key] = value;
152
+ symbolOnlyCache.set(table, out);
153
+ return out;
154
+ }
155
+ function escapeRegExp(text) {
156
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
157
+ }
145
158
  const DEFAULT_REMOVE = /['’]/g;
146
159
  let cache$1;
147
160
  function resolveLocale(locale) {
@@ -189,12 +202,16 @@ function build(options) {
189
202
  const wordExtra = preserveCharacters.map(escapeClassChar).join("");
190
203
  const wordClass = unicode ? `\\p{L}\\p{N}\\p{M}${wordExtra}` : `a-z0-9${lowercase ? "" : "A-Z"}${wordExtra}`;
191
204
  const allowedClass = wordClass + separatorChars.map(escapeClassChar).join("");
205
+ const tables = resolveTables(options.transliterate, unicode, locale);
206
+ const symbolTables = (locale === void 0 ? [symbols] : [locale.table, symbols]).map(symbolEntries);
207
+ const separatorRuns = separator === "" ? void 0 : new RegExp(`[${separatorChars.map(escapeClassChar).join("")}]+`, "gu");
208
+ const leadingMarks = unicode ? new RegExp(`(${separator === "" ? "^" : `(?:^|${escapeRegExp(separator)})`})\\p{M}+`, "gu") : void 0;
192
209
  return {
193
210
  separator,
194
211
  lowercase,
195
212
  unicode,
196
213
  locale,
197
- tables: resolveTables(options.transliterate, unicode, locale),
214
+ tables,
198
215
  decamelize: options.decamelize ?? false,
199
216
  replacements: options.replacements ?? [],
200
217
  remove,
@@ -206,14 +223,61 @@ function build(options) {
206
223
  bidi: options.bidi ?? "allow",
207
224
  allowedClass,
208
225
  wordClass,
209
- disallowed: new RegExp(`[^${allowedClass}]+`, "gu")
226
+ disallowed: new RegExp(`[^${allowedClass}]+`, "gu"),
227
+ compiled: tables === void 0 ? void 0 : compileTables(tables),
228
+ symbolTables,
229
+ compiledSymbols: compileTables(symbolTables),
230
+ separatorRuns,
231
+ leadingMarks
210
232
  };
211
233
  }
212
- function resolveOptions(options = {}) {
234
+ const DEFAULT_OPTIONS$1 = {};
235
+ let objectIds;
236
+ let nextObjectId = 0;
237
+ let byKey;
238
+ function objectId(value) {
239
+ objectIds ??= /* @__PURE__ */ new WeakMap();
240
+ let id = objectIds.get(value);
241
+ if (id === void 0) {
242
+ id = nextObjectId;
243
+ nextObjectId += 1;
244
+ objectIds.set(value, id);
245
+ }
246
+ return id;
247
+ }
248
+ function optionsKey(options) {
249
+ const locale = options.locale;
250
+ const transliterate = options.transliterate;
251
+ const remove = options.remove;
252
+ return JSON.stringify([
253
+ options.separator,
254
+ options.lowercase,
255
+ options.unicode,
256
+ typeof locale === "object" ? `#${objectId(locale)}` : locale,
257
+ Array.isArray(transliterate) ? transliterate.map((t) => objectId(t)) : transliterate,
258
+ options.decamelize,
259
+ options.replacements,
260
+ remove instanceof RegExp ? `${remove.source}/${remove.flags}` : remove,
261
+ options.preserveCharacters,
262
+ options.preserveLeadingUnderscore,
263
+ options.preserveTrailingSeparator,
264
+ options.maxLength,
265
+ options.scripts,
266
+ options.bidi
267
+ ]);
268
+ }
269
+ function resolveOptions(options = DEFAULT_OPTIONS$1) {
213
270
  cache$1 ??= /* @__PURE__ */ new WeakMap();
214
271
  const cached = cache$1.get(options);
215
272
  if (cached !== void 0) return cached;
216
- const resolved = build(options);
273
+ byKey ??= /* @__PURE__ */ new Map();
274
+ const key = optionsKey(options);
275
+ let resolved = byKey.get(key);
276
+ if (resolved === void 0) {
277
+ resolved = build(options);
278
+ if (byKey.size > 512) byKey.clear();
279
+ byKey.set(key, resolved);
280
+ }
217
281
  cache$1.set(options, resolved);
218
282
  return resolved;
219
283
  }
@@ -227,7 +291,7 @@ function patternFor(options) {
227
291
  if (cached !== void 0) return cached;
228
292
  const o = resolveOptions(options);
229
293
  const char = `[${o.wordClass}]`;
230
- const separator = escapeRegExp(o.separator);
294
+ const separator = escapeRegExp$1(o.separator);
231
295
  const lead = o.preserveLeadingUnderscore ? "_?" : "";
232
296
  const word = o.unicode ? `(?!\\p{M})${char}+` : `${char}+`;
233
297
  const body = o.separator === "" ? word : `${word}(?:${separator}${word})*`;
@@ -236,7 +300,8 @@ function patternFor(options) {
236
300
  cache.set(options, pattern);
237
301
  return pattern;
238
302
  }
239
- function isSlug(input, options = {}) {
303
+ const DEFAULT_OPTIONS = {};
304
+ function isSlug(input, options = DEFAULT_OPTIONS) {
240
305
  if (typeof input !== "string" || input === "" || input === "." || input === "..") return false;
241
306
  const o = resolveOptions(options);
242
307
  if (o.maxLength !== void 0 && input.length > o.maxLength) return false;
@@ -302,16 +367,6 @@ function truncateSlug(slug, maxLength, separator = "-") {
302
367
 
303
368
  //#endregion
304
369
  //#region src/slug/slugify.ts
305
- let symbolOnlyCache;
306
- function symbolEntries(table) {
307
- symbolOnlyCache ??= /* @__PURE__ */ new WeakMap();
308
- const cached = symbolOnlyCache.get(table);
309
- if (cached !== void 0) return cached;
310
- const out = {};
311
- for (const [key, value] of Object.entries(table)) if (!/[\p{L}\p{M}\p{N}]/u.test(key)) out[key] = value;
312
- symbolOnlyCache.set(table, out);
313
- return out;
314
- }
315
370
  let compatCache;
316
371
  function compatEntries(table) {
317
372
  compatCache ??= /* @__PURE__ */ new WeakMap();
@@ -331,35 +386,37 @@ function lowercaseOf(text, o) {
331
386
  if (o.unicode && o.locale?.lowercase !== void 0) return o.locale.lowercase(text);
332
387
  return text.replaceAll("İ", "i").toLowerCase();
333
388
  }
334
- function collapse(text, separator) {
335
- if (separator === "") return text;
336
- const runs = new RegExp(`[${uniqueChars(separator).map(escapeClassChar).join("")}]+`, "gu");
337
- let out = text.replace(runs, separator);
389
+ const NON_ASCII = /[^\x00-\x7F]/;
390
+ function collapse(text, o) {
391
+ if (o.separatorRuns === void 0) return text;
392
+ const separator = o.separator;
393
+ let out = text.replace(o.separatorRuns, separator);
338
394
  while (out.startsWith(separator)) out = out.slice(separator.length);
339
395
  while (out.endsWith(separator)) out = out.slice(0, -separator.length);
340
396
  return out;
341
397
  }
342
- function slugify(input, options = {}) {
398
+ function slugify(input, options) {
343
399
  if (typeof input !== "string") throw new TypeError("slugify: input must be a string");
344
400
  const o = resolveOptions(options);
345
- let s = stripControls(input).normalize("NFC");
401
+ let s = stripControls(input);
402
+ let ascii = !NON_ASCII.test(s);
403
+ if (!ascii) s = s.normalize("NFC");
346
404
  for (const [from, to] of o.replacements) if (from !== "") s = s.split(from).join(to);
347
- const symbolTables = (o.locale === void 0 ? [symbols] : [o.locale.table, symbols]).map(symbolEntries);
348
- s = applyCompat(s, o.tables ?? symbolTables).normalize("NFKC");
405
+ if (o.replacements.length > 0) ascii = !NON_ASCII.test(s);
406
+ if (!ascii) s = applyCompat(s, o.tables ?? o.symbolTables).normalize("NFKC");
349
407
  if (o.decamelize) s = decamelize(s);
350
- if (o.tables !== void 0) s = fold(s, o.tables, false).replace(/(?![\x00-\x7F])[\p{L}\p{M}\p{N}]/gu, "");
351
- else s = fold(s, symbolTables, false, false);
408
+ if (o.compiled !== void 0) {
409
+ s = fold(s, o.compiled, false);
410
+ if (!ascii && NON_ASCII.test(s)) s = s.replace(/(?![\x00-\x7F])[\p{L}\p{M}\p{N}]/gu, "");
411
+ } else s = fold(s, o.compiledSymbols, false, false);
352
412
  if (o.lowercase) s = lowercaseOf(s, o);
353
- if (o.unicode) s = s.normalize("NFKC");
413
+ if (o.unicode && !ascii) s = s.normalize("NFKC");
354
414
  if (o.remove !== void 0) s = s.replace(o.remove, "");
355
415
  const hadLeadingUnderscore = s.startsWith("_");
356
416
  s = s.replace(o.disallowed, o.separator);
357
- if (o.unicode) {
358
- const boundary = o.separator === "" ? "^" : `(?:^|${escapeRegExp(o.separator)})`;
359
- s = s.replace(new RegExp(`(${boundary})\\p{M}+`, "gu"), "$1");
360
- }
417
+ if (o.leadingMarks !== void 0 && !ascii) s = s.replace(o.leadingMarks, "$1");
361
418
  const hadTrailingSeparator = o.separator !== "" && s.endsWith(o.separator);
362
- s = collapse(s, o.separator);
419
+ s = collapse(s, o);
363
420
  if (o.maxLength !== void 0) s = truncateSlug(s, o.maxLength, o.separator);
364
421
  if (s === "." || s === "..") s = "";
365
422
  if (o.preserveLeadingUnderscore && hadLeadingUnderscore && s !== "" && !s.startsWith("_")) s = "_" + s;
@@ -135,21 +135,57 @@ const symbols = {
135
135
  //#endregion
136
136
  //#region src/transliterate/core.ts
137
137
  const DEFAULT_TABLES = [latin, symbols];
138
- let sequenceCache;
138
+ const NON_ASCII = /[^\x00-\x7F]/;
139
+ let tableIds;
140
+ let nextTableId = 0;
141
+ let compiledByArray;
142
+ let compiledByKey;
139
143
  function codePointCount(text) {
140
144
  let count = 0;
141
145
  for (const _ of text) count += 1;
142
146
  return count;
143
147
  }
144
- function sequencesOf(table) {
145
- sequenceCache ??= /* @__PURE__ */ new WeakMap();
146
- const cached = sequenceCache.get(table);
147
- if (cached !== void 0) return cached;
148
+ function tableId(table) {
149
+ tableIds ??= /* @__PURE__ */ new WeakMap();
150
+ let id = tableIds.get(table);
151
+ if (id === void 0) {
152
+ id = nextTableId;
153
+ nextTableId += 1;
154
+ tableIds.set(table, id);
155
+ }
156
+ return id;
157
+ }
158
+ function build(tables) {
159
+ const map = /* @__PURE__ */ new Map();
148
160
  const multi = [];
149
- for (const key of Object.keys(table)) if (codePointCount(key) > 1) multi.push([key, table[key] ?? ""]);
161
+ for (const table of tables) for (const key of Object.keys(table)) {
162
+ if (map.has(key)) continue;
163
+ const value = table[key] ?? "";
164
+ map.set(key, value);
165
+ if (codePointCount(key) > 1) multi.push([key, value]);
166
+ }
150
167
  multi.sort((a, b) => b[0].length - a[0].length);
151
- sequenceCache.set(table, multi);
152
- return multi;
168
+ const asciiKeys = [...map.keys()].filter((key) => key.length === 1 && (key.codePointAt(0) ?? 0) < 128);
169
+ return {
170
+ map,
171
+ sequences: multi,
172
+ ascii: asciiKeys.length === 0 ? void 0 : new RegExp(`[${asciiKeys.map((k) => k.replace(/[\]\\^-]/g, "\\$&")).join("")}]`, "g")
173
+ };
174
+ }
175
+ function compileTables(tables) {
176
+ compiledByArray ??= /* @__PURE__ */ new WeakMap();
177
+ const byArray = compiledByArray.get(tables);
178
+ if (byArray !== void 0) return byArray;
179
+ compiledByKey ??= /* @__PURE__ */ new Map();
180
+ const key = tables.map(tableId).join(",");
181
+ let compiled = compiledByKey.get(key);
182
+ if (compiled === void 0) {
183
+ compiled = build(tables);
184
+ if (compiledByKey.size > 256) compiledByKey.clear();
185
+ compiledByKey.set(key, compiled);
186
+ }
187
+ compiledByArray.set(tables, compiled);
188
+ return compiled;
153
189
  }
154
190
  function resolveTables(options) {
155
191
  const out = [];
@@ -170,12 +206,28 @@ function lookup(tables, key) {
170
206
  function stripMarks(input) {
171
207
  return input.normalize("NFD").replace(/\p{M}+/gu, "").normalize("NFC");
172
208
  }
209
+ let markCache;
210
+ function stripMarksCached(ch) {
211
+ markCache ??= /* @__PURE__ */ new Map();
212
+ let base = markCache.get(ch);
213
+ if (base === void 0) {
214
+ base = stripMarks(ch);
215
+ if (markCache.size > 8192) markCache.clear();
216
+ markCache.set(ch, base);
217
+ }
218
+ return base;
219
+ }
173
220
  function fold(input, tables, dropUnknown, foldMarks = true) {
221
+ const compiled = "sequences" in tables ? tables : compileTables(tables);
222
+ const map = compiled.map;
174
223
  let text = input;
175
- for (const table of tables) for (const [key, value] of sequencesOf(table)) if (text.includes(key)) text = text.split(key).join(value);
224
+ if (!NON_ASCII.test(text)) return compiled.ascii === void 0 ? text : text.replace(compiled.ascii, (m) => map.get(m) ?? m);
225
+ if (compiled.sequences.length > 0) {
226
+ for (const [key, value] of compiled.sequences) if (text.includes(key)) text = text.split(key).join(value);
227
+ }
176
228
  let out = "";
177
229
  for (const ch of text) {
178
- const direct = lookup(tables, ch);
230
+ const direct = map.get(ch);
179
231
  if (direct !== void 0) {
180
232
  out += direct;
181
233
  continue;
@@ -184,10 +236,10 @@ function fold(input, tables, dropUnknown, foldMarks = true) {
184
236
  out += ch;
185
237
  continue;
186
238
  }
187
- const base = foldMarks ? stripMarks(ch) : ch;
239
+ const base = foldMarks ? stripMarksCached(ch) : ch;
188
240
  if (base !== ch) {
189
241
  if (base === "") continue;
190
- out += lookup(tables, base) ?? (dropUnknown && (base.codePointAt(0) ?? 0) >= 128 ? "" : base);
242
+ out += map.get(base) ?? (dropUnknown && (base.codePointAt(0) ?? 0) >= 128 ? "" : base);
191
243
  continue;
192
244
  }
193
245
  out += dropUnknown ? "" : ch;
@@ -369,4 +421,4 @@ const latinLocales = {
369
421
  };
370
422
 
371
423
  //#endregion
372
- export { latin as C, symbols as S, fold as _, fi as a, stripMarks as b, it as c, nl as d, pt as f, DEFAULT_TABLES as g, vi as h, es as i, latinLocales as l, tr as m, da as n, fr as o, sv as p, de as r, hu as s, az as t, nb as u, lookup as v, transliterate as x, resolveTables as y };
424
+ export { symbols as C, transliterate as S, compileTables as _, fi as a, resolveTables as b, it as c, nl as d, pt as f, DEFAULT_TABLES as g, vi as h, es as i, latinLocales as l, tr as m, da as n, fr as o, sv as p, de as r, hu as s, az as t, nb as u, fold as v, latin as w, stripMarks as x, lookup as y };
@@ -1,13 +1,4 @@
1
1
  import { a as TransliterationTable, i as LocaleId, n as LatinLocaleId, r as Locale, t as CyrillicLocaleId } from "./shared/types-C1iMvUXh.mjs";
2
- //#region src/transliterate/arabic.d.ts
3
- export declare const arabic: TransliterationTable;
4
- export declare const persian: TransliterationTable;
5
- export declare const urdu: TransliterationTable;
6
- export declare const pashto: TransliterationTable;
7
- //#endregion
8
- //#region src/transliterate/armenian.d.ts
9
- export declare const armenian: TransliterationTable;
10
- //#endregion
11
2
  //#region src/transliterate/core.d.ts
12
3
  interface TransliterateOptions {
13
4
  readonly locale?: Locale;
@@ -15,12 +6,27 @@ interface TransliterateOptions {
15
6
  readonly unknown?: "keep" | "drop";
16
7
  }
17
8
  export declare const DEFAULT_TABLES: readonly TransliterationTable[];
9
+ type Sequences = ReadonlyArray<readonly [string, string]>;
10
+ interface CompiledTables {
11
+ readonly map: ReadonlyMap<string, string>;
12
+ readonly sequences: Sequences;
13
+ readonly ascii: RegExp | undefined;
14
+ }
18
15
  export declare function resolveTables(options: TransliterateOptions): readonly TransliterationTable[];
19
16
  export declare function lookup(tables: readonly TransliterationTable[], key: string): string | undefined;
20
17
  export declare function stripMarks(input: string): string;
21
- export declare function fold(input: string, tables: readonly TransliterationTable[], dropUnknown: boolean, foldMarks?: boolean): string;
18
+ export declare function fold(input: string, tables: readonly TransliterationTable[] | CompiledTables, dropUnknown: boolean, foldMarks?: boolean): string;
22
19
  export declare function transliterate(input: string, options?: TransliterateOptions): string;
23
20
  //#endregion
21
+ //#region src/transliterate/arabic.d.ts
22
+ export declare const arabic: TransliterationTable;
23
+ export declare const persian: TransliterationTable;
24
+ export declare const urdu: TransliterationTable;
25
+ export declare const pashto: TransliterationTable;
26
+ //#endregion
27
+ //#region src/transliterate/armenian.d.ts
28
+ export declare const armenian: TransliterationTable;
29
+ //#endregion
24
30
  //#region src/transliterate/cyrillic.d.ts
25
31
  export declare const cyrillic: TransliterationTable;
26
32
  export declare const cyrillicUk: TransliterationTable;
@@ -1,4 +1,4 @@
1
- import { C as latin, S as symbols, _ as fold, a as fi, b as stripMarks, c as it, d as nl, f as pt, g as DEFAULT_TABLES, h as vi, i as es, l as latinLocales, m as tr, n as da, o as fr, p as sv, r as de, s as hu, t as az, u as nb, v as lookup, x as transliterate, y as resolveTables } from "./shared/locales-latin-D8enWKzw.mjs";
1
+ import { C as symbols, S as transliterate, a as fi, b as resolveTables, c as it, d as nl, f as pt, g as DEFAULT_TABLES, h as vi, i as es, l as latinLocales, m as tr, n as da, o as fr, p as sv, r as de, s as hu, t as az, u as nb, v as fold, w as latin, x as stripMarks, y as lookup } from "./shared/locales-latin-DshBH0E6.mjs";
2
2
 
3
3
  //#region src/transliterate/arabic.ts
4
4
  const arabic = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cizgile",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "RFC 3986/3987-grounded URL slugs. Transliteration, Unicode slugs, IRI to URI, percent-encoding, dot-segment removal. Zero dependencies, tree-shakeable.",
5
5
  "keywords": [
6
6
  "esm",
@@ -62,9 +62,11 @@
62
62
  "coverage": "vitest run --coverage",
63
63
  "release": "bun run test && bun run build && bumpp --commit --tag --push --all",
64
64
  "prepack": "bun run build",
65
- "test:bun": "bun --bun vitest run"
65
+ "test:bun": "bun --bun vitest run",
66
+ "bench": "vitest bench --run"
66
67
  },
67
68
  "devDependencies": {
69
+ "@sindresorhus/slugify": "^3.0.0",
68
70
  "@types/bun": "1.4.0",
69
71
  "@vitest/coverage-v8": "4.1.11",
70
72
  "bumpp": "12.2.2",
@@ -73,6 +75,7 @@
73
75
  "oxlint-tsgolint": "7.0.2001",
74
76
  "rolldown": "1.2.6",
75
77
  "rolldown-plugin-dts": "0.28.4",
78
+ "slugify": "^1.6.9",
76
79
  "typescript": "7.0.2",
77
80
  "vitest": "4.1.11"
78
81
  },