slugen 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # slugen
2
+
3
+ A modern, fast slug generator for JavaScript and TypeScript.
4
+
5
+ Generate URL-friendly slugs, usernames, SEO-friendly links, or safely convert text into clean identifiers.
6
+
7
+
8
+ ## Features
9
+
10
+ - **Multi-locale support** — de, fr, tr, es, and more
11
+ - **Accurate normalization** of accented and language-specific characters
12
+ - **Smart symbol handling** with semantic replacements instead of blind removal
13
+ - **High performance**, suitable for batch and large-scale processing
14
+ - **Customizable separators** (`-`, `_`, `.`)
15
+ - **Deterministic output** with comprehensive test coverage
16
+
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install slugen
22
+ ```
23
+ or
24
+
25
+ ```bash
26
+ pnpm add slugen
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```ts
32
+ import { slugen } from "slugen";
33
+
34
+ slugen("Hello World");
35
+ // → "hello-world"
36
+
37
+ slugen("Istanbul's Best Coffee!");
38
+ // → "istanbuls-best-coffee"
39
+ ```
40
+
41
+ ## With Options
42
+ ```ts
43
+ import { slugen } from "slugen";
44
+
45
+ slugen("Hello World", {
46
+ locale: "fr", // or ["fr", "de"]
47
+ separator: "-",
48
+ lowercase: true,
49
+ symbols: true,
50
+ customReplacements: {
51
+ "&": "and",
52
+ },
53
+ });
54
+ // hello-world
55
+ ```
56
+
57
+ ## Separator
58
+
59
+ The default separator is -.
60
+ You can also use `_` or `.`
61
+
62
+ ```ts
63
+ slugen("Hello World", { separator: "-" });
64
+ // hello-world
65
+ slugen("Hello World", { separator: "_" });
66
+ // hello_world
67
+ slugen("Hello World", { separator: "." });
68
+ // hello.world
69
+ ```
70
+ The separator must be a single character.
71
+
72
+ ## Locale Support
73
+
74
+ By default, slugen uses Unicode normalization and removes diacritics.
75
+ You can optionally provide a locale for better language-specific handling.
76
+
77
+ ```ts
78
+ import { slugen } from "slugen";
79
+
80
+ slugen("Café français", { locale: "fr" });
81
+ // → "cafe-francais"
82
+ slugen("Español México", { locale: "es" });
83
+ // → "espanol-mexico"
84
+ slugen("İstanbul Boğazı", { locale: "tr" });
85
+ // istanbul-bogazi
86
+ slugen("Straße Größe", { locale: "de" });
87
+ // strasse-grosse
88
+
89
+ ```
90
+ ### Multiple locales (merged)
91
+ ```ts
92
+ slugen("İstanbul Straße Español", { locale: ["tr", "de", "es"]});
93
+ // istanbul-strasse-espanol
94
+ ```
95
+ Locales are applied in order and safely merged.
96
+
97
+ ## Symbol Handling
98
+
99
+ slugen categorizes symbols into three groups:
100
+
101
+ ### Examples:
102
+ ```ts
103
+ slugen("foo & bar");
104
+ // foo-and-bar
105
+ slugen("email@test.com");
106
+ // email-at-test-com
107
+ slugen("50€");
108
+ // 50-euro
109
+ slugen("foo & bar");
110
+ // foo-and-bar
111
+ slugen("100$ price");
112
+ // 100-dollar-price
113
+ slugen("50% off");
114
+ // 50-percent-off
115
+ ```
116
+
117
+ ### Disable Symbols
118
+ ```ts
119
+ slugen("foo & bar", { symbols: false });
120
+ // foo-bar
121
+ ```
122
+
123
+ ## Custom Replacements
124
+
125
+ ```ts
126
+ slugen("React & Vue", {
127
+ customReplacements: {
128
+ "&": "and",
129
+ "React": "react-js"
130
+ }
131
+ });
132
+ // react-js-and-vue
133
+
134
+ slugen("C++ Programming", {
135
+ customReplacements: {
136
+ "++": "plus-plus"
137
+ }
138
+ });
139
+ // c-plus-plus-programming
140
+ ```
141
+
142
+
143
+ ## Real-World Examples
144
+ ```ts
145
+ slugen("How to Learn JavaScript in 2025?");
146
+ // how-to-learn-javascript-in-2025
147
+ slugen("React vs Vue: Which is Better?");
148
+ // react-vs-vue-which-is-better
149
+ slugen("iPhone 15 Pro Max");
150
+ // iphone-15-pro-max
151
+ slugen("Samsung 55\" 4K Smart TV");
152
+ // samsung-55-4k-smart-tv
153
+ ```
154
+
155
+ ### Usernames:
156
+
157
+ ```ts
158
+ slugen("John Doe", { separator: "_" });
159
+ // john_doe
160
+
161
+ slugen("jane.smith@example", { separator: "_" });
162
+ // jane_smith_at_example
163
+ ```
164
+
165
+ ### Filenames:
166
+
167
+ ```ts
168
+ slugen("Project Report 2024.pdf", { separator: "_" });
169
+ // project_report_2024_pdf
170
+ ```
171
+
172
+
173
+ ## API Reference
174
+
175
+ ### ``slugen(text, options?)``
176
+
177
+ Generates a slug from the given text.
178
+
179
+ **Parameters:**
180
+ - `text` (string) - Text to convert into a slug
181
+ - `options` (object, optional) - Configuration options
182
+
183
+ **Options:**
184
+
185
+ | Option | Type | Default | Description |
186
+ | -------------------- | ------------------------ | ----------- | ----------------------------------------------- |
187
+ | `locale` | `string \| string[]` | `undefined` | Language support (e.g., `"tr"`, `"de"`, `"fr"`) |
188
+ | `separator` | `"-" \| "_" \| "."` | `"-"` | Character to separate words |
189
+ | `lowercase` | `boolean` | `true` | Convert to lowercase |
190
+ | `symbols` | `boolean` | `true` | Enable symbol normalization |
191
+ | `customReplacements` | `Record<string, string>` | `{}` | Custom character replacements |
192
+
193
+ **Returns:** `string` - Generated slug
194
+
195
+
196
+ ## Design Notes
197
+
198
+ - ``applySymbols`` only normalizes text; it does not decide slug structure
199
+ - A single regex pass handles final slug normalization
200
+ - Locale and separator behavior is deterministic
201
+ - Minimal API surface, easy to extend
202
+
203
+
204
+ ## Contributing
205
+
206
+ Contributions are welcome! Please open an issue or submit a pull request on GitHub.
207
+
208
+
209
+ ## License
210
+ MIT
211
+
@@ -0,0 +1,17 @@
1
+ interface SlugenOptions {
2
+ locale?: string | string[];
3
+ separator?: string;
4
+ lowercase?: boolean;
5
+ symbols?: boolean;
6
+ customReplacements?: Record<string, string>;
7
+ }
8
+
9
+ declare function slugen(input: string, options?: SlugenOptions): string;
10
+
11
+ type LocaleMap = Record<string, string>;
12
+ /**
13
+ * Locale definitions
14
+ */
15
+ declare const locales: Record<string, LocaleMap>;
16
+
17
+ export { type SlugenOptions, locales, slugen };
@@ -0,0 +1,17 @@
1
+ interface SlugenOptions {
2
+ locale?: string | string[];
3
+ separator?: string;
4
+ lowercase?: boolean;
5
+ symbols?: boolean;
6
+ customReplacements?: Record<string, string>;
7
+ }
8
+
9
+ declare function slugen(input: string, options?: SlugenOptions): string;
10
+
11
+ type LocaleMap = Record<string, string>;
12
+ /**
13
+ * Locale definitions
14
+ */
15
+ declare const locales: Record<string, LocaleMap>;
16
+
17
+ export { type SlugenOptions, locales, slugen };
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ // src/locales.ts
4
+ var locales = {
5
+ de: {
6
+ \u00C4: "a",
7
+ \u00E4: "a",
8
+ \u00D6: "o",
9
+ \u00F6: "o",
10
+ \u00DC: "u",
11
+ \u00FC: "u",
12
+ \u00DF: "ss"
13
+ },
14
+ fr: {
15
+ \u00C0: "a",
16
+ \u00C1: "a",
17
+ \u00C2: "a",
18
+ \u00C3: "a",
19
+ \u00C4: "a",
20
+ \u00C5: "a",
21
+ \u00E0: "a",
22
+ \u00E1: "a",
23
+ \u00E2: "a",
24
+ \u00E3: "a",
25
+ \u00E4: "a",
26
+ \u00E5: "a",
27
+ \u00C7: "c",
28
+ \u00E7: "c",
29
+ \u00C8: "e",
30
+ \u00C9: "e",
31
+ \u00CA: "e",
32
+ \u00CB: "e",
33
+ \u00E8: "e",
34
+ \u00E9: "e",
35
+ \u00EA: "e",
36
+ \u00EB: "e",
37
+ \u0152: "oe",
38
+ \u0153: "oe"
39
+ },
40
+ es: {
41
+ \u00D1: "n",
42
+ \u00F1: "n"
43
+ },
44
+ tr: {
45
+ \u0130: "i",
46
+ I: "i",
47
+ \u0131: "i",
48
+ \u015E: "s",
49
+ \u015F: "s",
50
+ \u011E: "g",
51
+ \u011F: "g",
52
+ \u00D6: "o",
53
+ \u00F6: "o",
54
+ \u00DC: "u",
55
+ \u00FC: "u",
56
+ \u00C7: "c",
57
+ \u00E7: "c"
58
+ },
59
+ pl: {
60
+ \u0141: "l",
61
+ \u0142: "l",
62
+ \u015A: "s",
63
+ \u015B: "s",
64
+ \u017B: "z",
65
+ \u017C: "z"
66
+ },
67
+ en: {}
68
+ };
69
+ var localeCache = /* @__PURE__ */ new Map();
70
+ function mergeLocales(localeKeys) {
71
+ const merged = {};
72
+ for (const key of localeKeys) {
73
+ const locale = locales[key];
74
+ if (!locale) continue;
75
+ Object.assign(merged, locale);
76
+ }
77
+ return merged;
78
+ }
79
+ function applyLocale(text, locale) {
80
+ if (!locale) return text;
81
+ const keys = Array.isArray(locale) ? locale : [locale];
82
+ const cacheKey = keys.sort().join("+");
83
+ let cached = localeCache.get(cacheKey);
84
+ if (!cached) {
85
+ const map = mergeLocales(keys);
86
+ if (Object.keys(map).length === 0) {
87
+ localeCache.set(cacheKey, { map, regex: /$^/ });
88
+ return text;
89
+ }
90
+ const pattern = new RegExp(`[${Object.keys(map).join("")}]`, "g");
91
+ cached = { map, regex: pattern };
92
+ localeCache.set(cacheKey, cached);
93
+ }
94
+ return text.replace(cached.regex, (char) => cached.map[char] ?? char);
95
+ }
96
+
97
+ // src/symbols.ts
98
+ var symbols = {
99
+ // Logical / textual (semantic)
100
+ "&": " and ",
101
+ "|": " or ",
102
+ "@": " at ",
103
+ "%": " percent ",
104
+ "+": " plus ",
105
+ "=": " equals ",
106
+ // Currency (semantic)
107
+ $: " dollar ",
108
+ "\u20AC": " euro ",
109
+ "\xA3": " pound ",
110
+ "\u20BA": " lira ",
111
+ "\xA5": " yen ",
112
+ "\u20B9": " rupee ",
113
+ // Separators
114
+ ".": " ",
115
+ "/": " ",
116
+ "\\": " ",
117
+ _: " ",
118
+ // Noise (strip)
119
+ "<": "",
120
+ ">": "",
121
+ "~": "",
122
+ "^": "",
123
+ "?": "",
124
+ "!": "",
125
+ ",": "",
126
+ ":": "",
127
+ ";": "",
128
+ "'": "",
129
+ '"': "",
130
+ "`": "",
131
+ "*": "",
132
+ "#": "",
133
+ "(": "",
134
+ ")": "",
135
+ "[": "",
136
+ "]": "",
137
+ "{": "",
138
+ "}": ""
139
+ };
140
+ var escapeRegex = (char) => char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ var BASE_SYMBOL_REGEX = new RegExp(`[${Object.keys(symbols).map(escapeRegex).join("")}]`, "g");
142
+ function applySymbols(text, custom) {
143
+ const map = custom ? { ...symbols, ...custom } : symbols;
144
+ const regex = map === symbols ? BASE_SYMBOL_REGEX : new RegExp(`[${Object.keys(map).map(escapeRegex).join("")}]`, "g");
145
+ return text.replace(regex, (char) => map[char] ?? "");
146
+ }
147
+
148
+ // src/slugen.ts
149
+ function escapeRegex2(char) {
150
+ return char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
+ }
152
+ function slugen(input, options = {}) {
153
+ if (!input) return "";
154
+ const separator = options.separator ?? "-";
155
+ const safeSeparator = escapeRegex2(separator);
156
+ let text = input;
157
+ if (options.symbols !== false) {
158
+ text = applySymbols(text, options.customReplacements);
159
+ }
160
+ text = applyLocale(text, options.locale);
161
+ text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
162
+ text = text.replace(/[^a-zA-Z0-9]+/g, separator);
163
+ text = text.replace(new RegExp(`^${safeSeparator}+|${safeSeparator}+$`, "g"), "");
164
+ if (options.lowercase !== false) {
165
+ text = text.toLowerCase();
166
+ }
167
+ return text;
168
+ }
169
+
170
+ exports.locales = locales;
171
+ exports.slugen = slugen;
172
+ //# sourceMappingURL=index.js.map
173
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/locales.ts","../src/symbols.ts","../src/slugen.ts"],"names":["escapeRegex"],"mappings":";;;AAKO,IAAM,OAAA,GAAqC;AAAA,EAChD,EAAA,EAAI;AAAA,IACF,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG;AAAA,GACL;AAAA,EAEA,EAAA,EAAI;AAAA,IACF,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,IAAA;AAAA,IACH,MAAA,EAAG;AAAA,GACL;AAAA,EAEA,EAAA,EAAI;AAAA,IACF,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG;AAAA,GACL;AAAA,EAEA,EAAA,EAAI;AAAA,IACF,MAAA,EAAG,GAAA;AAAA,IACH,CAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG;AAAA,GACL;AAAA,EAEA,EAAA,EAAI;AAAA,IACF,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG,GAAA;AAAA,IACH,MAAA,EAAG;AAAA,GACL;AAAA,EAEA,IAAI;AACN;AAKA,IAAM,WAAA,uBAAkB,GAAA,EAA+C;AAKvE,SAAS,aAAa,UAAA,EAAiC;AACrD,EAAA,MAAM,SAAoB,EAAC;AAE3B,EAAA,KAAA,MAAW,OAAO,UAAA,EAAY;AAC5B,IAAA,MAAM,MAAA,GAAS,QAAQ,GAAG,CAAA;AAC1B,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,MAAA,CAAO,MAAA,CAAO,QAAQ,MAAM,CAAA;AAAA,EAC9B;AAEA,EAAA,OAAO,MAAA;AACT;AASO,SAAS,WAAA,CAAY,MAAc,MAAA,EAAoC;AAC5E,EAAA,IAAI,CAAC,QAAQ,OAAO,IAAA;AAEpB,EAAA,MAAM,OAAO,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,GAAS,CAAC,MAAM,CAAA;AACrD,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,EAAK,CAAE,KAAK,GAAG,CAAA;AAErC,EAAA,IAAI,MAAA,GAAS,WAAA,CAAY,GAAA,CAAI,QAAQ,CAAA;AAErC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,GAAA,GAAM,aAAa,IAAI,CAAA;AAE7B,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,CAAE,WAAW,CAAA,EAAG;AACjC,MAAA,WAAA,CAAY,IAAI,QAAA,EAAU,EAAE,GAAA,EAAK,KAAA,EAAO,MAAM,CAAA;AAC9C,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,OAAA,GAAU,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAEhE,IAAA,MAAA,GAAS,EAAE,GAAA,EAAK,KAAA,EAAO,OAAA,EAAQ;AAC/B,IAAA,WAAA,CAAY,GAAA,CAAI,UAAU,MAAM,CAAA;AAAA,EAClC;AAEA,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,KAAA,EAAO,CAAC,SAAS,MAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,IAAK,IAAI,CAAA;AACvE;;;ACtHO,IAAM,OAAA,GAAkC;AAAA;AAAA,EAE7C,GAAA,EAAK,OAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,WAAA;AAAA,EACL,GAAA,EAAK,QAAA;AAAA,EACL,GAAA,EAAK,UAAA;AAAA;AAAA,EAGL,CAAA,EAAG,UAAA;AAAA,EACH,QAAA,EAAK,QAAA;AAAA,EACL,MAAA,EAAK,SAAA;AAAA,EACL,QAAA,EAAK,QAAA;AAAA,EACL,MAAA,EAAK,OAAA;AAAA,EACL,QAAA,EAAK,SAAA;AAAA;AAAA,EAGL,GAAA,EAAK,GAAA;AAAA,EACL,GAAA,EAAK,GAAA;AAAA,EACL,IAAA,EAAM,GAAA;AAAA,EACN,CAAA,EAAG,GAAA;AAAA;AAAA,EAGH,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK,EAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAGA,IAAM,cAAc,CAAC,IAAA,KAAiB,IAAA,CAAK,OAAA,CAAQ,uBAAuB,MAAM,CAAA;AAEhF,IAAM,iBAAA,GAAoB,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,OAAO,IAAA,CAAK,OAAO,CAAA,CAAE,GAAA,CAAI,WAAW,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,KAAK,GAAG,CAAA;AAExF,SAAS,YAAA,CAAa,MAAc,MAAA,EAAiC;AAC1E,EAAA,MAAM,MAAM,MAAA,GAAS,EAAE,GAAG,OAAA,EAAS,GAAG,QAAO,GAAI,OAAA;AACjD,EAAA,MAAM,QAAQ,GAAA,KAAQ,OAAA,GAAU,oBAAoB,IAAI,MAAA,CAAO,IAAI,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,CAAE,IAAI,WAAW,CAAA,CAAE,KAAK,EAAE,CAAC,KAAK,GAAG,CAAA;AAErH,EAAA,OAAO,IAAA,CAAK,QAAQ,KAAA,EAAO,CAAC,SAAS,GAAA,CAAI,IAAI,KAAK,EAAE,CAAA;AACtD;;;AC5DA,SAASA,aAAY,IAAA,EAAc;AACjC,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,qBAAA,EAAuB,MAAM,CAAA;AACnD;AAEO,SAAS,MAAA,CAAO,KAAA,EAAe,OAAA,GAAyB,EAAC,EAAW;AACzE,EAAA,IAAI,CAAC,OAAO,OAAO,EAAA;AAEnB,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,GAAA;AACvC,EAAA,MAAM,aAAA,GAAgBA,aAAY,SAAS,CAAA;AAC3C,EAAA,IAAI,IAAA,GAAO,KAAA;AAGX,EAAA,IAAI,OAAA,CAAQ,YAAY,KAAA,EAAO;AAC7B,IAAA,IAAA,GAAO,YAAA,CAAa,IAAA,EAAM,OAAA,CAAQ,kBAAkB,CAAA;AAAA,EACtD;AAGA,EAAA,IAAA,GAAO,WAAA,CAAY,IAAA,EAAM,OAAA,CAAQ,MAAM,CAAA;AAGvC,EAAA,IAAA,GAAO,KAAK,SAAA,CAAU,KAAK,CAAA,CAAE,OAAA,CAAQ,oBAAoB,EAAE,CAAA;AAG3D,EAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,gBAAA,EAAkB,SAAS,CAAA;AAG/C,EAAA,IAAA,GAAO,IAAA,CAAK,OAAA,CAAQ,IAAI,MAAA,CAAO,CAAA,CAAA,EAAI,aAAa,CAAA,EAAA,EAAK,aAAa,CAAA,EAAA,CAAA,EAAM,GAAG,CAAA,EAAG,EAAE,CAAA;AAGhF,EAAA,IAAI,OAAA,CAAQ,cAAc,KAAA,EAAO;AAC/B,IAAA,IAAA,GAAO,KAAK,WAAA,EAAY;AAAA,EAC1B;AAEA,EAAA,OAAO,IAAA;AACT","file":"index.js","sourcesContent":["type LocaleMap = Record<string, string>;\r\n\r\n/**\r\n * Locale definitions\r\n */\r\nexport const locales: Record<string, LocaleMap> = {\r\n de: {\r\n Ä: \"a\",\r\n ä: \"a\",\r\n Ö: \"o\",\r\n ö: \"o\",\r\n Ü: \"u\",\r\n ü: \"u\",\r\n ß: \"ss\",\r\n },\r\n\r\n fr: {\r\n À: \"a\",\r\n Á: \"a\",\r\n Â: \"a\",\r\n Ã: \"a\",\r\n Ä: \"a\",\r\n Å: \"a\",\r\n à: \"a\",\r\n á: \"a\",\r\n â: \"a\",\r\n ã: \"a\",\r\n ä: \"a\",\r\n å: \"a\",\r\n Ç: \"c\",\r\n ç: \"c\",\r\n È: \"e\",\r\n É: \"e\",\r\n Ê: \"e\",\r\n Ë: \"e\",\r\n è: \"e\",\r\n é: \"e\",\r\n ê: \"e\",\r\n ë: \"e\",\r\n Œ: \"oe\",\r\n œ: \"oe\",\r\n },\r\n\r\n es: {\r\n Ñ: \"n\",\r\n ñ: \"n\",\r\n },\r\n\r\n tr: {\r\n İ: \"i\",\r\n I: \"i\",\r\n ı: \"i\",\r\n Ş: \"s\",\r\n ş: \"s\",\r\n Ğ: \"g\",\r\n ğ: \"g\",\r\n Ö: \"o\",\r\n ö: \"o\",\r\n Ü: \"u\",\r\n ü: \"u\",\r\n Ç: \"c\",\r\n ç: \"c\",\r\n },\r\n\r\n pl: {\r\n Ł: \"l\",\r\n ł: \"l\",\r\n Ś: \"s\",\r\n ś: \"s\",\r\n Ż: \"z\",\r\n ż: \"z\",\r\n },\r\n\r\n en: {},\r\n};\r\n\r\n/**\r\n * Cache: localeKey -> { map, regex }\r\n */\r\nconst localeCache = new Map<string, { map: LocaleMap; regex: RegExp }>();\r\n\r\n/**\r\n * Merge multiple locales into a single map\r\n */\r\nfunction mergeLocales(localeKeys: string[]): LocaleMap {\r\n const merged: LocaleMap = {};\r\n\r\n for (const key of localeKeys) {\r\n const locale = locales[key];\r\n if (!locale) continue;\r\n Object.assign(merged, locale);\r\n }\r\n\r\n return merged;\r\n}\r\n\r\n/**\r\n * Apply locale replacements\r\n *\r\n * Supports:\r\n * - 'fr'\r\n * - ['fr', 'de']\r\n */\r\nexport function applyLocale(text: string, locale?: string | string[]): string {\r\n if (!locale) return text;\r\n\r\n const keys = Array.isArray(locale) ? locale : [locale];\r\n const cacheKey = keys.sort().join(\"+\");\r\n\r\n let cached = localeCache.get(cacheKey);\r\n\r\n if (!cached) {\r\n const map = mergeLocales(keys);\r\n\r\n if (Object.keys(map).length === 0) {\r\n localeCache.set(cacheKey, { map, regex: /$^/ });\r\n return text;\r\n }\r\n\r\n const pattern = new RegExp(`[${Object.keys(map).join(\"\")}]`, \"g\");\r\n\r\n cached = { map, regex: pattern };\r\n localeCache.set(cacheKey, cached);\r\n }\r\n\r\n return text.replace(cached.regex, (char) => cached!.map[char] ?? char);\r\n}\r\n","/**\r\n * Common symbols and their slug-friendly replacements\r\n * Policy:\r\n * - semantic symbols => \" word \"\r\n * - separators => \" \"\r\n * - noise => \"\"\r\n */\r\n\r\nexport const symbols: Record<string, string> = {\r\n // Logical / textual (semantic)\r\n \"&\": \" and \",\r\n \"|\": \" or \",\r\n \"@\": \" at \",\r\n \"%\": \" percent \",\r\n \"+\": \" plus \",\r\n \"=\": \" equals \",\r\n\r\n // Currency (semantic)\r\n $: \" dollar \",\r\n \"€\": \" euro \",\r\n \"£\": \" pound \",\r\n \"₺\": \" lira \",\r\n \"¥\": \" yen \",\r\n \"₹\": \" rupee \",\r\n\r\n // Separators\r\n \".\": \" \",\r\n \"/\": \" \",\r\n \"\\\\\": \" \",\r\n _: \" \",\r\n\r\n // Noise (strip)\r\n \"<\": \"\",\r\n \">\": \"\",\r\n \"~\": \"\",\r\n \"^\": \"\",\r\n \"?\": \"\",\r\n \"!\": \"\",\r\n \",\": \"\",\r\n \":\": \"\",\r\n \";\": \"\",\r\n \"'\": \"\",\r\n '\"': \"\",\r\n \"`\": \"\",\r\n \"*\": \"\",\r\n \"#\": \"\",\r\n \"(\": \"\",\r\n \")\": \"\",\r\n \"[\": \"\",\r\n \"]\": \"\",\r\n \"{\": \"\",\r\n \"}\": \"\",\r\n};\r\n\r\n/** Escape regex special chars safely */\r\nconst escapeRegex = (char: string) => char.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n\r\nconst BASE_SYMBOL_REGEX = new RegExp(`[${Object.keys(symbols).map(escapeRegex).join(\"\")}]`, \"g\");\r\n\r\nexport function applySymbols(text: string, custom?: Record<string, string>) {\r\n const map = custom ? { ...symbols, ...custom } : symbols;\r\n const regex = map === symbols ? BASE_SYMBOL_REGEX : new RegExp(`[${Object.keys(map).map(escapeRegex).join(\"\")}]`, \"g\");\r\n\r\n return text.replace(regex, (char) => map[char] ?? \"\");\r\n}\r\n","import { SlugenOptions } from \"./types\";\r\nimport { applyLocale } from \"./locales\";\r\nimport { applySymbols } from \"./symbols\";\r\n\r\nfunction escapeRegex(char: string) {\r\n return char.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n}\r\n\r\nexport function slugen(input: string, options: SlugenOptions = {}): string {\r\n if (!input) return \"\";\r\n\r\n const separator = options.separator ?? \"-\";\r\n const safeSeparator = escapeRegex(separator);\r\n let text = input;\r\n\r\n // Symbols (default true)\r\n if (options.symbols !== false) {\r\n text = applySymbols(text, options.customReplacements);\r\n }\r\n\r\n // Locale\r\n text = applyLocale(text, options.locale);\r\n\r\n // Normalize & remove diacritics\r\n text = text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\r\n\r\n // Replace non-alphanumeric with separator\r\n text = text.replace(/[^a-zA-Z0-9]+/g, separator);\r\n\r\n // Trim separator (SAFE)\r\n text = text.replace(new RegExp(`^${safeSeparator}+|${safeSeparator}+$`, \"g\"), \"\");\r\n\r\n // Lowercase (default true)\r\n if (options.lowercase !== false) {\r\n text = text.toLowerCase();\r\n }\r\n\r\n return text;\r\n}\r\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,170 @@
1
+ // src/locales.ts
2
+ var locales = {
3
+ de: {
4
+ \u00C4: "a",
5
+ \u00E4: "a",
6
+ \u00D6: "o",
7
+ \u00F6: "o",
8
+ \u00DC: "u",
9
+ \u00FC: "u",
10
+ \u00DF: "ss"
11
+ },
12
+ fr: {
13
+ \u00C0: "a",
14
+ \u00C1: "a",
15
+ \u00C2: "a",
16
+ \u00C3: "a",
17
+ \u00C4: "a",
18
+ \u00C5: "a",
19
+ \u00E0: "a",
20
+ \u00E1: "a",
21
+ \u00E2: "a",
22
+ \u00E3: "a",
23
+ \u00E4: "a",
24
+ \u00E5: "a",
25
+ \u00C7: "c",
26
+ \u00E7: "c",
27
+ \u00C8: "e",
28
+ \u00C9: "e",
29
+ \u00CA: "e",
30
+ \u00CB: "e",
31
+ \u00E8: "e",
32
+ \u00E9: "e",
33
+ \u00EA: "e",
34
+ \u00EB: "e",
35
+ \u0152: "oe",
36
+ \u0153: "oe"
37
+ },
38
+ es: {
39
+ \u00D1: "n",
40
+ \u00F1: "n"
41
+ },
42
+ tr: {
43
+ \u0130: "i",
44
+ I: "i",
45
+ \u0131: "i",
46
+ \u015E: "s",
47
+ \u015F: "s",
48
+ \u011E: "g",
49
+ \u011F: "g",
50
+ \u00D6: "o",
51
+ \u00F6: "o",
52
+ \u00DC: "u",
53
+ \u00FC: "u",
54
+ \u00C7: "c",
55
+ \u00E7: "c"
56
+ },
57
+ pl: {
58
+ \u0141: "l",
59
+ \u0142: "l",
60
+ \u015A: "s",
61
+ \u015B: "s",
62
+ \u017B: "z",
63
+ \u017C: "z"
64
+ },
65
+ en: {}
66
+ };
67
+ var localeCache = /* @__PURE__ */ new Map();
68
+ function mergeLocales(localeKeys) {
69
+ const merged = {};
70
+ for (const key of localeKeys) {
71
+ const locale = locales[key];
72
+ if (!locale) continue;
73
+ Object.assign(merged, locale);
74
+ }
75
+ return merged;
76
+ }
77
+ function applyLocale(text, locale) {
78
+ if (!locale) return text;
79
+ const keys = Array.isArray(locale) ? locale : [locale];
80
+ const cacheKey = keys.sort().join("+");
81
+ let cached = localeCache.get(cacheKey);
82
+ if (!cached) {
83
+ const map = mergeLocales(keys);
84
+ if (Object.keys(map).length === 0) {
85
+ localeCache.set(cacheKey, { map, regex: /$^/ });
86
+ return text;
87
+ }
88
+ const pattern = new RegExp(`[${Object.keys(map).join("")}]`, "g");
89
+ cached = { map, regex: pattern };
90
+ localeCache.set(cacheKey, cached);
91
+ }
92
+ return text.replace(cached.regex, (char) => cached.map[char] ?? char);
93
+ }
94
+
95
+ // src/symbols.ts
96
+ var symbols = {
97
+ // Logical / textual (semantic)
98
+ "&": " and ",
99
+ "|": " or ",
100
+ "@": " at ",
101
+ "%": " percent ",
102
+ "+": " plus ",
103
+ "=": " equals ",
104
+ // Currency (semantic)
105
+ $: " dollar ",
106
+ "\u20AC": " euro ",
107
+ "\xA3": " pound ",
108
+ "\u20BA": " lira ",
109
+ "\xA5": " yen ",
110
+ "\u20B9": " rupee ",
111
+ // Separators
112
+ ".": " ",
113
+ "/": " ",
114
+ "\\": " ",
115
+ _: " ",
116
+ // Noise (strip)
117
+ "<": "",
118
+ ">": "",
119
+ "~": "",
120
+ "^": "",
121
+ "?": "",
122
+ "!": "",
123
+ ",": "",
124
+ ":": "",
125
+ ";": "",
126
+ "'": "",
127
+ '"': "",
128
+ "`": "",
129
+ "*": "",
130
+ "#": "",
131
+ "(": "",
132
+ ")": "",
133
+ "[": "",
134
+ "]": "",
135
+ "{": "",
136
+ "}": ""
137
+ };
138
+ var escapeRegex = (char) => char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
139
+ var BASE_SYMBOL_REGEX = new RegExp(`[${Object.keys(symbols).map(escapeRegex).join("")}]`, "g");
140
+ function applySymbols(text, custom) {
141
+ const map = custom ? { ...symbols, ...custom } : symbols;
142
+ const regex = map === symbols ? BASE_SYMBOL_REGEX : new RegExp(`[${Object.keys(map).map(escapeRegex).join("")}]`, "g");
143
+ return text.replace(regex, (char) => map[char] ?? "");
144
+ }
145
+
146
+ // src/slugen.ts
147
+ function escapeRegex2(char) {
148
+ return char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
149
+ }
150
+ function slugen(input, options = {}) {
151
+ if (!input) return "";
152
+ const separator = options.separator ?? "-";
153
+ const safeSeparator = escapeRegex2(separator);
154
+ let text = input;
155
+ if (options.symbols !== false) {
156
+ text = applySymbols(text, options.customReplacements);
157
+ }
158
+ text = applyLocale(text, options.locale);
159
+ text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
160
+ text = text.replace(/[^a-zA-Z0-9]+/g, separator);
161
+ text = text.replace(new RegExp(`^${safeSeparator}+|${safeSeparator}+$`, "g"), "");
162
+ if (options.lowercase !== false) {
163
+ text = text.toLowerCase();
164
+ }
165
+ return text;
166
+ }
167
+
168
+ export { locales, slugen };
169
+ //# sourceMappingURL=index.mjs.map
170
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/locales.ts","../src/symbols.ts","../src/slugen.ts"],"names":["escapeRegex"],"mappings":"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","file":"index.mjs","sourcesContent":["type LocaleMap = Record<string, string>;\r\n\r\n/**\r\n * Locale definitions\r\n */\r\nexport const locales: Record<string, LocaleMap> = {\r\n de: {\r\n Ä: \"a\",\r\n ä: \"a\",\r\n Ö: \"o\",\r\n ö: \"o\",\r\n Ü: \"u\",\r\n ü: \"u\",\r\n ß: \"ss\",\r\n },\r\n\r\n fr: {\r\n À: \"a\",\r\n Á: \"a\",\r\n Â: \"a\",\r\n Ã: \"a\",\r\n Ä: \"a\",\r\n Å: \"a\",\r\n à: \"a\",\r\n á: \"a\",\r\n â: \"a\",\r\n ã: \"a\",\r\n ä: \"a\",\r\n å: \"a\",\r\n Ç: \"c\",\r\n ç: \"c\",\r\n È: \"e\",\r\n É: \"e\",\r\n Ê: \"e\",\r\n Ë: \"e\",\r\n è: \"e\",\r\n é: \"e\",\r\n ê: \"e\",\r\n ë: \"e\",\r\n Œ: \"oe\",\r\n œ: \"oe\",\r\n },\r\n\r\n es: {\r\n Ñ: \"n\",\r\n ñ: \"n\",\r\n },\r\n\r\n tr: {\r\n İ: \"i\",\r\n I: \"i\",\r\n ı: \"i\",\r\n Ş: \"s\",\r\n ş: \"s\",\r\n Ğ: \"g\",\r\n ğ: \"g\",\r\n Ö: \"o\",\r\n ö: \"o\",\r\n Ü: \"u\",\r\n ü: \"u\",\r\n Ç: \"c\",\r\n ç: \"c\",\r\n },\r\n\r\n pl: {\r\n Ł: \"l\",\r\n ł: \"l\",\r\n Ś: \"s\",\r\n ś: \"s\",\r\n Ż: \"z\",\r\n ż: \"z\",\r\n },\r\n\r\n en: {},\r\n};\r\n\r\n/**\r\n * Cache: localeKey -> { map, regex }\r\n */\r\nconst localeCache = new Map<string, { map: LocaleMap; regex: RegExp }>();\r\n\r\n/**\r\n * Merge multiple locales into a single map\r\n */\r\nfunction mergeLocales(localeKeys: string[]): LocaleMap {\r\n const merged: LocaleMap = {};\r\n\r\n for (const key of localeKeys) {\r\n const locale = locales[key];\r\n if (!locale) continue;\r\n Object.assign(merged, locale);\r\n }\r\n\r\n return merged;\r\n}\r\n\r\n/**\r\n * Apply locale replacements\r\n *\r\n * Supports:\r\n * - 'fr'\r\n * - ['fr', 'de']\r\n */\r\nexport function applyLocale(text: string, locale?: string | string[]): string {\r\n if (!locale) return text;\r\n\r\n const keys = Array.isArray(locale) ? locale : [locale];\r\n const cacheKey = keys.sort().join(\"+\");\r\n\r\n let cached = localeCache.get(cacheKey);\r\n\r\n if (!cached) {\r\n const map = mergeLocales(keys);\r\n\r\n if (Object.keys(map).length === 0) {\r\n localeCache.set(cacheKey, { map, regex: /$^/ });\r\n return text;\r\n }\r\n\r\n const pattern = new RegExp(`[${Object.keys(map).join(\"\")}]`, \"g\");\r\n\r\n cached = { map, regex: pattern };\r\n localeCache.set(cacheKey, cached);\r\n }\r\n\r\n return text.replace(cached.regex, (char) => cached!.map[char] ?? char);\r\n}\r\n","/**\r\n * Common symbols and their slug-friendly replacements\r\n * Policy:\r\n * - semantic symbols => \" word \"\r\n * - separators => \" \"\r\n * - noise => \"\"\r\n */\r\n\r\nexport const symbols: Record<string, string> = {\r\n // Logical / textual (semantic)\r\n \"&\": \" and \",\r\n \"|\": \" or \",\r\n \"@\": \" at \",\r\n \"%\": \" percent \",\r\n \"+\": \" plus \",\r\n \"=\": \" equals \",\r\n\r\n // Currency (semantic)\r\n $: \" dollar \",\r\n \"€\": \" euro \",\r\n \"£\": \" pound \",\r\n \"₺\": \" lira \",\r\n \"¥\": \" yen \",\r\n \"₹\": \" rupee \",\r\n\r\n // Separators\r\n \".\": \" \",\r\n \"/\": \" \",\r\n \"\\\\\": \" \",\r\n _: \" \",\r\n\r\n // Noise (strip)\r\n \"<\": \"\",\r\n \">\": \"\",\r\n \"~\": \"\",\r\n \"^\": \"\",\r\n \"?\": \"\",\r\n \"!\": \"\",\r\n \",\": \"\",\r\n \":\": \"\",\r\n \";\": \"\",\r\n \"'\": \"\",\r\n '\"': \"\",\r\n \"`\": \"\",\r\n \"*\": \"\",\r\n \"#\": \"\",\r\n \"(\": \"\",\r\n \")\": \"\",\r\n \"[\": \"\",\r\n \"]\": \"\",\r\n \"{\": \"\",\r\n \"}\": \"\",\r\n};\r\n\r\n/** Escape regex special chars safely */\r\nconst escapeRegex = (char: string) => char.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n\r\nconst BASE_SYMBOL_REGEX = new RegExp(`[${Object.keys(symbols).map(escapeRegex).join(\"\")}]`, \"g\");\r\n\r\nexport function applySymbols(text: string, custom?: Record<string, string>) {\r\n const map = custom ? { ...symbols, ...custom } : symbols;\r\n const regex = map === symbols ? BASE_SYMBOL_REGEX : new RegExp(`[${Object.keys(map).map(escapeRegex).join(\"\")}]`, \"g\");\r\n\r\n return text.replace(regex, (char) => map[char] ?? \"\");\r\n}\r\n","import { SlugenOptions } from \"./types\";\r\nimport { applyLocale } from \"./locales\";\r\nimport { applySymbols } from \"./symbols\";\r\n\r\nfunction escapeRegex(char: string) {\r\n return char.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n}\r\n\r\nexport function slugen(input: string, options: SlugenOptions = {}): string {\r\n if (!input) return \"\";\r\n\r\n const separator = options.separator ?? \"-\";\r\n const safeSeparator = escapeRegex(separator);\r\n let text = input;\r\n\r\n // Symbols (default true)\r\n if (options.symbols !== false) {\r\n text = applySymbols(text, options.customReplacements);\r\n }\r\n\r\n // Locale\r\n text = applyLocale(text, options.locale);\r\n\r\n // Normalize & remove diacritics\r\n text = text.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\r\n\r\n // Replace non-alphanumeric with separator\r\n text = text.replace(/[^a-zA-Z0-9]+/g, separator);\r\n\r\n // Trim separator (SAFE)\r\n text = text.replace(new RegExp(`^${safeSeparator}+|${safeSeparator}+$`, \"g\"), \"\");\r\n\r\n // Lowercase (default true)\r\n if (options.lowercase !== false) {\r\n text = text.toLowerCase();\r\n }\r\n\r\n return text;\r\n}\r\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "slugen",
3
+ "version": "0.1.1",
4
+ "description": "Modern and lightweight slug generator for URLs, permalinks, and identifiers with multi-language support",
5
+ "keywords": [
6
+ "slug",
7
+ "slug-generator",
8
+ "url-slug",
9
+ "permalink",
10
+ "seo",
11
+ "url",
12
+ "identifier",
13
+ "string-normalization",
14
+ "multilingual",
15
+ "i18n",
16
+ "typescript"
17
+ ],
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "sideEffects": false,
32
+ "homepage": "https://github.com/kabledev/slugen",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/kabledev/slugen.git"
36
+ },
37
+ "author": "Kable",
38
+ "license": "MIT",
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest tests/slugen.spec.ts --run",
44
+ "bench": "vitest tests/bench.spec.ts --run"
45
+ },
46
+ "devDependencies": {
47
+ "tsup": "^8.5.1",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.16"
50
+ }
51
+ }