1 // ♓🌟 Piscēs ∷ string.js
2 // ====================================================================
4 // Copyright © 2022 Lady [@ Lady’s Computer].
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
10 import { bind
, call
, identity
, makeCallable
} from "./function.js";
20 * A RegExp·like object which only matches entire strings.
22 * Matchers are callable objects and will return true if they are
23 * called with a string that they match, and false otherwise.
24 * Matchers will always return false if called with nonstrings,
25 * although other methods like `exec` may still return true.
30 const { prototype: rePrototype
} = RE
;
31 const { exec
: reExec
, toString
: reToString
} = rePrototype
;
33 Object
.getOwnPropertyDescriptor(rePrototype
, "dotAll").get;
35 Object
.getOwnPropertyDescriptor(rePrototype
, "global").get;
37 Object
.getOwnPropertyDescriptor(rePrototype
, "hasIndices").get;
39 Object
.getOwnPropertyDescriptor(rePrototype
, "ignoreCase").get;
41 Object
.getOwnPropertyDescriptor(rePrototype
, "multiline").get;
43 Object
.getOwnPropertyDescriptor(rePrototype
, "source").get;
45 Object
.getOwnPropertyDescriptor(rePrototype
, "sticky").get;
47 Object
.getOwnPropertyDescriptor(rePrototype
, "unicode").get;
49 const Matcher
= class extends identity
{
53 * Constructs a new Matcher from the provided source.
55 * If the provided source is a regular expression, then it must
56 * have the unicode flag set. Otherwise, it is interpreted as the
57 * string source of a regular expression with the unicode flag set.
59 * Other flags are taken from the provided regular expression
60 * object, if any are present.
62 * A name for the matcher may be provided as the second argument.
64 * ☡ If the provided source regular expression uses nongreedy
65 * quantifiers, it may not match the whole string even if a match
66 * with the whole string is possible. Surround the regular
67 * expression with `^(?:` and `)$` if you don’t want nongreedy
68 * regular expressions to fail when shorter matches are possible.
70 constructor(source
, name
= undefined) {
73 if (typeof $ !== "string") {
74 // The provided value is not a string.
77 // The provided value is a string. Set the `lastIndex` of
78 // the regular expression to 0 and see if the first attempt
79 // at a match matches the whole string.
81 return call(reExec
, regExp
, [$])?.[0] === $;
85 const regExp
= this.#regExp
= (() => {
87 call(reExec
, source
, [""]); // throws if source not a RegExp
89 return new RE(`${source}`, "u");
91 const unicode
= call(getUnicode
, source
, []);
93 // The provided regular expression does not have a unicode
96 `Piscēs: Cannot create Matcher from non‐Unicode RegExp: ${source}`,
99 // The provided regular expression has a unicode flag.
100 return new RE(source
);
103 return defineOwnProperties(
104 setPrototype(this, matcherPrototype
),
115 : `Matcher(${call(reToString, regExp, [])})`,
121 /** Gets whether the dotAll flag is present on this Matcher. */
123 return call(getDotAll
, this.#regExp
, []);
127 * Executes this Matcher on the provided value and returns the
128 * result if there is a match, or null otherwise.
130 * Matchers only match if they can match the entire value on the
134 const regExp
= this.#regExp
;
135 const string
= `${$}`;
136 regExp
.lastIndex
= 0;
137 const result
= call(reExec
, regExp
, [string
]);
138 if (result
?.[0] === string
) {
139 // The entire string was matched.
142 // The entire string was not matched.
147 /** Gets whether the global flag is present on this Matcher. */
149 return call(getGlobal
, this.#regExp
, []);
152 /** Gets whether the hasIndices flag is present on this Matcher. */
154 return call(getHasIndices
, this.#regExp
, []);
157 /** Gets whether the ignoreCase flag is present on this Matcher. */
159 return call(getIgnoreCase
, this.#regExp
, []);
162 /** Gets whether the multiline flag is present on this Matcher. */
164 return call(getMultiline
, this.#regExp
, []);
167 /** Gets the regular expression source for this Matcher. */
169 return call(getSource
, this.#regExp
, []);
172 /** Gets whether the sticky flag is present on this Matcher. */
174 return call(getSticky
, this.#regExp
, []);
178 * Gets whether the unicode flag is present on this Matcher.
180 * ※ This will always be true.
183 return call(getUnicode
, this.#regExp
, []);
186 const matcherPrototype
= setPrototype(
196 * Returns the result of converting the provided value to A·S·C·I·I
202 * Returns the result of converting the provided value to A·S·C·I·I
208 toLowerCase
: stringToLowercase
,
209 toUpperCase
: stringToUppercase
,
210 } = String
.prototype;
212 asciiLowercase
: ($) =>
216 makeCallable(stringToLowercase
),
218 asciiUppercase
: ($) =>
222 makeCallable(stringToUppercase
),
229 * Returns an iterator over the code units in the string
230 * representation of the provided value.
235 * Returns an iterator over the codepoints in the string
236 * representation of the provided value.
241 * Returns an iterator over the scalar values in the string
242 * representation of the provided value.
244 * Codepoints which are not valid Unicode scalar values are replaced
250 * Returns the result of converting the provided value to a string of
251 * scalar values by replacing (unpaired) surrogate values with
257 iterator
: iteratorSymbol
,
258 toStringTag
: toStringTagSymbol
,
260 const { [iteratorSymbol
]: arrayIterator
} = Array
.prototype;
261 const arrayIteratorPrototype
= Object
.getPrototypeOf(
262 [][iteratorSymbol
](),
264 const { next
: arrayIteratorNext
} = arrayIteratorPrototype
;
265 const iteratorPrototype
= Object
.getPrototypeOf(
266 arrayIteratorPrototype
,
268 const { [iteratorSymbol
]: stringIterator
} = String
.prototype;
269 const stringIteratorPrototype
= Object
.getPrototypeOf(
270 ""[iteratorSymbol
](),
272 const { next
: stringIteratorNext
} = stringIteratorPrototype
;
275 * An iterator object for iterating over code values (either code
276 * units or codepoints) in a string.
278 * ※ This class is not exposed, although its methods are (through
279 * the prototypes of string code value iterator objects).
281 const StringCodeValueIterator
= class extends identity
{
286 * Constructs a new string code value iterator from the provided
289 * If the provided base iterator is an array iterator, this is a
290 * code unit iterator. If the provided iterator is a string
291 * iterator and surrogates are allowed, this is a codepoint
292 * iterator. If the provided iterator is a string iterator and
293 * surrogates are not allowed, this is a scalar value iterator.
295 constructor(baseIterator
, allowSurrogates
= true) {
296 super(objectCreate(stringCodeValueIteratorPrototype
));
297 this.#allowSurrogates
= !!allowSurrogates
;
298 this.#baseIterator
= baseIterator
;
301 /** Provides the next code value in the iterator. */
303 const baseIterator
= this.#baseIterator
;
304 switch (getPrototype(baseIterator
)) {
305 case arrayIteratorPrototype
: {
306 // The base iterator is iterating over U·C·S characters.
310 } = call(arrayIteratorNext
, baseIterator
, []);
312 ? { value
: undefined, done
: true }
313 : { value
: getCodeUnit(ucsCharacter
, 0), done
: false };
315 case stringIteratorPrototype
: {
316 // The base iterator is iterating over Unicode characters.
320 } = call(stringIteratorNext
, baseIterator
, []);
322 // The base iterator has been exhausted.
323 return { value
: undefined, done
: true };
325 // The base iterator provided a character; yield the
327 const codepoint
= getCodepoint(character
, 0);
329 value
: this.#allowSurrogates
|| codepoint
<= 0xD7FF ||
338 // Should not be possible!
340 "Piscēs: Unrecognized base iterator type in %StringCodeValueIterator%.",
348 next
: stringCodeValueIteratorNext
,
349 } = StringCodeValueIterator
.prototype;
350 const stringCodeValueIteratorPrototype
= objectCreate(
356 value
: stringCodeValueIteratorNext
,
359 [toStringTagSymbol
]: {
362 value
: "String Code Value Iterator",
367 const scalarValueIterablePrototype
= {
371 stringCodeValueIteratorNext
,
372 new StringCodeValueIterator(
373 call(stringIterator
, this.source
, []),
384 new StringCodeValueIterator(call(arrayIterator
, `${$}`, [])),
386 new StringCodeValueIterator(
387 call(stringIterator
, `${$}`, []),
391 new StringCodeValueIterator(
392 call(stringIterator
, `${$}`, []),
395 scalarValueString
: ($) =>
396 stringFromCodepoints(...objectCreate(
397 scalarValueIterablePrototype
,
398 { source
: { value
: `${$}` } },
404 * Returns an iterator over the codepoints in the string representation
405 * of the provided value according to the algorithm of
406 * String::[Symbol.iterator].
408 export const characters
= makeCallable(
409 String
.prototype[Symbol
.iterator
],
413 * Returns the character at the provided position in the string
414 * representation of the provided value according to the algorithm of
415 * String::codePointAt.
417 export const getCharacter
= ($, pos
) => {
418 const codepoint
= getCodepoint($, pos
);
419 return codepoint
== null
421 : stringFromCodepoints(codepoint
);
425 * Returns the code unit at the provided position in the string
426 * representation of the provided value according to the algorithm of
429 export const getCodeUnit
= makeCallable(String
.prototype.charCodeAt
);
432 * Returns the codepoint at the provided position in the string
433 * representation of the provided value according to the algorithm of
434 * String::codePointAt.
436 export const getCodepoint
= makeCallable(String
.prototype.codePointAt
);
439 * Returns the index of the first occurrence of the search string in
440 * the string representation of the provided value according to the
441 * algorithm of String::indexOf.
443 export const getFirstSubstringIndex
= makeCallable(
444 String
.prototype.indexOf
,
448 * Returns the index of the last occurrence of the search string in the
449 * string representation of the provided value according to the
450 * algorithm of String::lastIndexOf.
452 export const getLastSubstringIndex
= makeCallable(
453 String
.prototype.lastIndexOf
,
457 * Returns the result of joining the provided iterable.
459 * If no separator is provided, it defaults to ",".
461 * If a value is nullish, it will be stringified as the empty string.
463 export const join
= (() => {
464 const { join
: arrayJoin
} = Array
.prototype;
465 const join
= ($, separator
= ",") =>
466 call(arrayJoin
, [...$], [`${separator}`]);
472 * Returns a string created from the raw value of the tagged template
475 * ※ This is an alias for String.raw.
480 * Returns a string created from the provided code units.
482 * ※ This is an alias for String.fromCharCode.
484 fromCharCode
: stringFromCodeUnits
,
487 * Returns a string created from the provided codepoints.
489 * ※ This is an alias for String.fromCodePoint.
491 fromCodePoint
: stringFromCodepoints
,
495 * Returns the result of splitting the provided value on A·S·C·I·I
498 export const splitOnASCIIWhitespace
= ($) =>
499 stringSplit(stripAndCollapseASCIIWhitespace($), " ");
502 * Returns the result of splitting the provided value on commas,
503 * trimming A·S·C·I·I whitespace from the resulting tokens.
505 export const splitOnCommas
= ($) =>
507 stripLeadingAndTrailingASCIIWhitespace(
510 /[\n\r\t\f ]*,[\n\r\t\f ]*/gu,
518 * Returns the result of catenating the string representations of the
519 * provided values, returning a new string according to the algorithm
522 export const stringCatenate
= makeCallable(String
.prototype.concat
);
525 * Returns whether the string representation of the provided value ends
526 * with the provided search string according to the algorithm of
529 export const stringEndsWith
= makeCallable(String
.prototype.endsWith
);
532 * Returns whether the string representation of the provided value
533 * contains the provided search string according to the algorithm of
536 export const stringIncludes
= makeCallable(String
.prototype.includes
);
539 * Returns the result of matching the string representation of the
540 * provided value with the provided matcher according to the algorithm
543 export const stringMatch
= makeCallable(String
.prototype.match
);
546 * Returns the result of matching the string representation of the
547 * provided value with the provided matcher according to the algorithm
548 * of String::matchAll.
550 export const stringMatchAll
= makeCallable(String
.prototype.matchAll
);
553 * Returns the normalized form of the string representation of the
554 * provided value according to the algorithm of String::matchAll.
556 export const stringNormalize
= makeCallable(
557 String
.prototype.normalize
,
561 * Returns the result of padding the end of the string representation
562 * of the provided value padded until it is the desired length
563 * according to the algorithm of String::padEnd.
565 export const stringPadEnd
= makeCallable(String
.prototype.padEnd
);
568 * Returns the result of padding the start of the string representation
569 * of the provided value padded until it is the desired length
570 * according to the algorithm of String::padStart.
572 export const stringPadStart
= makeCallable(String
.prototype.padStart
);
575 * Returns the result of repeating the string representation of the
576 * provided value the provided number of times according to the
577 * algorithm of String::repeat.
579 export const stringRepeat
= makeCallable(String
.prototype.repeat
);
582 * Returns the result of replacing the string representation of the
583 * provided value with the provided replacement, using the provided
584 * matcher and according to the algorithm of String::replace.
586 export const stringReplace
= makeCallable(String
.prototype.replace
);
589 * Returns the result of replacing the string representation of the
590 * provided value with the provided replacement, using the provided
591 * matcher and according to the algorithm of String::replaceAll.
593 export const stringReplaceAll
= makeCallable(
594 String
.prototype.replaceAll
,
598 * Returns the result of searching the string representation of the
599 * provided value using the provided matcher and according to the
600 * algorithm of String::search.
602 export const stringSearch
= makeCallable(String
.prototype.search
);
605 * Returns a slice of the string representation of the provided value
606 * according to the algorithm of String::slice.
608 export const stringSlice
= makeCallable(String
.prototype.slice
);
611 * Returns the result of splitting of the string representation of the
612 * provided value on the provided separator according to the algorithm
615 export const stringSplit
= makeCallable(String
.prototype.split
);
618 * Returns whether the string representation of the provided value
619 * starts with the provided search string according to the algorithm of
620 * String::startsWith.
622 export const stringStartsWith
= makeCallable(
623 String
.prototype.startsWith
,
627 * Returns the `[[StringData]]` of the provided value.
629 * ☡ This function will throw if the provided object does not have a
630 * `[[StringData]]` internal slot.
632 export const stringValue
= makeCallable(String
.prototype.valueOf
);
635 * Returns the result of stripping leading and trailing A·S·C·I·I
636 * whitespace from the provided value and collapsing other A·S·C·I·I
637 * whitespace in the string representation of the provided value.
639 export const stripAndCollapseASCIIWhitespace
= ($) =>
640 stripLeadingAndTrailingASCIIWhitespace(
649 * Returns the result of stripping leading and trailing A·S·C·I·I
650 * whitespace from the string representation of the provided value.
652 export const stripLeadingAndTrailingASCIIWhitespace
= (() => {
653 const { exec
: reExec
} = RegExp
.prototype;
655 call(reExec
, /^[\n\r\t\f ]*([^]*?)[\n\r\t\f ]*$/u, [$])[1];
659 * Returns a substring of the string representation of the provided
660 * value according to the algorithm of String::substring.
662 export const substring
= makeCallable(String
.prototype.substring
);
665 * Returns the result of converting the provided value to a string.
667 * ☡ This method throws for symbols and other objects without a string
670 export const toString
= ($) => `${$}`;