@nyaomaru/divider 2.0.6 → 2.0.7

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/dist/index.cjs CHANGED
@@ -122,7 +122,10 @@ function isNoneMode(value) {
122
122
  function normalizeSeparators(separators) {
123
123
  return Array.from(new Set(separators)).filter(
124
124
  (separator) => !isEmptyString(separator)
125
- );
125
+ ).sort((left, right) => right.length - left.length);
126
+ }
127
+ function createCacheKey(normalizedSeparators) {
128
+ return normalizedSeparators.join(CACHE_KEY_SEPARATOR);
126
129
  }
127
130
  var RegexCache = class {
128
131
  cache = /* @__PURE__ */ new Map();
@@ -138,6 +141,15 @@ var RegexCache = class {
138
141
  */
139
142
  get(separators) {
140
143
  const key = this.createKey(separators);
144
+ return this.getByKey(key);
145
+ }
146
+ /**
147
+ * Retrieves a cached RegExp for a precomputed cache key.
148
+ *
149
+ * @param key - Cache key string
150
+ * @returns Cached RegExp or null if not found
151
+ */
152
+ getByKey(key) {
141
153
  const regex = this.cache.get(key);
142
154
  if (regex) {
143
155
  this.cache.delete(key);
@@ -153,6 +165,15 @@ var RegexCache = class {
153
165
  */
154
166
  set(separators, regex) {
155
167
  const key = this.createKey(separators);
168
+ this.setByKey(key, regex);
169
+ }
170
+ /**
171
+ * Stores a RegExp in the cache for a precomputed cache key.
172
+ *
173
+ * @param key - Cache key string
174
+ * @param regex - Compiled RegExp to cache
175
+ */
176
+ setByKey(key, regex) {
156
177
  if (this.cache.has(key)) {
157
178
  this.cache.delete(key);
158
179
  }
@@ -173,7 +194,7 @@ var RegexCache = class {
173
194
  */
174
195
  createKey(separators) {
175
196
  const normalizedSeparators = normalizeSeparators(separators);
176
- return normalizedSeparators.join(CACHE_KEY_SEPARATOR);
197
+ return createCacheKey(normalizedSeparators);
177
198
  }
178
199
  /**
179
200
  * Gets current cache size for debugging/monitoring.
@@ -194,15 +215,16 @@ var RegexCache = class {
194
215
  var regexCache = new RegexCache();
195
216
  function getRegex(separators) {
196
217
  if (isEmptyArray(separators)) return null;
197
- const cached = regexCache.get(separators);
198
- if (cached) return cached;
199
218
  const uniqueSeparators = normalizeSeparators(separators);
200
219
  if (uniqueSeparators.length === 0) {
201
220
  return null;
202
221
  }
222
+ const cacheKey = createCacheKey(uniqueSeparators);
223
+ const cached = regexCache.getByKey(cacheKey);
224
+ if (cached) return cached;
203
225
  const pattern = uniqueSeparators.map(escapeRegExp).join("|");
204
226
  const regex = new RegExp(`(?:${pattern})`, "g");
205
- regexCache.set(separators, regex);
227
+ regexCache.setByKey(cacheKey, regex);
206
228
  return regex;
207
229
  }
208
230
  function escapeRegExp(str) {
@@ -326,6 +348,13 @@ function classifySeparators(args) {
326
348
  );
327
349
  }
328
350
 
351
+ // src/utils/transform-divider-input.ts
352
+ function transformDividerInput(input, transform, options) {
353
+ const result = isString(input) ? transform(input) : input.map(transform);
354
+ const resolvedOptions = options ?? {};
355
+ return applyDividerOptions(result, resolvedOptions);
356
+ }
357
+
329
358
  // src/core/divider.ts
330
359
  function divider(input, ...args) {
331
360
  if (!isValidInput(input)) {
@@ -342,8 +371,7 @@ function divider(input, ...args) {
342
371
  const applyDivision = (str) => divideString(str, numSeparators, strSeparators, {
343
372
  preserveEmpty: options.preserveEmpty
344
373
  });
345
- const result = isString(input) ? applyDivision(input) : input.map(applyDivision);
346
- return applyDividerOptions(result, options);
374
+ return transformDividerInput(input, applyDivision, options);
347
375
  }
348
376
 
349
377
  // src/core/divider-first.ts
@@ -394,15 +422,15 @@ function dividerLoop(input, size, options) {
394
422
  console.warn("dividerLoop: chunk size must be a positive number");
395
423
  return [];
396
424
  }
397
- const resolvedOptions = options ?? {};
398
425
  const {
399
426
  startOffset = PERFORMANCE_CONSTANTS.DEFAULT_START_OFFSET,
400
427
  maxChunks = PERFORMANCE_CONSTANTS.DEFAULT_MAX_CHUNKS
401
- } = resolvedOptions;
402
- const result = isString(input) ? createChunksFromString(input, size, startOffset, maxChunks) : input.map(
403
- (str) => createChunksFromString(str, size, startOffset, maxChunks)
428
+ } = options ?? {};
429
+ return transformDividerInput(
430
+ input,
431
+ (str) => createChunksFromString(str, size, startOffset, maxChunks),
432
+ options
404
433
  );
405
- return applyDividerOptions(result, resolvedOptions);
406
434
  }
407
435
 
408
436
  // src/utils/divide.ts
@@ -412,9 +440,7 @@ function divideNumberString(str) {
412
440
 
413
441
  // src/core/divider-number-string.ts
414
442
  function dividerNumberString(input, options) {
415
- const result = isString(input) ? divideNumberString(input) : input.map(divideNumberString);
416
- const resolvedOptions = options ?? {};
417
- return applyDividerOptions(result, resolvedOptions);
443
+ return transformDividerInput(input, divideNumberString, options);
418
444
  }
419
445
 
420
446
  // src/utils/quoted.ts
@@ -422,13 +448,41 @@ function dividePreserve(input, separator) {
422
448
  if (isEmptyString(input)) return [""];
423
449
  return divider(input, separator, { preserveEmpty: true });
424
450
  }
425
- function countUnescaped(text, quote) {
426
- const pair = quote + quote;
451
+ var countUnescapedSingleChar = (text, quote) => {
452
+ let count = 0;
453
+ for (let index = 0; index < text.length; index++) {
454
+ if (text[index] !== quote) continue;
455
+ if (text[index + 1] === quote) {
456
+ index++;
457
+ continue;
458
+ }
459
+ count++;
460
+ }
461
+ return count;
462
+ };
463
+ var countUnescapedMultiChar = (text, quote) => {
464
+ const escapedPair = quote + quote;
465
+ const quoteLength = quote.length;
466
+ const escapedPairLength = escapedPair.length;
427
467
  let count = 0;
428
- for (const chunk of dividePreserve(text, pair)) {
429
- count += dividePreserve(chunk, quote).length - 1;
468
+ for (let index = 0; index < text.length; ) {
469
+ if (text.startsWith(escapedPair, index)) {
470
+ index += escapedPairLength;
471
+ continue;
472
+ }
473
+ if (text.startsWith(quote, index)) {
474
+ count++;
475
+ index += quoteLength;
476
+ continue;
477
+ }
478
+ index++;
430
479
  }
431
480
  return count;
481
+ };
482
+ function countUnescaped(text, quote) {
483
+ if (isEmptyString(quote)) return 0;
484
+ if (quote.length === 1) return countUnescapedSingleChar(text, quote);
485
+ return countUnescapedMultiChar(text, quote);
432
486
  }
433
487
  function stripOuterQuotes(text, quoteChar, { lenient = true } = {}) {
434
488
  const escapedPair = quoteChar + quoteChar;
@@ -480,7 +534,8 @@ var buildQuotedFields = (line, delimiter, quote, trim, lenient) => {
480
534
  const pieces = dividePreserve(line, delimiter);
481
535
  const state = {
482
536
  fields: [],
483
- current: ""
537
+ current: "",
538
+ insideQuotes: false
484
539
  };
485
540
  for (const piece of pieces) {
486
541
  appendPiece(state, piece, delimiter, quote, trim, lenient);
@@ -490,10 +545,18 @@ var buildQuotedFields = (line, delimiter, quote, trim, lenient) => {
490
545
  }
491
546
  return state.fields;
492
547
  };
548
+ var advanceQuoteState = (insideQuotes, segment, quote) => {
549
+ let nextInsideQuotes = insideQuotes;
550
+ for (const char of segment) {
551
+ if (char === quote) nextInsideQuotes = !nextInsideQuotes;
552
+ }
553
+ return nextInsideQuotes;
554
+ };
493
555
  var appendPiece = (state, piece, delimiter, quote, trim, lenient) => {
494
- state.current = isEmptyString(state.current) ? piece : state.current + delimiter + piece;
495
- const insideQuotes = countUnescaped(state.current, quote) % 2 === 1;
496
- if (!insideQuotes) {
556
+ const segment = isEmptyString(state.current) ? piece : delimiter + piece;
557
+ state.current += segment;
558
+ state.insideQuotes = quote.length === 1 ? advanceQuoteState(state.insideQuotes, segment, quote) : countUnescaped(state.current, quote) % 2 === 1;
559
+ if (!state.insideQuotes) {
497
560
  flushField(state, quote, trim, lenient);
498
561
  }
499
562
  };
@@ -502,6 +565,7 @@ var flushField = (state, quote, trim, lenient) => {
502
565
  if (trim) fieldValue = fieldValue.trim();
503
566
  state.fields.push(fieldValue);
504
567
  state.current = "";
568
+ state.insideQuotes = false;
505
569
  };
506
570
 
507
571
  // src/presets/csv-divider.ts
@@ -553,8 +617,27 @@ function tryExtractQuery(input) {
553
617
  const url = new URL(input);
554
618
  return url.search.startsWith(QUERY_SEPARATORS.QUESTION_MARK) ? url.search.slice(1) : url.search;
555
619
  } catch {
556
- return input;
620
+ return extractQueryFromQuestionMark(input);
621
+ }
622
+ }
623
+ function extractQueryFromQuestionMark(input) {
624
+ const fragmentIndex = input.indexOf("#");
625
+ const withoutFragment = fragmentIndex >= 0 ? input.slice(0, fragmentIndex) : input;
626
+ const questionMarkIndex = withoutFragment.indexOf(
627
+ QUERY_SEPARATORS.QUESTION_MARK
628
+ );
629
+ if (questionMarkIndex < 0) {
630
+ return withoutFragment;
631
+ }
632
+ if (questionMarkIndex === 0) {
633
+ return withoutFragment.slice(1);
634
+ }
635
+ const prefix = withoutFragment.slice(0, questionMarkIndex);
636
+ const hasQuerySeparatorBefore = prefix.includes(QUERY_SEPARATORS.AMPERSAND) || prefix.includes(QUERY_SEPARATORS.EQUALS);
637
+ if (hasQuerySeparatorBefore) {
638
+ return withoutFragment;
557
639
  }
640
+ return withoutFragment.slice(questionMarkIndex + 1);
558
641
  }
559
642
  function stripLeadingQuestionMark(query) {
560
643
  return query.startsWith(QUERY_SEPARATORS.QUESTION_MARK) ? query.slice(1) : query;
package/dist/index.d.cts CHANGED
@@ -21,21 +21,42 @@ declare const QUERY_DECODE_MODES: {
21
21
  readonly RAW: "raw";
22
22
  };
23
23
 
24
+ /**
25
+ * Supported exclusion modes used to filter divider output.
26
+ */
24
27
  type DividerExcludeMode = (typeof DIVIDER_EXCLUDE_MODES)[keyof typeof DIVIDER_EXCLUDE_MODES];
28
+ /**
29
+ * Single string input accepted by divider functions.
30
+ */
25
31
  type StringInput = string;
32
+ /**
33
+ * Readonly string array input accepted by divider functions.
34
+ */
26
35
  type StringArrayInput = readonly string[];
36
+ /**
37
+ * Supported input shapes for divider functions.
38
+ */
27
39
  type DividerInput = StringInput | StringArrayInput;
40
+ /**
41
+ * Flat divider result.
42
+ */
28
43
  type DividerStringResult = string[];
44
+ /**
45
+ * Nested divider result for `string[]` inputs when `flatten` is not enabled.
46
+ */
29
47
  type DividerArrayResult = string[][];
48
+ /**
49
+ * Shared options supported by divider functions.
50
+ */
30
51
  type DividerOptions = {
31
52
  /** If true, flattens nested arrays into a single array */
32
- flatten?: boolean;
53
+ readonly flatten?: boolean;
33
54
  /** If true, trims whitespace from each divided segment */
34
- trim?: boolean;
55
+ readonly trim?: boolean;
35
56
  /** If true, retains empty segments produced by division */
36
- preserveEmpty?: boolean;
57
+ readonly preserveEmpty?: boolean;
37
58
  /** Controls how empty or whitespace segments are handled */
38
- exclude?: DividerExcludeMode;
59
+ readonly exclude?: DividerExcludeMode;
39
60
  };
40
61
  /**
41
62
  * Represents the absence of user-specified divider options while retaining
@@ -52,37 +73,76 @@ type DividerEmptyOptions = {
52
73
  readonly exclude?: never;
53
74
  };
54
75
  type DividerInferredOptions = DividerOptions | DividerEmptyOptions;
55
- type HasFlattenOption<TOptions extends DividerInferredOptions> = TOptions extends {
76
+ /**
77
+ * Computes whether the provided options explicitly enable `flatten`.
78
+ */
79
+ type IsFlattenEnabled<TOptions extends DividerInferredOptions> = TOptions extends {
56
80
  readonly flatten?: infer Flag;
57
- } ? Flag extends true ? true : false : false;
58
- type DividerResult<T extends DividerInput, TOptions extends DividerInferredOptions = DividerEmptyOptions> = T extends StringInput ? DividerStringResult : HasFlattenOption<TOptions> extends true ? DividerStringResult : DividerArrayResult;
81
+ } ? [Flag] extends [true] ? true : false : false;
82
+ /**
83
+ * Resolves the divider output shape from the input and options.
84
+ */
85
+ type DividerResult<T extends DividerInput, TOptions extends DividerInferredOptions = DividerEmptyOptions> = T extends StringInput ? DividerStringResult : IsFlattenEnabled<TOptions> extends true ? DividerStringResult : DividerArrayResult;
86
+ /**
87
+ * Numeric separator interpreted as an index.
88
+ */
89
+ type NumericSeparator = number;
90
+ /**
91
+ * String separator interpreted as a delimiter.
92
+ */
93
+ type StringSeparator = string;
94
+ /**
95
+ * Any supported divider separator.
96
+ */
97
+ type DividerSeparator = NumericSeparator | StringSeparator;
98
+ /**
99
+ * A readonly list of separators without options.
100
+ */
101
+ type DividerSeparators = readonly DividerSeparator[];
102
+ /**
103
+ * Any single divider argument.
104
+ */
105
+ type DividerArg = DividerSeparator | DividerOptions;
106
+ /**
107
+ * Divider arguments with an optional trailing options object.
108
+ *
109
+ * WHY: Runtime only consumes options when they are passed last. This tuple
110
+ * shape keeps the public type contract aligned with that behavior.
111
+ */
112
+ type DividerArgs = DividerSeparators | readonly [...DividerSeparator[], DividerOptions];
113
+ /**
114
+ * Extracts trailing divider options from a variadic argument tuple.
115
+ */
116
+ type ExtractedDividerOptions<TArgs extends readonly unknown[]> = TArgs extends readonly [...unknown[], infer Last] ? Last extends DividerOptions ? Last : DividerEmptyOptions : DividerEmptyOptions;
59
117
  /**
60
118
  * Computes the divider return type based on the input and provided arguments.
61
119
  *
62
120
  * WHY: When no arguments are provided, divider returns the input as-is for
63
121
  * string arrays. This type keeps the signature aligned with runtime behavior.
64
122
  */
65
- type DividerReturn<T extends DividerInput, TArgs extends DividerArgs> = T extends StringInput ? DividerStringResult : TArgs['length'] extends 0 ? T : HasFlattenOption<ExtractedDividerOptions<TArgs>> extends true ? DividerStringResult : DividerArrayResult;
66
- type ExtractedDividerOptions<TArgs extends DividerArgs> = TArgs extends readonly [...infer Rest, infer Last] ? Rest extends DividerArg[] ? Last extends DividerOptions ? Last : DividerEmptyOptions : DividerEmptyOptions : DividerEmptyOptions;
123
+ type DividerReturn<T extends DividerInput, TArgs extends DividerArgs> = T extends StringInput ? DividerStringResult : TArgs['length'] extends 0 ? T : IsFlattenEnabled<ExtractedDividerOptions<TArgs>> extends true ? DividerStringResult : DividerArrayResult;
124
+ /**
125
+ * Additional options supported by `dividerLoop`.
126
+ */
67
127
  type DividerLoopOptions = DividerOptions & {
68
128
  /** Starting position for the division (0-based) */
69
- startOffset?: number;
129
+ readonly startOffset?: number;
70
130
  /** Maximum number of chunks to produce */
71
- maxChunks?: number;
131
+ readonly maxChunks?: number;
72
132
  };
133
+ /**
134
+ * Represents the absence of loop-specific options while preserving inference.
135
+ */
73
136
  type DividerLoopEmptyOptions = DividerEmptyOptions & {
74
137
  /** Placeholder to signal startOffset is intentionally absent */
75
138
  readonly startOffset?: never;
76
139
  /** Placeholder to signal maxChunks is intentionally absent */
77
140
  readonly maxChunks?: never;
78
141
  };
142
+ /**
143
+ * Accepted option shapes for `dividerLoop`.
144
+ */
79
145
  type DividerLoopOptionsLike = DividerLoopOptions | DividerLoopEmptyOptions;
80
- type NumericSeparator = number;
81
- type StringSeparator = string;
82
- type DividerSeparator = NumericSeparator | StringSeparator;
83
- type DividerSeparators = readonly DividerSeparator[];
84
- type DividerArg = DividerSeparator | DividerOptions;
85
- type DividerArgs = readonly DividerArg[];
86
146
 
87
147
  /**
88
148
  * Main divider function that splits input based on numeric positions or string delimiters.
@@ -96,7 +156,7 @@ type DividerArgs = readonly DividerArg[];
96
156
  * @param args - Array of separators (numbers/strings) and optional options object
97
157
  * @returns Divided string segments based on input type and options
98
158
  */
99
- declare function divider<T extends DividerInput, const TArgs extends readonly DividerArg[]>(input: T, ...args: TArgs): DividerReturn<T, TArgs>;
159
+ declare function divider<T extends DividerInput, const TArgs extends DividerArgs>(input: T, ...args: TArgs): DividerReturn<T, TArgs>;
100
160
 
101
161
  /**
102
162
  * Extracts the first segment after dividing the input using specified separators.
@@ -160,22 +220,22 @@ declare function dividerNumberString<T extends DividerInput, O extends DividerIn
160
220
 
161
221
  type EmailDividerOptions = Pick<DividerOptions, 'trim'> & {
162
222
  /** Split top-level domain from the rest of the email address. */
163
- splitTLD?: boolean;
223
+ readonly splitTLD?: boolean;
164
224
  };
165
225
  type CsvDividerOptions = Pick<DividerOptions, 'trim'> & {
166
226
  /** Character used for quoting values. */
167
- quoteChar?: string;
227
+ readonly quoteChar?: string;
168
228
  /** Character used to separate CSV fields. */
169
- delimiter?: string;
229
+ readonly delimiter?: string;
170
230
  };
171
231
  type PathDividerOptions = Pick<DividerOptions, 'trim'> & {
172
232
  /** Collapse empty segments produced by leading/trailing or repeated separators. */
173
- collapse?: boolean;
233
+ readonly collapse?: boolean;
174
234
  };
175
235
  type QueryDecodeMode = (typeof QUERY_DECODE_MODES)[keyof typeof QUERY_DECODE_MODES];
176
236
  type QueryDividerOptions = Pick<DividerOptions, 'trim'> & {
177
237
  /** Decoding mode: 'auto' applies standard URL decoding; 'raw' leaves values untouched. */
178
- mode?: QueryDecodeMode;
238
+ readonly mode?: QueryDecodeMode;
179
239
  };
180
240
 
181
241
  /**
package/dist/index.d.ts CHANGED
@@ -21,21 +21,42 @@ declare const QUERY_DECODE_MODES: {
21
21
  readonly RAW: "raw";
22
22
  };
23
23
 
24
+ /**
25
+ * Supported exclusion modes used to filter divider output.
26
+ */
24
27
  type DividerExcludeMode = (typeof DIVIDER_EXCLUDE_MODES)[keyof typeof DIVIDER_EXCLUDE_MODES];
28
+ /**
29
+ * Single string input accepted by divider functions.
30
+ */
25
31
  type StringInput = string;
32
+ /**
33
+ * Readonly string array input accepted by divider functions.
34
+ */
26
35
  type StringArrayInput = readonly string[];
36
+ /**
37
+ * Supported input shapes for divider functions.
38
+ */
27
39
  type DividerInput = StringInput | StringArrayInput;
40
+ /**
41
+ * Flat divider result.
42
+ */
28
43
  type DividerStringResult = string[];
44
+ /**
45
+ * Nested divider result for `string[]` inputs when `flatten` is not enabled.
46
+ */
29
47
  type DividerArrayResult = string[][];
48
+ /**
49
+ * Shared options supported by divider functions.
50
+ */
30
51
  type DividerOptions = {
31
52
  /** If true, flattens nested arrays into a single array */
32
- flatten?: boolean;
53
+ readonly flatten?: boolean;
33
54
  /** If true, trims whitespace from each divided segment */
34
- trim?: boolean;
55
+ readonly trim?: boolean;
35
56
  /** If true, retains empty segments produced by division */
36
- preserveEmpty?: boolean;
57
+ readonly preserveEmpty?: boolean;
37
58
  /** Controls how empty or whitespace segments are handled */
38
- exclude?: DividerExcludeMode;
59
+ readonly exclude?: DividerExcludeMode;
39
60
  };
40
61
  /**
41
62
  * Represents the absence of user-specified divider options while retaining
@@ -52,37 +73,76 @@ type DividerEmptyOptions = {
52
73
  readonly exclude?: never;
53
74
  };
54
75
  type DividerInferredOptions = DividerOptions | DividerEmptyOptions;
55
- type HasFlattenOption<TOptions extends DividerInferredOptions> = TOptions extends {
76
+ /**
77
+ * Computes whether the provided options explicitly enable `flatten`.
78
+ */
79
+ type IsFlattenEnabled<TOptions extends DividerInferredOptions> = TOptions extends {
56
80
  readonly flatten?: infer Flag;
57
- } ? Flag extends true ? true : false : false;
58
- type DividerResult<T extends DividerInput, TOptions extends DividerInferredOptions = DividerEmptyOptions> = T extends StringInput ? DividerStringResult : HasFlattenOption<TOptions> extends true ? DividerStringResult : DividerArrayResult;
81
+ } ? [Flag] extends [true] ? true : false : false;
82
+ /**
83
+ * Resolves the divider output shape from the input and options.
84
+ */
85
+ type DividerResult<T extends DividerInput, TOptions extends DividerInferredOptions = DividerEmptyOptions> = T extends StringInput ? DividerStringResult : IsFlattenEnabled<TOptions> extends true ? DividerStringResult : DividerArrayResult;
86
+ /**
87
+ * Numeric separator interpreted as an index.
88
+ */
89
+ type NumericSeparator = number;
90
+ /**
91
+ * String separator interpreted as a delimiter.
92
+ */
93
+ type StringSeparator = string;
94
+ /**
95
+ * Any supported divider separator.
96
+ */
97
+ type DividerSeparator = NumericSeparator | StringSeparator;
98
+ /**
99
+ * A readonly list of separators without options.
100
+ */
101
+ type DividerSeparators = readonly DividerSeparator[];
102
+ /**
103
+ * Any single divider argument.
104
+ */
105
+ type DividerArg = DividerSeparator | DividerOptions;
106
+ /**
107
+ * Divider arguments with an optional trailing options object.
108
+ *
109
+ * WHY: Runtime only consumes options when they are passed last. This tuple
110
+ * shape keeps the public type contract aligned with that behavior.
111
+ */
112
+ type DividerArgs = DividerSeparators | readonly [...DividerSeparator[], DividerOptions];
113
+ /**
114
+ * Extracts trailing divider options from a variadic argument tuple.
115
+ */
116
+ type ExtractedDividerOptions<TArgs extends readonly unknown[]> = TArgs extends readonly [...unknown[], infer Last] ? Last extends DividerOptions ? Last : DividerEmptyOptions : DividerEmptyOptions;
59
117
  /**
60
118
  * Computes the divider return type based on the input and provided arguments.
61
119
  *
62
120
  * WHY: When no arguments are provided, divider returns the input as-is for
63
121
  * string arrays. This type keeps the signature aligned with runtime behavior.
64
122
  */
65
- type DividerReturn<T extends DividerInput, TArgs extends DividerArgs> = T extends StringInput ? DividerStringResult : TArgs['length'] extends 0 ? T : HasFlattenOption<ExtractedDividerOptions<TArgs>> extends true ? DividerStringResult : DividerArrayResult;
66
- type ExtractedDividerOptions<TArgs extends DividerArgs> = TArgs extends readonly [...infer Rest, infer Last] ? Rest extends DividerArg[] ? Last extends DividerOptions ? Last : DividerEmptyOptions : DividerEmptyOptions : DividerEmptyOptions;
123
+ type DividerReturn<T extends DividerInput, TArgs extends DividerArgs> = T extends StringInput ? DividerStringResult : TArgs['length'] extends 0 ? T : IsFlattenEnabled<ExtractedDividerOptions<TArgs>> extends true ? DividerStringResult : DividerArrayResult;
124
+ /**
125
+ * Additional options supported by `dividerLoop`.
126
+ */
67
127
  type DividerLoopOptions = DividerOptions & {
68
128
  /** Starting position for the division (0-based) */
69
- startOffset?: number;
129
+ readonly startOffset?: number;
70
130
  /** Maximum number of chunks to produce */
71
- maxChunks?: number;
131
+ readonly maxChunks?: number;
72
132
  };
133
+ /**
134
+ * Represents the absence of loop-specific options while preserving inference.
135
+ */
73
136
  type DividerLoopEmptyOptions = DividerEmptyOptions & {
74
137
  /** Placeholder to signal startOffset is intentionally absent */
75
138
  readonly startOffset?: never;
76
139
  /** Placeholder to signal maxChunks is intentionally absent */
77
140
  readonly maxChunks?: never;
78
141
  };
142
+ /**
143
+ * Accepted option shapes for `dividerLoop`.
144
+ */
79
145
  type DividerLoopOptionsLike = DividerLoopOptions | DividerLoopEmptyOptions;
80
- type NumericSeparator = number;
81
- type StringSeparator = string;
82
- type DividerSeparator = NumericSeparator | StringSeparator;
83
- type DividerSeparators = readonly DividerSeparator[];
84
- type DividerArg = DividerSeparator | DividerOptions;
85
- type DividerArgs = readonly DividerArg[];
86
146
 
87
147
  /**
88
148
  * Main divider function that splits input based on numeric positions or string delimiters.
@@ -96,7 +156,7 @@ type DividerArgs = readonly DividerArg[];
96
156
  * @param args - Array of separators (numbers/strings) and optional options object
97
157
  * @returns Divided string segments based on input type and options
98
158
  */
99
- declare function divider<T extends DividerInput, const TArgs extends readonly DividerArg[]>(input: T, ...args: TArgs): DividerReturn<T, TArgs>;
159
+ declare function divider<T extends DividerInput, const TArgs extends DividerArgs>(input: T, ...args: TArgs): DividerReturn<T, TArgs>;
100
160
 
101
161
  /**
102
162
  * Extracts the first segment after dividing the input using specified separators.
@@ -160,22 +220,22 @@ declare function dividerNumberString<T extends DividerInput, O extends DividerIn
160
220
 
161
221
  type EmailDividerOptions = Pick<DividerOptions, 'trim'> & {
162
222
  /** Split top-level domain from the rest of the email address. */
163
- splitTLD?: boolean;
223
+ readonly splitTLD?: boolean;
164
224
  };
165
225
  type CsvDividerOptions = Pick<DividerOptions, 'trim'> & {
166
226
  /** Character used for quoting values. */
167
- quoteChar?: string;
227
+ readonly quoteChar?: string;
168
228
  /** Character used to separate CSV fields. */
169
- delimiter?: string;
229
+ readonly delimiter?: string;
170
230
  };
171
231
  type PathDividerOptions = Pick<DividerOptions, 'trim'> & {
172
232
  /** Collapse empty segments produced by leading/trailing or repeated separators. */
173
- collapse?: boolean;
233
+ readonly collapse?: boolean;
174
234
  };
175
235
  type QueryDecodeMode = (typeof QUERY_DECODE_MODES)[keyof typeof QUERY_DECODE_MODES];
176
236
  type QueryDividerOptions = Pick<DividerOptions, 'trim'> & {
177
237
  /** Decoding mode: 'auto' applies standard URL decoding; 'raw' leaves values untouched. */
178
- mode?: QueryDecodeMode;
238
+ readonly mode?: QueryDecodeMode;
179
239
  };
180
240
 
181
241
  /**
package/dist/index.js CHANGED
@@ -88,7 +88,10 @@ function isNoneMode(value) {
88
88
  function normalizeSeparators(separators) {
89
89
  return Array.from(new Set(separators)).filter(
90
90
  (separator) => !isEmptyString(separator)
91
- );
91
+ ).sort((left, right) => right.length - left.length);
92
+ }
93
+ function createCacheKey(normalizedSeparators) {
94
+ return normalizedSeparators.join(CACHE_KEY_SEPARATOR);
92
95
  }
93
96
  var RegexCache = class {
94
97
  cache = /* @__PURE__ */ new Map();
@@ -104,6 +107,15 @@ var RegexCache = class {
104
107
  */
105
108
  get(separators) {
106
109
  const key = this.createKey(separators);
110
+ return this.getByKey(key);
111
+ }
112
+ /**
113
+ * Retrieves a cached RegExp for a precomputed cache key.
114
+ *
115
+ * @param key - Cache key string
116
+ * @returns Cached RegExp or null if not found
117
+ */
118
+ getByKey(key) {
107
119
  const regex = this.cache.get(key);
108
120
  if (regex) {
109
121
  this.cache.delete(key);
@@ -119,6 +131,15 @@ var RegexCache = class {
119
131
  */
120
132
  set(separators, regex) {
121
133
  const key = this.createKey(separators);
134
+ this.setByKey(key, regex);
135
+ }
136
+ /**
137
+ * Stores a RegExp in the cache for a precomputed cache key.
138
+ *
139
+ * @param key - Cache key string
140
+ * @param regex - Compiled RegExp to cache
141
+ */
142
+ setByKey(key, regex) {
122
143
  if (this.cache.has(key)) {
123
144
  this.cache.delete(key);
124
145
  }
@@ -139,7 +160,7 @@ var RegexCache = class {
139
160
  */
140
161
  createKey(separators) {
141
162
  const normalizedSeparators = normalizeSeparators(separators);
142
- return normalizedSeparators.join(CACHE_KEY_SEPARATOR);
163
+ return createCacheKey(normalizedSeparators);
143
164
  }
144
165
  /**
145
166
  * Gets current cache size for debugging/monitoring.
@@ -160,15 +181,16 @@ var RegexCache = class {
160
181
  var regexCache = new RegexCache();
161
182
  function getRegex(separators) {
162
183
  if (isEmptyArray(separators)) return null;
163
- const cached = regexCache.get(separators);
164
- if (cached) return cached;
165
184
  const uniqueSeparators = normalizeSeparators(separators);
166
185
  if (uniqueSeparators.length === 0) {
167
186
  return null;
168
187
  }
188
+ const cacheKey = createCacheKey(uniqueSeparators);
189
+ const cached = regexCache.getByKey(cacheKey);
190
+ if (cached) return cached;
169
191
  const pattern = uniqueSeparators.map(escapeRegExp).join("|");
170
192
  const regex = new RegExp(`(?:${pattern})`, "g");
171
- regexCache.set(separators, regex);
193
+ regexCache.setByKey(cacheKey, regex);
172
194
  return regex;
173
195
  }
174
196
  function escapeRegExp(str) {
@@ -292,6 +314,13 @@ function classifySeparators(args) {
292
314
  );
293
315
  }
294
316
 
317
+ // src/utils/transform-divider-input.ts
318
+ function transformDividerInput(input, transform, options) {
319
+ const result = isString(input) ? transform(input) : input.map(transform);
320
+ const resolvedOptions = options ?? {};
321
+ return applyDividerOptions(result, resolvedOptions);
322
+ }
323
+
295
324
  // src/core/divider.ts
296
325
  function divider(input, ...args) {
297
326
  if (!isValidInput(input)) {
@@ -308,8 +337,7 @@ function divider(input, ...args) {
308
337
  const applyDivision = (str) => divideString(str, numSeparators, strSeparators, {
309
338
  preserveEmpty: options.preserveEmpty
310
339
  });
311
- const result = isString(input) ? applyDivision(input) : input.map(applyDivision);
312
- return applyDividerOptions(result, options);
340
+ return transformDividerInput(input, applyDivision, options);
313
341
  }
314
342
 
315
343
  // src/core/divider-first.ts
@@ -360,15 +388,15 @@ function dividerLoop(input, size, options) {
360
388
  console.warn("dividerLoop: chunk size must be a positive number");
361
389
  return [];
362
390
  }
363
- const resolvedOptions = options ?? {};
364
391
  const {
365
392
  startOffset = PERFORMANCE_CONSTANTS.DEFAULT_START_OFFSET,
366
393
  maxChunks = PERFORMANCE_CONSTANTS.DEFAULT_MAX_CHUNKS
367
- } = resolvedOptions;
368
- const result = isString(input) ? createChunksFromString(input, size, startOffset, maxChunks) : input.map(
369
- (str) => createChunksFromString(str, size, startOffset, maxChunks)
394
+ } = options ?? {};
395
+ return transformDividerInput(
396
+ input,
397
+ (str) => createChunksFromString(str, size, startOffset, maxChunks),
398
+ options
370
399
  );
371
- return applyDividerOptions(result, resolvedOptions);
372
400
  }
373
401
 
374
402
  // src/utils/divide.ts
@@ -378,9 +406,7 @@ function divideNumberString(str) {
378
406
 
379
407
  // src/core/divider-number-string.ts
380
408
  function dividerNumberString(input, options) {
381
- const result = isString(input) ? divideNumberString(input) : input.map(divideNumberString);
382
- const resolvedOptions = options ?? {};
383
- return applyDividerOptions(result, resolvedOptions);
409
+ return transformDividerInput(input, divideNumberString, options);
384
410
  }
385
411
 
386
412
  // src/utils/quoted.ts
@@ -388,13 +414,41 @@ function dividePreserve(input, separator) {
388
414
  if (isEmptyString(input)) return [""];
389
415
  return divider(input, separator, { preserveEmpty: true });
390
416
  }
391
- function countUnescaped(text, quote) {
392
- const pair = quote + quote;
417
+ var countUnescapedSingleChar = (text, quote) => {
418
+ let count = 0;
419
+ for (let index = 0; index < text.length; index++) {
420
+ if (text[index] !== quote) continue;
421
+ if (text[index + 1] === quote) {
422
+ index++;
423
+ continue;
424
+ }
425
+ count++;
426
+ }
427
+ return count;
428
+ };
429
+ var countUnescapedMultiChar = (text, quote) => {
430
+ const escapedPair = quote + quote;
431
+ const quoteLength = quote.length;
432
+ const escapedPairLength = escapedPair.length;
393
433
  let count = 0;
394
- for (const chunk of dividePreserve(text, pair)) {
395
- count += dividePreserve(chunk, quote).length - 1;
434
+ for (let index = 0; index < text.length; ) {
435
+ if (text.startsWith(escapedPair, index)) {
436
+ index += escapedPairLength;
437
+ continue;
438
+ }
439
+ if (text.startsWith(quote, index)) {
440
+ count++;
441
+ index += quoteLength;
442
+ continue;
443
+ }
444
+ index++;
396
445
  }
397
446
  return count;
447
+ };
448
+ function countUnescaped(text, quote) {
449
+ if (isEmptyString(quote)) return 0;
450
+ if (quote.length === 1) return countUnescapedSingleChar(text, quote);
451
+ return countUnescapedMultiChar(text, quote);
398
452
  }
399
453
  function stripOuterQuotes(text, quoteChar, { lenient = true } = {}) {
400
454
  const escapedPair = quoteChar + quoteChar;
@@ -446,7 +500,8 @@ var buildQuotedFields = (line, delimiter, quote, trim, lenient) => {
446
500
  const pieces = dividePreserve(line, delimiter);
447
501
  const state = {
448
502
  fields: [],
449
- current: ""
503
+ current: "",
504
+ insideQuotes: false
450
505
  };
451
506
  for (const piece of pieces) {
452
507
  appendPiece(state, piece, delimiter, quote, trim, lenient);
@@ -456,10 +511,18 @@ var buildQuotedFields = (line, delimiter, quote, trim, lenient) => {
456
511
  }
457
512
  return state.fields;
458
513
  };
514
+ var advanceQuoteState = (insideQuotes, segment, quote) => {
515
+ let nextInsideQuotes = insideQuotes;
516
+ for (const char of segment) {
517
+ if (char === quote) nextInsideQuotes = !nextInsideQuotes;
518
+ }
519
+ return nextInsideQuotes;
520
+ };
459
521
  var appendPiece = (state, piece, delimiter, quote, trim, lenient) => {
460
- state.current = isEmptyString(state.current) ? piece : state.current + delimiter + piece;
461
- const insideQuotes = countUnescaped(state.current, quote) % 2 === 1;
462
- if (!insideQuotes) {
522
+ const segment = isEmptyString(state.current) ? piece : delimiter + piece;
523
+ state.current += segment;
524
+ state.insideQuotes = quote.length === 1 ? advanceQuoteState(state.insideQuotes, segment, quote) : countUnescaped(state.current, quote) % 2 === 1;
525
+ if (!state.insideQuotes) {
463
526
  flushField(state, quote, trim, lenient);
464
527
  }
465
528
  };
@@ -468,6 +531,7 @@ var flushField = (state, quote, trim, lenient) => {
468
531
  if (trim) fieldValue = fieldValue.trim();
469
532
  state.fields.push(fieldValue);
470
533
  state.current = "";
534
+ state.insideQuotes = false;
471
535
  };
472
536
 
473
537
  // src/presets/csv-divider.ts
@@ -519,8 +583,27 @@ function tryExtractQuery(input) {
519
583
  const url = new URL(input);
520
584
  return url.search.startsWith(QUERY_SEPARATORS.QUESTION_MARK) ? url.search.slice(1) : url.search;
521
585
  } catch {
522
- return input;
586
+ return extractQueryFromQuestionMark(input);
587
+ }
588
+ }
589
+ function extractQueryFromQuestionMark(input) {
590
+ const fragmentIndex = input.indexOf("#");
591
+ const withoutFragment = fragmentIndex >= 0 ? input.slice(0, fragmentIndex) : input;
592
+ const questionMarkIndex = withoutFragment.indexOf(
593
+ QUERY_SEPARATORS.QUESTION_MARK
594
+ );
595
+ if (questionMarkIndex < 0) {
596
+ return withoutFragment;
597
+ }
598
+ if (questionMarkIndex === 0) {
599
+ return withoutFragment.slice(1);
600
+ }
601
+ const prefix = withoutFragment.slice(0, questionMarkIndex);
602
+ const hasQuerySeparatorBefore = prefix.includes(QUERY_SEPARATORS.AMPERSAND) || prefix.includes(QUERY_SEPARATORS.EQUALS);
603
+ if (hasQuerySeparatorBefore) {
604
+ return withoutFragment;
523
605
  }
606
+ return withoutFragment.slice(questionMarkIndex + 1);
524
607
  }
525
608
  function stripLeadingQuestionMark(query) {
526
609
  return query.startsWith(QUERY_SEPARATORS.QUESTION_MARK) ? query.slice(1) : query;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nyaomaru/divider",
3
3
  "type": "module",
4
- "version": "2.0.6",
4
+ "version": "2.0.7",
5
5
  "description": "To divide string or string[] with a given separator",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",