cizgile 0.2.0 → 0.3.0
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 +53 -33
- package/dist/index.d.mts +20 -12
- package/dist/index.mjs +124 -53
- package/dist/shared/{locales-latin-Bm94HAeL.mjs → registry-_m94akct.mjs} +154 -13
- package/dist/shared/{types-DbyhfPRc.d.mts → types-B3EVWvDh.d.mts} +1 -1
- package/dist/transliterate.d.mts +64 -2
- package/dist/transliterate.mjs +7344 -11
- package/dist/uri.d.mts +68 -17
- package/dist/uri.mjs +399 -236
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<br>
|
|
3
|
-
<img src=".github/assets/cover.svg?v=
|
|
3
|
+
<img src=".github/assets/cover.svg?v=bcfc1d9" alt="cizgile — Zero-dependency URL slug engine" width="100%">
|
|
4
4
|
<br><br>
|
|
5
5
|
<b style="font-size: 2em;">cizgile</b>
|
|
6
6
|
<br><br>
|
|
@@ -30,23 +30,23 @@ slugify("Straße Über Ärger", { locale: "de" }) // "strasse-ueber-aerger"
|
|
|
30
30
|
slugify("你好 World", { unicode: true }) // "你好-world"
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
No dependencies. ESM only. Node 20+, Bun
|
|
33
|
+
No dependencies. ESM only. Node 20+, Bun and Deno are exercised in CI; the code touches no host API, so browsers and edge workers run it as-is.
|
|
34
34
|
|
|
35
35
|
## Why cizgile
|
|
36
36
|
|
|
37
37
|
- **Slugs that are correct by construction.** Every ASCII slug is a valid URL path segment (RFC 3986 `segment-nz-nc`) — no percent-encoding needed, no `.` or `..`, no accidental scheme prefix.
|
|
38
|
-
- **Speaks your language.**
|
|
38
|
+
- **Speaks your language.** 45 locales (`tr`, `de`, `pl`, `sv`, `uk`, `hi`, `ta`, `ja`, `ko`, …) and 19 scripts (Latin, Cyrillic, Greek, Arabic, Armenian, Georgian, Dhivehi, Hebrew, Hangul, kana, Devanagari and the seven other Indic scripts). `ß` → `ss`, `İ` → `i`, `Щ` → `shch`, `서울` → `seoul`.
|
|
39
39
|
- **Unicode slugs when you want them.** `你好-world` stays readable, and `iriToUri` gives you the exact percent-encoded form for the wire.
|
|
40
40
|
- **A real URL toolkit underneath.** Resolve, normalise, compare, validate and relativise URLs by the RFC, cross-checked against the WHATWG `URL` parser.
|
|
41
41
|
- **Small and tree-shakeable.** `import { slugify }` ships the Latin table only; other scripts load only when you import them.
|
|
42
42
|
|
|
43
43
|
## Three entry points
|
|
44
44
|
|
|
45
|
-
| import | what you get
|
|
46
|
-
| ----------------------- |
|
|
47
|
-
| `cizgile` | `slugify`, `isSlug`, `createSlugger`, `truncateSlug`, `decamelize`, script and bidi guards
|
|
48
|
-
| `cizgile/transliterate` | `transliterate`, per-script tables, locales, `defineLocale`
|
|
49
|
-
| `cizgile/uri` | percent-encoding, `resolveUri`, `normalizeUri`, `relativize`, validators, IRI ↔ URI, punycode
|
|
45
|
+
| import | what you get |
|
|
46
|
+
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `cizgile` | `slugify`, `isSlug`, `createSlugger`, `truncateSlug`, `measure`, `decamelize`, script and bidi guards |
|
|
48
|
+
| `cizgile/transliterate` | `transliterate`, per-script tables, locales, `defineLocale` |
|
|
49
|
+
| `cizgile/uri` | percent-encoding, `resolveUri`, `normalizeUri`, `relativize`, validators, IRI ↔ URI, punycode |
|
|
50
50
|
|
|
51
51
|
## Slugs
|
|
52
52
|
|
|
@@ -62,12 +62,14 @@ slugify("Hello World", { separator: "_" }) // "hello_world"
|
|
|
62
62
|
slugify("Donald E. Knuth", { lowercase: false }) // "Donald-E-Knuth"
|
|
63
63
|
slugify("getHTTPResponse", { decamelize: true }) // "get-http-response"
|
|
64
64
|
slugify("the quick brown fox", { maxLength: 9 }) // "the-quick"
|
|
65
|
+
slugify("Ünïcödé Büro", { unicode: true, maxLength: 11, maxLengthUnit: "bytes" }) // "ünïcödé"
|
|
66
|
+
slugify("!!!", { fallback: "untitled" }) // "untitled"
|
|
65
67
|
slugify("C++ & Rust", { replacements: [["C++", "cpp"]] }) // "cpp-and-rust"
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
### Languages and scripts
|
|
69
71
|
|
|
70
|
-
Locale ids for Latin-script languages; Cyrillic locales and other scripts come from `cizgile/transliterate` so they only end up in your bundle when you use them.
|
|
72
|
+
Locale ids for Latin-script languages; Cyrillic locales and other scripts come from `cizgile/transliterate` so they only end up in your bundle when you use them (or register them once with `registerLocale` and use their ids as strings).
|
|
71
73
|
|
|
72
74
|
```ts
|
|
73
75
|
import { slugify } from "cizgile"
|
|
@@ -87,7 +89,7 @@ const swiss = defineLocale(de, { id: "de-CH", table: { ß: "ss" } })
|
|
|
87
89
|
slugify("Straße", { locale: swiss }) // "strasse"
|
|
88
90
|
```
|
|
89
91
|
|
|
90
|
-
Locale ids: `az ca cs da de es et fi fr hr hu is it lt lv nb nl pl pt ro sk sl sv tr vi`. Locale objects: those plus `be bg kk mk ru sr uk` (Cyrillic) and `el he ja ko` (Greek, Hebrew, kana, Hangul).
|
|
92
|
+
Locale ids: `az ca cs da de es et fi fr hr hu is it lt lv nb nl pl pt ro sk sl sv tr vi`. Locale objects: those plus `be bg kk mk ru sr uk` (Cyrillic) and `el he hi ja ko` (Greek, Hebrew, Devanagari, kana, Hangul), `bn pa gu or ta te kn ml` (Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam). `registerLocale(ru, uk)` makes those ids usable as strings too. Every Latin and Cyrillic locale spells `&`, `%`, `$` and `£` in its language: `slugify("50% off", { locale: "de" })` is `"50-prozent-off"`, without a locale `"50-off"`.
|
|
91
93
|
|
|
92
94
|
### Unicode slugs
|
|
93
95
|
|
|
@@ -136,39 +138,41 @@ isSlug("你好-world", { unicode: true }) // true
|
|
|
136
138
|
|
|
137
139
|
### All options
|
|
138
140
|
|
|
139
|
-
| option | default | what it does
|
|
140
|
-
| --------------------------- | --------- |
|
|
141
|
-
| `separator` | `"-"` | Joins words. Any URL-safe punctuation (`- _ . ~ !$&'()*+,;= @`), several of them (`"--"`), or `""`.
|
|
142
|
-
| `lowercase` | `true` | `false` keeps the original case.
|
|
143
|
-
| `unicode` | `false` | Keep letters from every script instead of transliterating to ASCII.
|
|
144
|
-
| `locale` | — | Language-specific rules: a locale id or a `Locale` object.
|
|
145
|
-
| `transliterate` | `true` | `false` skips the Latin and symbol tables (the locale table and accent folding still apply); an array adds script tables.
|
|
146
|
-
| `decamelize` | `false` | `fooBar` → `foo-bar`, `HTMLParser` → `html-parser`.
|
|
147
|
-
| `replacements` | `[]` | `[from, to]` pairs applied first; spaces in `to` become separators.
|
|
148
|
-
| `remove` | `/['’]/g` | A global regex of characters to delete rather than turn into separators (`don't` → `dont`); `false` keeps them.
|
|
149
|
-
| `preserveCharacters` | `[]` | Extra URL-safe single characters to keep, e.g. `["."]` for version numbers. The separator or anything outside `segment-nz-nc` throws.
|
|
150
|
-
| `preserveLeadingUnderscore` | `false` | `_draft` → `_draft`.
|
|
151
|
-
| `preserveTrailingSeparator` | `false` | Keep a trailing separator while the user is still typing.
|
|
152
|
-
| `maxLength` | — | Cut at a word boundary, never inside a character (emoji sequences, combining marks).
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
141
|
+
| option | default | what it does |
|
|
142
|
+
| --------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| `separator` | `"-"` | Joins words. Any URL-safe punctuation (`- _ . ~ !$&'()*+,;= @`), several of them (`"--"`), or `""`. |
|
|
144
|
+
| `lowercase` | `true` | `false` keeps the original case. |
|
|
145
|
+
| `unicode` | `false` | Keep letters from every script instead of transliterating to ASCII. |
|
|
146
|
+
| `locale` | — | Language-specific rules: a locale id or a `Locale` object. |
|
|
147
|
+
| `transliterate` | `true` | `false` skips the Latin and symbol tables (the locale table and accent folding still apply); `"none"` keeps only accent folding and the symbol words; an array adds script tables. |
|
|
148
|
+
| `decamelize` | `false` | `fooBar` → `foo-bar`, `HTMLParser` → `html-parser`. |
|
|
149
|
+
| `replacements` | `[]` | `[from, to]` pairs applied first; spaces in `to` become separators. |
|
|
150
|
+
| `remove` | `/['’]/g` | A global regex of characters to delete rather than turn into separators (`don't` → `dont`); `false` keeps them. |
|
|
151
|
+
| `preserveCharacters` | `[]` | Extra URL-safe single characters to keep, e.g. `["."]` for version numbers. The separator or anything outside `segment-nz-nc` throws. |
|
|
152
|
+
| `preserveLeadingUnderscore` | `false` | `_draft` → `_draft`. |
|
|
153
|
+
| `preserveTrailingSeparator` | `false` | Keep a trailing separator while the user is still typing. |
|
|
154
|
+
| `maxLength` | — | Cut at a word boundary, never inside a character (emoji sequences, combining marks). |
|
|
155
|
+
| `maxLengthUnit` | `"units"` | What `maxLength` counts: UTF-16 code units like `.length`, `"code-points"`, `"graphemes"`, or UTF-8 `"bytes"` for a column or filename budget. |
|
|
156
|
+
| `fallback` | — | Used when the result would be `""`: a string or a function of the input, slugified with the same options (`"untitled"`, then `untitled-2` in a slugger). |
|
|
157
|
+
| `scripts` | `"any"` | Unicode mode: UTS #39 mixed-script restriction level. |
|
|
158
|
+
| `bidi` | `"allow"` | Unicode mode: RFC 3987 §4.2 direction rule — `"encode"` or `"throw"` on violation. |
|
|
155
159
|
|
|
156
160
|
The pipeline runs in this order: strip control/format characters → NFC → `replacements` → NFKC → `decamelize` → transliterate (locale → your tables → Latin → symbols → strip accents) → lowercase → `remove` → separators → `maxLength` → guards. Output is idempotent: `slugify(slugify(x)) === slugify(x)`.
|
|
157
161
|
|
|
158
162
|
## Transliteration on its own
|
|
159
163
|
|
|
160
164
|
```ts
|
|
161
|
-
import { transliterate, cyrillic, hangul, kana, locales } from "cizgile/transliterate"
|
|
165
|
+
import { transliterate, cyrillic, hangul, kana, devanagari, locales } from "cizgile/transliterate"
|
|
162
166
|
|
|
163
167
|
transliterate("Straße Ærø") // "Strasse AEro"
|
|
164
168
|
transliterate("Привет", { tables: [cyrillic] }) // "Privet"
|
|
165
169
|
transliterate("Ängsö", { locale: locales.sv }) // "Aengsoe"
|
|
166
|
-
transliterate("서울 ひらがな", { tables: [hangul, kana] }) // "seoul hiragana"
|
|
170
|
+
transliterate("서울 ひらがな नमस्ते", { tables: [hangul, kana, devanagari] }) // "seoul hiragana namaste"
|
|
167
171
|
transliterate("नमस्ते 你好") // "नमस्ते 你好" — unknown scripts are kept intact (use unknown: "drop" to remove)
|
|
168
172
|
transliterate("final x² Ⅷ", { nfkc: true }) // "final x2 VIII"
|
|
169
173
|
```
|
|
170
174
|
|
|
171
|
-
Tables: `latin symbols cyrillic cyrillicUk cyrillicBg cyrillicMk cyrillicSr greek arabic persian urdu pashto armenian georgian dhivehi hebrew hangul kana`, plus `allScripts`. Where a letter is spelled differently at the start of a word (Armenian `ե`, Ukrainian `є ї й ю я`), the capital carries the word-initial form.
|
|
175
|
+
Tables: `latin symbols cyrillic cyrillicUk cyrillicBg cyrillicMk cyrillicSr greek arabic persian urdu pashto armenian georgian dhivehi hebrew devanagari bengali gurmukhi gujarati oriya tamil telugu kannada malayalam hangul kana`, plus `allScripts`. Where a letter is spelled differently at the start of a word (Armenian `ե`, Ukrainian `є ї й ю я`), the capital carries the word-initial form. Devanagari and the other Indic scripts are romanised syllable by syllable (inherent `a`, no vowel length, no schwa deletion: `भारत` is `bharata`), Hangul jamo by jamo (Revised Romanization without sound-change rules, so `한국어` is `hangukeo`), kana with Hepburn (the long-vowel mark and sokuon are dropped); kanji and Han are left as they are. `defineLocale` and `mergeTables` return new objects — nothing global is ever mutated.
|
|
172
176
|
|
|
173
177
|
## URL toolkit
|
|
174
178
|
|
|
@@ -187,12 +191,18 @@ import {
|
|
|
187
191
|
isAbsoluteUri,
|
|
188
192
|
isIPv6Address,
|
|
189
193
|
extractUri,
|
|
194
|
+
getOrigin,
|
|
195
|
+
sortQuery,
|
|
196
|
+
joinPaths,
|
|
190
197
|
iriToUri,
|
|
191
198
|
uriToIri,
|
|
192
199
|
domainToAscii,
|
|
193
200
|
} from "cizgile/uri"
|
|
194
201
|
|
|
195
202
|
resolveUri("http://a/b/c/d;p?q", "../../g") // "http://a/g"
|
|
203
|
+
getOrigin("HTTP://Example.com:80/a?b") // "http://example.com"
|
|
204
|
+
sortQuery("http://a/p?b=2&a=1") // "http://a/p?a=1&b=2"
|
|
205
|
+
joinPaths("/api/", "/v1", "../v2/") // "/api/v2/"
|
|
196
206
|
relativize("http://a/b/c/d;p?q", "http://a/b/g") // "../g"
|
|
197
207
|
normalizeUri("HTTP://www.EXAMPLE.com:80/%7e%41/./b/../c") // "http://www.example.com/~A/c"
|
|
198
208
|
equivalentUris("http://example.com", "http://example.com:80/") // true
|
|
@@ -214,13 +224,14 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
214
224
|
`encodePathSegment(seg, { noColon })`, `encodePath(path, { relative })`, `encodeQuery`, `encodeFragment`, `encodeForm`.
|
|
215
225
|
|
|
216
226
|
**Hosts (§3.2.2)**
|
|
217
|
-
`isIPv4Address` `isIPv6Address` `isIPvFuture` `isIPLiteral` `isRegName` `isHost` `parseHost` `parseAuthority` `serializeAuthority
|
|
227
|
+
`isIPv4Address` `isIPv6Address` `isIPvFuture` `isIPLiteral` `isRegName` `isHost` `parseHost` `parseAuthority` `serializeAuthority` `normalizeIPv6Address` (RFC 5952). `0x7f.0.0.1` and `2130706433` are registered names, not addresses (§7.4).
|
|
218
228
|
|
|
219
229
|
**Parsing and validation (§4, Appendix A/B)**
|
|
220
|
-
`parseUri` `serializeUri` — components stay distinct from "absent"; the serializer inserts `/.` or `./` where the grammar requires it.
|
|
230
|
+
`parseUri(uri, { authority })` `serializeUri` — components stay distinct from "absent"; `authority: true` also gives `userinfo`, `host`, `port` and `portNumber`, and the serializer accepts those in place of `authority` and inserts `/.` or `./` where the grammar requires it.
|
|
221
231
|
`isUriReference` `isUri` `isAbsoluteUri` `isRelativeReference` `classifyReference` `pathForm` — validating parser built from the ABNF.
|
|
222
232
|
`isIriReference` `isIri` `isIunreserved` `isIpchar` — the same for IRIs (RFC 3987 §2.2).
|
|
223
233
|
`extractUri(text)` — Appendix C: strips `<>`, quotes, `URL:` prefixes, trailing punctuation and line-wrap whitespace; a markdown link or an `href`/`src` attribute yields its URL.
|
|
234
|
+
`findUris(text)` — the URIs in ordinary prose (`scheme://`, `www.`, `mailto:` and friends) with their offsets, trailing punctuation trimmed.
|
|
224
235
|
|
|
225
236
|
**Resolution (§5)**
|
|
226
237
|
`resolveUri(base, ref, { strict, allowRelativeBase })` — every §5.4 example passes; strict by default (`http:g` stays `http:g`).
|
|
@@ -229,7 +240,10 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
229
240
|
`isSameDocumentReference(base, ref, { normalize })`.
|
|
230
241
|
|
|
231
242
|
**Normalisation and comparison (§6)**
|
|
232
|
-
`normalizeUri(uri, { defaultPorts, schemeBased, userinfo })` — case, percent-encoding, dot segments, default ports, empty path →
|
|
243
|
+
`normalizeUri(uri, { defaultPorts, schemeBased, userinfo, trailingSlash, emptyQuery, emptyFragment, host, strict })` — case, percent-encoding, dot segments, default ports, empty path → `/`, IPv6 hosts in RFC 5952 form (`[0:0:0:0:0:0:0:1]` → `[::1]`); `userinfo: "strip-password" | "strip"` for logs; `trailingSlash: "add" | "remove"`, `emptyQuery`/`emptyFragment: "remove"`, `host: "idna" | "unicode"` and `strict` (throw on a bad host or port) are opt-in.
|
|
244
|
+
`getOrigin(uri)` `isSameOrigin(a, b)` — RFC 6454 origins with default-port elision. `stripFragment(uri)`.
|
|
245
|
+
`parseQuery(query)` `stringifyQuery(pairs)` `sortQuery(uri)` — `application/x-www-form-urlencoded` pairs in order, and a URI with its parameters sorted by name then value.
|
|
246
|
+
`joinPaths(...pieces)` — single slashes, no dot segments, the first piece's leading and the last piece's trailing slash kept.
|
|
233
247
|
`normalizePath(path, { trailingSlash })`.
|
|
234
248
|
`equivalentUris(a, b, { level, base, ignoreFragment, defaultPorts })` — `"simple"`, `"syntax"` or `"scheme"` (default). Never maps IRIs to URIs (RFC 3987 §5.3.1).
|
|
235
249
|
|
|
@@ -292,6 +306,10 @@ nothing after the first call.
|
|
|
292
306
|
|
|
293
307
|
RFC 3986 (with errata 2033, 4547, 4789, 5428), RFC 3987, RFC 3492 (all nineteen §7.1 sample strings), RFC 8820, RFC 9844, the UTS #46 mapping step, the WHATWG URL Standard's percent-encode sets, Unicode UTS #39 restriction levels and UAX #29 grapheme boundaries, Google Search Central's URL guidance. The test suite runs every example those documents contain.
|
|
294
308
|
|
|
309
|
+
## Changelog
|
|
310
|
+
|
|
311
|
+
Release notes live on the [GitHub Releases page](https://github.com/productdevbook/cizgile/releases); each release lists the commits since the previous tag. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to work on the library and [SECURITY.md](SECURITY.md) for reporting a vulnerability.
|
|
312
|
+
|
|
295
313
|
## Development
|
|
296
314
|
|
|
297
315
|
```sh
|
|
@@ -299,6 +317,8 @@ bun install
|
|
|
299
317
|
bun run test # oxlint, oxfmt, tsc, vitest under node, then vitest under bun
|
|
300
318
|
bun run build # rolldown → dist/*.mjs + dist/*.d.mts
|
|
301
319
|
bun run coverage
|
|
320
|
+
bun run bench # vitest bench against @sindresorhus/slugify, simov/slugify and the built-ins
|
|
321
|
+
bun run bench:baseline # write bench/baseline.json locally; the Bench workflow compares each run with the last successful one on main
|
|
302
322
|
bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
303
323
|
```
|
|
304
324
|
|
|
@@ -307,7 +327,7 @@ bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
|
307
327
|
- [simov/slugify](https://github.com/simov/slugify) — the charmap + per-locale override idea and most Cyrillic, Greek, Arabic and symbol values.
|
|
308
328
|
- [sindresorhus/slugify](https://github.com/sindresorhus/slugify) and [sindresorhus/transliterate](https://github.com/sindresorhus/transliterate) — `decamelize`, custom replacements, the counter slugger, and the Armenian, Georgian and Dhivehi tables.
|
|
309
329
|
- [Django](https://github.com/django/django) and [Rails](https://github.com/rails/rails) — the reference behaviours the parity tests are written against.
|
|
310
|
-
- The [WHATWG URL Standard](https://url.spec.whatwg.org/) — percent-encode sets and the parser every result is cross-checked with.
|
|
330
|
+
- The [WHATWG URL Standard](https://url.spec.whatwg.org/) — percent-encode sets and the parser every result is cross-checked with, and the RFC 3986-compatible subset of its [`urltestdata.json`](https://github.com/web-platform-tests/wpt/blob/master/url/resources/urltestdata.json) (web-platform-tests, BSD 3-Clause) vendored as a fixture.
|
|
311
331
|
- [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) by Berners-Lee, Fielding and Masinter, and [RFC 3987](https://datatracker.ietf.org/doc/html/rfc3987) by Duerst and Suignard.
|
|
312
332
|
- [Rolldown](https://rolldown.rs), [Oxc](https://oxc.rs), [Vitest](https://vitest.dev), [Bun](https://bun.sh) and [TypeScript](https://www.typescriptlang.org).
|
|
313
333
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as LocaleId, n as LatinLocaleId, o as TransliterationTable, r as Locale } from "./shared/types-
|
|
1
|
+
import { i as LocaleId, n as LatinLocaleId, o as TransliterationTable, r as Locale } from "./shared/types-B3EVWvDh.mjs";
|
|
2
2
|
//#region src/slug/bidi.d.ts
|
|
3
3
|
/** RFC 3987 section 4.2: whether `text` can be a URL component without mixing text directions in a way that renders ambiguously. */
|
|
4
4
|
export declare function isBidiSafeComponent(text: string): boolean;
|
|
@@ -22,6 +22,14 @@ export declare function detectScripts(text: string): string[];
|
|
|
22
22
|
/** Applies a UTS #39 restriction `level` to the scripts in `text`; `ok` is false when the mix exceeds it. */
|
|
23
23
|
export declare function checkScripts(text: string, level?: ScriptRestriction): ScriptCheck;
|
|
24
24
|
//#endregion
|
|
25
|
+
//#region src/slug/truncate.d.ts
|
|
26
|
+
/** How `maxLength` counts: UTF-16 code units (like `.length`), code points, grapheme clusters, or UTF-8 bytes. */
|
|
27
|
+
type LengthUnit = "units" | "code-points" | "graphemes" | "bytes";
|
|
28
|
+
/** The length of `text` in the given unit. */
|
|
29
|
+
export declare function measure(text: string, unit?: LengthUnit): number;
|
|
30
|
+
/** Cuts `slug` to at most `maxLength` in the given `unit` (UTF-16 code units by default) at a `separator` boundary, never inside a grapheme cluster. */
|
|
31
|
+
export declare function truncateSlug(slug: string, maxLength: number, separator?: string, unit?: LengthUnit): string;
|
|
32
|
+
//#endregion
|
|
25
33
|
//#region src/slug/options.d.ts
|
|
26
34
|
/** Options for `slugify` and `createSlugger`. Every option has a default; an empty object is the everyday call. */
|
|
27
35
|
interface SlugifyOptions {
|
|
@@ -31,10 +39,10 @@ interface SlugifyOptions {
|
|
|
31
39
|
readonly lowercase?: boolean;
|
|
32
40
|
/** Keeps letters from every script instead of transliterating to ASCII; `false` by default. */
|
|
33
41
|
readonly unicode?: boolean;
|
|
34
|
-
/** Language rules: a Latin locale id such as `"tr"`, or a `Locale` object from `cizgile/transliterate`. */
|
|
35
|
-
readonly locale?: LatinLocaleId | Locale;
|
|
36
|
-
/** `false` skips the Latin and symbol tables (the locale table and accent folding still apply); an array adds script tables such as `cyrillic`. */
|
|
37
|
-
readonly transliterate?: boolean | readonly TransliterationTable[];
|
|
42
|
+
/** Language rules: a Latin locale id such as `"tr"`, the id of a locale passed to `registerLocale`, or a `Locale` object from `cizgile/transliterate`. */
|
|
43
|
+
readonly locale?: LatinLocaleId | (string & {}) | Locale;
|
|
44
|
+
/** `false` skips the Latin and symbol tables (the locale table and accent folding still apply); `"none"` skips every letter table and keeps only accent folding and the symbol words; an array adds script tables such as `cyrillic`. */
|
|
45
|
+
readonly transliterate?: boolean | "none" | readonly TransliterationTable[];
|
|
38
46
|
/** Splits camelCase before slugging: `"fooBar"` becomes `"foo-bar"`; `false` by default. */
|
|
39
47
|
readonly decamelize?: boolean;
|
|
40
48
|
/** `[from, to]` pairs applied before anything else; spaces in `to` become separators. */
|
|
@@ -47,15 +55,19 @@ interface SlugifyOptions {
|
|
|
47
55
|
readonly preserveLeadingUnderscore?: boolean;
|
|
48
56
|
/** Keeps a trailing separator, for input still being typed. */
|
|
49
57
|
readonly preserveTrailingSeparator?: boolean;
|
|
50
|
-
/** Cuts at a word boundary, never inside a grapheme cluster. Counts UTF-16 code units like `.length
|
|
58
|
+
/** Cuts at a word boundary, never inside a grapheme cluster. Counts in `maxLengthUnit`, UTF-16 code units like `.length` by default. */
|
|
51
59
|
readonly maxLength?: number;
|
|
60
|
+
/** What `maxLength` counts: `"units"` (default), `"code-points"`, `"graphemes"` or UTF-8 `"bytes"`. */
|
|
61
|
+
readonly maxLengthUnit?: LengthUnit;
|
|
62
|
+
/** Used when the result would be `""`: a string, or a function of the input; the value is slugified with the same options. */
|
|
63
|
+
readonly fallback?: string | ((input: string) => string);
|
|
52
64
|
/** Unicode mode only: the UTS #39 restriction level the result must satisfy; `"any"` by default. */
|
|
53
65
|
readonly scripts?: ScriptRestriction;
|
|
54
66
|
/** Unicode mode only: what to do when the result mixes text directions (RFC 3987 section 4.2); `"allow"` by default. */
|
|
55
67
|
readonly bidi?: "allow" | "encode" | "throw";
|
|
56
68
|
}
|
|
57
69
|
/** The `slugify` options that shape what a valid slug looks like. */
|
|
58
|
-
type IsSlugOptions = Pick<SlugifyOptions, "separator" | "lowercase" | "unicode" | "locale" | "preserveCharacters" | "preserveLeadingUnderscore" | "preserveTrailingSeparator" | "maxLength" | "scripts" | "bidi">;
|
|
70
|
+
type IsSlugOptions = Pick<SlugifyOptions, "separator" | "lowercase" | "unicode" | "locale" | "preserveCharacters" | "preserveLeadingUnderscore" | "preserveTrailingSeparator" | "maxLength" | "maxLengthUnit" | "scripts" | "bidi">;
|
|
59
71
|
//#endregion
|
|
60
72
|
//#region src/slug/is-slug.d.ts
|
|
61
73
|
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
@@ -87,8 +99,4 @@ export declare const slugifyWithCounter: (defaults?: SlugifyOptions) => Slugger;
|
|
|
87
99
|
*/
|
|
88
100
|
export declare function slugify(input: string, options?: SlugifyOptions): string;
|
|
89
101
|
//#endregion
|
|
90
|
-
|
|
91
|
-
/** Cuts `slug` to at most `maxLength` UTF-16 code units at a `separator` boundary, never inside a grapheme cluster. */
|
|
92
|
-
export declare function truncateSlug(slug: string, maxLength: number, separator?: string): string;
|
|
93
|
-
//#endregion
|
|
94
|
-
export type { IsSlugOptions, LatinLocaleId, Locale, LocaleId, ScriptCheck, ScriptRestriction, Slugger, SlugifyOptions, TransliterationTable };
|
|
102
|
+
export type { IsSlugOptions, LatinLocaleId, LengthUnit, Locale, LocaleId, ScriptCheck, ScriptRestriction, Slugger, SlugifyOptions, TransliterationTable };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { M as applyCompat, N as compileTables, P as fold, j as DEFAULT_TABLES, n as registeredLocale, r as registryVersion, v as latinLocales, z as symbols } from "./shared/registry-_m94akct.mjs";
|
|
2
2
|
import { h as isSegmentNzNc, r as percentEncode } from "./shared/percent-DG-pZhJI.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/slug/bidi.ts
|
|
@@ -168,14 +168,15 @@ let cache$1;
|
|
|
168
168
|
function resolveLocale(locale) {
|
|
169
169
|
if (locale === void 0) return void 0;
|
|
170
170
|
if (typeof locale === "string") {
|
|
171
|
-
const found = latinLocales[locale];
|
|
172
|
-
if (found === void 0) throw new TypeError(`slugify: unknown locale "${locale}"; pass a Locale object exported by cizgile/transliterate`);
|
|
171
|
+
const found = latinLocales[locale] ?? registeredLocale(locale);
|
|
172
|
+
if (found === void 0) throw new TypeError(`slugify: unknown locale "${locale}"; pass a Locale object exported by cizgile/transliterate, or register it with registerLocale()`);
|
|
173
173
|
return found;
|
|
174
174
|
}
|
|
175
175
|
return locale;
|
|
176
176
|
}
|
|
177
177
|
function resolveTables(option, unicode, locale) {
|
|
178
178
|
if (unicode) return void 0;
|
|
179
|
+
if (option === "none") return (locale === void 0 ? [symbols] : [locale.table, symbols]).map(symbolEntries);
|
|
179
180
|
const out = [];
|
|
180
181
|
if (locale !== void 0) {
|
|
181
182
|
out.push(locale.table);
|
|
@@ -207,6 +208,15 @@ function build(options) {
|
|
|
207
208
|
}
|
|
208
209
|
const maxLength = options.maxLength;
|
|
209
210
|
if (maxLength !== void 0 && (!Number.isInteger(maxLength) || maxLength < 0)) throw new RangeError("slugify: maxLength must be a non-negative integer");
|
|
211
|
+
const maxLengthUnit = options.maxLengthUnit ?? "units";
|
|
212
|
+
if (![
|
|
213
|
+
"units",
|
|
214
|
+
"code-points",
|
|
215
|
+
"graphemes",
|
|
216
|
+
"bytes"
|
|
217
|
+
].includes(maxLengthUnit)) throw new TypeError(`slugify: unknown maxLengthUnit ${JSON.stringify(maxLengthUnit)}`);
|
|
218
|
+
const fallback = options.fallback;
|
|
219
|
+
if (fallback !== void 0 && typeof fallback !== "string" && typeof fallback !== "function") throw new TypeError("slugify: fallback must be a string or a function");
|
|
210
220
|
const wordExtra = preserveCharacters.map(escapeClassChar).join("");
|
|
211
221
|
const wordClass = unicode ? `\\p{L}\\p{N}\\p{M}${wordExtra}` : `a-z0-9${lowercase ? "" : "A-Z"}${wordExtra}`;
|
|
212
222
|
const allowedClass = wordClass + separatorChars.map(escapeClassChar).join("");
|
|
@@ -227,6 +237,8 @@ function build(options) {
|
|
|
227
237
|
preserveLeadingUnderscore: options.preserveLeadingUnderscore ?? false,
|
|
228
238
|
preserveTrailingSeparator: options.preserveTrailingSeparator ?? false,
|
|
229
239
|
maxLength,
|
|
240
|
+
maxLengthUnit,
|
|
241
|
+
fallback,
|
|
230
242
|
scripts: options.scripts ?? "any",
|
|
231
243
|
bidi: options.bidi ?? "allow",
|
|
232
244
|
allowedClass,
|
|
@@ -257,6 +269,7 @@ function optionsKey(options) {
|
|
|
257
269
|
const locale = options.locale;
|
|
258
270
|
const transliterate = options.transliterate;
|
|
259
271
|
const remove = options.remove;
|
|
272
|
+
const fallback = options.fallback;
|
|
260
273
|
return JSON.stringify([
|
|
261
274
|
options.separator,
|
|
262
275
|
options.lowercase,
|
|
@@ -270,60 +283,31 @@ function optionsKey(options) {
|
|
|
270
283
|
options.preserveLeadingUnderscore,
|
|
271
284
|
options.preserveTrailingSeparator,
|
|
272
285
|
options.maxLength,
|
|
286
|
+
options.maxLengthUnit,
|
|
287
|
+
typeof fallback === "function" ? `#${objectId(fallback)}` : fallback,
|
|
273
288
|
options.scripts,
|
|
274
289
|
options.bidi
|
|
275
290
|
]);
|
|
276
291
|
}
|
|
277
292
|
function resolveOptions(options = DEFAULT_OPTIONS$1) {
|
|
278
293
|
cache$1 ??= /* @__PURE__ */ new WeakMap();
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
294
|
+
const registered = typeof options.locale === "string" && !(options.locale in latinLocales);
|
|
295
|
+
if (!registered) {
|
|
296
|
+
const cached = cache$1.get(options);
|
|
297
|
+
if (cached !== void 0) return cached;
|
|
298
|
+
}
|
|
281
299
|
byKey ??= /* @__PURE__ */ new Map();
|
|
282
|
-
const key = optionsKey(options);
|
|
300
|
+
const key = registered ? `${registryVersion()}:${optionsKey(options)}` : optionsKey(options);
|
|
283
301
|
let resolved = byKey.get(key);
|
|
284
302
|
if (resolved === void 0) {
|
|
285
303
|
resolved = build(options);
|
|
286
304
|
if (byKey.size > 512) byKey.clear();
|
|
287
305
|
byKey.set(key, resolved);
|
|
288
306
|
}
|
|
289
|
-
cache$1.set(options, resolved);
|
|
307
|
+
if (!registered) cache$1.set(options, resolved);
|
|
290
308
|
return resolved;
|
|
291
309
|
}
|
|
292
310
|
|
|
293
|
-
//#endregion
|
|
294
|
-
//#region src/slug/is-slug.ts
|
|
295
|
-
let cache;
|
|
296
|
-
function patternFor(options) {
|
|
297
|
-
cache ??= /* @__PURE__ */ new WeakMap();
|
|
298
|
-
const cached = cache.get(options);
|
|
299
|
-
if (cached !== void 0) return cached;
|
|
300
|
-
const o = resolveOptions(options);
|
|
301
|
-
const char = `[${o.wordClass}]`;
|
|
302
|
-
const separator = escapeRegExp$1(o.separator);
|
|
303
|
-
const lead = o.preserveLeadingUnderscore ? "_?" : "";
|
|
304
|
-
const word = o.unicode ? `(?!\\p{M})${char}+` : `${char}+`;
|
|
305
|
-
const body = o.separator === "" ? word : `${word}(?:${separator}${word})*`;
|
|
306
|
-
const tail = o.preserveTrailingSeparator && o.separator !== "" ? `(?:${separator})?` : "";
|
|
307
|
-
const pattern = new RegExp(`^${lead}${body}${tail}$`, "u");
|
|
308
|
-
cache.set(options, pattern);
|
|
309
|
-
return pattern;
|
|
310
|
-
}
|
|
311
|
-
const DEFAULT_OPTIONS = {};
|
|
312
|
-
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
313
|
-
function isSlug(input, options = DEFAULT_OPTIONS) {
|
|
314
|
-
if (typeof input !== "string" || input === "" || input === "." || input === "..") return false;
|
|
315
|
-
const o = resolveOptions(options);
|
|
316
|
-
if (o.maxLength !== void 0 && input.length > o.maxLength) return false;
|
|
317
|
-
if (o.separator !== "" && input.includes(o.separator + o.separator)) return false;
|
|
318
|
-
if (o.unicode) {
|
|
319
|
-
if (input !== input.normalize("NFKC")) return false;
|
|
320
|
-
if (o.lowercase && input !== lowercaseOf(input, o)) return false;
|
|
321
|
-
if (o.scripts !== "any" && !checkScripts(input, o.scripts).ok) return false;
|
|
322
|
-
if (o.bidi !== "allow" && !isBidiSafeComponent(input)) return false;
|
|
323
|
-
}
|
|
324
|
-
return patternFor(options).test(input);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
311
|
//#endregion
|
|
328
312
|
//#region src/slug/truncate.ts
|
|
329
313
|
let segmenter;
|
|
@@ -339,6 +323,53 @@ function isJoinerBefore(text, index) {
|
|
|
339
323
|
function isClusterExtender(cp) {
|
|
340
324
|
return cp >= 56320 && cp <= 57343 || cp === 8205 || cp >= 65024 && cp <= 65039 || cp >= 127995 && cp <= 127999 || cp >= 4448 && cp <= 4607 || cp >= 917536 && cp <= 917631 || /\p{M}/u.test(String.fromCodePoint(cp));
|
|
341
325
|
}
|
|
326
|
+
function utf8Length(cp) {
|
|
327
|
+
return cp < 128 ? 1 : cp < 2048 ? 2 : cp < 65536 ? 3 : 4;
|
|
328
|
+
}
|
|
329
|
+
/** The length of `text` in the given unit. */
|
|
330
|
+
function measure(text, unit = "units") {
|
|
331
|
+
if (unit === "units") return text.length;
|
|
332
|
+
let count = 0;
|
|
333
|
+
if (unit === "graphemes") {
|
|
334
|
+
const seg = graphemeSegmenter();
|
|
335
|
+
if (seg !== void 0) {
|
|
336
|
+
for (const _ of seg.segment(text)) count += 1;
|
|
337
|
+
return count;
|
|
338
|
+
}
|
|
339
|
+
let index = 0;
|
|
340
|
+
for (const ch of text) {
|
|
341
|
+
if (!(isClusterExtender(ch.codePointAt(0) ?? 0) || isJoinerBefore(text, index)) || index === 0) count += 1;
|
|
342
|
+
index += ch.length;
|
|
343
|
+
}
|
|
344
|
+
return count;
|
|
345
|
+
}
|
|
346
|
+
for (const ch of text) count += unit === "bytes" ? utf8Length(ch.codePointAt(0) ?? 0) : 1;
|
|
347
|
+
return count;
|
|
348
|
+
}
|
|
349
|
+
function prefixEnd(text, limit, unit) {
|
|
350
|
+
if (unit === "units") return Math.min(limit, text.length);
|
|
351
|
+
let used = 0;
|
|
352
|
+
if (unit === "graphemes") {
|
|
353
|
+
const seg = graphemeSegmenter();
|
|
354
|
+
if (seg !== void 0) {
|
|
355
|
+
let end = 0;
|
|
356
|
+
for (const { index, segment } of seg.segment(text)) {
|
|
357
|
+
if (used === limit) break;
|
|
358
|
+
used += 1;
|
|
359
|
+
end = index + segment.length;
|
|
360
|
+
}
|
|
361
|
+
return end;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
let end = 0;
|
|
365
|
+
for (const ch of text) {
|
|
366
|
+
const cost = unit === "bytes" ? utf8Length(ch.codePointAt(0) ?? 0) : 1;
|
|
367
|
+
if (used + cost > limit) break;
|
|
368
|
+
used += cost;
|
|
369
|
+
end += ch.length;
|
|
370
|
+
}
|
|
371
|
+
return unit === "graphemes" ? graphemeBoundary(text, end, { fallback: true }) : end;
|
|
372
|
+
}
|
|
342
373
|
function graphemeBoundary(text, limit, options = {}) {
|
|
343
374
|
const seg = options.fallback === true ? void 0 : graphemeSegmenter();
|
|
344
375
|
if (seg !== void 0) {
|
|
@@ -361,13 +392,14 @@ function stripTrailingSeparator(text, separator) {
|
|
|
361
392
|
for (let k = separator.length - 1; k > 0; k--) if (out.endsWith(separator.slice(0, k))) return out.slice(0, -k);
|
|
362
393
|
return out;
|
|
363
394
|
}
|
|
364
|
-
/** Cuts `slug` to at most `maxLength` UTF-16 code units at a `separator` boundary, never inside a grapheme cluster. */
|
|
365
|
-
function truncateSlug(slug, maxLength, separator = "-") {
|
|
395
|
+
/** Cuts `slug` to at most `maxLength` in the given `unit` (UTF-16 code units by default) at a `separator` boundary, never inside a grapheme cluster. */
|
|
396
|
+
function truncateSlug(slug, maxLength, separator = "-", unit = "units") {
|
|
366
397
|
if (!Number.isInteger(maxLength) || maxLength < 0) throw new RangeError("truncateSlug: maxLength must be a non-negative integer");
|
|
367
|
-
if (slug
|
|
398
|
+
if (measure(slug, unit) <= maxLength) return slug;
|
|
368
399
|
if (maxLength === 0) return "";
|
|
369
|
-
|
|
370
|
-
|
|
400
|
+
const limit = prefixEnd(slug, maxLength, unit);
|
|
401
|
+
let cut = slug.slice(0, limit);
|
|
402
|
+
if (separator !== "" && !slug.startsWith(separator, limit)) {
|
|
371
403
|
const last = cut.lastIndexOf(separator);
|
|
372
404
|
if (last > 0) cut = cut.slice(0, last);
|
|
373
405
|
}
|
|
@@ -375,6 +407,40 @@ function truncateSlug(slug, maxLength, separator = "-") {
|
|
|
375
407
|
return stripTrailingSeparator(cut, separator);
|
|
376
408
|
}
|
|
377
409
|
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/slug/is-slug.ts
|
|
412
|
+
let cache;
|
|
413
|
+
function patternFor(options) {
|
|
414
|
+
cache ??= /* @__PURE__ */ new WeakMap();
|
|
415
|
+
const cached = cache.get(options);
|
|
416
|
+
if (cached !== void 0) return cached;
|
|
417
|
+
const o = resolveOptions(options);
|
|
418
|
+
const char = `[${o.wordClass}]`;
|
|
419
|
+
const separator = escapeRegExp$1(o.separator);
|
|
420
|
+
const lead = o.preserveLeadingUnderscore ? "_?" : "";
|
|
421
|
+
const word = o.unicode ? `(?!\\p{M})${char}+` : `${char}+`;
|
|
422
|
+
const body = o.separator === "" ? word : `${word}(?:${separator}${word})*`;
|
|
423
|
+
const tail = o.preserveTrailingSeparator && o.separator !== "" ? `(?:${separator})?` : "";
|
|
424
|
+
const pattern = new RegExp(`^${lead}${body}${tail}$`, "u");
|
|
425
|
+
cache.set(options, pattern);
|
|
426
|
+
return pattern;
|
|
427
|
+
}
|
|
428
|
+
const DEFAULT_OPTIONS = {};
|
|
429
|
+
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
430
|
+
function isSlug(input, options = DEFAULT_OPTIONS) {
|
|
431
|
+
if (typeof input !== "string" || input === "" || input === "." || input === "..") return false;
|
|
432
|
+
const o = resolveOptions(options);
|
|
433
|
+
if (o.maxLength !== void 0 && measure(input, o.maxLengthUnit) > o.maxLength) return false;
|
|
434
|
+
if (o.separator !== "" && input.includes(o.separator + o.separator)) return false;
|
|
435
|
+
if (o.unicode) {
|
|
436
|
+
if (input !== input.normalize("NFKC")) return false;
|
|
437
|
+
if (o.lowercase && input !== lowercaseOf(input, o)) return false;
|
|
438
|
+
if (o.scripts !== "any" && !checkScripts(input, o.scripts).ok) return false;
|
|
439
|
+
if (o.bidi !== "allow" && !isBidiSafeComponent(input)) return false;
|
|
440
|
+
}
|
|
441
|
+
return patternFor(options).test(input);
|
|
442
|
+
}
|
|
443
|
+
|
|
378
444
|
//#endregion
|
|
379
445
|
//#region src/slug/slugify.ts
|
|
380
446
|
const NON_ASCII = /[^\x00-\x7F]/;
|
|
@@ -414,10 +480,15 @@ function slugify(input, options) {
|
|
|
414
480
|
if (o.leadingMarks !== void 0 && !ascii) s = s.replace(o.leadingMarks, "$1");
|
|
415
481
|
const hadTrailingSeparator = o.separator !== "" && s.endsWith(o.separator);
|
|
416
482
|
s = collapse(s, o);
|
|
417
|
-
if (o.maxLength !== void 0) s = truncateSlug(s, o.maxLength, o.separator);
|
|
483
|
+
if (o.maxLength !== void 0) s = truncateSlug(s, o.maxLength, o.separator, o.maxLengthUnit);
|
|
418
484
|
if (s === "." || s === "..") s = "";
|
|
419
485
|
if (o.preserveLeadingUnderscore && hadLeadingUnderscore && s !== "" && !s.startsWith("_")) s = "_" + s;
|
|
420
486
|
if (o.preserveTrailingSeparator && hadTrailingSeparator && s !== "") s += o.separator;
|
|
487
|
+
if (s === "" && o.fallback !== void 0) {
|
|
488
|
+
const text = typeof o.fallback === "string" ? o.fallback : o.fallback(input);
|
|
489
|
+
const { fallback: _fallback, ...rest } = options ?? {};
|
|
490
|
+
return slugify(text, rest);
|
|
491
|
+
}
|
|
421
492
|
if (o.unicode && s !== "") {
|
|
422
493
|
if (o.scripts !== "any") {
|
|
423
494
|
const check = checkScripts(s, o.scripts);
|
|
@@ -433,11 +504,11 @@ function slugify(input, options) {
|
|
|
433
504
|
|
|
434
505
|
//#endregion
|
|
435
506
|
//#region src/slug/slugger.ts
|
|
436
|
-
function withSuffix(base, separator, n, maxLength) {
|
|
507
|
+
function withSuffix(base, separator, n, maxLength, unit) {
|
|
437
508
|
const suffix = separator + String(n);
|
|
438
|
-
if (maxLength === void 0 || base
|
|
439
|
-
const room = maxLength - suffix
|
|
440
|
-
const head = room > 0 ? truncateSlug(base, room, separator) : "";
|
|
509
|
+
if (maxLength === void 0 || measure(base + suffix, unit) <= maxLength) return base + suffix;
|
|
510
|
+
const room = maxLength - measure(suffix, unit);
|
|
511
|
+
const head = room > 0 ? truncateSlug(base, room, separator, unit) : "";
|
|
441
512
|
return head === "" ? String(n).slice(0, maxLength) : head + suffix;
|
|
442
513
|
}
|
|
443
514
|
/** A slugger that never repeats a slug: the second `"Hello"` becomes `"hello-2"`. `defaults` apply to every call. */
|
|
@@ -460,7 +531,7 @@ function createSlugger(defaults = {}) {
|
|
|
460
531
|
let candidate;
|
|
461
532
|
do {
|
|
462
533
|
n += 1;
|
|
463
|
-
candidate = withSuffix(base, separator, n, merged.maxLength);
|
|
534
|
+
candidate = withSuffix(base, separator, n, merged.maxLength, merged.maxLengthUnit ?? "units");
|
|
464
535
|
} while (issued.has(candidate));
|
|
465
536
|
counters.set(base, n);
|
|
466
537
|
issued.add(candidate);
|
|
@@ -483,4 +554,4 @@ function createSlugger(defaults = {}) {
|
|
|
483
554
|
const slugifyWithCounter = createSlugger;
|
|
484
555
|
|
|
485
556
|
//#endregion
|
|
486
|
-
export { checkScripts, createSlugger, decamelize, detectScripts, isBidiSafeComponent, isSlug, slugify, slugifyWithCounter, truncateSlug };
|
|
557
|
+
export { checkScripts, createSlugger, decamelize, detectScripts, isBidiSafeComponent, isSlug, measure, slugify, slugifyWithCounter, truncateSlug };
|