UNPKG

utfstring

Version:
825 lines (824 loc) 33.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UtfString = exports.surrogatePairs = void 0; const utils_1 = require("./utils"); /** Regular expression for matching surrogate pairs. */ exports.surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; /** * Creates a regular expression that matches surrogate pairs. * @returns The regular expression that matches surrogate pairs. */ function createSurrogatePairScanner() { return new RegExp(exports.surrogatePairs.source, "g"); } /** * Creates a regular expression that matches every character of a string and is UTF-safe. * @returns The regular expression for matching every character of a string. */ function createUtfSafeCharScannerHandlingSurrogatePairs() { const sources = new Array(); sources.push(exports.surrogatePairs.source); sources.push("[^]"); return new RegExp(sources.join("|"), "g"); } /** * Class with UTF-safe string operations. */ class UtfString { /** * Creates a new UTF-safe string object. * @param unsafeString The unsafe string. */ constructor(unsafeString) { if (!(0, utils_1.isDefined)(unsafeString)) { this.unsafeString = ""; } else if (unsafeString instanceof UtfString) { this.unsafeString = unsafeString.toString(); } else { this.unsafeString = String(unsafeString); } return new Proxy(this, { get: (obj, key) => { return typeof key === "string" && Number.isInteger(Number(key)) ? obj.charAt(parseInt(key, 0)) : obj[key]; }, }); } /** * Iterator that enables the usage of for-of loops on instances of this class. * @returns An iterator that returns each character in the string separately. */ *[Symbol.iterator]() { for (let i = 0; i < this.length; ++i) { yield this.charAt(i); } } /** * Returns the character at the given index from the string. * @param index The index of the wanted character. * @returns The character at the given index. */ charAt(index) { const ctor = this.getClass(); const char = ctor.charAt(this.unsafeString, index); return new ctor(char); } /** * Returns the Unicode codepoint at the given index. * @param index The index of the wanted Unicode codepoint. * @returns The Unicode codepoint at the given index. */ charCodeAt(index) { return this.getClass().charCodeAt(this.unsafeString, index); } /** * Returns the Unicode codepoint at the given index. * @param index The index of the wanted Unicode codepoint. * @returns The Unicode codepoint at the given index. */ codePointAt(index) { return this.getClass().charCodeAt(this.unsafeString, index); } /** * Creates a new UTF-safe string object by concatenating the given strings. * @param arr The strings to concatenate. * @returns A new UTF-safe string object with the concatenated strings. */ concat(...arr) { const ctor = this.getClass(); return new ctor(this.unsafeString + arr.map((x) => String(x)).join("")); } /** * Determines whether the string ends with the characters of a specified search string. * @param str The characters to be searched for at the end of the string. * @param endPos The end position at which the search string is expected to be found. * @returns True if the string ends with the given search string, false otherwise. */ endsWith(str, endPos) { return this.unsafeString.endsWith(str.toString(), endPos); } /** * Checks if the given string equals the string. * @param str The string to compare. * @returns True if the strings are equals, false otherwise. */ equals(str) { return this.unsafeString === (str === null || str === void 0 ? void 0 : str.toString()); } /** * Finds the byte index for the given character index in the string. * Note: a "byte index" is really a "JavaScript string index", not a true byte offset. * Use this function to convert a UTF character boundary to a JavaScript string index. * @param charIndex The character index for which to find the byte index. * @returns The byte index for the character index in the string. * -1 if the character index is equal to or higher than the length of the string. */ findByteIndex(charIndex) { return this.getClass().findByteIndex(this.unsafeString, charIndex); } /** * Finds the character index for the given byte index in the string. * Note: a "byte index" is really a "JavaSciprt string index", not a true byte offset. * Use this function to convert a JavaScript string index to (the closest) UTF character boundary. * @param byteIndex The byte index for which to find the character index. * @returns The character index for the byte index in the string. * -1 if the byte index is equal to or higher than the number of bytes in the string. */ findCharIndex(byteIndex) { return this.getClass().findCharIndex(this.unsafeString, byteIndex); } /** * Checks if the search value is within the string. * @param searchValue The value to search. * @param start Optional start offset for the search. * @returns True if the search value was found in the string, false otherwise. */ includes(searchValue, start = 0) { return this.indexOf(searchValue, start) !== -1; } /** * Finds the first instance of the search value within the string. Starts at an optional offset. * @param searchValue The value to search. * @param start Optional start offset for the search. * @returns The first instance of the search value within the string. * -1 if the search value could not be found. */ indexOf(searchValue, start = 0) { return this.getClass().indexOf(this.unsafeString, searchValue.toString(), start); } /** * Finds the last instance of the search value within the string. * Starts searching backwards at an optional offset, which can be negative. * @param searchValue The value to search. * @param start Optional start offset for the search. * @returns The last instance of the search value within the string. * -1 if the search value could not be found. */ lastIndexOf(searchValue, start) { return this.getClass().lastIndexOf(this.unsafeString, searchValue.toString(), start); } /** * Returns the number of logical characters in the string. * @returns The number of logical characters in the string. */ get length() { return this.getClass().lengthOf(this.unsafeString); } match(matcher) { return this.toString().match(matcher instanceof UtfString ? matcher.toString() : matcher); } /** * Creates a new UTF-safe string object by padding the string with a given string (repeated, if needed) * so that the resulting string reaches a given length. The padding is applied at the end of the string. * @param targetLength The length of the resulting string once the string has been padded. * @param padString The string to pad the string with. * @returns A new UTF-safe string object of the specified target length with the padding string applied at the end. */ padEnd(targetLength, padString) { const ctor = this.getClass(); return new ctor(ctor.padEnd(this.unsafeString, targetLength, padString === null || padString === void 0 ? void 0 : padString.toString())); } /** * Creates a new UTF-safe string object by padding the string with a given string (repeated, if needed) * so that the resulting string reaches a given length. The padding is applied at the start of the string. * @param targetLength The length of the resulting string once the string has been padded. * @param padString The string to pad the string with. * @returns A new UTF-safe string object of the specified target length * with the padding string applied at the start. */ padStart(targetLength, padString) { const ctor = this.getClass(); return new ctor(ctor.padStart(this.unsafeString, targetLength, padString === null || padString === void 0 ? void 0 : padString.toString())); } /** * Returns a new string which contains the specified number of copies of the string on which it was called. * @param count The number of times the string should be repeated. * @returns The new string which contains a specified number of copies of the original string. */ repeat(count) { const ctor = this.getClass(); return new ctor(this.unsafeString.repeat(count)); } replace(pattern, replacement) { if (pattern instanceof UtfString) { pattern = pattern.toString(); } if (replacement instanceof UtfString) { replacement = replacement.toString(); } const ctor = this.getClass(); return new ctor(this.unsafeString.replace(pattern, replacement)); } /** * Returns the characters between the two given indices in the string. * @param start The index from which to start extracting the characters. * @param finish The index at which to end extracting the characters. * @returns The characters between the two given indices. */ slice(start, finish) { const ctor = this.getClass(); const str = ctor.slice(this.unsafeString, start, finish); return new ctor(str); } split(seperator, limit) { if (seperator === "") { return [...this].slice(0, limit); } const ctor = this.getClass(); return this.unsafeString.split(seperator, limit).map((str) => new ctor(str)); } /** * Determines whether the string starts with the characters of a specified search string. * @param str The characters to be searched for at the start of the string. * @param startPos The start position at which the search string is expected to be found. * @returns True if the string starts with the given search string, false otherwise. */ startsWith(str, startPos) { return this.unsafeString.startsWith(str.toString(), startPos); } /** * Returns the characters starting at the given start index up to the start index plus the given length. * @param start The index from which to start extracting the characters. * @param length The number of characters to extract. * @returns The characters starting at the given start index up to the start index plus the given length. */ substr(start, length) { const ctor = this.getClass(); const str = ctor.substr(this.unsafeString, start, length); return new ctor(str); } /** * Returns the characters starting at the given start index up to the end index. * @param start The index from which to start extracting the characters. * @param end The index to which characters are extracted. * @returns The characters starting at the given start index up to the end index. */ substring(start, end) { const ctor = this.getClass(); const str = ctor.substring(this.unsafeString, start, end); return new ctor(str); } /** * Converts the string into an array of UTF-16 bytes. * @returns The UTF-16 bytes created from the string. */ toBytes() { return this.getClass().stringToBytes(this.unsafeString); } /** * Converts the string into an array of individual logical characters. * Note that each entry in the returned array may be more than one UTF-16 character. * @returns The array containing the individual logical characters taken from the string. */ toCharArray() { return this.getClass().stringToCharArray(this.unsafeString); } /** * Converts the string into an array of codepoints. * @returns The codepoints taken from the string. */ toCodePoints() { return this.getClass().stringToCodePoints(this.unsafeString); } /** * Returns a new string in which all the alphabetic characters are converted to lower case, * without modifying the original string. * @return A new string object in which all alphabetic characters are in lower case. */ toLowerCase() { const ctor = this.getClass(); return new ctor(this.unsafeString.toLowerCase()); } /** * Returns the unsafe string the object is hiding. * @returns The unsafe string. */ toString() { return this.unsafeString; } /** * Returns a new string in which all the alphabetic characters are converted to upper case, * without modifying the original string. * @return A new string object in which all alphabetic characters are in upper case. */ toUpperCase() { const ctor = this.getClass(); return new ctor(this.unsafeString.toUpperCase()); } /** * Removes whitespace from both ends of the string and returns a new string, without modifying the original string. * @returns A new string object without any whitespace at the beginning or the end. */ trim() { const ctor = this.getClass(); return new ctor(this.unsafeString.trim()); } /** * Removes whitespace from the end of the string and returns a new string, without modifying the original string. * @return A new string object without any whitespace at the end. */ trimEnd() { const ctor = this.getClass(); return new ctor(this.unsafeString.trimEnd()); } /** * Removes whitespace from the beginning of the string and returns a new string, * without modifying the original string. * @return A new string object without any whitespace at the beginning. */ trimLeft() { const ctor = this.getClass(); return new ctor(this.unsafeString.trimLeft()); } /** * Removes whitespace from the end of the string and returns a new string, without modifying the original string. * @return A new string object without any whitespace at the end. */ trimRight() { const ctor = this.getClass(); return new ctor(this.unsafeString.trimRight()); } /** * Removes whitespace from the beginning of the string and returns a new string, * without modifying the original string. * @return A new string object without any whitespace at the beginning. */ trimStart() { const ctor = this.getClass(); return new ctor(this.unsafeString.trimStart()); } /** * Returns the constructor function of this object. * @returns The constructor function of this object. */ getClass() { // gets the constructor function at runtime and therefore also works in derived classes return Object.getPrototypeOf(this).constructor; } /** * Converts an array of UTF-16 bytes into a string. * @param arr The array of UTF-16 bytes that should be converted. * @returns The string created from the array of UTF-16 bytes. */ static bytesToString(arr) { const result = new Array(); for (let i = 0; i < arr.length; i += 2) { const hi = arr[i]; const low = arr[i + 1]; const combined = (hi << 8) | low; result.push(String.fromCharCode(combined)); } return result.join(""); } /** * Returns the character at the given index from the given string. * @param str The string from which to get the character. * @param index The index of the wanted character. * @returns The character at the given index. */ static charAt(str, index) { const byteIndex = this.findByteIndex(str, index); if (byteIndex < 0) { return ""; } const characters = str.slice(byteIndex, byteIndex + 8); const scanner = this.createUtfSafeCharScanner(); const match = scanner.exec(characters); return match === null ? characters[0] : match[0]; } /** * Returns the Unicode codepoint at the given index from the given string. * @param str The string from which to get the Unicode codepoint. * @param index The index of the wanted Unicode codepoint. * @returns The Unicode codepoint at the given index. */ static charCodeAt(str, index) { const byteIndex = this.findSurrogateByteIndex(str, index); if (byteIndex < 0) { return NaN; } const code = str.charCodeAt(byteIndex); if (0xd800 <= code && code <= 0xdbff) { const hi = code; const low = str.charCodeAt(byteIndex + 1); return (hi - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000; } return code; } /** * Converts an array of codepoints into a string. * @param arr The codepoints that should be converted. * @returns The string created from the codepoints. */ static codePointsToString(arr) { const chars = arr.map((a) => this.stringFromCharCode(a)); return chars.join(""); } /** * Finds the byte index for the given character index in the given string. * Note: a "byte index" is really a "JavaScript string index", not a true byte offset. * Use this function to convert a UTF character boundary to a JavaScript string index. * @param str The string in which to search the byte index. * @param charIndex The character index for which to find the byte index. * @returns The byte index for the character index in the string. * -1 if the character index is equal to or higher than the length of the string. */ static findByteIndex(str, charIndex) { if (charIndex >= this.lengthOf(str)) { return -1; } return this.scan(str, this.createUtfSafeCharScanner(), charIndex); } /** * Finds the character index for the given byte index in the given string. * Note: a "byte index" is really a "JavaSciprt string index", not a true byte offset. * Use this function to convert a JavaScript string index to (the closest) UTF character boundary. * @param str The string in which to search the character index. * @param byteIndex The byte index for which to find the character index. * @returns The character index for the byte index in the string. * -1 if the byte index is equal to or higher than the number of bytes in the string. */ static findCharIndex(str, byteIndex) { if (byteIndex >= str.length) { return -1; } // optimization: don't iterate unless necessary if (!this.containsUnsafeUtfChars(str)) { return byteIndex; } const scanner = this.createUtfSafeCharScanner(); let charCount = 0; while (scanner.exec(str) !== null) { if (scanner.lastIndex > byteIndex) { break; } charCount++; } return charCount; } /** * Converts an array of UTF-16 bytes into a UTF-safe string object. * @param arr The array of UTF-16 bytes that should be converted. * @returns The UTF-safe string object created from the array of UTF-16 bytes. */ static fromBytes(arr) { return new this(this.bytesToString(arr)); } /** * Returns a UTF-safe string object for the given Unicode codepoint. * @param charCode The Unicode codepoint. * @returns The UTF-safe string object for the given Unicode codepoint. */ static fromCharCode(charCode) { return new this(this.stringFromCharCode(charCode)); } /** * Converts an array of codepoints into a UTF-safe string object. * @param arr The codepoints that should be converted. * @returns The UTF-safe string object created from the codepoints. */ static fromCodePoints(arr) { return new this(this.codePointsToString(arr)); } /** * Finds the first instance of the search value within the string. Starts at an optional offset. * @param str The string in which to search for the value. * @param searchValue The value to search. * @param start Optional start offset for the search. * @returns The first instance of the search value within the string. * -1 if the search value could not be found. */ static indexOf(str, searchValue, start = 0) { const startByteIndex = this.findByteIndex(str, start); if (startByteIndex < 0) { return -1; } const index = str.indexOf(searchValue, startByteIndex); return index < 0 ? -1 : this.findCharIndex(str, index); } /** * Concatenates the strings from the given array into a new UTF-safe string object. * @param items The array of strings which are joined. * @param seperator The seperator string inserted between the concatenated strings. * @returns A new string object that contains the concatenated strings from the given array. */ static join(items, seperator = ",") { const text = items.map((x) => ((0, utils_1.isDefined)(x) ? x.toString() : "")).join(seperator.toString()); return new this(text); } /** * Finds the last instance of the search value within the string. * Starts searching backwards at an optional offset, which can be negative. * @param str The string in which to search for the value. * @param searchValue The value to search. * @param start Optional start offset for the search. * @returns The last instance of the search value within the string. * -1 if the search value could not be found. */ static lastIndexOf(str, searchValue, start) { let index; if (!(0, utils_1.isDefined)(start)) { index = str.lastIndexOf(searchValue); } else { const startByteIndex = this.findByteIndex(str, start); if (startByteIndex < 0) { return -1; } index = str.lastIndexOf(searchValue, startByteIndex); } return index < 0 ? -1 : this.findCharIndex(str, index); } /** * Returns the number of logical characters in the given string. * @param str The string whose length is calculated. * @returns The number of logical characters in the given string. */ static lengthOf(str) { // findCharIndex will return -1 if string is empty, so add 1 return this.findCharIndex(str, str.length - 1) + 1; } /** * Creates a new string by padding the string with a given string (repeated, if needed) so that the resulting string * reaches a given length. The padding is applied at the end of the string. * @param str The string that should be padded to the target length. * @param targetLength The length of the resulting string once the string has been padded. * @param padString The string to pad the string with. * @returns A new string of the specified target length with the padding string applied at the end. */ static padEnd(str, targetLength, padString = " ") { if (targetLength <= this.lengthOf(str)) { return str; } let iPadStr = 0; let newStr = str; do { newStr += this.charAt(padString, iPadStr); ++iPadStr; if (iPadStr >= this.lengthOf(padString)) { iPadStr = 0; } } while (this.lengthOf(newStr) < targetLength); return newStr; } /** * Creates a new string by padding the string with a given string (repeated, if needed) so that the resulting string * reaches a given length. The padding is applied at the start of the string. * @param str The string that should be padded to the target length. * @param targetLength The length of the resulting string once the string has been padded. * @param padString The string to pad the string with. * @returns A new string of the specified target length with the padding string applied at the start. */ static padStart(str, targetLength, padString = " ") { if (targetLength <= this.lengthOf(str)) { return str; } let iPadStr = 0; let fullPadding = ""; do { fullPadding += this.charAt(padString, iPadStr); ++iPadStr; if (iPadStr >= this.lengthOf(padString)) { iPadStr = 0; } } while (this.lengthOf(fullPadding + str) < targetLength); return fullPadding + str; } /** * Returns the characters between the two given indices in the given string. * @param str The string from which to extract the characters. * @param start The index from which to start extracting the characters. * @param end The index at which to end extracting the characters. * @returns The characters between the two given indices. */ static slice(str, start, end) { if (!(0, utils_1.isNumber)(start)) { start = 0; } else if (start < 0) { start = this.lengthOf(str) + start; } if (!(0, utils_1.isNumber)(end) || end >= this.lengthOf(str)) { end = str.length; } else if (end < 0) { end = this.lengthOf(str) + end; } let startByteIndex = this.findByteIndex(str, start); if (startByteIndex < 0) { startByteIndex = str.length; } let finishByteIndex = this.findByteIndex(str, end); if (finishByteIndex < 0) { finishByteIndex = str.length; } return str.slice(startByteIndex, finishByteIndex); } /** * Returns the string for the given Unicode codepoint. * @param charCode The Unicode codepoint. * @returns The string for the given Unicode codepoint. */ static stringFromCharCode(charCode) { if (charCode > 0xffff) { charCode -= 0x10000; return String.fromCharCode(0xd800 + (charCode >> 10), 0xdc00 + (charCode & 0x3ff)); } else { return String.fromCharCode(charCode); } } /** * Converts a string into an array of UTF-16 bytes. * @param str The string that should be converted. * @returns The UTF-16 bytes created from the string. */ static stringToBytes(str) { let result = new Array(); for (let i = 0; i < str.length; i++) { let chr = str.charCodeAt(i); const byteArray = new Array(); while (chr > 0) { byteArray.push(chr & 0xff); chr >>= 8; } // all utf-16 characters are two bytes if (byteArray.length === 1) { byteArray.push(0); } // assume big-endian result = result.concat(byteArray.reverse()); } return result; } /** * Converts the given string into an array of individual logical characters. * Note that each entry in the returned array may be more than one UTF-16 character. * @param str The string that should be converted. * @returns The array containing the individual logical characters taken from the string. */ static stringToCharArray(str) { const result = new Array(); const scanner = this.createUtfSafeCharScanner(); let match; do { match = scanner.exec(str); if (match === null) { break; } result.push(match[0]); } while (match !== null); return result; } /** * Converts a string into an array of codepoints. * @param str The string that should be converted. * @returns The codepoints taken from the string. */ static stringToCodePoints(str) { const result = new Array(); for (let i = 0; i < str.length; i++) { const codePoint = this.charCodeAt(str, i); if (!codePoint) { break; } result.push(codePoint); } return result; } /** * Returns the characters starting at the given start index up to the start index plus the given length. * @param str The string from which to extract the characters. * @param start The index from which to start extracting the characters. * @param length The number of characters to extract. * @returns The characters starting at the given start index up to the start index plus the given length. */ static substr(str, start, length) { if (!(0, utils_1.isNumber)(start)) { start = 0; } if ((0, utils_1.isDefined)(length)) { if (isNaN(length)) { length = 0; } if (length <= 0) { return ""; } } if (start < 0) { start = Math.max(this.lengthOf(str) + start, 0); } if (!(0, utils_1.isDefined)(length)) { return this.slice(str, start); } else { return this.slice(str, start, start + length); } } /** * Returns the characters starting at the given start index up to the end index. * @param str The string from which to extract the characters. * @param start The index from which to start extracting the characters. * @param end The index to which characters are extracted. * @returns The characters starting at the given start index up to the end index. */ static substring(str, start, end) { if (!(0, utils_1.isNumber)(start) || start < 0) { start = 0; } if (!(0, utils_1.isDefined)(end)) { end = this.lengthOf(str); } else if (isNaN(end)) { end = 0; } else if (end < 0) { end = 0; } if (start > end) { [start, end] = [end, start]; } return this.slice(str, start, end); } /** * Finds the byte index of a surrogate pair in the given string up until a specific character index. * @param str The string in which to search. * @param charIndex The character index up until which to search. * @returns The byte index of a surrogate pair in the given string. * -1 if no surrogate pair was found. */ static findSurrogateByteIndex(str, charIndex) { return this.scan(str, createSurrogatePairScanner(), charIndex); } /** * Scans a given string up until a specific character index using a regular expression * and returns the byte index at which the scan found a match. * @param str The string that is scanned. * @param scanner The scanner that is used to scan the string. * @param charIndex The character index up until which the scan should be performed. * @returns The byte index at which the scan found a match. * -1 if the scan did not find a match. */ static scan(str, scanner, charIndex) { // optimization: don't iterate unless it's necessary if (!this.containsUnsafeUtfChars(str)) { return charIndex; } let byteIndex = 0; let curCharIndex = 0; while (true) { const match = scanner.exec(str); const nextIdx = match ? match.index : str.length; while (curCharIndex < charIndex) { if (byteIndex === nextIdx) { if (curCharIndex < charIndex) { curCharIndex++; if (match) { byteIndex += match[0].length; } else { byteIndex++; } } break; } byteIndex++; curCharIndex++; } if (curCharIndex === charIndex) { break; } else if (byteIndex >= str.length || !match) { return -1; } } return byteIndex; } /** * Checks if the given string contains surrogate pairs. * @param str The string that is checked. * @returns True if the given string contains surrogate pairs, false otherwise. */ static containsUnsafeUtfChars(str) { const scanner = this.createUnsafeUtfCharFinder(); return scanner.test(str); } /** * Creates a char scanner that matches unsafe UTF chars. * @returns A char scanner that matches unsafe UTF chars. */ static createUnsafeUtfCharFinder() { return createSurrogatePairScanner(); } /** * Creates a UTF-safe char scanner. * @returns A UTF-safe char scanner. */ static createUtfSafeCharScanner() { return createUtfSafeCharScannerHandlingSurrogatePairs(); } } exports.UtfString = UtfString;