cizgile 0.1.0 → 0.2.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 +56 -31
- package/dist/index.d.mts +41 -2
- package/dist/index.mjs +112 -56
- package/dist/shared/locales-latin-Bm94HAeL.mjs +595 -0
- package/dist/shared/percent-DG-pZhJI.mjs +268 -0
- package/dist/shared/types-DbyhfPRc.d.mts +24 -0
- package/dist/transliterate.d.mts +126 -12
- package/dist/transliterate.mjs +545 -3
- package/dist/uri.d.mts +125 -0
- package/dist/uri.mjs +457 -5
- package/package.json +7 -2
- package/dist/shared/iri-CzbwHYgr.mjs +0 -572
- package/dist/shared/locales-latin-D8enWKzw.mjs +0 -372
- 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=f85c00e" 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>
|
|
@@ -34,7 +35,7 @@ No dependencies. ESM only. Node 20+, Bun, Deno, browsers, edge workers.
|
|
|
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.** 36 locales (`tr`, `de`, `pl`, `sv`, `uk`, `ja`, `ko`, …) and 10 scripts (Latin, Cyrillic, Greek, Arabic, Armenian, Georgian, Dhivehi, Hebrew, Hangul, kana). `ß` → `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.
|
|
@@ -70,20 +71,23 @@ Locale ids for Latin-script languages; Cyrillic locales and other scripts come f
|
|
|
70
71
|
|
|
71
72
|
```ts
|
|
72
73
|
import { slugify } from "cizgile"
|
|
73
|
-
import { cyrillic, greek, uk, defineLocale, de } from "cizgile/transliterate"
|
|
74
|
+
import { cyrillic, greek, uk, ja, ko, defineLocale, de } from "cizgile/transliterate"
|
|
74
75
|
|
|
75
76
|
slugify("Çay & Simit", { locale: "tr" }) // "cay-ve-simit"
|
|
76
77
|
slugify("Fisch & Chips", { locale: "de" }) // "fisch-und-chips"
|
|
77
78
|
slugify("Ærø", { locale: "da" }) // "aeroe"
|
|
79
|
+
slugify("Zażółć & jaźń", { locale: "pl" }) // "zazolc-i-jazn"
|
|
78
80
|
slugify("Київ", { locale: uk }) // "kyiv"
|
|
79
81
|
slugify("Привет мир", { transliterate: [cyrillic] }) // "privet-mir"
|
|
80
82
|
slugify("Καλημέρα", { transliterate: [greek] }) // "kalimera"
|
|
83
|
+
slugify("서울 & 부산", { locale: ko }) // "seoul-mit-busan"
|
|
84
|
+
slugify("とうきょう", { locale: ja }) // "toukyou"
|
|
81
85
|
|
|
82
86
|
const swiss = defineLocale(de, { id: "de-CH", table: { ß: "ss" } })
|
|
83
87
|
slugify("Straße", { locale: swiss }) // "strasse"
|
|
84
88
|
```
|
|
85
89
|
|
|
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
|
|
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).
|
|
87
91
|
|
|
88
92
|
### Unicode slugs
|
|
89
93
|
|
|
@@ -128,41 +132,43 @@ isSlug("hello_world", { separator: "_" }) // true
|
|
|
128
132
|
isSlug("你好-world", { unicode: true }) // true
|
|
129
133
|
```
|
|
130
134
|
|
|
131
|
-
`isSlug` accepts exactly what `slugify` would produce under the same options.
|
|
135
|
+
`isSlug` accepts exactly what `slugify` would produce under the same options, `locale` included.
|
|
132
136
|
|
|
133
137
|
### All options
|
|
134
138
|
|
|
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). Counts UTF-16 code units like `.length`.
|
|
149
|
-
| `scripts` | `"any"` | Unicode mode: UTS #39 mixed-script restriction level.
|
|
150
|
-
| `bidi` | `"allow"` | Unicode mode: RFC 3987 §4.2 direction rule — `"encode"` or `"throw"` on violation.
|
|
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). Counts UTF-16 code units like `.length`. |
|
|
153
|
+
| `scripts` | `"any"` | Unicode mode: UTS #39 mixed-script restriction level. |
|
|
154
|
+
| `bidi` | `"allow"` | Unicode mode: RFC 3987 §4.2 direction rule — `"encode"` or `"throw"` on violation. |
|
|
151
155
|
|
|
152
156
|
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
157
|
|
|
154
158
|
## Transliteration on its own
|
|
155
159
|
|
|
156
160
|
```ts
|
|
157
|
-
import { transliterate, cyrillic, locales } from "cizgile/transliterate"
|
|
161
|
+
import { transliterate, cyrillic, hangul, kana, locales } from "cizgile/transliterate"
|
|
158
162
|
|
|
159
163
|
transliterate("Straße Ærø") // "Strasse AEro"
|
|
160
164
|
transliterate("Привет", { tables: [cyrillic] }) // "Privet"
|
|
161
165
|
transliterate("Ängsö", { locale: locales.sv }) // "Aengsoe"
|
|
162
|
-
transliterate("
|
|
166
|
+
transliterate("서울 ひらがな", { tables: [hangul, kana] }) // "seoul hiragana"
|
|
167
|
+
transliterate("नमस्ते 你好") // "नमस्ते 你好" — unknown scripts are kept intact (use unknown: "drop" to remove)
|
|
168
|
+
transliterate("final x² Ⅷ", { nfkc: true }) // "final x2 VIII"
|
|
163
169
|
```
|
|
164
170
|
|
|
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.
|
|
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. Hangul is romanised 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
172
|
|
|
167
173
|
## URL toolkit
|
|
168
174
|
|
|
@@ -214,7 +220,7 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
214
220
|
`parseUri` `serializeUri` — components stay distinct from "absent"; the serializer inserts `/.` or `./` where the grammar requires it.
|
|
215
221
|
`isUriReference` `isUri` `isAbsoluteUri` `isRelativeReference` `classifyReference` `pathForm` — validating parser built from the ABNF.
|
|
216
222
|
`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.
|
|
223
|
+
`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.
|
|
218
224
|
|
|
219
225
|
**Resolution (§5)**
|
|
220
226
|
`resolveUri(base, ref, { strict, allowRelativeBase })` — every §5.4 example passes; strict by default (`http:g` stays `http:g`).
|
|
@@ -225,15 +231,15 @@ iriToUri("http://例え.jp/résumé", { host: "punycode" }) // "http://xn--r8jz4
|
|
|
225
231
|
**Normalisation and comparison (§6)**
|
|
226
232
|
`normalizeUri(uri, { defaultPorts, schemeBased, userinfo })` — case, percent-encoding, dot segments, default ports, empty path → `/`; `userinfo: "strip-password" | "strip"` for logs.
|
|
227
233
|
`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).
|
|
234
|
+
`equivalentUris(a, b, { level, base, ignoreFragment, defaultPorts })` — `"simple"`, `"syntax"` or `"scheme"` (default). Never maps IRIs to URIs (RFC 3987 §5.3.1).
|
|
229
235
|
|
|
230
236
|
**IRIs (RFC 3987)**
|
|
231
237
|
`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.
|
|
238
|
+
`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
239
|
`uriToIri(uri)` — decodes only what §3.2 allows, per component.
|
|
234
|
-
`punycodeEncode` `punycodeDecode` `domainToAscii` `domainToUnicode` — RFC 3492, no dependencies.
|
|
240
|
+
`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
241
|
|
|
236
|
-
Deliberately not implemented: RFC 6874 IPv6 zone identifiers (reverted by RFC 9844)
|
|
242
|
+
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
243
|
|
|
238
244
|
</details>
|
|
239
245
|
|
|
@@ -244,11 +250,30 @@ If you are an assistant writing code with this library, these are the facts that
|
|
|
244
250
|
- Import paths: `cizgile` (slugs), `cizgile/transliterate` (tables, locales), `cizgile/uri` (URLs). ESM only, no default exports, no side effects, no runtime dependencies.
|
|
245
251
|
- `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
252
|
- 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.
|
|
253
|
+
- 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
254
|
- `createSlugger()` is the way to get unique slugs in a document or import job; do not append counters yourself.
|
|
249
255
|
- 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
256
|
- Every exported function has an explicit TypeScript signature; the `.d.mts` files in `dist/` are the authoritative API.
|
|
251
257
|
|
|
258
|
+
## Performance
|
|
259
|
+
|
|
260
|
+
Measured with `bun run bench` (vitest bench, Node 24, one core of a desktop CPU). Higher is better.
|
|
261
|
+
|
|
262
|
+
| input | cizgile | `@sindresorhus/slugify` | `slugify` (simov) |
|
|
263
|
+
| ------------------------------------- | -------------- | ----------------------- | ----------------- |
|
|
264
|
+
| ASCII title (60 chars) | **396k ops/s** | 136k | 147k |
|
|
265
|
+
| Latin with diacritics | **235k ops/s** | 96k | 179k |
|
|
266
|
+
| Turkish, `locale: "tr"` | 212k ops/s | 99k | **216k** |
|
|
267
|
+
| Cyrillic, `transliterate: [cyrillic]` | 179k ops/s | 86k | **188k** |
|
|
268
|
+
| 2.5 KB of mixed text | **6.4k ops/s** | 4.3k | 3.4k |
|
|
269
|
+
| `isSlug` | 3.7M ops/s | — | — |
|
|
270
|
+
|
|
271
|
+
`resolveUri` runs at ~0.8M ops/s (the built-in `URL` parser: ~0.9M), `removeDotSegments` at 2M,
|
|
272
|
+
`percentEncode` at 0.9M on mixed text and 3.6M on a pure-ASCII segment (`encodeURIComponent`: 3.1M and 4.7M — it is native),
|
|
273
|
+
`normalizeUri` at 0.37M, `iriToUri` at 0.66M.
|
|
274
|
+
Options objects are resolved once and cached structurally, so inline `{ locale: "tr" }` literals cost
|
|
275
|
+
nothing after the first call.
|
|
276
|
+
|
|
252
277
|
## How it compares
|
|
253
278
|
|
|
254
279
|
| input | cizgile | Django `slugify` | Rails `parameterize` | `@sindresorhus/slugify` |
|
|
@@ -265,7 +290,7 @@ If you are an assistant writing code with this library, these are the facts that
|
|
|
265
290
|
|
|
266
291
|
## Specifications
|
|
267
292
|
|
|
268
|
-
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.
|
|
293
|
+
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.
|
|
269
294
|
|
|
270
295
|
## Development
|
|
271
296
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,55 +1,94 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as LocaleId, n as LatinLocaleId, o as TransliterationTable, r as Locale } from "./shared/types-DbyhfPRc.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
|
|
17
25
|
//#region src/slug/options.d.ts
|
|
26
|
+
/** Options for `slugify` and `createSlugger`. Every option has a default; an empty object is the everyday call. */
|
|
18
27
|
interface SlugifyOptions {
|
|
28
|
+
/** Joins words; `"-"` by default. Any URL-safe punctuation (`- _ . ~ ! $ & ' ( ) * + , ; = @`) or `""`. */
|
|
19
29
|
readonly separator?: string;
|
|
30
|
+
/** Lowercases the result; `true` by default. */
|
|
20
31
|
readonly lowercase?: boolean;
|
|
32
|
+
/** Keeps letters from every script instead of transliterating to ASCII; `false` by default. */
|
|
21
33
|
readonly unicode?: boolean;
|
|
34
|
+
/** Language rules: a Latin locale id such as `"tr"`, or a `Locale` object from `cizgile/transliterate`. */
|
|
22
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`. */
|
|
23
37
|
readonly transliterate?: boolean | readonly TransliterationTable[];
|
|
38
|
+
/** Splits camelCase before slugging: `"fooBar"` becomes `"foo-bar"`; `false` by default. */
|
|
24
39
|
readonly decamelize?: boolean;
|
|
40
|
+
/** `[from, to]` pairs applied before anything else; spaces in `to` become separators. */
|
|
25
41
|
readonly replacements?: ReadonlyArray<readonly [string, string]>;
|
|
42
|
+
/** A global regex of characters to delete rather than turn into separators; apostrophes by default, `false` for none. */
|
|
26
43
|
readonly remove?: RegExp | false;
|
|
44
|
+
/** Extra URL-safe single characters to keep, such as `["."]` for version numbers. */
|
|
27
45
|
readonly preserveCharacters?: readonly string[];
|
|
46
|
+
/** Keeps a leading `_`: `"_draft"` stays `"_draft"`. */
|
|
28
47
|
readonly preserveLeadingUnderscore?: boolean;
|
|
48
|
+
/** Keeps a trailing separator, for input still being typed. */
|
|
29
49
|
readonly preserveTrailingSeparator?: boolean;
|
|
50
|
+
/** Cuts at a word boundary, never inside a grapheme cluster. Counts UTF-16 code units like `.length`. */
|
|
30
51
|
readonly maxLength?: number;
|
|
52
|
+
/** Unicode mode only: the UTS #39 restriction level the result must satisfy; `"any"` by default. */
|
|
31
53
|
readonly scripts?: ScriptRestriction;
|
|
54
|
+
/** Unicode mode only: what to do when the result mixes text directions (RFC 3987 section 4.2); `"allow"` by default. */
|
|
32
55
|
readonly bidi?: "allow" | "encode" | "throw";
|
|
33
56
|
}
|
|
34
|
-
|
|
57
|
+
/** 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">;
|
|
35
59
|
//#endregion
|
|
36
60
|
//#region src/slug/is-slug.d.ts
|
|
61
|
+
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
37
62
|
export declare function isSlug(input: string, options?: IsSlugOptions): boolean;
|
|
38
63
|
//#endregion
|
|
39
64
|
//#region src/slug/slugger.d.ts
|
|
65
|
+
/** A `slugify` that remembers what it handed out; see `createSlugger`. */
|
|
40
66
|
interface Slugger {
|
|
67
|
+
/** Slugifies `input`, appending `-2`, `-3`, ... when the slug was already handed out. */
|
|
41
68
|
(input: string, options?: SlugifyOptions): string;
|
|
69
|
+
/** Forgets every slug handed out or reserved so far. */
|
|
42
70
|
reset(): void;
|
|
71
|
+
/** Whether `slug` has already been handed out or reserved. */
|
|
43
72
|
has(slug: string): boolean;
|
|
73
|
+
/** Marks `slug` as taken so it is never handed out again. */
|
|
44
74
|
reserve(slug: string): void;
|
|
45
75
|
}
|
|
76
|
+
/** A slugger that never repeats a slug: the second `"Hello"` becomes `"hello-2"`. `defaults` apply to every call. */
|
|
46
77
|
export declare function createSlugger(defaults?: SlugifyOptions): Slugger;
|
|
78
|
+
/** Alias of `createSlugger`, under the name `@sindresorhus/slugify` uses. */
|
|
47
79
|
export declare const slugifyWithCounter: (defaults?: SlugifyOptions) => Slugger;
|
|
48
80
|
//#endregion
|
|
49
81
|
//#region src/slug/slugify.d.ts
|
|
82
|
+
/**
|
|
83
|
+
* Turns text into a URL slug: an RFC 3986 `segment-nz-nc` in ASCII mode, NFKC letters, digits and marks in unicode mode.
|
|
84
|
+
* Returns `""` when nothing usable remains and never throws on ordinary text.
|
|
85
|
+
* @throws {TypeError} when `input` is not a string or an option is malformed (`preserveCharacters`, a non-global `remove`, an unknown locale id).
|
|
86
|
+
* @throws {RangeError} for an invalid `separator` or `maxLength`, and in unicode mode when `scripts` or `bidi: "throw"` rejects the result.
|
|
87
|
+
*/
|
|
50
88
|
export declare function slugify(input: string, options?: SlugifyOptions): string;
|
|
51
89
|
//#endregion
|
|
52
90
|
//#region src/slug/truncate.d.ts
|
|
91
|
+
/** Cuts `slug` to at most `maxLength` UTF-16 code units at a `separator` boundary, never inside a grapheme cluster. */
|
|
53
92
|
export declare function truncateSlug(slug: string, maxLength: number, separator?: string): string;
|
|
54
93
|
//#endregion
|
|
55
94
|
export type { IsSlugOptions, LatinLocaleId, Locale, LocaleId, ScriptCheck, ScriptRestriction, Slugger, SlugifyOptions, TransliterationTable };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { A as fold, D as DEFAULT_TABLES, F as symbols, O as applyCompat, k as compileTables, m as latinLocales } from "./shared/locales-latin-Bm94HAeL.mjs";
|
|
2
|
+
import { h as isSegmentNzNc, r as percentEncode } from "./shared/percent-DG-pZhJI.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/slug/bidi.ts
|
|
5
5
|
const RTL = /\p{Script=Hebrew}|\p{Script=Arabic}|\p{Script=Syriac}|\p{Script=Thaana}|\p{Script=Nko}|\p{Script=Samaritan}|\p{Script=Mandaic}|\p{Script=Adlam}/u;
|
|
@@ -11,6 +11,7 @@ function isRtl(ch) {
|
|
|
11
11
|
function isLtr(ch) {
|
|
12
12
|
return LETTER.test(ch) && !RTL.test(ch);
|
|
13
13
|
}
|
|
14
|
+
/** RFC 3987 section 4.2: whether `text` can be a URL component without mixing text directions in a way that renders ambiguously. */
|
|
14
15
|
function isBidiSafeComponent(text) {
|
|
15
16
|
let hasRtl = false;
|
|
16
17
|
let hasLtr = false;
|
|
@@ -30,13 +31,14 @@ function isBidiSafeComponent(text) {
|
|
|
30
31
|
|
|
31
32
|
//#endregion
|
|
32
33
|
//#region src/slug/decamelize.ts
|
|
34
|
+
/** Inserts spaces at camelCase and acronym boundaries: `"getHTTPResponse"` becomes `"get HTTP Response"`. */
|
|
33
35
|
function decamelize(input) {
|
|
34
36
|
return input.replace(/(\p{Lu}{2,})(\p{N}+)/gu, "$1 $2").replace(/([\p{Ll}\p{N}]+)(\p{Lu}{2,})/gu, "$1 $2").replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2").replace(/(\p{Lu}+)(\p{Lu}[\p{Ll}\p{N}]+)/gu, (match, head, tail) => tail.length === 2 && tail.endsWith("s") ? match : `${head} ${tail}`);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
//#endregion
|
|
38
40
|
//#region src/slug/charset.ts
|
|
39
|
-
function escapeRegExp(text) {
|
|
41
|
+
function escapeRegExp$1(text) {
|
|
40
42
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
43
|
}
|
|
42
44
|
function escapeClassChar(text) {
|
|
@@ -111,6 +113,7 @@ function scriptsOf(ch) {
|
|
|
111
113
|
for (const [name, re] of scriptMatchers()) if (re.test(ch)) out.push(name);
|
|
112
114
|
return out;
|
|
113
115
|
}
|
|
116
|
+
/** The Unicode script names used by `text`, ignoring Common and Inherited characters. */
|
|
114
117
|
function detectScripts(text) {
|
|
115
118
|
const definite = /* @__PURE__ */ new Set();
|
|
116
119
|
const ambiguous = [];
|
|
@@ -132,6 +135,7 @@ function allowed(scripts, level) {
|
|
|
132
135
|
if (level === "highly-restrictive") return false;
|
|
133
136
|
return others.length === 1 && !EXCLUDED_WITH_LATIN.has(others[0] ?? "");
|
|
134
137
|
}
|
|
138
|
+
/** Applies a UTS #39 restriction `level` to the scripts in `text`; `ok` is false when the mix exceeds it. */
|
|
135
139
|
function checkScripts(text, level = "moderately-restrictive") {
|
|
136
140
|
const scripts = detectScripts(text);
|
|
137
141
|
return {
|
|
@@ -142,7 +146,24 @@ function checkScripts(text, level = "moderately-restrictive") {
|
|
|
142
146
|
|
|
143
147
|
//#endregion
|
|
144
148
|
//#region src/slug/options.ts
|
|
149
|
+
let symbolOnlyCache;
|
|
150
|
+
function symbolEntries(table) {
|
|
151
|
+
symbolOnlyCache ??= /* @__PURE__ */ new WeakMap();
|
|
152
|
+
const cached = symbolOnlyCache.get(table);
|
|
153
|
+
if (cached !== void 0) return cached;
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const [key, value] of Object.entries(table)) if (!/[\p{L}\p{M}\p{N}]/u.test(key)) out[key] = value;
|
|
156
|
+
symbolOnlyCache.set(table, out);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
function escapeRegExp(text) {
|
|
160
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
161
|
+
}
|
|
145
162
|
const DEFAULT_REMOVE = /['’]/g;
|
|
163
|
+
function lowercaseOf(text, o) {
|
|
164
|
+
if (o.unicode && o.locale?.lowercase !== void 0) return o.locale.lowercase(text);
|
|
165
|
+
return text.replaceAll("İ", "i").toLowerCase();
|
|
166
|
+
}
|
|
146
167
|
let cache$1;
|
|
147
168
|
function resolveLocale(locale) {
|
|
148
169
|
if (locale === void 0) return void 0;
|
|
@@ -189,12 +210,16 @@ function build(options) {
|
|
|
189
210
|
const wordExtra = preserveCharacters.map(escapeClassChar).join("");
|
|
190
211
|
const wordClass = unicode ? `\\p{L}\\p{N}\\p{M}${wordExtra}` : `a-z0-9${lowercase ? "" : "A-Z"}${wordExtra}`;
|
|
191
212
|
const allowedClass = wordClass + separatorChars.map(escapeClassChar).join("");
|
|
213
|
+
const tables = resolveTables(options.transliterate, unicode, locale);
|
|
214
|
+
const symbolTables = (locale === void 0 ? [symbols] : [locale.table, symbols]).map(symbolEntries);
|
|
215
|
+
const separatorRuns = separator === "" ? void 0 : new RegExp(`[${separatorChars.map(escapeClassChar).join("")}]+`, "gu");
|
|
216
|
+
const leadingMarks = unicode ? new RegExp(`(${separator === "" ? "^" : `(?:^|${escapeRegExp(separator)})`})\\p{M}+`, "gu") : void 0;
|
|
192
217
|
return {
|
|
193
218
|
separator,
|
|
194
219
|
lowercase,
|
|
195
220
|
unicode,
|
|
196
221
|
locale,
|
|
197
|
-
tables
|
|
222
|
+
tables,
|
|
198
223
|
decamelize: options.decamelize ?? false,
|
|
199
224
|
replacements: options.replacements ?? [],
|
|
200
225
|
remove,
|
|
@@ -206,14 +231,61 @@ function build(options) {
|
|
|
206
231
|
bidi: options.bidi ?? "allow",
|
|
207
232
|
allowedClass,
|
|
208
233
|
wordClass,
|
|
209
|
-
disallowed: new RegExp(`[^${allowedClass}]+`, "gu")
|
|
234
|
+
disallowed: new RegExp(`[^${allowedClass}]+`, "gu"),
|
|
235
|
+
compiled: tables === void 0 ? void 0 : compileTables(tables),
|
|
236
|
+
symbolTables,
|
|
237
|
+
compiledSymbols: compileTables(symbolTables),
|
|
238
|
+
separatorRuns,
|
|
239
|
+
leadingMarks
|
|
210
240
|
};
|
|
211
241
|
}
|
|
212
|
-
|
|
242
|
+
const DEFAULT_OPTIONS$1 = {};
|
|
243
|
+
let objectIds;
|
|
244
|
+
let nextObjectId = 0;
|
|
245
|
+
let byKey;
|
|
246
|
+
function objectId(value) {
|
|
247
|
+
objectIds ??= /* @__PURE__ */ new WeakMap();
|
|
248
|
+
let id = objectIds.get(value);
|
|
249
|
+
if (id === void 0) {
|
|
250
|
+
id = nextObjectId;
|
|
251
|
+
nextObjectId += 1;
|
|
252
|
+
objectIds.set(value, id);
|
|
253
|
+
}
|
|
254
|
+
return id;
|
|
255
|
+
}
|
|
256
|
+
function optionsKey(options) {
|
|
257
|
+
const locale = options.locale;
|
|
258
|
+
const transliterate = options.transliterate;
|
|
259
|
+
const remove = options.remove;
|
|
260
|
+
return JSON.stringify([
|
|
261
|
+
options.separator,
|
|
262
|
+
options.lowercase,
|
|
263
|
+
options.unicode,
|
|
264
|
+
typeof locale === "object" ? `#${objectId(locale)}` : locale,
|
|
265
|
+
Array.isArray(transliterate) ? transliterate.map((t) => objectId(t)) : transliterate,
|
|
266
|
+
options.decamelize,
|
|
267
|
+
options.replacements,
|
|
268
|
+
remove instanceof RegExp ? `${remove.source}/${remove.flags}` : remove,
|
|
269
|
+
options.preserveCharacters,
|
|
270
|
+
options.preserveLeadingUnderscore,
|
|
271
|
+
options.preserveTrailingSeparator,
|
|
272
|
+
options.maxLength,
|
|
273
|
+
options.scripts,
|
|
274
|
+
options.bidi
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
function resolveOptions(options = DEFAULT_OPTIONS$1) {
|
|
213
278
|
cache$1 ??= /* @__PURE__ */ new WeakMap();
|
|
214
279
|
const cached = cache$1.get(options);
|
|
215
280
|
if (cached !== void 0) return cached;
|
|
216
|
-
|
|
281
|
+
byKey ??= /* @__PURE__ */ new Map();
|
|
282
|
+
const key = optionsKey(options);
|
|
283
|
+
let resolved = byKey.get(key);
|
|
284
|
+
if (resolved === void 0) {
|
|
285
|
+
resolved = build(options);
|
|
286
|
+
if (byKey.size > 512) byKey.clear();
|
|
287
|
+
byKey.set(key, resolved);
|
|
288
|
+
}
|
|
217
289
|
cache$1.set(options, resolved);
|
|
218
290
|
return resolved;
|
|
219
291
|
}
|
|
@@ -227,7 +299,7 @@ function patternFor(options) {
|
|
|
227
299
|
if (cached !== void 0) return cached;
|
|
228
300
|
const o = resolveOptions(options);
|
|
229
301
|
const char = `[${o.wordClass}]`;
|
|
230
|
-
const separator = escapeRegExp(o.separator);
|
|
302
|
+
const separator = escapeRegExp$1(o.separator);
|
|
231
303
|
const lead = o.preserveLeadingUnderscore ? "_?" : "";
|
|
232
304
|
const word = o.unicode ? `(?!\\p{M})${char}+` : `${char}+`;
|
|
233
305
|
const body = o.separator === "" ? word : `${word}(?:${separator}${word})*`;
|
|
@@ -236,14 +308,16 @@ function patternFor(options) {
|
|
|
236
308
|
cache.set(options, pattern);
|
|
237
309
|
return pattern;
|
|
238
310
|
}
|
|
239
|
-
|
|
311
|
+
const DEFAULT_OPTIONS = {};
|
|
312
|
+
/** Whether `input` is exactly what `slugify` would produce under the same options. */
|
|
313
|
+
function isSlug(input, options = DEFAULT_OPTIONS) {
|
|
240
314
|
if (typeof input !== "string" || input === "" || input === "." || input === "..") return false;
|
|
241
315
|
const o = resolveOptions(options);
|
|
242
316
|
if (o.maxLength !== void 0 && input.length > o.maxLength) return false;
|
|
243
317
|
if (o.separator !== "" && input.includes(o.separator + o.separator)) return false;
|
|
244
318
|
if (o.unicode) {
|
|
245
319
|
if (input !== input.normalize("NFKC")) return false;
|
|
246
|
-
if (o.lowercase && input !== input
|
|
320
|
+
if (o.lowercase && input !== lowercaseOf(input, o)) return false;
|
|
247
321
|
if (o.scripts !== "any" && !checkScripts(input, o.scripts).ok) return false;
|
|
248
322
|
if (o.bidi !== "allow" && !isBidiSafeComponent(input)) return false;
|
|
249
323
|
}
|
|
@@ -287,6 +361,7 @@ function stripTrailingSeparator(text, separator) {
|
|
|
287
361
|
for (let k = separator.length - 1; k > 0; k--) if (out.endsWith(separator.slice(0, k))) return out.slice(0, -k);
|
|
288
362
|
return out;
|
|
289
363
|
}
|
|
364
|
+
/** Cuts `slug` to at most `maxLength` UTF-16 code units at a `separator` boundary, never inside a grapheme cluster. */
|
|
290
365
|
function truncateSlug(slug, maxLength, separator = "-") {
|
|
291
366
|
if (!Number.isInteger(maxLength) || maxLength < 0) throw new RangeError("truncateSlug: maxLength must be a non-negative integer");
|
|
292
367
|
if (slug.length <= maxLength) return slug;
|
|
@@ -302,64 +377,43 @@ function truncateSlug(slug, maxLength, separator = "-") {
|
|
|
302
377
|
|
|
303
378
|
//#endregion
|
|
304
379
|
//#region src/slug/slugify.ts
|
|
305
|
-
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
const out = {};
|
|
311
|
-
for (const [key, value] of Object.entries(table)) if (!/[\p{L}\p{M}\p{N}]/u.test(key)) out[key] = value;
|
|
312
|
-
symbolOnlyCache.set(table, out);
|
|
313
|
-
return out;
|
|
314
|
-
}
|
|
315
|
-
let compatCache;
|
|
316
|
-
function compatEntries(table) {
|
|
317
|
-
compatCache ??= /* @__PURE__ */ new WeakMap();
|
|
318
|
-
const cached = compatCache.get(table);
|
|
319
|
-
if (cached !== void 0) return cached;
|
|
320
|
-
const out = [];
|
|
321
|
-
for (const [key, value] of Object.entries(table)) if (key.normalize("NFKC") !== key) out.push([key, value]);
|
|
322
|
-
compatCache.set(table, out);
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
function applyCompat(text, tables) {
|
|
326
|
-
let out = text;
|
|
327
|
-
for (const table of tables) for (const [key, value] of compatEntries(table)) if (out.includes(key)) out = out.split(key).join(value);
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
function lowercaseOf(text, o) {
|
|
331
|
-
if (o.unicode && o.locale?.lowercase !== void 0) return o.locale.lowercase(text);
|
|
332
|
-
return text.replaceAll("İ", "i").toLowerCase();
|
|
333
|
-
}
|
|
334
|
-
function collapse(text, separator) {
|
|
335
|
-
if (separator === "") return text;
|
|
336
|
-
const runs = new RegExp(`[${uniqueChars(separator).map(escapeClassChar).join("")}]+`, "gu");
|
|
337
|
-
let out = text.replace(runs, separator);
|
|
380
|
+
const NON_ASCII = /[^\x00-\x7F]/;
|
|
381
|
+
function collapse(text, o) {
|
|
382
|
+
if (o.separatorRuns === void 0) return text;
|
|
383
|
+
const separator = o.separator;
|
|
384
|
+
let out = text.replace(o.separatorRuns, separator);
|
|
338
385
|
while (out.startsWith(separator)) out = out.slice(separator.length);
|
|
339
386
|
while (out.endsWith(separator)) out = out.slice(0, -separator.length);
|
|
340
387
|
return out;
|
|
341
388
|
}
|
|
342
|
-
|
|
389
|
+
/**
|
|
390
|
+
* Turns text into a URL slug: an RFC 3986 `segment-nz-nc` in ASCII mode, NFKC letters, digits and marks in unicode mode.
|
|
391
|
+
* Returns `""` when nothing usable remains and never throws on ordinary text.
|
|
392
|
+
* @throws {TypeError} when `input` is not a string or an option is malformed (`preserveCharacters`, a non-global `remove`, an unknown locale id).
|
|
393
|
+
* @throws {RangeError} for an invalid `separator` or `maxLength`, and in unicode mode when `scripts` or `bidi: "throw"` rejects the result.
|
|
394
|
+
*/
|
|
395
|
+
function slugify(input, options) {
|
|
343
396
|
if (typeof input !== "string") throw new TypeError("slugify: input must be a string");
|
|
344
397
|
const o = resolveOptions(options);
|
|
345
|
-
let s = stripControls(input)
|
|
398
|
+
let s = stripControls(input);
|
|
399
|
+
let ascii = !NON_ASCII.test(s);
|
|
400
|
+
if (!ascii) s = s.normalize("NFC");
|
|
346
401
|
for (const [from, to] of o.replacements) if (from !== "") s = s.split(from).join(to);
|
|
347
|
-
|
|
348
|
-
s = applyCompat(s, o.tables ?? symbolTables).normalize("NFKC");
|
|
402
|
+
if (o.replacements.length > 0) ascii = !NON_ASCII.test(s);
|
|
403
|
+
if (!ascii) s = applyCompat(s, o.tables ?? o.symbolTables).normalize("NFKC");
|
|
349
404
|
if (o.decamelize) s = decamelize(s);
|
|
350
|
-
if (o.
|
|
351
|
-
|
|
405
|
+
if (o.compiled !== void 0) {
|
|
406
|
+
s = fold(s, o.compiled, false);
|
|
407
|
+
if (!ascii && NON_ASCII.test(s)) s = s.replace(/(?![\x00-\x7F])[\p{L}\p{M}\p{N}]/gu, "");
|
|
408
|
+
} else s = fold(s, o.compiledSymbols, false, false);
|
|
352
409
|
if (o.lowercase) s = lowercaseOf(s, o);
|
|
353
|
-
if (o.unicode) s = s.normalize("NFKC");
|
|
410
|
+
if (o.unicode && !ascii) s = s.normalize("NFKC");
|
|
354
411
|
if (o.remove !== void 0) s = s.replace(o.remove, "");
|
|
355
412
|
const hadLeadingUnderscore = s.startsWith("_");
|
|
356
413
|
s = s.replace(o.disallowed, o.separator);
|
|
357
|
-
if (o.
|
|
358
|
-
const boundary = o.separator === "" ? "^" : `(?:^|${escapeRegExp(o.separator)})`;
|
|
359
|
-
s = s.replace(new RegExp(`(${boundary})\\p{M}+`, "gu"), "$1");
|
|
360
|
-
}
|
|
414
|
+
if (o.leadingMarks !== void 0 && !ascii) s = s.replace(o.leadingMarks, "$1");
|
|
361
415
|
const hadTrailingSeparator = o.separator !== "" && s.endsWith(o.separator);
|
|
362
|
-
s = collapse(s, o
|
|
416
|
+
s = collapse(s, o);
|
|
363
417
|
if (o.maxLength !== void 0) s = truncateSlug(s, o.maxLength, o.separator);
|
|
364
418
|
if (s === "." || s === "..") s = "";
|
|
365
419
|
if (o.preserveLeadingUnderscore && hadLeadingUnderscore && s !== "" && !s.startsWith("_")) s = "_" + s;
|
|
@@ -371,7 +425,7 @@ function slugify(input, options = {}) {
|
|
|
371
425
|
}
|
|
372
426
|
if (o.bidi !== "allow" && !isBidiSafeComponent(s)) {
|
|
373
427
|
if (o.bidi === "throw") throw new RangeError(`slugify: ${JSON.stringify(s)} mixes text directions (RFC 3987 section 4.2)`);
|
|
374
|
-
s =
|
|
428
|
+
s = percentEncode(s, "segment-nz-nc");
|
|
375
429
|
}
|
|
376
430
|
}
|
|
377
431
|
return s;
|
|
@@ -386,6 +440,7 @@ function withSuffix(base, separator, n, maxLength) {
|
|
|
386
440
|
const head = room > 0 ? truncateSlug(base, room, separator) : "";
|
|
387
441
|
return head === "" ? String(n).slice(0, maxLength) : head + suffix;
|
|
388
442
|
}
|
|
443
|
+
/** A slugger that never repeats a slug: the second `"Hello"` becomes `"hello-2"`. `defaults` apply to every call. */
|
|
389
444
|
function createSlugger(defaults = {}) {
|
|
390
445
|
const issued = /* @__PURE__ */ new Set();
|
|
391
446
|
const counters = /* @__PURE__ */ new Map();
|
|
@@ -424,6 +479,7 @@ function createSlugger(defaults = {}) {
|
|
|
424
479
|
}
|
|
425
480
|
});
|
|
426
481
|
}
|
|
482
|
+
/** Alias of `createSlugger`, under the name `@sindresorhus/slugify` uses. */
|
|
427
483
|
const slugifyWithCounter = createSlugger;
|
|
428
484
|
|
|
429
485
|
//#endregion
|