utilitish 0.0.9 → 0.0.11

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 CHANGED
@@ -16,16 +16,19 @@ Vous pouvez définir une configuration globale qui s'appliquera à tous les appe
16
16
  import { setSlugifyConfig } from 'utilitish';
17
17
 
18
18
  // Configuration pour remplacer les symboles de genre
19
- setSlugifyConfig({
20
- customReplacements: {
21
- '': 'feminin',
22
- '♂': 'masculin',
23
- },
24
- separator: '_',
25
- });
19
+ setSlugifyConfig(
20
+ SlugifyConfig.builder()
21
+ .withSeparator('_')
22
+ .withCustomReplacements({
23
+ '♀': 'feminin',
24
+ '♂': 'masculin',
25
+ '@': 'at',
26
+ })
27
+ .build(),
28
+ );
26
29
 
27
30
  'Test ♀'.slugify(); // "test_feminin"
28
- 'User♂@domain.com'.slugify(); // "user_masculin_at_domain_com"
31
+ 'User♂@domain.com'.slugify(); // "usermasculinatdomain_com"
29
32
  ```
30
33
 
31
34
  ### Configuration par appel
@@ -37,7 +40,7 @@ Vous pouvez aussi passer une configuration spécifique à chaque appel :
37
40
  'Hello World'.slugify(); // "hello-world"
38
41
 
39
42
  // Override la config pour cet appel
40
- 'Hello World'.slugify({ separator: '_' }); // "hello_world"
43
+ 'Hello World'.slugify(SlugifyConfig.builder().withSeparator('_').build()); // "hello_world"
41
44
  ```
42
45
 
43
46
  ### Options de configuration
@@ -24,13 +24,11 @@ describe('Object.prototype', () => {
24
24
  const source = { b: { d: 3 }, e: 4 };
25
25
  const merged = obj.deepMerge(source);
26
26
  expect(merged).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 });
27
- console.log(merged);
28
27
  });
29
28
  it('should overwrite primitive values', () => {
30
29
  const obj = { a: 1, b: 2 };
31
30
  const merged = obj.deepMerge({ b: 3 });
32
31
  expect(merged).toEqual({ a: 1, b: 3 });
33
- console.log(merged);
34
32
  });
35
33
  describe('error handling', () => {
36
34
  it('should throw if source is not an object', () => {
@@ -152,31 +152,31 @@ declare global {
152
152
  *
153
153
  * @example
154
154
  * // Custom replacements
155
- * 'Test ♀'.slugify({ customReplacements: { "♀": "feminin" } }); // 'test-feminin'
156
- * 'User♂@domain.com'.slugify({ customReplacements: { "♂": "masculin", "@": "at" } }); // 'usermasculinatdomain-com'
155
+ * 'Test ♀'.slugify(SlugifyConfig.builder().withCustomReplacements({ "♀": "feminin" }).build()); // 'test-feminin'
156
+ * 'User♂@domain.com'.slugify(SlugifyConfig.builder().withCustomReplacements({ "♂": "masculin", "@": "at" }).build()); // 'usermasculinatdomain-com'
157
157
  *
158
158
  * @example
159
159
  * // Custom separator
160
- * 'Hello World'.slugify({ separator: "_" }); // 'hello_world'
161
- * 'Hello World'.slugify({ separator: "--" }); // 'hello--world'
160
+ * 'Hello World'.slugify(SlugifyConfig.builder().withSeparator("_").build()); // 'hello_world'
161
+ * 'Hello World'.slugify(SlugifyConfig.builder().withSeparator("--").build()); // 'hello--world'
162
162
  *
163
163
  * @example
164
164
  * // Preserve accents
165
- * 'Éléphant'.slugify({ removeAccents: false }); // 'éléphant'
165
+ * 'Éléphant'.slugify(SlugifyConfig.builder().withRemoveAccents(false).build()); // 'éléphant'
166
166
  *
167
167
  * @example
168
168
  * // Disable lowercasing
169
- * 'Hello World'.slugify({ lowercase: false }); // 'Hello-World'
169
+ * 'Hello World'.slugify(SlugifyConfig.builder().withCase('default').build()); // 'Hello-World'
170
170
  *
171
171
  * @example
172
172
  * // Max length
173
- * 'Very long string'.slugify({ maxLength: 8 }); // 'very-lon'
173
+ * 'Very long string'.slugify(SlugifyConfig.builder().withMaxLength(8).build()); // 'very-lon'
174
174
  *
175
175
  * @example
176
176
  * // Custom transformers
177
- * 'hello world'.slugify({
178
- * transformers: [(str) => str.replace(/world/g, 'universe')]
179
- * }); // 'hello-universe'
177
+ * 'hello world'.slugify(SlugifyConfig.builder()
178
+ * .withTransformers([(str) => str.replace(/world/g, 'universe')])
179
+ * .build()); // 'hello-universe'
180
180
  *
181
181
  * @remarks
182
182
  * - Uses global configuration by default (see setSlugifyConfig)
@@ -187,12 +187,33 @@ declare global {
187
187
  * - Removes leading/trailing separators
188
188
  * - Collapses multiple consecutive separators into one
189
189
  *
190
+ * @see SlugifyConfig
190
191
  * @see setSlugifyConfig
191
192
  * @see getSlugifyConfig
192
193
  * @see resetSlugifyConfig
193
194
  */
194
195
  slugify(): string;
195
196
  slugify(config: SlugifyConfig): string;
197
+ /**
198
+ * Compares two strings by slugifying both and checking if they are equal.
199
+ * Useful for case-insensitive and accent-insensitive string comparison.
200
+ *
201
+ * @this {string} The first string to compare
202
+ * @param {string} other - The second string to compare
203
+ * @returns {boolean} True if both slugified strings are equal, false otherwise
204
+ * @throws {TypeError} If the parameter is not a string
205
+ *
206
+ * @example
207
+ * 'Hello World'.compareSlugify('hello-world'); // true
208
+ * 'Héllo Wørld'.compareSlugify('hello-world'); // true
209
+ * 'Hello World'.compareSlugify('goodbye-world'); // false
210
+ *
211
+ * @remarks
212
+ * - Both strings are slugified using the default or global configuration
213
+ * - Useful for comparing user-provided strings with stored slugs
214
+ * - Guard ensures the parameter is a valid string type
215
+ */
216
+ compareSlugify(other: string): boolean;
196
217
  /**
197
218
  * Replaces a substring between `start` and `end` indices with a given string.
198
219
  * Provides precise control over which portion of the string to replace.
@@ -73,6 +73,15 @@ const slugify_utils_1 = require("../utils/slugify.utils");
73
73
  return '';
74
74
  return this.charAt(0).toUpperCase() + this.slice(1);
75
75
  });
76
+ /**
77
+ * @see String.prototype.compareSlugify
78
+ */
79
+ (0, core_utils_1.defineIfNotExists)(String.prototype, 'compareSlugify', function (other) {
80
+ if (typeof other !== 'string') {
81
+ throw new TypeError('Parameter must be a string');
82
+ }
83
+ return this.slugify() === other.slugify();
84
+ });
76
85
  /**
77
86
  * @see String.prototype.replaceRange
78
87
  */
@@ -78,71 +78,125 @@ describe('String.prototype', () => {
78
78
  });
79
79
  describe('custom replacements', () => {
80
80
  it('should apply custom replacements per call', () => {
81
- expect('Test ♀'.slugify({ customReplacements: { '♀': 'feminin' } })).toBe('test-feminin');
82
- expect('User♂@domain.com'.slugify({ customReplacements: { '♂': 'masculin', '@': 'at' } })).toBe('usermasculinatdomain-com');
81
+ expect('Test ♀'.slugify(slugify_config_1.SlugifyConfig.builder().withCustomReplacements({ '♀': 'feminin' }).build())).toBe('test-feminin');
82
+ expect('User♂@domain.com'.slugify(slugify_config_1.SlugifyConfig.builder().withCustomReplacements({ '♂': 'masculin', '@': 'at' }).build())).toBe('usermasculinatdomain-com');
83
83
  });
84
84
  it('should handle special regex characters in replacements', () => {
85
- expect('Test [bracket]'.slugify({ customReplacements: { '[': 'left', ']': 'right' } })).toBe('test-leftbracketright');
85
+ expect('Test [bracket]'.slugify(slugify_config_1.SlugifyConfig.builder().withCustomReplacements({ '[': 'left', ']': 'right' }).build())).toBe('test-leftbracketright');
86
86
  });
87
87
  });
88
88
  describe('separator customization', () => {
89
89
  it('should use custom separator globally', () => {
90
- (0, slugify_config_1.setSlugifyConfig)({ separator: '_' });
90
+ (0, slugify_config_1.setSlugifyConfig)(slugify_config_1.SlugifyConfig.builder().withSeparator('_').build());
91
91
  expect('Hello World'.slugify()).toBe('hello_world');
92
92
  expect('Test String'.slugify()).toBe('test_string');
93
93
  });
94
94
  it('should use custom separator per call', () => {
95
- expect('Hello World'.slugify({ separator: '_' })).toBe('hello_world');
96
- expect('Test String'.slugify({ separator: '__' })).toBe('test__string');
95
+ expect('Hello World'.slugify(slugify_config_1.SlugifyConfig.builder().withSeparator('_').build())).toBe('hello_world');
96
+ expect('Test String'.slugify(slugify_config_1.SlugifyConfig.builder().withSeparator('__').build())).toBe('test__string');
97
97
  });
98
98
  });
99
99
  describe('case transformation', () => {
100
100
  it('should respect lowercase setting globally', () => {
101
- (0, slugify_config_1.setSlugifyConfig)({ lowercase: false });
101
+ (0, slugify_config_1.setSlugifyConfig)(slugify_config_1.SlugifyConfig.builder().withCase('default').build());
102
102
  expect('Hello World'.slugify()).toBe('Hello-World');
103
103
  });
104
104
  it('should respect lowercase setting per call', () => {
105
- expect('Hello World'.slugify({ lowercase: false })).toBe('Hello-World');
105
+ expect('Hello World'.slugify(slugify_config_1.SlugifyConfig.builder().withCase('default').build())).toBe('Hello-World');
106
106
  });
107
107
  });
108
108
  describe('accent removal', () => {
109
109
  it('should respect removeAccents setting per call', () => {
110
- expect('Éléphant'.slugify({ removeAccents: false })).toBe('éléphant');
110
+ expect('Éléphant'.slugify(slugify_config_1.SlugifyConfig.builder().withRemoveAccents(false).build())).toBe('éléphant');
111
111
  });
112
112
  });
113
113
  describe('allowed characters', () => {
114
114
  it('should use custom allowed characters', () => {
115
- expect('Hello123!@#'.slugify({ allowedChars: /[a-zA-Z0-9_]/ })).toBe('hello123');
115
+ expect('Hello123!@#'.slugify(slugify_config_1.SlugifyConfig.builder()
116
+ .withAllowedChars(/[a-zA-Z0-9_]/)
117
+ .build())).toBe('hello123');
118
+ });
119
+ it('should throw for invalid allowedChars regex in call', () => {
120
+ expect(() => slugify_config_1.SlugifyConfig.builder()
121
+ .withAllowedChars(/foo|bar/)
122
+ .build()).toThrowError(/Invalid allowedChars:/);
123
+ });
124
+ it('should throw for invalid allowedChars regex in global config', () => {
125
+ expect(() => (0, slugify_config_1.setSlugifyConfig)(slugify_config_1.SlugifyConfig.builder()
126
+ .withAllowedChars(/foo|bar/)
127
+ .build())).toThrowError(/Invalid allowedChars:/);
128
+ });
129
+ });
130
+ describe('separator special characters', () => {
131
+ it('should handle regex-special separators without breaking', () => {
132
+ expect('a b+c'.slugify(slugify_config_1.SlugifyConfig.builder().withSeparator('.').build())).toBe('a.b.c');
133
+ expect('a.b.c'.slugify(slugify_config_1.SlugifyConfig.builder().withSeparator('*').build())).toBe('a*b*c');
116
134
  });
117
135
  });
118
136
  describe('max length', () => {
119
137
  it('should truncate to max length', () => {
120
- expect('very-long-string-here'.slugify({ maxLength: 10 })).toBe('very-long');
121
- expect('short'.slugify({ maxLength: 10 })).toBe('short');
138
+ expect('very-long-string-here'.slugify(slugify_config_1.SlugifyConfig.builder().withMaxLength(10).build())).toBe('very-long');
139
+ expect('short'.slugify(slugify_config_1.SlugifyConfig.builder().withMaxLength(10).build())).toBe('short');
122
140
  });
123
141
  it('should remove trailing separator after truncation', () => {
124
- expect('hello-world-test'.slugify({ maxLength: 8 })).toBe('hello-wo');
142
+ expect('hello-world-test'.slugify(slugify_config_1.SlugifyConfig.builder().withMaxLength(8).build())).toBe('hello-wo');
125
143
  });
126
144
  });
127
145
  describe('custom transformers', () => {
128
146
  it('should apply custom transformers', () => {
129
- const config = {
130
- transformers: [
131
- (str) => str.replace(/test/g, 'example'),
132
- (str) => str.toUpperCase(),
133
- ],
134
- lowercase: false,
135
- };
147
+ const config = slugify_config_1.SlugifyConfig.builder()
148
+ .withTransformers([
149
+ (str) => str.replace(/test/g, 'example'),
150
+ (str) => str.toUpperCase(),
151
+ ])
152
+ .withCase('upper')
153
+ .build();
136
154
  expect('this is a test'.slugify(config)).toBe('THIS-IS-A-EXAMPLE');
137
155
  });
138
156
  });
139
157
  describe('configuration merging', () => {
140
158
  it('should merge global and local config correctly', () => {
141
159
  // Local config should override global defaults
142
- expect('Test ♀'.slugify({ separator: '_', customReplacements: { '♀': 'female' } })).toBe('test_female');
160
+ expect('Test ♀'.slugify(slugify_config_1.SlugifyConfig.builder().withSeparator('_').withCustomReplacements({ '♀': 'female' }).build())).toBe('test_female');
143
161
  });
144
162
  });
145
163
  });
164
+ describe('compareSlugify()', () => {
165
+ beforeEach(() => {
166
+ (0, slugify_config_1.resetSlugifyConfig)(); // Reset to defaults before each test
167
+ });
168
+ it('should return true when slugified strings are equal', () => {
169
+ expect('Hello World'.compareSlugify('hello-world')).toBe(true);
170
+ expect('hello world'.compareSlugify('hello-world')).toBe(true);
171
+ expect('HELLO WORLD'.compareSlugify('hello-world')).toBe(true);
172
+ });
173
+ it('should handle accents and special characters', () => {
174
+ expect('Café'.compareSlugify('cafe')).toBe(true);
175
+ expect('Éléphant'.compareSlugify('elephant')).toBe(true);
176
+ expect('naïve café'.compareSlugify('naive-cafe')).toBe(true);
177
+ expect('résumé'.compareSlugify('resume')).toBe(true);
178
+ });
179
+ it('should return false when slugified strings are different', () => {
180
+ expect('Hello World'.compareSlugify('goodbye-world')).toBe(false);
181
+ expect('test string'.compareSlugify('different-string')).toBe(false);
182
+ });
183
+ it('should handle extra spaces and special characters', () => {
184
+ expect(' Hello --- World '.compareSlugify('hello-world')).toBe(true);
185
+ expect('Hello!!!World???'.compareSlugify('hello-world')).toBe(true);
186
+ });
187
+ it('should throw TypeError if parameter is not a string', () => {
188
+ expect(() => 'hello'.compareSlugify(123)).toThrowError(TypeError);
189
+ expect(() => 'hello'.compareSlugify(null)).toThrowError(TypeError);
190
+ expect(() => 'hello'.compareSlugify(undefined)).toThrowError(TypeError);
191
+ expect(() => 'hello'.compareSlugify({})).toThrowError(TypeError);
192
+ expect(() => 'hello'.compareSlugify([])).toThrowError(TypeError);
193
+ });
194
+ it('should work with empty strings', () => {
195
+ expect(''.compareSlugify('')).toBe(true);
196
+ expect(' '.compareSlugify('')).toBe(true);
197
+ expect('hello'.compareSlugify('')).toBe(false);
198
+ });
199
+ });
146
200
  describe('replaceRange()', () => {
147
201
  it('should replace a single character at the given index', () => {
148
202
  expect('hello'.replaceRange(1, 2, 'a')).toBe('hallo');
@@ -1,73 +1,93 @@
1
1
  /**
2
- * Configuration types and interfaces for the slugify functionality.
2
+ * Configuration class for the slugify functionality.
3
3
  * Provides comprehensive customization options for URL slug generation.
4
4
  */
5
- export interface SlugifyConfig {
5
+ export declare class SlugifyConfig {
6
+ private readonly _customReplacements;
7
+ private readonly _separator;
8
+ private readonly _case;
9
+ private readonly _removeAccents;
10
+ private readonly _allowedChars?;
11
+ private readonly _maxLength?;
12
+ private readonly _transformers;
13
+ private constructor();
14
+ static builder(): SlugifyConfigBuilder;
15
+ static default(): SlugifyConfig;
16
+ static create(builder: SlugifyConfigBuilder): SlugifyConfig;
17
+ get customReplacements(): Record<string, string>;
18
+ get separator(): string;
19
+ get case(): 'lower' | 'upper' | 'default';
20
+ get removeAccents(): boolean;
21
+ get allowedChars(): RegExp | undefined;
22
+ get maxLength(): number | undefined;
23
+ get transformers(): Array<(str: string) => string>;
24
+ merge(other: SlugifyConfig): SlugifyConfig;
25
+ private escapeRegex;
26
+ getCharClass(): RegExp;
27
+ applyCustomReplacements(str: string): string;
28
+ applyTransformers(str: string): string;
29
+ applyRemoveAccents(str: string): string;
30
+ replaceNonAllowedChars(str: string): string;
31
+ trimSeparators(str: string): string;
32
+ applyCase(str: string): string;
33
+ applyMaxLength(str: string): string;
34
+ collapseSeparators(str: string): string;
6
35
  /**
7
- * Custom character replacements applied before other transformations.
8
- * Useful for replacing special symbols with words or other characters.
9
- *
10
- * @example
11
- * { "♀": "feminin", "♂": "masculin", "@": "at" }
36
+ * Applies the slugify transformations to the given string using this configuration.
37
+ * @param str - The input string to slugify
38
+ * @returns The slugified string
12
39
  */
13
- customReplacements?: Record<string, string>;
40
+ slugify(str: string): string;
14
41
  /**
15
- * Character used to separate words in the slug.
16
- * @default "-"
17
- */
18
- separator?: string;
19
- /**
20
- * Whether to convert the result to lowercase.
21
- * @default true
22
- */
23
- lowercase?: boolean;
24
- /**
25
- * Whether to remove accents and diacritical marks.
26
- * @default true
27
- */
28
- removeAccents?: boolean;
29
- /**
30
- * Regular expression pattern for characters to keep.
31
- * Characters not matching this pattern will be replaced with the separator.
32
- * @default "/[a-zA-Z0-9]/"
42
+ * Static method to slugify a string with a temporary configuration built from the builder.
43
+ * @param str - The input string to slugify
44
+ * @param builderFn - A function that configures the builder
45
+ * @returns The slugified string
33
46
  */
47
+ static slugifyWith(str: string, builderFn: (builder: SlugifyConfigBuilder) => SlugifyConfigBuilder): string;
48
+ }
49
+ export declare class SlugifyConfigBuilder {
50
+ customReplacements: Record<string, string>;
51
+ separator: string;
52
+ case: 'lower' | 'upper' | 'default';
53
+ removeAccents: boolean;
34
54
  allowedChars?: RegExp;
35
- /**
36
- * Maximum length of the resulting slug (excluding separator trimming).
37
- * If exceeded, the slug will be truncated.
38
- * @default undefined (no limit)
39
- */
40
55
  maxLength?: number;
41
- /**
42
- * Custom transformation functions applied in order.
43
- * Each function receives the current string state and should return the transformed string.
44
- *
45
- * @example
46
- * [(str) => str.replace(/custom-pattern/g, 'replacement')]
47
- */
48
- transformers?: Array<(str: string) => string>;
56
+ transformers: Array<(str: string) => string>;
57
+ withCustomReplacements(replacements: Record<string, string>): this;
58
+ withSeparator(separator: string): this;
59
+ withCase(caseValue: 'lower' | 'upper' | 'default'): this;
60
+ withRemoveAccents(remove: boolean): this;
61
+ withAllowedChars(regex?: RegExp): this;
62
+ withMaxLength(length?: number): this;
63
+ withTransformers(transformers: Array<(str: string) => string>): this;
64
+ build(): SlugifyConfig;
49
65
  }
50
66
  /**
51
67
  * Default configuration for slugify operations.
52
68
  * Provides sensible defaults that can be overridden by users.
53
69
  */
54
- export declare const defaultSlugifyConfig: SlugifyConfig;
70
+ export declare function getDefaultSlugifyConfig(): SlugifyConfig;
55
71
  /**
56
72
  * Sets the global slugify configuration that will be used by default.
57
- * Merges the provided config with the existing global config.
73
+ * Replaces the current global config with the provided config.
58
74
  *
59
- * @param config - Partial configuration object to merge with current global config
75
+ * @param config - Configuration object to set as global config
60
76
  *
61
77
  * @example
62
- * setSlugifyConfig({
63
- * customReplacements: { "♀": "feminin", "♂": "masculin" },
64
- * separator: "_"
65
- * });
78
+ * setSlugifyConfig(
79
+ * SlugifyConfig.builder()
80
+ * .withCustomReplacements({ "♀": "feminin", "♂": "masculin" })
81
+ * .withSeparator("_")
82
+ * .build()
83
+ * );
66
84
  */
85
+ export declare function assertCharClass(regex: RegExp): void;
86
+ export declare function assertSlugifyConfig(builderOrConfig: SlugifyConfigBuilder | SlugifyConfig): void;
67
87
  export declare function setSlugifyConfig(config: SlugifyConfig): void;
68
88
  /**
69
89
  * Gets the current global slugify configuration.
70
- * Returns a copy to prevent external mutations.
90
+ * Returns the current global config instance.
71
91
  *
72
92
  * @returns Current global configuration object
73
93
  */
@@ -1,58 +1,271 @@
1
1
  "use strict";
2
- /**
3
- * Configuration types and interfaces for the slugify functionality.
4
- * Provides comprehensive customization options for URL slug generation.
5
- */
6
2
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.defaultSlugifyConfig = void 0;
3
+ exports.SlugifyConfigBuilder = exports.SlugifyConfig = void 0;
4
+ exports.getDefaultSlugifyConfig = getDefaultSlugifyConfig;
5
+ exports.assertCharClass = assertCharClass;
6
+ exports.assertSlugifyConfig = assertSlugifyConfig;
8
7
  exports.setSlugifyConfig = setSlugifyConfig;
9
8
  exports.getSlugifyConfig = getSlugifyConfig;
10
9
  exports.resetSlugifyConfig = resetSlugifyConfig;
10
+ /**
11
+ * Configuration class for the slugify functionality.
12
+ * Provides comprehensive customization options for URL slug generation.
13
+ */
14
+ class SlugifyConfig {
15
+ constructor(builder) {
16
+ this._customReplacements = builder.customReplacements;
17
+ this._separator = builder.separator;
18
+ this._case = builder.case;
19
+ this._removeAccents = builder.removeAccents;
20
+ this._allowedChars = builder.allowedChars;
21
+ this._maxLength = builder.maxLength;
22
+ this._transformers = builder.transformers;
23
+ }
24
+ static builder() {
25
+ return new SlugifyConfigBuilder();
26
+ }
27
+ static default() {
28
+ return SlugifyConfig.builder().build();
29
+ }
30
+ static create(builder) {
31
+ assertSlugifyConfig(builder);
32
+ const config = new SlugifyConfig(builder);
33
+ return Object.freeze(config);
34
+ }
35
+ get customReplacements() {
36
+ return { ...this._customReplacements };
37
+ }
38
+ get separator() {
39
+ return this._separator;
40
+ }
41
+ get case() {
42
+ return this._case;
43
+ }
44
+ get removeAccents() {
45
+ return this._removeAccents;
46
+ }
47
+ get allowedChars() {
48
+ return this._allowedChars;
49
+ }
50
+ get maxLength() {
51
+ return this._maxLength;
52
+ }
53
+ get transformers() {
54
+ return [...this._transformers];
55
+ }
56
+ merge(other) {
57
+ return SlugifyConfig.builder()
58
+ .withCustomReplacements({ ...this.customReplacements, ...other.customReplacements })
59
+ .withSeparator(other.separator || this.separator)
60
+ .withCase(other.case || this.case)
61
+ .withRemoveAccents(other.removeAccents !== undefined ? other.removeAccents : this.removeAccents)
62
+ .withAllowedChars(other.allowedChars || this.allowedChars)
63
+ .withMaxLength(other.maxLength || this.maxLength)
64
+ .withTransformers([...this.transformers, ...other.transformers])
65
+ .build();
66
+ }
67
+ escapeRegex(str) {
68
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
+ }
70
+ getCharClass() {
71
+ return this._allowedChars || (this._removeAccents ? /[a-zA-Z0-9]/ : /[a-zA-Z0-9\u00C0-\u017F]/);
72
+ }
73
+ applyCustomReplacements(str) {
74
+ let result = str;
75
+ for (const [pattern, replacement] of Object.entries(this._customReplacements)) {
76
+ const escapedPattern = this.escapeRegex(pattern);
77
+ result = result.replace(new RegExp(escapedPattern, 'g'), replacement);
78
+ }
79
+ return result;
80
+ }
81
+ applyTransformers(str) {
82
+ return this._transformers.reduce((result, transformer) => transformer(result), str);
83
+ }
84
+ applyRemoveAccents(str) {
85
+ if (!this._removeAccents)
86
+ return str;
87
+ return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
88
+ }
89
+ replaceNonAllowedChars(str) {
90
+ const charClass = this.getCharClass().source;
91
+ const safeCharClass = charClass.startsWith('[') && charClass.endsWith(']') ? charClass.slice(1, -1) : charClass;
92
+ return str.replace(new RegExp(`[^${safeCharClass}]+`, 'g'), this._separator);
93
+ }
94
+ trimSeparators(str) {
95
+ const safeSeparator = this.escapeRegex(this._separator);
96
+ return str.replace(new RegExp(`^${safeSeparator}+|${safeSeparator}+$`, 'g'), '');
97
+ }
98
+ applyCase(str) {
99
+ if (this._case === 'lower')
100
+ return str.toLowerCase();
101
+ if (this._case === 'upper')
102
+ return str.toUpperCase();
103
+ return str;
104
+ }
105
+ applyMaxLength(str) {
106
+ if (!this._maxLength || this._maxLength <= 0)
107
+ return str;
108
+ let result = str.slice(0, this._maxLength);
109
+ const safeSeparator = this.escapeRegex(this._separator);
110
+ return result.replace(new RegExp(`${safeSeparator}$`), '');
111
+ }
112
+ collapseSeparators(str) {
113
+ const safeSeparator = this.escapeRegex(this._separator);
114
+ return str.replace(new RegExp(`${safeSeparator}+`, 'g'), this._separator);
115
+ }
116
+ /**
117
+ * Applies the slugify transformations to the given string using this configuration.
118
+ * @param str - The input string to slugify
119
+ * @returns The slugified string
120
+ */
121
+ slugify(str) {
122
+ let result = str;
123
+ result = this.applyCustomReplacements(result);
124
+ result = this.applyTransformers(result);
125
+ result = this.applyRemoveAccents(result);
126
+ result = this.replaceNonAllowedChars(result);
127
+ result = this.trimSeparators(result);
128
+ result = this.applyCase(result);
129
+ result = this.applyMaxLength(result);
130
+ result = this.collapseSeparators(result);
131
+ return result;
132
+ }
133
+ /**
134
+ * Static method to slugify a string with a temporary configuration built from the builder.
135
+ * @param str - The input string to slugify
136
+ * @param builderFn - A function that configures the builder
137
+ * @returns The slugified string
138
+ */
139
+ static slugifyWith(str, builderFn) {
140
+ const config = builderFn(SlugifyConfig.builder()).build();
141
+ return config.slugify(str);
142
+ }
143
+ }
144
+ exports.SlugifyConfig = SlugifyConfig;
145
+ class SlugifyConfigBuilder {
146
+ constructor() {
147
+ this.customReplacements = {};
148
+ this.separator = '-';
149
+ this.case = 'lower';
150
+ this.removeAccents = true;
151
+ this.transformers = [];
152
+ }
153
+ withCustomReplacements(replacements) {
154
+ this.customReplacements = { ...replacements };
155
+ return this;
156
+ }
157
+ withSeparator(separator) {
158
+ this.separator = separator;
159
+ return this;
160
+ }
161
+ withCase(caseValue) {
162
+ this.case = caseValue;
163
+ return this;
164
+ }
165
+ withRemoveAccents(remove) {
166
+ this.removeAccents = remove;
167
+ return this;
168
+ }
169
+ withAllowedChars(regex) {
170
+ this.allowedChars = regex;
171
+ return this;
172
+ }
173
+ withMaxLength(length) {
174
+ this.maxLength = length;
175
+ return this;
176
+ }
177
+ withTransformers(transformers) {
178
+ this.transformers = [...transformers];
179
+ return this;
180
+ }
181
+ build() {
182
+ const config = SlugifyConfig.create(this);
183
+ return config;
184
+ }
185
+ }
186
+ exports.SlugifyConfigBuilder = SlugifyConfigBuilder;
11
187
  /**
12
188
  * Default configuration for slugify operations.
13
189
  * Provides sensible defaults that can be overridden by users.
14
190
  */
15
- exports.defaultSlugifyConfig = {
16
- customReplacements: {},
17
- separator: '-',
18
- lowercase: true,
19
- removeAccents: true,
20
- allowedChars: undefined, // Will be determined dynamically based on removeAccents
21
- maxLength: undefined,
22
- transformers: [],
23
- };
191
+ function getDefaultSlugifyConfig() {
192
+ return SlugifyConfig.default();
193
+ }
24
194
  /**
25
195
  * Global configuration instance that can be modified by users.
26
196
  * Starts with default values but can be updated via setSlugifyConfig().
27
197
  */
28
- let globalSlugifyConfig = { ...exports.defaultSlugifyConfig };
198
+ let globalSlugifyConfig = getDefaultSlugifyConfig();
29
199
  /**
30
200
  * Sets the global slugify configuration that will be used by default.
31
- * Merges the provided config with the existing global config.
201
+ * Replaces the current global config with the provided config.
32
202
  *
33
- * @param config - Partial configuration object to merge with current global config
203
+ * @param config - Configuration object to set as global config
34
204
  *
35
205
  * @example
36
- * setSlugifyConfig({
37
- * customReplacements: { "♀": "feminin", "♂": "masculin" },
38
- * separator: "_"
39
- * });
206
+ * setSlugifyConfig(
207
+ * SlugifyConfig.builder()
208
+ * .withCustomReplacements({ "♀": "feminin", "♂": "masculin" })
209
+ * .withSeparator("_")
210
+ * .build()
211
+ * );
40
212
  */
213
+ function assertCharClass(regex) {
214
+ const src = regex.source;
215
+ if (!src.startsWith('[') || !src.endsWith(']')) {
216
+ throw new Error(`Invalid allowedChars: must be a character class like /[a-z]/, received ${regex}`);
217
+ }
218
+ }
219
+ function assertSlugifyConfig(builderOrConfig) {
220
+ const allowedChars = builderOrConfig.allowedChars;
221
+ const separator = builderOrConfig.separator;
222
+ const caseValue = builderOrConfig.case;
223
+ const removeAccents = builderOrConfig.removeAccents;
224
+ const maxLength = builderOrConfig.maxLength;
225
+ const customReplacements = builderOrConfig.customReplacements;
226
+ const transformers = builderOrConfig.transformers;
227
+ if (allowedChars !== undefined) {
228
+ if (!(allowedChars instanceof RegExp)) {
229
+ throw new TypeError(`Invalid allowedChars: expected a RegExp, received ${typeof allowedChars}`);
230
+ }
231
+ assertCharClass(allowedChars);
232
+ }
233
+ if (typeof separator !== 'string') {
234
+ throw new TypeError(`Invalid separator: expected a string, received ${typeof separator}`);
235
+ }
236
+ if (caseValue !== 'lower' && caseValue !== 'upper' && caseValue !== 'default') {
237
+ throw new TypeError(`Invalid case value: expected 'lower' | 'upper' | 'default', received ${caseValue}`);
238
+ }
239
+ if (typeof removeAccents !== 'boolean') {
240
+ throw new TypeError(`Invalid removeAccents: expected a boolean, received ${typeof removeAccents}`);
241
+ }
242
+ if (maxLength !== undefined) {
243
+ if (typeof maxLength !== 'number' || !Number.isInteger(maxLength) || maxLength < 0) {
244
+ throw new TypeError(`Invalid maxLength: expected a non-negative integer, received ${maxLength}`);
245
+ }
246
+ }
247
+ if (typeof customReplacements !== 'object' || customReplacements === null) {
248
+ throw new TypeError(`Invalid customReplacements: expected an object, received ${typeof customReplacements}`);
249
+ }
250
+ if (!Array.isArray(transformers) || !transformers.every((fn) => typeof fn === 'function')) {
251
+ throw new TypeError(`Invalid transformers: expected an array of functions`);
252
+ }
253
+ }
41
254
  function setSlugifyConfig(config) {
42
- globalSlugifyConfig = { ...globalSlugifyConfig, ...config };
255
+ globalSlugifyConfig = config;
43
256
  }
44
257
  /**
45
258
  * Gets the current global slugify configuration.
46
- * Returns a copy to prevent external mutations.
259
+ * Returns the current global config instance.
47
260
  *
48
261
  * @returns Current global configuration object
49
262
  */
50
263
  function getSlugifyConfig() {
51
- return { ...globalSlugifyConfig };
264
+ return globalSlugifyConfig;
52
265
  }
53
266
  /**
54
267
  * Resets the global slugify configuration to default values.
55
268
  */
56
269
  function resetSlugifyConfig() {
57
- globalSlugifyConfig = { ...exports.defaultSlugifyConfig };
270
+ globalSlugifyConfig = getDefaultSlugifyConfig();
58
271
  }
@@ -12,48 +12,7 @@ const slugify_config_1 = require("./slugify.config");
12
12
  */
13
13
  function slugifyString(str, config) {
14
14
  // Merge provided config with global config
15
- const finalConfig = { ...(0, slugify_config_1.getSlugifyConfig)(), ...config };
16
- let result = str;
17
- // 1. Apply custom replacements first
18
- if (finalConfig.customReplacements && Object.keys(finalConfig.customReplacements).length > 0) {
19
- for (const [pattern, replacement] of Object.entries(finalConfig.customReplacements)) {
20
- // Escape special regex characters in the pattern
21
- const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
- result = result.replace(new RegExp(escapedPattern, 'g'), replacement);
23
- }
24
- }
25
- // 2. Apply custom transformers
26
- if (finalConfig.transformers && finalConfig.transformers.length > 0) {
27
- for (const transformer of finalConfig.transformers) {
28
- result = transformer(result);
29
- }
30
- }
31
- // 3. Remove accents if configured
32
- if (finalConfig.removeAccents) {
33
- result = result.normalize('NFD').replace(/[̀-ͯ]/g, '');
34
- }
35
- // 4. Replace non-allowed characters with separator
36
- const separator = finalConfig.separator || '-';
37
- // Determine allowed characters based on removeAccents setting
38
- const defaultAllowedChars = finalConfig.removeAccents ? /[a-zA-Z0-9]/ : /[a-zA-Z0-9\u00C0-\u017F]/; // Include accented Latin characters if accents are preserved
39
- const allowedRegex = finalConfig.allowedChars || defaultAllowedChars;
40
- const allowedPattern = allowedRegex.source;
41
- // Extract the character class content (remove surrounding brackets if present)
42
- const charClass = allowedPattern.startsWith('[') && allowedPattern.endsWith(']') ? allowedPattern.slice(1, -1) : allowedPattern;
43
- result = result.replace(new RegExp(`[^${charClass}]+`, 'g'), separator);
44
- // 5. Remove leading/trailing separators
45
- result = result.replace(new RegExp(`^${separator}+|${separator}+$`, 'g'), '');
46
- // 6. Convert to lowercase if configured
47
- if (finalConfig.lowercase) {
48
- result = result.toLowerCase();
49
- }
50
- // 7. Apply max length if configured
51
- if (finalConfig.maxLength && finalConfig.maxLength > 0) {
52
- result = result.slice(0, finalConfig.maxLength);
53
- // Remove trailing separator after truncation
54
- result = result.replace(new RegExp(`${separator}$`), '');
55
- }
56
- // 8. Collapse multiple consecutive separators
57
- result = result.replace(new RegExp(`${separator}+`, 'g'), separator);
58
- return result;
15
+ const globalConfig = (0, slugify_config_1.getSlugifyConfig)();
16
+ const finalConfig = config ? globalConfig.merge(config) : globalConfig;
17
+ return finalConfig.slugify(str);
59
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utilitish",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/FDonovan12/utilitish.git"