]> Lady’s Gitweb - Pisces/commitdiff
Add string functions and unit tests
authorLady <redacted>
Fri, 22 Jul 2022 02:47:55 +0000 (19:47 -0700)
committerLady <redacted>
Fri, 12 May 2023 03:56:47 +0000 (20:56 -0700)
string.js
string.test.js [new file with mode: 0644]

index 17e8736bec4447b861025cebf3e2cc723560fe8f..d65d2946d7db01255ca16bcf717dd6f3705697c6 100644 (file)
--- a/string.js
+++ b/string.js
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
+import { bind, call, identity, makeCallable } from "./function.js";
+import { getPrototype, objectCreate } from "./object.js";
+
+export const {
+  /**
+   * Returns the result of converting the provided value to A·S·C·I·I
+   * lowercase.
+   */
+  asciiLowercase,
+
+  /**
+   * Returns the result of converting the provided value to A·S·C·I·I
+   * uppercase.
+   */
+  asciiUppercase,
+} = (() => {
+  const {
+    toLowerCase: stringToLowercase,
+    toUpperCase: stringToUppercase,
+  } = String.prototype;
+  return {
+    asciiLowercase: ($) =>
+      stringReplaceAll(
+        `${$}`,
+        /[A-Z]/gu,
+        makeCallable(stringToLowercase),
+      ),
+    asciiUppercase: ($) =>
+      stringReplaceAll(
+        `${$}`,
+        /[a-z]/gu,
+        makeCallable(stringToUppercase),
+      ),
+  };
+})();
+
+export const {
+  /**
+   * Returns an iterator over the code units in the string
+   * representation of the provided value.
+   */
+  codeUnits,
+
+  /**
+   * Returns an iterator over the codepoints in the string
+   * representation of the provided value.
+   */
+  codepoints,
+
+  /**
+   * Returns an iterator over the scalar values in the string
+   * representation of the provided value.
+   *
+   * Codepoints which are not valid Unicode scalar values are replaced
+   * with U+FFFF.
+   */
+  scalarValues,
+
+  /**
+   * Returns the result of converting the provided value to a string of
+   * scalar values by replacing (unpaired) surrogate values with
+   * U+FFFD.
+   */
+  scalarValueString,
+} = (() => {
+  const {
+    iterator: iteratorSymbol,
+    toStringTag: toStringTagSymbol,
+  } = Symbol;
+  const { [iteratorSymbol]: arrayIterator } = Array.prototype;
+  const arrayIteratorPrototype = Object.getPrototypeOf(
+    [][iteratorSymbol](),
+  );
+  const { next: arrayIteratorNext } = arrayIteratorPrototype;
+  const iteratorPrototype = Object.getPrototypeOf(
+    arrayIteratorPrototype,
+  );
+  const { [iteratorSymbol]: stringIterator } = String.prototype;
+  const stringIteratorPrototype = Object.getPrototypeOf(
+    ""[iteratorSymbol](),
+  );
+  const { next: stringIteratorNext } = stringIteratorPrototype;
+
+  /**
+   * An iterator object for iterating over code values (either code
+   * units or codepoints) in a string.
+   *
+   * ※ This constructor is not exposed.
+   */
+  const StringCodeValueIterator = class extends identity {
+    #allowSurrogates;
+    #baseIterator;
+
+    /**
+     * Constructs a new string code value iterator from the provided
+     * base iterator.
+     *
+     * If the provided base iterator is an array iterator, this is a
+     * code unit iterator.  If the provided iterator is a string
+     * iterator and surrogates are allowed, this is a codepoint
+     * iterator. If the provided iterator is a string iterator and
+     * surrogates are not allowed, this is a scalar value iterator.
+     */
+    constructor(baseIterator, allowSurrogates = true) {
+      super(objectCreate(stringCodeValueIteratorPrototype));
+      this.#allowSurrogates = !!allowSurrogates;
+      this.#baseIterator = baseIterator;
+    }
+
+    /** Provides the next code value in the iterator. */
+    next() {
+      const baseIterator = this.#baseIterator;
+      switch (getPrototype(baseIterator)) {
+        case arrayIteratorPrototype: {
+          // The base iterator is iterating over U·C·S characters.
+          const {
+            value: ucsCharacter,
+            done,
+          } = call(arrayIteratorNext, baseIterator, []);
+          return done
+            ? { value: undefined, done: true }
+            : { value: getCodeUnit(ucsCharacter, 0), done: false };
+        }
+        case stringIteratorPrototype: {
+          // The base iterator is iterating over Unicode characters.
+          const {
+            value: character,
+            done,
+          } = call(stringIteratorNext, baseIterator, []);
+          if (done) {
+            // The base iterator has been exhausted.
+            return { value: undefined, done: true };
+          } else {
+            // The base iterator provided a character; yield the
+            // codepoint.
+            const codepoint = getCodepoint(character, 0);
+            return {
+              value: this.#allowSurrogates || codepoint <= 0xD7FF ||
+                  codepoint >= 0xE000
+                ? codepoint
+                : 0xFFFD,
+              done: false,
+            };
+          }
+        }
+        default: {
+          // Should not be possible!
+          throw new TypeError(
+            "Piscēs: Unrecognized base iterator type in %StringCodeValueIterator%.",
+          );
+        }
+      }
+    }
+  };
+
+  const {
+    next: stringCodeValueIteratorNext,
+  } = StringCodeValueIterator.prototype;
+  const stringCodeValueIteratorPrototype = objectCreate(
+    iteratorPrototype,
+    {
+      next: {
+        configurable: true,
+        enumerable: false,
+        value: stringCodeValueIteratorNext,
+        writable: true,
+      },
+      [toStringTagSymbol]: {
+        configurable: true,
+        enumerable: false,
+        value: "String Code Value Iterator",
+        writable: false,
+      },
+    },
+  );
+  const scalarValueIterablePrototype = {
+    [iteratorSymbol]() {
+      return {
+        next: bind(
+          stringCodeValueIteratorNext,
+          new StringCodeValueIterator(
+            call(stringIterator, this.source, []),
+            false,
+          ),
+          [],
+        ),
+      };
+    },
+  };
+
+  return {
+    codeUnits: ($) =>
+      new StringCodeValueIterator(call(arrayIterator, $, [])),
+    codepoints: ($) =>
+      new StringCodeValueIterator(
+        call(stringIterator, $, []),
+        true,
+      ),
+    scalarValues: ($) =>
+      new StringCodeValueIterator(
+        call(stringIterator, $, []),
+        false,
+      ),
+    scalarValueString: ($) =>
+      stringFromCodepoints(...objectCreate(
+        scalarValueIterablePrototype,
+        { source: { value: $ } },
+      )),
+  };
+})();
+
 /**
- * Returns the result of converting the provided value to A·S·C·I·I
- * lowercase.
+ * Returns an iterator over the codepoints in the string representation
+ * of the provided value according to the algorithm of
+ * String::[Symbol.iterator].
  */
-export const asciiLowercase = ($) =>
-  `${$}`.replaceAll(
-    /[A-Z]/gu,
-    Function.prototype.call.bind(String.prototype.toLowerCase),
-  );
+export const characters = makeCallable(
+  String.prototype[Symbol.iterator],
+);
 
 /**
- * Returns the result of converting the provided value to A·S·C·I·I
- * uppercase.
+ * Returns the character at the provided position in the string
+ * representation of the provided value according to the algorithm of
+ * String::codePointAt.
  */
-export const asciiUppercase = ($) =>
-  `${$}`.replaceAll(
-    /[a-z]/gu,
-    Function.prototype.call.bind(String.prototype.toUpperCase),
-  );
+export const getCharacter = ($, pos) => {
+  const codepoint = getCodepoint($, pos);
+  return codepoint == null
+    ? undefined
+    : stringFromCodepoints(codepoint);
+};
 
 /**
- * Returns the result of converting the provided value to a string of
- * scalar values by replacing (unpaired) surrogate values with U+FFFD.
+ * Returns the code unit at the provided position in the string
+ * representation of the provided value according to the algorithm of
+ * String::charAt.
  */
-export const scalarValueString = ($) =>
-  String.fromCodePoint(
-    ...function* () {
-      for (const char of `${$}`) {
-        const scalar = char.codePointAt(0);
-        yield scalar >= 0xD800 && scalar <= 0xDFFF ? 0xFFFD : scalar;
-      }
-    }(),
-  );
+export const getCodeUnit = makeCallable(String.prototype.charCodeAt);
+
+/**
+ * Returns the codepoint at the provided position in the string
+ * representation of the provided value according to the algorithm of
+ * String::codePointAt.
+ */
+export const getCodepoint = makeCallable(String.prototype.codePointAt);
+
+/**
+ * Returns the index of the first occurrence of the search string in
+ * the string representation of the provided value according to the
+ * algorithm of String::indexOf.
+ */
+export const getFirstSubstringIndex = makeCallable(
+  String.prototype.indexOf,
+);
+
+/**
+ * Returns the index of the last occurrence of the search string in the
+ * string representation of the provided value according to the
+ * algorithm of String::lastIndexOf.
+ */
+export const getLastSubstringIndex = makeCallable(
+  String.prototype.lastIndexOf,
+);
+
+/**
+ * Returns the result of joining the provided iterable.
+ *
+ * If no separator is provided, it defaults to ",".
+ *
+ * If a value is nullish, it will be stringified as the empty string.
+ */
+export const join = (() => {
+  const { join: arrayJoin } = Array.prototype;
+  const join = ($, separator = ",") =>
+    call(arrayJoin, [...$], [`${separator}`]);
+  return join;
+})();
+
+export const {
+  /**
+   * Returns a string created from the raw value of the tagged template
+   * literal.
+   *
+   * ※ This is an alias for String.raw.
+   */
+  raw: rawString,
+
+  /**
+   * Returns a string created from the provided code units.
+   *
+   * ※ This is an alias for String.fromCharCode.
+   */
+  fromCharCode: stringFromCodeUnits,
+
+  /**
+   * Returns a string created from the provided codepoints.
+   *
+   * ※ This is an alias for String.fromCodePoint.
+   */
+  fromCodePoint: stringFromCodepoints,
+} = String;
 
 /**
  * Returns the result of splitting the provided value on A·S·C·I·I
  * whitespace.
  */
 export const splitOnASCIIWhitespace = ($) =>
-  stripAndCollapseASCIIWhitespace($).split(" ");
+  stringSplit(stripAndCollapseASCIIWhitespace($), " ");
 
 /**
  * Returns the result of splitting the provided value on commas,
  * trimming A·S·C·I·I whitespace from the resulting tokens.
  */
 export const splitOnCommas = ($) =>
-  stripLeadingAndTrailingASCIIWhitespace(
-    `${$}`.replaceAll(
-      /[\n\r\t\f ]*,[\n\r\t\f ]*/gu,
-      ",",
+  stringSplit(
+    stripLeadingAndTrailingASCIIWhitespace(
+      stringReplaceAll(
+        `${$}`,
+        /[\n\r\t\f ]*,[\n\r\t\f ]*/gu,
+        ",",
+      ),
     ),
-  ).split(",");
+    ",",
+  );
 
 /**
- * Returns the result of stripping leading and trailing A·S·C·I·I
- * whitespace from the provided value.
+ * Returns the result of catenating the string representations of the
+ * provided values, returning a new string according to the algorithm
+ * of String::concat.
  */
-export const stripLeadingAndTrailingASCIIWhitespace = ($) =>
-  /^[\n\r\t\f ]*([^]*?)[\n\r\t\f ]*$/u.exec($)[1];
+export const stringCatenate = makeCallable(String.prototype.concat);
+
+/**
+ * Returns whether the string representation of the provided value ends
+ * with the provided search string according to the algorithm of
+ * String::endsWith.
+ */
+export const stringEndsWith = makeCallable(String.prototype.endsWith);
+
+/**
+ * Returns whether the string representation of the provided value
+ * contains the provided search string according to the algorithm of
+ * String::includes.
+ */
+export const stringIncludes = makeCallable(String.prototype.includes);
+
+/**
+ * Returns the result of matching the string representation of the
+ * provided value with the provided matcher according to the algorithm
+ * of String::match.
+ */
+export const stringMatch = makeCallable(String.prototype.match);
+
+/**
+ * Returns the result of matching the string representation of the
+ * provided value with the provided matcher according to the algorithm
+ * of String::matchAll.
+ */
+export const stringMatchAll = makeCallable(String.prototype.matchAll);
+
+/**
+ * Returns the normalized form of the string representation of the
+ * provided value according to the algorithm of String::matchAll.
+ */
+export const stringNormalize = makeCallable(
+  String.prototype.normalize,
+);
+
+/**
+ * Returns the result of padding the end of the string representation
+ * of the provided value padded until it is the desired length
+ * according to the algorithm of String::padEnd.
+ */
+export const stringPadEnd = makeCallable(String.prototype.padEnd);
+
+/**
+ * Returns the result of padding the start of the string representation
+ * of the provided value padded until it is the desired length
+ * according to the algorithm of String::padStart.
+ */
+export const stringPadStart = makeCallable(String.prototype.padStart);
+
+/**
+ * Returns the result of repeating the string representation of the
+ * provided value the provided number of times according to the
+ * algorithm of String::repeat.
+ */
+export const stringRepeat = makeCallable(String.prototype.repeat);
+
+/**
+ * Returns the result of replacing the string representation of the
+ * provided value with the provided replacement, using the provided
+ * matcher and according to the algorithm of String::replace.
+ */
+export const stringReplace = makeCallable(String.prototype.replace);
+
+/**
+ * Returns the result of replacing the string representation of the
+ * provided value with the provided replacement, using the provided
+ * matcher and according to the algorithm of String::replaceAll.
+ */
+export const stringReplaceAll = makeCallable(
+  String.prototype.replaceAll,
+);
+
+/**
+ * Returns the result of searching the string representation of the
+ * provided value using the provided matcher and according to the
+ * algorithm of String::search.
+ */
+export const stringSearch = makeCallable(String.prototype.search);
+
+/**
+ * Returns a slice of the string representation of the provided value
+ * according to the algorithm of String::slice.
+ */
+export const stringSlice = makeCallable(String.prototype.slice);
+
+/**
+ * Returns the result of splitting of the string representation of the
+ * provided value on the provided separator according to the algorithm
+ * of String::split.
+ */
+export const stringSplit = makeCallable(String.prototype.split);
+
+/**
+ * Returns whether the string representation of the provided value
+ * starts with the provided search string according to the algorithm of
+ * String::startsWith.
+ */
+export const stringStartsWith = makeCallable(
+  String.prototype.startsWith,
+);
+
+/**
+ * Returns the `[[StringData]]` of the provided value.
+ *
+ * ☡ This function will throw if the provided object does not have a
+ * `[[StringData]]` internal slot.
+ */
+export const stringValue = makeCallable(String.prototype.valueOf);
 
 /**
  * Returns the result of stripping leading and trailing A·S·C·I·I
  * whitespace from the provided value and collapsing other A·S·C·I·I
- * whitespace in the provided value.
+ * whitespace in the string representation of the provided value.
  */
 export const stripAndCollapseASCIIWhitespace = ($) =>
   stripLeadingAndTrailingASCIIWhitespace(
-    `${$}`.replaceAll(
+    stringReplaceAll(
+      `${$}`,
       /[\n\r\t\f ]+/gu,
       " ",
     ),
   );
+
+/**
+ * Returns the result of stripping leading and trailing A·S·C·I·I
+ * whitespace from the string representation of the provided value.
+ */
+export const stripLeadingAndTrailingASCIIWhitespace = (() => {
+  const { exec: reExec } = RegExp.prototype;
+  return ($) =>
+    call(reExec, /^[\n\r\t\f ]*([^]*?)[\n\r\t\f ]*$/u, [$])[1];
+})();
+
+/**
+ * Returns a substring of the string representation of the provided
+ * value according to the algorithm of String::substring.
+ */
+export const substring = makeCallable(String.prototype.substring);
diff --git a/string.test.js b/string.test.js
new file mode 100644 (file)
index 0000000..5d713f0
--- /dev/null
@@ -0,0 +1,371 @@
+// ♓🌟 Piscēs ∷ string.test.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import {
+  assertEquals,
+  assertStrictEquals,
+  describe,
+  it,
+} from "./dev-deps.js";
+import {
+  asciiLowercase,
+  asciiUppercase,
+  codepoints,
+  codeUnits,
+  getCharacter,
+  join,
+  scalarValues,
+  scalarValueString,
+  splitOnASCIIWhitespace,
+  splitOnCommas,
+  stripAndCollapseASCIIWhitespace,
+  stripLeadingAndTrailingASCIIWhitespace,
+} from "./string.js";
+
+describe("asciiLowercase", () => {
+  it("[[Call]] lowercases (just) A·S·C·I·I letters", () => {
+    assertStrictEquals(asciiLowercase("aBſÆss FtɁɂß"), "abſÆss ftɁɂß");
+  });
+});
+
+describe("asciiUppercase", () => {
+  it("[[Call]] uppercases (just) A·S·C·I·I letters", () => {
+    assertStrictEquals(asciiUppercase("aBſÆss FtɁɂß"), "ABſÆSS FTɁɂß");
+  });
+});
+
+describe("codeUnits", () => {
+  it("[[Call]] returns an iterable", () => {
+    assertStrictEquals(
+      typeof codeUnits("")[Symbol.iterator],
+      "function",
+    );
+  });
+
+  it("[[Call]] returns an iterator", () => {
+    assertStrictEquals(typeof codeUnits("").next, "function");
+  });
+
+  it("[[Call]] returns a string code value iterator", () => {
+    assertStrictEquals(
+      codeUnits("")[Symbol.toStringTag],
+      "String Code Value Iterator",
+    );
+  });
+
+  it("[[Call]] iterates over the code units", () => {
+    assertEquals([
+      ...codeUnits("Ii🎙\uDFFF\uDD96\uD83C\uD800🆗☺"),
+    ], [
+      0x49,
+      0x69,
+      0xD83C,
+      0xDF99,
+      0xDFFF,
+      0xDD96,
+      0xD83C,
+      0xD800,
+      0xD83C,
+      0xDD97,
+      0x263A,
+    ]);
+  });
+});
+
+describe("codepoints", () => {
+  it("[[Call]] returns an iterable", () => {
+    assertStrictEquals(
+      typeof codepoints("")[Symbol.iterator],
+      "function",
+    );
+  });
+
+  it("[[Call]] returns an iterator", () => {
+    assertStrictEquals(typeof codepoints("").next, "function");
+  });
+
+  it("[[Call]] returns a string code value iterator", () => {
+    assertStrictEquals(
+      codepoints("")[Symbol.toStringTag],
+      "String Code Value Iterator",
+    );
+  });
+
+  it("[[Call]] iterates over the codepoints", () => {
+    assertEquals([
+      ...codepoints("Ii🎙\uDFFF\uDD96\uD83C\uD800🆗☺"),
+    ], [
+      0x49,
+      0x69,
+      0x1F399,
+      0xDFFF,
+      0xDD96,
+      0xD83C,
+      0xD800,
+      0x1F197,
+      0x263A,
+    ]);
+  });
+});
+
+describe("getCharacter", () => {
+  it("[[Call]] returns the character at the provided position", () => {
+    assertStrictEquals(getCharacter("Ii🎙🆗☺", 4), "🆗");
+  });
+
+  it("[[Call]] returns a low surrogate if the provided position splits a character", () => {
+    assertStrictEquals(getCharacter("Ii🎙🆗☺", 5), "\uDD97");
+  });
+
+  it("[[Call]] returns undefined for an out‐of‐bounds index", () => {
+    assertStrictEquals(getCharacter("Ii🎙🆗☺", -1), void {});
+    assertStrictEquals(getCharacter("Ii🎙🆗☺", 7), void {});
+  });
+});
+
+describe("join", () => {
+  it("[[Call]] joins the provided iterator with the provided separartor", () => {
+    assertStrictEquals(join([1, 2, 3, 4].values(), "☂"), "1☂2☂3☂4");
+  });
+
+  it('[[Call]] uses "," if no separator is provided', () => {
+    assertStrictEquals(join([1, 2, 3, 4].values()), "1,2,3,4");
+  });
+
+  it("[[Call]] uses the empty sting for nullish values", () => {
+    assertStrictEquals(
+      join([null, , null, undefined].values(), "☂"),
+      "☂☂☂",
+    );
+  });
+});
+
+describe("scalarValueString", () => {
+  it("[[Call]] replaces invalid values", () => {
+    assertStrictEquals(
+      scalarValueString("Ii🎙\uDFFF\uDD96\uD83C\uD800🆗☺"),
+      "Ii🎙\uFFFD\uFFFD\uFFFD\uFFFD🆗☺",
+    );
+  });
+});
+
+describe("scalarValues", () => {
+  it("[[Call]] returns an iterable", () => {
+    assertStrictEquals(
+      typeof scalarValues("")[Symbol.iterator],
+      "function",
+    );
+  });
+
+  it("[[Call]] returns an iterator", () => {
+    assertStrictEquals(typeof scalarValues("").next, "function");
+  });
+
+  it("[[Call]] returns a string code value iterator", () => {
+    assertStrictEquals(
+      scalarValues("")[Symbol.toStringTag],
+      "String Code Value Iterator",
+    );
+  });
+
+  it("[[Call]] iterates over the scalar values", () => {
+    assertEquals([
+      ...scalarValues("Ii🎙\uDFFF\uDD96\uD83C\uD800🆗☺"),
+    ], [
+      0x49,
+      0x69,
+      0x1F399,
+      0xFFFD,
+      0xFFFD,
+      0xFFFD,
+      0xFFFD,
+      0x1F197,
+      0x263A,
+    ]);
+  });
+});
+
+describe("splitOnASCIIWhitespace", () => {
+  it("[[Call]] splits on sequences of spaces", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️   🅱️ 🆎  🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] splits on sequences of tabs", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️\t\t\t🅱️\t🆎\t\t🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] splits on sequences of carriage returns", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️\r\r\r🅱️\r🆎\r\r🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] splits on sequences of newlines", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️\r\r\r🅱️\r🆎\r\r🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] splits on sequences of form feeds", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️\f\f\f🅱️\f🆎\f\f🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] splits on mixed whitespace", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("🅰️\f \t\n🅱️\r\n\r🆎\n\f🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] returns an array of just the empty string for the empty string", () => {
+    assertEquals(splitOnASCIIWhitespace(""), [""]);
+  });
+
+  it("[[Call]] returns a single token if there are no spaces", () => {
+    assertEquals(splitOnASCIIWhitespace("abcd"), ["abcd"]);
+  });
+
+  it("[[Call]] does not split on other kinds of whitespace", () => {
+    assertEquals(
+      splitOnASCIIWhitespace("a\u202F\u205F\xa0\v\0\bb"),
+      ["a\u202F\u205F\xa0\v\0\bb"],
+    );
+  });
+
+  it("[[Call]] trims leading and trailing whitespace", () => {
+    assertEquals(
+      splitOnASCIIWhitespace(
+        "\f\r\n\r\n \n\t🅰️\f \t\n🅱️\r🆎\n\f🅾️\n\f",
+      ),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+});
+
+describe("splitOnCommas", () => {
+  it("[[Call]] splits on commas", () => {
+    assertEquals(
+      splitOnCommas("🅰️,🅱️,🆎,🅾️"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+  });
+
+  it("[[Call]] returns an array of just the empty string for the empty string", () => {
+    assertEquals(splitOnCommas(""), [""]);
+  });
+
+  it("[[Call]] returns a single token if there are no commas", () => {
+    assertEquals(splitOnCommas("abcd"), ["abcd"]);
+  });
+
+  it("[[Call]] splits into empty strings if there are only commas", () => {
+    assertEquals(splitOnCommas(",,,"), ["", "", "", ""]);
+  });
+
+  it("[[Call]] trims leading and trailing whitespace", () => {
+    assertEquals(
+      splitOnCommas("\f\r\n\r\n \n\t🅰️,🅱️,🆎,🅾️\n\f"),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+    assertEquals(
+      splitOnCommas("\f\r\n\r\n \n\t,,,\n\f"),
+      ["", "", "", ""],
+    );
+  });
+
+  it("[[Call]] removes whitespace from the split tokens", () => {
+    assertEquals(
+      splitOnCommas(
+        "\f\r\n\r\n \n\t🅰️\f , \t\n🅱️,\r\n\r🆎\n\f,🅾️\n\f",
+      ),
+      ["🅰️", "🅱️", "🆎", "🅾️"],
+    );
+    assertEquals(
+      splitOnCommas("\f\r\n\r\n \n\t\f , \t\n,\r\n\r\n\f,\n\f"),
+      ["", "", "", ""],
+    );
+  });
+});
+
+describe("stripAndCollapseASCIIWhitespace", () => {
+  it("[[Call]] collapses mixed inner whitespace", () => {
+    assertEquals(
+      stripAndCollapseASCIIWhitespace("🅰️\f \t\n🅱️\r\n\r🆎\n\f🅾️"),
+      "🅰️ 🅱️ 🆎 🅾️",
+    );
+  });
+
+  it("[[Call]] trims leading and trailing whitespace", () => {
+    assertStrictEquals(
+      stripAndCollapseASCIIWhitespace(
+        "\f\r\n\r\n \n\t\f 🅰️\f \t\n🅱️\r\n\r🆎\n\f🅾️\n\f",
+      ),
+      "🅰️ 🅱️ 🆎 🅾️",
+    );
+  });
+
+  it("[[Call]] returns the empty string for strings of whitespace", () => {
+    assertStrictEquals(
+      stripAndCollapseASCIIWhitespace("\f\r\n\r\n \n\t\f \n\f"),
+      "",
+    );
+  });
+
+  it("[[Call]] does not collapse other kinds of whitespace", () => {
+    assertEquals(
+      stripAndCollapseASCIIWhitespace("a\u202F\u205F\xa0\v\0\bb"),
+      "a\u202F\u205F\xa0\v\0\bb",
+    );
+  });
+});
+
+describe("stripLeadingAndTrailingASCIIWhitespace", () => {
+  it("[[Call]] trims leading and trailing whitespace", () => {
+    assertStrictEquals(
+      stripLeadingAndTrailingASCIIWhitespace(
+        "\f\r\n\r\n \n\t\f 🅰️🅱️🆎🅾️\n\f",
+      ),
+      "🅰️🅱️🆎🅾️",
+    );
+  });
+
+  it("[[Call]] returns the empty string for strings of whitespace", () => {
+    assertStrictEquals(
+      stripLeadingAndTrailingASCIIWhitespace("\f\r\n\r\n \n\t\f \n\f"),
+      "",
+    );
+  });
+
+  it("[[Call]] does not trim other kinds of whitespace", () => {
+    assertEquals(
+      stripLeadingAndTrailingASCIIWhitespace(
+        "\v\u202F\u205Fx\0\b\xa0",
+      ),
+      "\v\u202F\u205Fx\0\b\xa0",
+    );
+  });
+
+  it("[[Call]] does not adjust inner whitespace", () => {
+    assertEquals(
+      stripLeadingAndTrailingASCIIWhitespace("a   b"),
+      "a   b",
+    );
+  });
+});
This page took 0.038414 seconds and 4 git commands to generate.