cizgile 0.1.1 → 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 +71 -44
- package/dist/index.d.mts +55 -8
- package/dist/index.mjs +140 -70
- package/dist/shared/percent-DG-pZhJI.mjs +268 -0
- package/dist/shared/registry-_m94akct.mjs +736 -0
- package/dist/shared/types-B3EVWvDh.d.mts +24 -0
- package/dist/transliterate.d.mts +172 -2
- package/dist/transliterate.mjs +7882 -7
- package/dist/uri.d.mts +185 -9
- package/dist/uri.mjs +687 -72
- package/package.json +5 -2
- package/dist/shared/iri-CzbwHYgr.mjs +0 -572
- package/dist/shared/locales-latin-DshBH0E6.mjs +0 -424
- package/dist/shared/types-C1iMvUXh.d.mts +0 -13
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>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
Turn any title into a clean URL slug — in any language — and work with URLs the way RFC 3986 and RFC 3987 describe them. Pure TypeScript, works everywhere.
|
|
10
10
|
<br><br>
|
|
11
11
|
<a href="https://npmjs.com/package/cizgile"><img src="https://img.shields.io/npm/v/cizgile?style=flat&colorA=18181B&colorB=34d399" alt="npm version"></a>
|
|
12
|
+
<a href="https://github.com/productdevbook/cizgile/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/productdevbook/cizgile/ci.yml?style=flat&colorA=18181B&colorB=34d399" alt="ci"></a>
|
|
12
13
|
<a href="https://npmjs.com/package/cizgile"><img src="https://img.shields.io/npm/dm/cizgile?style=flat&colorA=18181B&colorB=34d399" alt="npm downloads"></a>
|
|
13
14
|
<a href="https://bundlephobia.com/result?p=cizgile"><img src="https://img.shields.io/bundlephobia/minzip/cizgile?style=flat&colorA=18181B&colorB=34d399" alt="bundle size"></a>
|
|
14
15
|
<a href="https://github.com/productdevbook/cizgile/blob/main/LICENSE"><img src="https://img.shields.io/github/license/productdevbook/cizgile?style=flat&colorA=18181B&colorB=34d399" alt="license"></a>
|
|
@@ -29,23 +30,23 @@ slugify("Straße Über Ärger", { locale: "de" }) // "strasse-ueber-aerger"
|
|
|
29
30
|
slugify("你好 World", { unicode: true }) // "你好-world"
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
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.
|
|
33
34
|
|
|
34
35
|
## Why cizgile
|
|
35
36
|
|
|
36
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.
|
|
37
|
-
- **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`.
|
|
38
39
|
- **Unicode slugs when you want them.** `你好-world` stays readable, and `iriToUri` gives you the exact percent-encoded form for the wire.
|
|
39
40
|
- **A real URL toolkit underneath.** Resolve, normalise, compare, validate and relativise URLs by the RFC, cross-checked against the WHATWG `URL` parser.
|
|
40
41
|
- **Small and tree-shakeable.** `import { slugify }` ships the Latin table only; other scripts load only when you import them.
|
|
41
42
|
|
|
42
43
|
## Three entry points
|
|
43
44
|
|
|
44
|
-
| import | what you get
|
|
45
|
-
| ----------------------- |
|
|
46
|
-
| `cizgile` | `slugify`, `isSlug`, `createSlugger`, `truncateSlug`, `decamelize`, script and bidi guards
|
|
47
|
-
| `cizgile/transliterate` | `transliterate`, per-script tables, locales, `defineLocale`
|
|
48
|
-
| `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 |
|
|
49
50
|
|
|
50
51
|
## Slugs
|
|
51
52
|
|
|
@@ -61,29 +62,34 @@ slugify("Hello World", { separator: "_" }) // "hello_world"
|
|
|
61
62
|
slugify("Donald E. Knuth", { lowercase: false }) // "Donald-E-Knuth"
|
|
62
63
|
slugify("getHTTPResponse", { decamelize: true }) // "get-http-response"
|
|
63
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"
|
|
64
67
|
slugify("C++ & Rust", { replacements: [["C++", "cpp"]] }) // "cpp-and-rust"
|
|
65
68
|
```
|
|
66
69
|
|
|
67
70
|
### Languages and scripts
|
|
68
71
|
|
|
69
|
-
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).
|
|
70
73
|
|
|
71
74
|
```ts
|
|
72
75
|
import { slugify } from "cizgile"
|
|
73
|
-
import { cyrillic, greek, uk, defineLocale, de } from "cizgile/transliterate"
|
|
76
|
+
import { cyrillic, greek, uk, ja, ko, defineLocale, de } from "cizgile/transliterate"
|
|
74
77
|
|
|
75
78
|
slugify("Çay & Simit", { locale: "tr" }) // "cay-ve-simit"
|
|
76
79
|
slugify("Fisch & Chips", { locale: "de" }) // "fisch-und-chips"
|
|
77
80
|
slugify("Ærø", { locale: "da" }) // "aeroe"
|
|
81
|
+
slugify("Zażółć & jaźń", { locale: "pl" }) // "zazolc-i-jazn"
|
|
78
82
|
slugify("Київ", { locale: uk }) // "kyiv"
|
|
79
83
|
slugify("Привет мир", { transliterate: [cyrillic] }) // "privet-mir"
|
|
80
84
|
slugify("Καλημέρα", { transliterate: [greek] }) // "kalimera"
|
|
85
|
+
slugify("서울 & 부산", { locale: ko }) // "seoul-mit-busan"
|
|
86
|
+
slugify("とうきょう", { locale: ja }) // "toukyou"
|
|
81
87
|
|
|
82
88
|
const swiss = defineLocale(de, { id: "de-CH", table: { ß: "ss" } })
|
|
83
89
|
slugify("Straße", { locale: swiss }) // "strasse"
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
Locale ids: `az da de es fi fr hu it nb nl pt sv tr vi`. Locale objects: those plus `bg mk ru sr uk`.
|
|
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"`.
|
|
87
93
|
|
|
88
94
|
### Unicode slugs
|
|
89
95
|
|
|
@@ -128,41 +134,45 @@ isSlug("hello_world", { separator: "_" }) // true
|
|
|
128
134
|
isSlug("你好-world", { unicode: true }) // true
|
|
129
135
|
```
|
|
130
136
|
|
|
131
|
-
`isSlug` accepts exactly what `slugify` would produce under the same options.
|
|
137
|
+
`isSlug` accepts exactly what `slugify` would produce under the same options, `locale` included.
|
|
132
138
|
|
|
133
139
|
### All options
|
|
134
140
|
|
|
135
|
-
| option | default | what it does
|
|
136
|
-
| --------------------------- | --------- |
|
|
137
|
-
| `separator` | `"-"` | Joins words. Any URL-safe punctuation (`- _ . ~ !$&'()*+,;= @`) or `""`.
|
|
138
|
-
| `lowercase` | `true` | `false` keeps the original case.
|
|
139
|
-
| `unicode` | `false` | Keep letters from every script instead of transliterating to ASCII.
|
|
140
|
-
| `locale` | — | Language-specific rules: a locale id or a `Locale` object.
|
|
141
|
-
| `transliterate` | `true` | `false` skips the tables (
|
|
142
|
-
| `decamelize` | `false` | `fooBar` → `foo-bar`, `HTMLParser` → `html-parser`.
|
|
143
|
-
| `replacements` | `[]` | `[from, to]` pairs applied first; spaces in `to` become separators.
|
|
144
|
-
| `remove` | `/['’]/g` |
|
|
145
|
-
| `preserveCharacters` | `[]` | Extra URL-safe characters to keep, e.g. `["."]` for version numbers.
|
|
146
|
-
| `preserveLeadingUnderscore` | `false` | `_draft` → `_draft`.
|
|
147
|
-
| `preserveTrailingSeparator` | `false` | Keep a trailing separator while the user is still typing.
|
|
148
|
-
| `maxLength` | — | Cut at a word boundary, never inside a character (emoji sequences, combining marks).
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
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. |
|
|
151
159
|
|
|
152
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)`.
|
|
153
161
|
|
|
154
162
|
## Transliteration on its own
|
|
155
163
|
|
|
156
164
|
```ts
|
|
157
|
-
import { transliterate, cyrillic, locales } from "cizgile/transliterate"
|
|
165
|
+
import { transliterate, cyrillic, hangul, kana, devanagari, locales } from "cizgile/transliterate"
|
|
158
166
|
|
|
159
167
|
transliterate("Straße Ærø") // "Strasse AEro"
|
|
160
168
|
transliterate("Привет", { tables: [cyrillic] }) // "Privet"
|
|
161
169
|
transliterate("Ängsö", { locale: locales.sv }) // "Aengsoe"
|
|
162
|
-
transliterate("
|
|
170
|
+
transliterate("서울 ひらがな नमस्ते", { tables: [hangul, kana, devanagari] }) // "seoul hiragana namaste"
|
|
171
|
+
transliterate("नमस्ते 你好") // "नमस्ते 你好" — unknown scripts are kept intact (use unknown: "drop" to remove)
|
|
172
|
+
transliterate("final x² Ⅷ", { nfkc: true }) // "final x2 VIII"
|
|
163
173
|
```
|
|
164
174
|
|
|
165
|
-
Tables: `latin symbols cyrillic cyrillicUk cyrillicBg cyrillicMk cyrillicSr greek arabic persian urdu pashto armenian georgian dhivehi`, plus `allScripts`. Where a letter is spelled differently at the start of a word (Armenian `ե`, Ukrainian `є ї й ю я`), the capital carries the word-initial form. `defineLocale` and `mergeTables` return new objects — nothing global is ever mutated.
|
|
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.
|
|
166
176
|
|
|
167
177
|
## URL toolkit
|
|
168
178
|
|
|
@@ -181,12 +191,18 @@ import {
|
|
|
181
191
|
isAbsoluteUri,
|
|
182
192
|
isIPv6Address,
|
|
183
193
|
extractUri,
|
|
194
|
+
getOrigin,
|
|
195
|
+
sortQuery,
|
|
196
|
+
joinPaths,
|
|
184
197
|
iriToUri,
|
|
185
198
|
uriToIri,
|
|
186
199
|
domainToAscii,
|
|
187
200
|
} from "cizgile/uri"
|
|
188
201
|
|
|
189
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/"
|
|
190
206
|
relativize("http://a/b/c/d;p?q", "http://a/b/g") // "../g"
|
|
191
207
|
normalizeUri("HTTP://www.EXAMPLE.com:80/%7e%41/./b/../c") // "http://www.example.com/~A/c"
|
|
192
208
|
equivalentUris("http://example.com", "http://example.com:80/") // true
|
|
@@ -208,13 +224,14 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
208
224
|
`encodePathSegment(seg, { noColon })`, `encodePath(path, { relative })`, `encodeQuery`, `encodeFragment`, `encodeForm`.
|
|
209
225
|
|
|
210
226
|
**Hosts (§3.2.2)**
|
|
211
|
-
`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).
|
|
212
228
|
|
|
213
229
|
**Parsing and validation (§4, Appendix A/B)**
|
|
214
|
-
`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.
|
|
215
231
|
`isUriReference` `isUri` `isAbsoluteUri` `isRelativeReference` `classifyReference` `pathForm` — validating parser built from the ABNF.
|
|
216
232
|
`isIriReference` `isIri` `isIunreserved` `isIpchar` — the same for IRIs (RFC 3987 §2.2).
|
|
217
|
-
`extractUri(text)` — Appendix C: strips `<>`, quotes, `URL:` prefixes, trailing punctuation and line-wrap whitespace.
|
|
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.
|
|
218
235
|
|
|
219
236
|
**Resolution (§5)**
|
|
220
237
|
`resolveUri(base, ref, { strict, allowRelativeBase })` — every §5.4 example passes; strict by default (`http:g` stays `http:g`).
|
|
@@ -223,17 +240,20 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
223
240
|
`isSameDocumentReference(base, ref, { normalize })`.
|
|
224
241
|
|
|
225
242
|
**Normalisation and comparison (§6)**
|
|
226
|
-
`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.
|
|
227
247
|
`normalizePath(path, { trailingSlash })`.
|
|
228
|
-
`equivalentUris(a, b, { level, base, ignoreFragment })` — `"simple"`, `"syntax"` or `"scheme"` (default). Never maps IRIs to URIs (RFC 3987 §5.3.1).
|
|
248
|
+
`equivalentUris(a, b, { level, base, ignoreFragment, defaultPorts })` — `"simple"`, `"syntax"` or `"scheme"` (default). Never maps IRIs to URIs (RFC 3987 §5.3.1).
|
|
229
249
|
|
|
230
250
|
**IRIs (RFC 3987)**
|
|
231
251
|
`isUcschar` `isIprivate` `isBidiControl` `hasBidiControls`.
|
|
232
|
-
`iriToUri(iri, { bidi, nfc, strict, host })` — percent-encodes without altering characters (§3.1 step 1c); `host: "punycode"` converts the domain; `strict` rejects characters no IRI may contain.
|
|
252
|
+
`iriToUri(iri, { bidi, nfc, strict, host })` — percent-encodes without altering characters (§3.1 step 1c); `bidi: "throw" | "strip"` handles direction controls; `host: "punycode"` converts the domain; `strict` rejects characters no IRI may contain.
|
|
233
253
|
`uriToIri(uri)` — decodes only what §3.2 allows, per component.
|
|
234
|
-
`punycodeEncode` `punycodeDecode` `domainToAscii` `domainToUnicode` — RFC 3492, no dependencies.
|
|
254
|
+
`punycodeEncode` `punycodeDecode` `domainToAscii` `domainToUnicode` — RFC 3492, no dependencies. `domainToAscii` maps and lowercases labels the way UTS #46 does, then rejects what no DNS name may carry: empty labels, labels over 63 octets, names over 253, leading or trailing hyphens, non-LDH characters and `xn--` labels that do not round-trip.
|
|
235
255
|
|
|
236
|
-
Deliberately not implemented: RFC 6874 IPv6 zone identifiers (reverted by RFC 9844)
|
|
256
|
+
Deliberately not implemented: RFC 6874 IPv6 zone identifiers (reverted by RFC 9844), the network-based normalisation of §6.2.4, and the UTS #46 status table, CONTEXTJ/CONTEXTO and the RFC 5893 bidi rule for domains.
|
|
237
257
|
|
|
238
258
|
</details>
|
|
239
259
|
|
|
@@ -244,7 +264,7 @@ If you are an assistant writing code with this library, these are the facts that
|
|
|
244
264
|
- Import paths: `cizgile` (slugs), `cizgile/transliterate` (tables, locales), `cizgile/uri` (URLs). ESM only, no default exports, no side effects, no runtime dependencies.
|
|
245
265
|
- `slugify(text, options?)` returns `""` for input with nothing usable — it never throws on ordinary text. It throws `RangeError`/`TypeError` only for invalid options (`separator: "/"`, `preserveCharacters` containing the separator, a non-global `remove` regex, a negative `maxLength`) and, in unicode mode, when `scripts` or `bidi: "throw"` rejects the result.
|
|
246
266
|
- The ASCII output is always a valid path segment; put it in a URL as-is. For `unicode: true` output, call `iriToUri(slug)` before putting it on the wire.
|
|
247
|
-
- Cyrillic and other non-Latin scripts are opt-in: pass `transliterate: [cyrillic]` or a locale object such as `uk` from `cizgile/transliterate`. Without them, Cyrillic text produces `""` in ASCII mode.
|
|
267
|
+
- Cyrillic and other non-Latin scripts are opt-in: pass `transliterate: [cyrillic]` or a locale object such as `uk`, `ja` or `ko` from `cizgile/transliterate`. Without them, Cyrillic text produces `""` in ASCII mode. `transliterate()` keeps scripts it has no table for intact; `allScripts` loads every table.
|
|
248
268
|
- `createSlugger()` is the way to get unique slugs in a document or import job; do not append counters yourself.
|
|
249
269
|
- 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
270
|
- Every exported function has an explicit TypeScript signature; the `.d.mts` files in `dist/` are the authoritative API.
|
|
@@ -262,8 +282,9 @@ Measured with `bun run bench` (vitest bench, Node 24, one core of a desktop CPU)
|
|
|
262
282
|
| 2.5 KB of mixed text | **6.4k ops/s** | 4.3k | 3.4k |
|
|
263
283
|
| `isSlug` | 3.7M ops/s | — | — |
|
|
264
284
|
|
|
265
|
-
`resolveUri` runs at ~0.8M ops/s (the built-in `URL` parser: ~
|
|
266
|
-
`percentEncode` at 0.
|
|
285
|
+
`resolveUri` runs at ~0.8M ops/s (the built-in `URL` parser: ~0.9M), `removeDotSegments` at 2M,
|
|
286
|
+
`percentEncode` at 0.9M on mixed text and 3.6M on a pure-ASCII segment (`encodeURIComponent`: 3.1M and 4.7M — it is native),
|
|
287
|
+
`normalizeUri` at 0.37M, `iriToUri` at 0.66M.
|
|
267
288
|
Options objects are resolved once and cached structurally, so inline `{ locale: "tr" }` literals cost
|
|
268
289
|
nothing after the first call.
|
|
269
290
|
|
|
@@ -283,7 +304,11 @@ nothing after the first call.
|
|
|
283
304
|
|
|
284
305
|
## Specifications
|
|
285
306
|
|
|
286
|
-
RFC 3986 (with errata 2033, 4547, 4789, 5428), RFC 3987, RFC 3492, RFC 8820, RFC 9844, 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.
|
|
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.
|
|
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.
|
|
287
312
|
|
|
288
313
|
## Development
|
|
289
314
|
|
|
@@ -292,6 +317,8 @@ bun install
|
|
|
292
317
|
bun run test # oxlint, oxfmt, tsc, vitest under node, then vitest under bun
|
|
293
318
|
bun run build # rolldown → dist/*.mjs + dist/*.d.mts
|
|
294
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
|
|
295
322
|
bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
296
323
|
```
|
|
297
324
|
|
|
@@ -300,7 +327,7 @@ bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
|
300
327
|
- [simov/slugify](https://github.com/simov/slugify) — the charmap + per-locale override idea and most Cyrillic, Greek, Arabic and symbol values.
|
|
301
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.
|
|
302
329
|
- [Django](https://github.com/django/django) and [Rails](https://github.com/rails/rails) — the reference behaviours the parity tests are written against.
|
|
303
|
-
- 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.
|
|
304
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.
|
|
305
332
|
- [Rolldown](https://rolldown.rs), [Oxc](https://oxc.rs), [Vitest](https://vitest.dev), [Bun](https://bun.sh) and [TypeScript](https://www.typescriptlang.org).
|
|
306
333
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,55 +1,102 @@
|
|
|
1
|
-
import {
|
|
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
|
+
/** RFC 3987 section 4.2: whether `text` can be a URL component without mixing text directions in a way that renders ambiguously. */
|
|
3
4
|
export declare function isBidiSafeComponent(text: string): boolean;
|
|
4
5
|
//#endregion
|
|
5
6
|
//#region src/slug/decamelize.d.ts
|
|
7
|
+
/** Inserts spaces at camelCase and acronym boundaries: `"getHTTPResponse"` becomes `"get HTTP Response"`. */
|
|
6
8
|
export declare function decamelize(input: string): string;
|
|
7
9
|
//#endregion
|
|
8
10
|
//#region src/slug/scripts.d.ts
|
|
11
|
+
/** UTS #39 mixed-script restriction levels, from a single script (`"single"`) to no restriction (`"any"`). */
|
|
9
12
|
type ScriptRestriction = "single" | "highly-restrictive" | "moderately-restrictive" | "any";
|
|
13
|
+
/** The result of `checkScripts`. */
|
|
10
14
|
interface ScriptCheck {
|
|
15
|
+
/** Whether the text stays within the requested restriction level. */
|
|
11
16
|
readonly ok: boolean;
|
|
17
|
+
/** The Unicode scripts found in the text. */
|
|
12
18
|
readonly scripts: readonly string[];
|
|
13
19
|
}
|
|
20
|
+
/** The Unicode script names used by `text`, ignoring Common and Inherited characters. */
|
|
14
21
|
export declare function detectScripts(text: string): string[];
|
|
22
|
+
/** Applies a UTS #39 restriction `level` to the scripts in `text`; `ok` is false when the mix exceeds it. */
|
|
15
23
|
export declare function checkScripts(text: string, level?: ScriptRestriction): ScriptCheck;
|
|
16
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
|
|
17
33
|
//#region src/slug/options.d.ts
|
|
34
|
+
/** Options for `slugify` and `createSlugger`. Every option has a default; an empty object is the everyday call. */
|
|
18
35
|
interface SlugifyOptions {
|
|
36
|
+
/** Joins words; `"-"` by default. Any URL-safe punctuation (`- _ . ~ ! $ & ' ( ) * + , ; = @`) or `""`. */
|
|
19
37
|
readonly separator?: string;
|
|
38
|
+
/** Lowercases the result; `true` by default. */
|
|
20
39
|
readonly lowercase?: boolean;
|
|
40
|
+
/** Keeps letters from every script instead of transliterating to ASCII; `false` by default. */
|
|
21
41
|
readonly unicode?: boolean;
|
|
22
|
-
|
|
23
|
-
readonly
|
|
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[];
|
|
46
|
+
/** Splits camelCase before slugging: `"fooBar"` becomes `"foo-bar"`; `false` by default. */
|
|
24
47
|
readonly decamelize?: boolean;
|
|
48
|
+
/** `[from, to]` pairs applied before anything else; spaces in `to` become separators. */
|
|
25
49
|
readonly replacements?: ReadonlyArray<readonly [string, string]>;
|
|
50
|
+
/** A global regex of characters to delete rather than turn into separators; apostrophes by default, `false` for none. */
|
|
26
51
|
readonly remove?: RegExp | false;
|
|
52
|
+
/** Extra URL-safe single characters to keep, such as `["."]` for version numbers. */
|
|
27
53
|
readonly preserveCharacters?: readonly string[];
|
|
54
|
+
/** Keeps a leading `_`: `"_draft"` stays `"_draft"`. */
|
|
28
55
|
readonly preserveLeadingUnderscore?: boolean;
|
|
56
|
+
/** Keeps a trailing separator, for input still being typed. */
|
|
29
57
|
readonly preserveTrailingSeparator?: boolean;
|
|
58
|
+
/** Cuts at a word boundary, never inside a grapheme cluster. Counts in `maxLengthUnit`, UTF-16 code units like `.length` by default. */
|
|
30
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);
|
|
64
|
+
/** Unicode mode only: the UTS #39 restriction level the result must satisfy; `"any"` by default. */
|
|
31
65
|
readonly scripts?: ScriptRestriction;
|
|
66
|
+
/** Unicode mode only: what to do when the result mixes text directions (RFC 3987 section 4.2); `"allow"` by default. */
|
|
32
67
|
readonly bidi?: "allow" | "encode" | "throw";
|
|
33
68
|
}
|
|
34
|
-
|
|
69
|
+
/** The `slugify` options that shape what a valid slug looks like. */
|
|
70
|
+
type IsSlugOptions = Pick<SlugifyOptions, "separator" | "lowercase" | "unicode" | "locale" | "preserveCharacters" | "preserveLeadingUnderscore" | "preserveTrailingSeparator" | "maxLength" | "maxLengthUnit" | "scripts" | "bidi">;
|
|
35
71
|
//#endregion
|
|
36
72
|
//#region src/slug/is-slug.d.ts
|
|
73
|
+
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
37
74
|
export declare function isSlug(input: string, options?: IsSlugOptions): boolean;
|
|
38
75
|
//#endregion
|
|
39
76
|
//#region src/slug/slugger.d.ts
|
|
77
|
+
/** A `slugify` that remembers what it handed out; see `createSlugger`. */
|
|
40
78
|
interface Slugger {
|
|
79
|
+
/** Slugifies `input`, appending `-2`, `-3`, ... when the slug was already handed out. */
|
|
41
80
|
(input: string, options?: SlugifyOptions): string;
|
|
81
|
+
/** Forgets every slug handed out or reserved so far. */
|
|
42
82
|
reset(): void;
|
|
83
|
+
/** Whether `slug` has already been handed out or reserved. */
|
|
43
84
|
has(slug: string): boolean;
|
|
85
|
+
/** Marks `slug` as taken so it is never handed out again. */
|
|
44
86
|
reserve(slug: string): void;
|
|
45
87
|
}
|
|
88
|
+
/** A slugger that never repeats a slug: the second `"Hello"` becomes `"hello-2"`. `defaults` apply to every call. */
|
|
46
89
|
export declare function createSlugger(defaults?: SlugifyOptions): Slugger;
|
|
90
|
+
/** Alias of `createSlugger`, under the name `@sindresorhus/slugify` uses. */
|
|
47
91
|
export declare const slugifyWithCounter: (defaults?: SlugifyOptions) => Slugger;
|
|
48
92
|
//#endregion
|
|
49
93
|
//#region src/slug/slugify.d.ts
|
|
94
|
+
/**
|
|
95
|
+
* Turns text into a URL slug: an RFC 3986 `segment-nz-nc` in ASCII mode, NFKC letters, digits and marks in unicode mode.
|
|
96
|
+
* Returns `""` when nothing usable remains and never throws on ordinary text.
|
|
97
|
+
* @throws {TypeError} when `input` is not a string or an option is malformed (`preserveCharacters`, a non-global `remove`, an unknown locale id).
|
|
98
|
+
* @throws {RangeError} for an invalid `separator` or `maxLength`, and in unicode mode when `scripts` or `bidi: "throw"` rejects the result.
|
|
99
|
+
*/
|
|
50
100
|
export declare function slugify(input: string, options?: SlugifyOptions): string;
|
|
51
101
|
//#endregion
|
|
52
|
-
|
|
53
|
-
export declare function truncateSlug(slug: string, maxLength: number, separator?: string): string;
|
|
54
|
-
//#endregion
|
|
55
|
-
export type { IsSlugOptions, LatinLocaleId, Locale, LocaleId, ScriptCheck, ScriptRestriction, Slugger, SlugifyOptions, TransliterationTable };
|
|
102
|
+
export type { IsSlugOptions, LatinLocaleId, LengthUnit, Locale, LocaleId, ScriptCheck, ScriptRestriction, Slugger, SlugifyOptions, TransliterationTable };
|