cizgile 0.2.0 → 1.0.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 +88 -35
- package/dist/idna.d.mts +60 -0
- package/dist/idna.mjs +313 -0
- package/dist/index.d.mts +20 -12
- package/dist/index.mjs +124 -53
- package/dist/shared/punycode-MAFJqHdd.mjs +180 -0
- package/dist/shared/{locales-latin-Bm94HAeL.mjs → registry-D787niCh.mjs} +162 -15
- package/dist/shared/{types-DbyhfPRc.d.mts → types-D5P1JMcX.d.mts} +2 -2
- package/dist/transliterate.d.mts +95 -2
- package/dist/transliterate.mjs +8771 -11
- package/dist/uri.d.mts +68 -17
- package/dist/uri.mjs +283 -298
- package/package.json +19 -14
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=7e268b8" 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,24 @@ 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
|
|
33
|
+
No dependencies. ESM only. Node 20+ and Bun 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.** 50 locales (`tr`, `de`, `pl`, `sv`, `uk`, `hi`, `ta`, `th`, `ja`, `ko`, …) and 24 scripts (Latin, Cyrillic, Greek, Arabic, Armenian, Georgian, Dhivehi, Hebrew, Hangul, kana, Devanagari and the seven other Indic scripts, Sinhala, Thai, Lao, Khmer, Myanmar). `ß` → `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
|
+
## Four 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
|
+
| `cizgile/uri/idna` | full UTS #46 `idnaToAscii` / `idnaToUnicode`: status table, CONTEXTJ/CONTEXTO, RFC 5893 bidi rule |
|
|
50
51
|
|
|
51
52
|
## Slugs
|
|
52
53
|
|
|
@@ -62,12 +63,14 @@ slugify("Hello World", { separator: "_" }) // "hello_world"
|
|
|
62
63
|
slugify("Donald E. Knuth", { lowercase: false }) // "Donald-E-Knuth"
|
|
63
64
|
slugify("getHTTPResponse", { decamelize: true }) // "get-http-response"
|
|
64
65
|
slugify("the quick brown fox", { maxLength: 9 }) // "the-quick"
|
|
66
|
+
slugify("Ünïcödé Büro", { unicode: true, maxLength: 11, maxLengthUnit: "bytes" }) // "ünïcödé"
|
|
67
|
+
slugify("!!!", { fallback: "untitled" }) // "untitled"
|
|
65
68
|
slugify("C++ & Rust", { replacements: [["C++", "cpp"]] }) // "cpp-and-rust"
|
|
66
69
|
```
|
|
67
70
|
|
|
68
71
|
### Languages and scripts
|
|
69
72
|
|
|
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.
|
|
73
|
+
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
74
|
|
|
72
75
|
```ts
|
|
73
76
|
import { slugify } from "cizgile"
|
|
@@ -87,7 +90,7 @@ const swiss = defineLocale(de, { id: "de-CH", table: { ß: "ss" } })
|
|
|
87
90
|
slugify("Straße", { locale: swiss }) // "strasse"
|
|
88
91
|
```
|
|
89
92
|
|
|
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).
|
|
93
|
+
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 si` (Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam, Sinhala) and `th lo km my` (Thai, Lao, Khmer, Burmese). `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
94
|
|
|
92
95
|
### Unicode slugs
|
|
93
96
|
|
|
@@ -136,39 +139,41 @@ isSlug("你好-world", { unicode: true }) // true
|
|
|
136
139
|
|
|
137
140
|
### All options
|
|
138
141
|
|
|
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
|
-
| `
|
|
142
|
+
| option | default | what it does |
|
|
143
|
+
| --------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
+
| `separator` | `"-"` | Joins words. Any URL-safe punctuation (`- _ . ~ !$&'()*+,;= @`), several of them (`"--"`), or `""`. |
|
|
145
|
+
| `lowercase` | `true` | `false` keeps the original case. |
|
|
146
|
+
| `unicode` | `false` | Keep letters from every script instead of transliterating to ASCII. |
|
|
147
|
+
| `locale` | — | Language-specific rules: a locale id or a `Locale` object. |
|
|
148
|
+
| `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. |
|
|
149
|
+
| `decamelize` | `false` | `fooBar` → `foo-bar`, `HTMLParser` → `html-parser`. |
|
|
150
|
+
| `replacements` | `[]` | `[from, to]` pairs applied first; spaces in `to` become separators. |
|
|
151
|
+
| `remove` | `/['’]/g` | A global regex of characters to delete rather than turn into separators (`don't` → `dont`); `false` keeps them. |
|
|
152
|
+
| `preserveCharacters` | `[]` | Extra URL-safe single characters to keep, e.g. `["."]` for version numbers. The separator or anything outside `segment-nz-nc` throws. |
|
|
153
|
+
| `preserveLeadingUnderscore` | `false` | `_draft` → `_draft`. |
|
|
154
|
+
| `preserveTrailingSeparator` | `false` | Keep a trailing separator while the user is still typing. |
|
|
155
|
+
| `maxLength` | — | Cut at a word boundary, never inside a character (emoji sequences, combining marks). |
|
|
156
|
+
| `maxLengthUnit` | `"units"` | What `maxLength` counts: UTF-16 code units like `.length`, `"code-points"`, `"graphemes"`, or UTF-8 `"bytes"` for a column or filename budget. |
|
|
157
|
+
| `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). |
|
|
158
|
+
| `scripts` | `"any"` | Unicode mode: UTS #39 mixed-script restriction level. |
|
|
159
|
+
| `bidi` | `"allow"` | Unicode mode: RFC 3987 §4.2 direction rule — `"encode"` or `"throw"` on violation. |
|
|
155
160
|
|
|
156
161
|
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
162
|
|
|
158
163
|
## Transliteration on its own
|
|
159
164
|
|
|
160
165
|
```ts
|
|
161
|
-
import { transliterate, cyrillic, hangul, kana, locales } from "cizgile/transliterate"
|
|
166
|
+
import { transliterate, cyrillic, hangul, kana, devanagari, locales } from "cizgile/transliterate"
|
|
162
167
|
|
|
163
168
|
transliterate("Straße Ærø") // "Strasse AEro"
|
|
164
169
|
transliterate("Привет", { tables: [cyrillic] }) // "Privet"
|
|
165
170
|
transliterate("Ängsö", { locale: locales.sv }) // "Aengsoe"
|
|
166
|
-
transliterate("서울 ひらがな", { tables: [hangul, kana] }) // "seoul hiragana"
|
|
171
|
+
transliterate("서울 ひらがな नमस्ते", { tables: [hangul, kana, devanagari] }) // "seoul hiragana namaste"
|
|
167
172
|
transliterate("नमस्ते 你好") // "नमस्ते 你好" — unknown scripts are kept intact (use unknown: "drop" to remove)
|
|
168
173
|
transliterate("final x² Ⅷ", { nfkc: true }) // "final x2 VIII"
|
|
169
174
|
```
|
|
170
175
|
|
|
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.
|
|
176
|
+
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 sinhala thai lao khmer myanmar 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`; Sinhala `කොළඹ` is `kolamba`), 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. The Thai, Lao, Khmer and Myanmar tables need context, so besides letters they carry rules (a table key starting with U+0001 is a regular expression, its value the replacement). Thai follows the Royal Thai General System without tone marks or vowel length: the vowels written before the consonant (`เ แ โ ใ ไ`) move behind it, a consonant is read as initial or final from its neighbours, a bare consonant takes `a` or, before the final, `o`, and silent letters under `์` are dropped (`ไทย` is `thai`, `เมือง` is `mueang`, `โรงเรียน` is `rongrian`, `กรุงเทพ` is `krungthep`). Lao uses the same logic with a simple BGN/PCGN-style spelling (`ວຽງຈັນ` is `viangchan`), Khmer reads each vowel sign by the series of its base consonant, UNGEGN-style (`ភ្នំពេញ` is `phnumpenh`, `កម្ពុជា` is `kampuchea`), and Myanmar keeps the inherent `a` unless the asat, a vowel sign or a stacked consonant follows (`မြန်မာ` is `myanma`, `ရန်ကုန်` is `yankun`). None of them has a dictionary or word segmentation: words are not split, the repetition mark `ๆ` is dropped, and irregular spellings, linking syllables (`พัทยา` is `phatya`, not `phatthaya`), Khmer and Lao silent letters and the Myanmar rhyme and tone rules are not handled, so output is a deterministic transcription of the spelling rather than of the pronunciation. `defineLocale` and `mergeTables` return new objects — nothing global is ever mutated.
|
|
172
177
|
|
|
173
178
|
## URL toolkit
|
|
174
179
|
|
|
@@ -187,12 +192,18 @@ import {
|
|
|
187
192
|
isAbsoluteUri,
|
|
188
193
|
isIPv6Address,
|
|
189
194
|
extractUri,
|
|
195
|
+
getOrigin,
|
|
196
|
+
sortQuery,
|
|
197
|
+
joinPaths,
|
|
190
198
|
iriToUri,
|
|
191
199
|
uriToIri,
|
|
192
200
|
domainToAscii,
|
|
193
201
|
} from "cizgile/uri"
|
|
194
202
|
|
|
195
203
|
resolveUri("http://a/b/c/d;p?q", "../../g") // "http://a/g"
|
|
204
|
+
getOrigin("HTTP://Example.com:80/a?b") // "http://example.com"
|
|
205
|
+
sortQuery("http://a/p?b=2&a=1") // "http://a/p?a=1&b=2"
|
|
206
|
+
joinPaths("/api/", "/v1", "../v2/") // "/api/v2/"
|
|
196
207
|
relativize("http://a/b/c/d;p?q", "http://a/b/g") // "../g"
|
|
197
208
|
normalizeUri("HTTP://www.EXAMPLE.com:80/%7e%41/./b/../c") // "http://www.example.com/~A/c"
|
|
198
209
|
equivalentUris("http://example.com", "http://example.com:80/") // true
|
|
@@ -214,13 +225,14 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
214
225
|
`encodePathSegment(seg, { noColon })`, `encodePath(path, { relative })`, `encodeQuery`, `encodeFragment`, `encodeForm`.
|
|
215
226
|
|
|
216
227
|
**Hosts (§3.2.2)**
|
|
217
|
-
`isIPv4Address` `isIPv6Address` `isIPvFuture` `isIPLiteral` `isRegName` `isHost` `parseHost` `parseAuthority` `serializeAuthority
|
|
228
|
+
`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
229
|
|
|
219
230
|
**Parsing and validation (§4, Appendix A/B)**
|
|
220
|
-
`parseUri` `serializeUri` — components stay distinct from "absent"; the serializer inserts `/.` or `./` where the grammar requires it.
|
|
231
|
+
`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
232
|
`isUriReference` `isUri` `isAbsoluteUri` `isRelativeReference` `classifyReference` `pathForm` — validating parser built from the ABNF.
|
|
222
233
|
`isIriReference` `isIri` `isIunreserved` `isIpchar` — the same for IRIs (RFC 3987 §2.2).
|
|
223
234
|
`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.
|
|
235
|
+
`findUris(text)` — the URIs in ordinary prose (`scheme://`, `www.`, `mailto:` and friends) with their offsets, trailing punctuation trimmed.
|
|
224
236
|
|
|
225
237
|
**Resolution (§5)**
|
|
226
238
|
`resolveUri(base, ref, { strict, allowRelativeBase })` — every §5.4 example passes; strict by default (`http:g` stays `http:g`).
|
|
@@ -229,7 +241,10 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
229
241
|
`isSameDocumentReference(base, ref, { normalize })`.
|
|
230
242
|
|
|
231
243
|
**Normalisation and comparison (§6)**
|
|
232
|
-
`normalizeUri(uri, { defaultPorts, schemeBased, userinfo })` — case, percent-encoding, dot segments, default ports, empty path →
|
|
244
|
+
`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.
|
|
245
|
+
`getOrigin(uri)` `isSameOrigin(a, b)` — RFC 6454 origins with default-port elision. `stripFragment(uri)`.
|
|
246
|
+
`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.
|
|
247
|
+
`joinPaths(...pieces)` — single slashes, no dot segments, the first piece's leading and the last piece's trailing slash kept.
|
|
233
248
|
`normalizePath(path, { trailingSlash })`.
|
|
234
249
|
`equivalentUris(a, b, { level, base, ignoreFragment, defaultPorts })` — `"simple"`, `"syntax"` or `"scheme"` (default). Never maps IRIs to URIs (RFC 3987 §5.3.1).
|
|
235
250
|
|
|
@@ -239,7 +254,39 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
239
254
|
`uriToIri(uri)` — decodes only what §3.2 allows, per component.
|
|
240
255
|
`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.
|
|
241
256
|
|
|
242
|
-
Deliberately not implemented: RFC 6874 IPv6 zone identifiers (reverted by RFC 9844)
|
|
257
|
+
Deliberately not implemented: RFC 6874 IPv6 zone identifiers (reverted by RFC 9844) and the network-based normalisation of §6.2.4. The UTS #46 status table, CONTEXTJ/CONTEXTO and the RFC 5893 bidi rule live in the optional `cizgile/uri/idna` entry below, so `cizgile/uri` stays small.
|
|
258
|
+
|
|
259
|
+
### Full IDNA (`cizgile/uri/idna`)
|
|
260
|
+
|
|
261
|
+
`domainToAscii` in `cizgile/uri` approximates UTS #46: it removes format characters, applies NFKC and lowercases, so it does not know which code points are disallowed, ignored or deviations, and it does not check joiners or the bidi rule. `cizgile/uri/idna` carries the real Unicode 16.0.0 table (about 45 KB minified, which is why it is a separate import) and passes every case of the official `IdnaTestV2.txt`, nontransitional and transitional, down to the error codes.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { idnaToAscii, idnaToUnicode, idnaToAsciiResult } from "cizgile/uri/idna"
|
|
265
|
+
|
|
266
|
+
idnaToAscii("Bücher.DE") // "xn--bcher-kva.de"
|
|
267
|
+
idnaToAscii("faß.de", { transitionalProcessing: true }) // "fass.de"
|
|
268
|
+
idnaToUnicode("xn--r8jz45g.jp") // "例え.jp"
|
|
269
|
+
idnaToAscii("a_b.com", { beStrict: true }) // throws RangeError: "a_b.com" is not valid (U1)
|
|
270
|
+
idnaToAsciiResult("aא") // { result: "xn--a-...", errors: ["B5", "B6"] }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
`idnaToAscii` and `idnaToUnicode` throw a `RangeError` that lists the error codes, like `domainToAscii`. `idnaToAsciiResult` and `idnaToUnicodeResult` never throw: they return `{ result, errors }` with the best-effort result and every UTS #46 code (`P4`, `V1` to `V7`, `U1`, `B1` to `B6`, `C1`, `C2`, `A3`, `A4_1`, `A4_2`, `X4_2`, and `O1` for CONTEXTO). `codePointStatus(cp)` exposes the table itself.
|
|
274
|
+
|
|
275
|
+
Options default to the WHATWG URL Standard's domain to ASCII:
|
|
276
|
+
|
|
277
|
+
| option | default | effect |
|
|
278
|
+
| ------------------------ | ------- | ---------------------------------------------------------------------------------------- |
|
|
279
|
+
| `beStrict` | `false` | turns on `useSTD3ASCIIRules`, `checkHyphens` and `verifyDnsLength` unless set explicitly |
|
|
280
|
+
| `useSTD3ASCIIRules` | `false` | only `a-z`, `0-9`, `-` among ASCII characters (`U1`) |
|
|
281
|
+
| `checkHyphens` | `false` | no `--` in positions 3 and 4, no hyphen at either end (`V2`, `V3`) |
|
|
282
|
+
| `checkBidi` | `true` | RFC 5893 rules 1 to 6 for every label of a bidi domain name (`B1` to `B6`) |
|
|
283
|
+
| `checkJoiners` | `true` | RFC 5892 CONTEXTJ for U+200C and U+200D (`C1`, `C2`) |
|
|
284
|
+
| `checkContextO` | `false` | RFC 5892 A.3 to A.9: middle dot, keraia, geresh, gershayim, katakana middle dot, digits |
|
|
285
|
+
| `transitionalProcessing` | `false` | IDNA2003 mapping of `ß`, `ς`, U+200C, U+200D (deprecated) |
|
|
286
|
+
| `verifyDnsLength` | `false` | labels 1 to 63, name 1 to 253 without the root dot (`A4_1`, `A4_2`; `X4_2` in ToUnicode) |
|
|
287
|
+
| `ignoreInvalidPunycode` | `false` | keep an undecodable `xn--` label instead of `P4` |
|
|
288
|
+
|
|
289
|
+
Unicode 16 folded the old `disallowed_STD3_*` statuses into the validity criteria, so the table has five statuses and `useSTD3ASCIIRules` is a plain check. `String.prototype.normalize` (NFC) comes from the runtime; everything else is data generated by `scripts/gen-idna.mjs`, which also refreshes the test fixture. CONTEXTO is not part of UTS #46 and `IdnaTestV2.txt` does not test it, so `checkContextO` is separate from `beStrict`.
|
|
243
290
|
|
|
244
291
|
</details>
|
|
245
292
|
|
|
@@ -292,6 +339,10 @@ nothing after the first call.
|
|
|
292
339
|
|
|
293
340
|
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
341
|
|
|
342
|
+
## Changelog
|
|
343
|
+
|
|
344
|
+
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.
|
|
345
|
+
|
|
295
346
|
## Development
|
|
296
347
|
|
|
297
348
|
```sh
|
|
@@ -299,6 +350,8 @@ bun install
|
|
|
299
350
|
bun run test # oxlint, oxfmt, tsc, vitest under node, then vitest under bun
|
|
300
351
|
bun run build # rolldown → dist/*.mjs + dist/*.d.mts
|
|
301
352
|
bun run coverage
|
|
353
|
+
bun run bench # vitest bench against @sindresorhus/slugify, simov/slugify and the built-ins
|
|
354
|
+
bun run bench:baseline # write bench/baseline.json locally; the Bench workflow compares each run with the last successful one on main
|
|
302
355
|
bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
303
356
|
```
|
|
304
357
|
|
|
@@ -307,7 +360,7 @@ bun run release # bumpp: bump, tag, push — the tag publishes to npm
|
|
|
307
360
|
- [simov/slugify](https://github.com/simov/slugify) — the charmap + per-locale override idea and most Cyrillic, Greek, Arabic and symbol values.
|
|
308
361
|
- [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
362
|
- [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.
|
|
363
|
+
- 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
364
|
- [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
365
|
- [Rolldown](https://rolldown.rs), [Oxc](https://oxc.rs), [Vitest](https://vitest.dev), [Bun](https://bun.sh) and [TypeScript](https://www.typescriptlang.org).
|
|
313
366
|
|
package/dist/idna.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//#region src/idna/data.d.ts
|
|
2
|
+
/** The Unicode version of the tables behind `cizgile/uri/idna`. */
|
|
3
|
+
export declare const IDNA_UNICODE_VERSION = "16.0.0";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/idna/process.d.ts
|
|
6
|
+
/** Options of the UTS #46 processing. The defaults are those of the WHATWG URL Standard's domain to ASCII. */
|
|
7
|
+
interface IdnaOptions {
|
|
8
|
+
/** Turn on `useSTD3ASCIIRules`, `checkHyphens` and `verifyDnsLength` unless they are set explicitly. Default `false`. */
|
|
9
|
+
beStrict?: boolean;
|
|
10
|
+
/** Only `a-z`, `0-9` and `-` among ASCII characters (error `U1`). Default `false`, or `true` with `beStrict`. */
|
|
11
|
+
useSTD3ASCIIRules?: boolean;
|
|
12
|
+
/** No hyphen in positions 3 and 4 and none at either end (`V2`, `V3`); when off, a label may not start with `xn--` (`V4`). Default `false`, or `true` with `beStrict`. */
|
|
13
|
+
checkHyphens?: boolean;
|
|
14
|
+
/** The RFC 5893 bidi rule for labels of a bidi domain name (`B1` to `B6`). Default `true`. */
|
|
15
|
+
checkBidi?: boolean;
|
|
16
|
+
/** RFC 5892 CONTEXTJ: where U+200C and U+200D may appear (`C1`, `C2`). Default `true`. */
|
|
17
|
+
checkJoiners?: boolean;
|
|
18
|
+
/** RFC 5892 CONTEXTO, appendix A.3 to A.9: middle dot, keraia, geresh, gershayim, katakana middle dot, Arabic-Indic digits (`O1`). Not part of UTS #46. Default `false`. */
|
|
19
|
+
checkContextO?: boolean;
|
|
20
|
+
/** Map `ß`, `ς`, U+200C and U+200D the IDNA2003 way (deprecated). Default `false`. */
|
|
21
|
+
transitionalProcessing?: boolean;
|
|
22
|
+
/** Label length 1 to 63, name length 1 to 253 without the root dot (`A4_1`, `A4_2`), and empty labels in `idnaToUnicode` (`X4_2`). Default `false`, or `true` with `beStrict`. */
|
|
23
|
+
verifyDnsLength?: boolean;
|
|
24
|
+
/** Keep an `xn--` label whose Punycode does not decode as it is instead of reporting `P4`. Default `false`. */
|
|
25
|
+
ignoreInvalidPunycode?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** The outcome of UTS #46 processing: the converted domain, always produced, and the error codes it recorded. */
|
|
28
|
+
interface IdnaResult {
|
|
29
|
+
/** The converted domain, as far as processing got. */
|
|
30
|
+
readonly result: string;
|
|
31
|
+
/** Error codes such as `V7`, `B1`, `C1` or `A4_2`, sorted; empty when the domain is valid. */
|
|
32
|
+
readonly errors: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** UTS #46 ToASCII: maps, normalizes, validates and Punycode-encodes `domain`, and reports every error code instead of throwing. `result` is produced even when `errors` is not empty. */
|
|
35
|
+
export declare function idnaToAsciiResult(domain: string, options?: IdnaOptions): IdnaResult;
|
|
36
|
+
/** UTS #46 ToUnicode: maps, normalizes, validates and decodes the `xn--` labels of `domain`, and reports every error code instead of throwing. */
|
|
37
|
+
export declare function idnaToUnicodeResult(domain: string, options?: IdnaOptions): IdnaResult;
|
|
38
|
+
/**
|
|
39
|
+
* UTS #46 ToASCII with the full Unicode 16 status table. Unlike `domainToAscii` in `cizgile/uri` it knows every valid, ignored, mapped, deviation and disallowed code point, and checks CONTEXTJ and the bidi rule.
|
|
40
|
+
* @throws {RangeError} when processing records an error; the message lists the codes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function idnaToAscii(domain: string, options?: IdnaOptions): string;
|
|
43
|
+
/**
|
|
44
|
+
* UTS #46 ToUnicode with the full Unicode 16 status table.
|
|
45
|
+
* @throws {RangeError} when processing records an error; the message lists the codes.
|
|
46
|
+
*/
|
|
47
|
+
export declare function idnaToUnicode(domain: string, options?: IdnaOptions): string;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/idna/tables.d.ts
|
|
50
|
+
/** The UTS #46 status of a code point and, for `mapped` and `deviation`, what it maps to. */
|
|
51
|
+
type CodePointStatus = {
|
|
52
|
+
readonly status: "valid" | "ignored" | "disallowed";
|
|
53
|
+
} | {
|
|
54
|
+
readonly status: "mapped" | "deviation";
|
|
55
|
+
readonly to: readonly number[];
|
|
56
|
+
};
|
|
57
|
+
/** Looks up the UTS #46 status of one code point. */
|
|
58
|
+
export declare function codePointStatus(cp: number): CodePointStatus;
|
|
59
|
+
//#endregion
|
|
60
|
+
export type { CodePointStatus, IdnaOptions, IdnaResult };
|