trousse 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/urls.js +48 -16
  2. package/package.json +1 -1
package/dist/urls.js CHANGED
@@ -427,6 +427,18 @@ const resolveUrl = (url, base) => {
427
427
  if (!parsed || !isHttpUrl(parsed)) return;
428
428
  return parsed.href;
429
429
  };
430
+ const valueShapeEscapeRegex = /(%(?:26|23|25|2B|3B))/gi;
431
+ const nameShapeEscapeRegex = /(%(?:26|23|25|2B|3B|3D))/gi;
432
+ const decodeQueryPart = (part, shapeRegex) => {
433
+ return part.replace(plusRegex, "%20").split(shapeRegex).map((chunk, index) => index % 2 ? chunk.toUpperCase() : decodePercent(chunk)).join("");
434
+ };
435
+ const canonicalizeQuery = (search) => {
436
+ return `?${search.slice(1).split("&").map((pair) => {
437
+ const at = pair.indexOf("=");
438
+ if (at === -1) return decodeQueryPart(pair, nameShapeEscapeRegex);
439
+ return `${decodeQueryPart(pair.slice(0, at), nameShapeEscapeRegex)}=${decodeQueryPart(pair.slice(at + 1), valueShapeEscapeRegex)}`;
440
+ }).join("&")}`;
441
+ };
430
442
  const decodeAndNormalizeEncoding = (value) => {
431
443
  if (!value.includes("%")) return value;
432
444
  return value.replace(percentEscapeRegex, (hex) => {
@@ -471,7 +483,10 @@ const normalizeUrl = (url, options) => {
471
483
  if (options.collapseSlashes) pathname = pathname.replace(repeatedSlashesRegex, "/");
472
484
  if (options.stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) pathname = pathname.slice(0, -1);
473
485
  parsed.pathname = pathname;
474
- if (options.normalizeEncoding && parsed.search.includes("%")) parsed.search = decodeAndNormalizeEncoding(parsed.search);
486
+ if (options.normalizeEncoding && parsed.search.length > 1) {
487
+ parsed.search = canonicalizeQuery(parsed.search);
488
+ parsed.search = parsed.search.replace(percentEscapeRegex, (hex) => hex.toUpperCase());
489
+ }
475
490
  if (options.normalizeEncoding && parsed.hash.includes("%")) parsed.hash = decodeAndNormalizeEncoding(parsed.hash);
476
491
  if (options.stripQuery) parsed.search = "";
477
492
  if (parsed.search && (options.stripQueryParams || options.lowercaseQuery)) {
@@ -522,7 +537,15 @@ const createUrlVariants = (url) => {
522
537
  const sameQuerySpellings = [
523
538
  ["+", "%20"],
524
539
  [":", "%3A"],
525
- ["@", "%40"]
540
+ ["@", "%40"],
541
+ ["/", "%2F"],
542
+ [",", "%2C"],
543
+ ["(", "%28"],
544
+ [")", "%29"],
545
+ ["*", "%2A"],
546
+ ["!", "%21"],
547
+ ["'", "%27"],
548
+ ["$", "%24"]
526
549
  ];
527
550
  const percentEscapeRegex = /%[0-9a-f]{2}/gi;
528
551
  const addLowercaseHexSpellings = (url, spellings) => {
@@ -532,25 +555,34 @@ const addLowercaseHexSpellings = (url, spellings) => {
532
555
  });
533
556
  return [.../* @__PURE__ */ new Set([...spellings, ...lowercaseHexSpellings])];
534
557
  };
558
+ const maxMixedQuerySpellings = 4;
559
+ const respellQuery = (spelling, queryIndex, pairs, toEncoded) => {
560
+ const fragmentIndex = spelling.indexOf("#");
561
+ const queryEnd = fragmentIndex === -1 ? spelling.length : fragmentIndex;
562
+ let query = spelling.slice(queryIndex, queryEnd);
563
+ for (const [written, encoded] of pairs) query = toEncoded ? query.replaceAll(written, encoded) : query.replaceAll(encoded, written);
564
+ return `${spelling.slice(0, queryIndex)}${query}${spelling.slice(queryEnd)}`;
565
+ };
535
566
  const createUrlSpellings = (url) => {
536
567
  const uppercaseHexUrl = url.replace(percentEscapeRegex, (hex) => hex.toUpperCase());
537
- let spellings = [.../* @__PURE__ */ new Set([url, uppercaseHexUrl])];
538
568
  const hashIndex = url.indexOf("#");
539
569
  const queryEnd = hashIndex === -1 ? url.length : hashIndex;
540
570
  const queryIndex = url.indexOf("?");
541
- if (queryIndex === -1 || queryIndex > queryEnd) return addLowercaseHexSpellings(url, spellings);
542
- for (const [written, encoded] of sameQuerySpellings) {
543
- const swapped = spellings.flatMap((spelling) => {
544
- const fragmentIndex = spelling.indexOf("#");
545
- const spellingQueryEnd = fragmentIndex === -1 ? spelling.length : fragmentIndex;
546
- const head = spelling.slice(0, queryIndex);
547
- const query = spelling.slice(queryIndex, spellingQueryEnd);
548
- const fragment = spelling.slice(spellingQueryEnd);
549
- return [`${head}${query.replaceAll(encoded, written)}${fragment}`, `${head}${query.replaceAll(written, encoded)}${fragment}`];
550
- });
551
- spellings = [...new Set(swapped)];
552
- }
553
- return addLowercaseHexSpellings(url, [url, ...spellings]);
571
+ if (queryIndex === -1 || queryIndex > queryEnd) return addLowercaseHexSpellings(url, [.../* @__PURE__ */ new Set([url, uppercaseHexUrl])]);
572
+ const query = uppercaseHexUrl.slice(queryIndex, queryEnd);
573
+ const present = sameQuerySpellings.filter(([written, encoded]) => {
574
+ return query.includes(written) || query.includes(encoded);
575
+ });
576
+ const bases = [.../* @__PURE__ */ new Set([url, uppercaseHexUrl])];
577
+ const throughout = bases.flatMap((base) => [respellQuery(base, queryIndex, present, false), respellQuery(base, queryIndex, present, true)]);
578
+ let mixes = bases;
579
+ if (present.length <= maxMixedQuerySpellings) for (const pair of present) mixes = mixes.flatMap((spelling) => [respellQuery(spelling, queryIndex, [pair], false), respellQuery(spelling, queryIndex, [pair], true)]);
580
+ return addLowercaseHexSpellings(url, [.../* @__PURE__ */ new Set([
581
+ url,
582
+ uppercaseHexUrl,
583
+ ...throughout,
584
+ ...mixes
585
+ ])]);
554
586
  };
555
587
  const genericSecondLevels = [
556
588
  "ac",
package/package.json CHANGED
@@ -46,5 +46,5 @@
46
46
  "dependencies": {
47
47
  "entities": "^8.1.0"
48
48
  },
49
- "version": "3.6.0"
49
+ "version": "3.7.0"
50
50
  }