chrome-devtools-frontend
Version:
Chrome DevTools UI
483 lines (436 loc) • 14.6 kB
text/typescript
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export const escapeCharacters = (inputString: string, charsToEscape: string): string => {
let foundChar = false;
for (let i = 0; i < charsToEscape.length; ++i) {
if (inputString.indexOf(charsToEscape.charAt(i)) !== -1) {
foundChar = true;
break;
}
}
if (!foundChar) {
return String(inputString);
}
let result = '';
for (let i = 0; i < inputString.length; ++i) {
if (charsToEscape.indexOf(inputString.charAt(i)) !== -1) {
result += '\\';
}
result += inputString.charAt(i);
}
return result;
};
const toHexadecimal = (charCode: number, padToLength: number): string => {
return charCode.toString(16).toUpperCase().padStart(padToLength, '0');
};
// Remember to update the third group in the regexps patternsToEscape and
// patternsToEscapePlusSingleQuote when adding new entries in this map.
const escapedReplacements = new Map([
['\b', '\\b'],
['\f', '\\f'],
['\n', '\\n'],
['\r', '\\r'],
['\t', '\\t'],
['\v', '\\v'],
['\'', '\\\''],
['\\', '\\\\'],
['<!--', '\\x3C!--'],
['<script', '\\x3Cscript'],
['</script', '\\x3C/script'],
]);
export const formatAsJSLiteral = (content: string): string => {
const patternsToEscape = /(\\|<(?:!--|\/?script))|(\p{Control})|(\p{Surrogate})/gu;
const patternsToEscapePlusSingleQuote = /(\\|'|<(?:!--|\/?script))|(\p{Control})|(\p{Surrogate})/gu;
const escapePattern = (match: string, pattern: string, controlChar: string, loneSurrogate: string): string => {
if (controlChar) {
if (escapedReplacements.has(controlChar)) {
// @ts-ignore https://github.com/microsoft/TypeScript/issues/13086
return escapedReplacements.get(controlChar);
}
const twoDigitHex = toHexadecimal(controlChar.charCodeAt(0), 2);
return '\\x' + twoDigitHex;
}
if (loneSurrogate) {
const fourDigitHex = toHexadecimal(loneSurrogate.charCodeAt(0), 4);
return '\\u' + fourDigitHex;
}
if (pattern) {
return escapedReplacements.get(pattern) || '';
}
return match;
};
let escapedContent = '';
let quote = '';
if (!content.includes('\'')) {
quote = '\'';
escapedContent = content.replaceAll(patternsToEscape, escapePattern);
} else if (!content.includes('"')) {
quote = '"';
escapedContent = content.replaceAll(patternsToEscape, escapePattern);
} else if (!content.includes('`') && !content.includes('${')) {
quote = '`';
escapedContent = content.replaceAll(patternsToEscape, escapePattern);
} else {
quote = '\'';
escapedContent = content.replaceAll(patternsToEscapePlusSingleQuote, escapePattern);
}
return `${quote}${escapedContent}${quote}`;
};
/**
* This implements a subset of the sprintf() function described in the Single UNIX
* Specification. It supports the %s, %f, %d, and %% formatting specifiers, and
* understands the %m$d notation to select the m-th parameter for this substitution,
* as well as the optional precision for %s, %f, and %d.
*
* @param fmt format string.
* @param args parameters to the format string.
* @returns the formatted output string.
*/
export const sprintf = (fmt: string, ...args: unknown[]): string => {
let argIndex = 0;
const RE = /%(?:(\d+)\$)?(?:\.(\d*))?([%dfs])/g;
return fmt.replaceAll(RE, (_: string, index?: string, precision?: string, specifier?: string): string => {
if (specifier === '%') {
return '%';
}
if (index !== undefined) {
argIndex = parseInt(index, 10) - 1;
if (argIndex < 0) {
throw new RangeError(`Invalid parameter index ${argIndex + 1}`);
}
}
if (argIndex >= args.length) {
throw new RangeError(`Expected at least ${argIndex + 1} format parameters, but only ${args.length} where given.`);
}
if (specifier === 's') {
const argValue = String(args[argIndex++]);
if (precision !== undefined) {
return argValue.substring(0, Number(precision));
}
return argValue;
}
let argValue = Number(args[argIndex++]);
if (isNaN(argValue)) {
argValue = 0;
}
if (specifier === 'd') {
return String(Math.floor(argValue)).padStart(Number(precision), '0');
}
if (precision !== undefined) {
return argValue.toFixed(Number(precision));
}
return String(argValue);
});
};
export const toBase64 = (inputString: string): string => {
/* note to the reader: we can't use btoa here because we need to
* support Unicode correctly. See the test cases for this function and
* also
* https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem
*/
function encodeBits(b: number): number {
return b < 26 ? b + 65 : b < 52 ? b + 71 : b < 62 ? b - 4 : b === 62 ? 43 : b === 63 ? 47 : 65;
}
const encoder = new TextEncoder();
const data = encoder.encode(inputString.toString());
const n = data.length;
let encoded = '';
if (n === 0) {
return encoded;
}
let shift;
let v = 0;
for (let i = 0; i < n; i++) {
shift = i % 3;
v |= data[i] << (16 >>> shift & 24);
if (shift === 2) {
encoded += String.fromCharCode(
encodeBits(v >>> 18 & 63), encodeBits(v >>> 12 & 63), encodeBits(v >>> 6 & 63), encodeBits(v & 63));
v = 0;
}
}
if (shift === 0) {
encoded += String.fromCharCode(encodeBits(v >>> 18 & 63), encodeBits(v >>> 12 & 63), 61, 61);
} else if (shift === 1) {
encoded += String.fromCharCode(encodeBits(v >>> 18 & 63), encodeBits(v >>> 12 & 63), encodeBits(v >>> 6 & 63), 61);
}
return encoded;
};
export const findIndexesOfSubString = (inputString: string, searchString: string): number[] => {
const matches = [];
let i = inputString.indexOf(searchString);
while (i !== -1) {
matches.push(i);
i = inputString.indexOf(searchString, i + searchString.length);
}
return matches;
};
export const findLineEndingIndexes = (inputString: string): number[] => {
const endings = findIndexesOfSubString(inputString, '\n');
endings.push(inputString.length);
return endings;
};
export const isWhitespace = (inputString: string): boolean => {
return /^\s*$/.test(inputString);
};
export const trimURL = (url: string, baseURLDomain?: string): string => {
let result = url.replace(/^(https|http|file):\/\//i, '');
if (baseURLDomain) {
if (result.toLowerCase().startsWith(baseURLDomain.toLowerCase())) {
result = result.substr(baseURLDomain.length);
}
}
return result;
};
export const collapseWhitespace = (inputString: string): string => {
return inputString.replace(/[\s\xA0]+/g, ' ');
};
export const reverse = (inputString: string): string => {
return inputString.split('').reverse().join('');
};
export const replaceControlCharacters = (inputString: string): string => {
// Replace C0 and C1 control character sets with replacement character.
// Do not replace '\t', \n' and '\r'.
return inputString.replace(/[\0-\x08\x0B\f\x0E-\x1F\x80-\x9F]/g, '\uFFFD');
};
export const countWtf8Bytes = (inputString: string): number => {
let count = 0;
for (let i = 0; i < inputString.length; i++) {
const c = inputString.charCodeAt(i);
if (c <= 0x7F) {
count++;
} else if (c <= 0x07FF) {
count += 2;
} else if (c < 0xD800 || 0xDFFF < c) {
count += 3;
} else {
if (c <= 0xDBFF && i + 1 < inputString.length) {
// The current character is a leading surrogate, and there is a
// next character.
const next = inputString.charCodeAt(i + 1);
if (0xDC00 <= next && next <= 0xDFFF) {
// The next character is a trailing surrogate, meaning this
// is a surrogate pair.
count += 4;
i++;
continue;
}
}
count += 3;
}
}
return count;
};
export const stripLineBreaks = (inputStr: string): string => {
return inputStr.replace(/(\r)?\n/g, '');
};
export const toTitleCase = (inputStr: string): string => {
return inputStr.substring(0, 1).toUpperCase() + inputStr.substring(1);
};
export const removeURLFragment = (inputStr: string): string => {
const url = new URL(inputStr);
url.hash = '';
return url.toString();
};
const SPECIAL_REGEX_CHARACTERS = '^[]{}()\\.^$*+?|-,';
export const regexSpecialCharacters = function(): string {
return SPECIAL_REGEX_CHARACTERS;
};
export const filterRegex = function(query: string): RegExp {
let regexString = '^(?:.*\\0)?'; // Start from beginning or after a \0
for (let i = 0; i < query.length; ++i) {
let c = query.charAt(i);
if (SPECIAL_REGEX_CHARACTERS.indexOf(c) !== -1) {
c = '\\' + c;
}
regexString += '[^\\0' + c + ']*' + c;
}
return new RegExp(regexString, 'i');
};
export const createSearchRegex = function(query: string, caseSensitive: boolean, isRegex: boolean): RegExp {
const regexFlags = caseSensitive ? 'g' : 'gi';
let regexObject;
if (isRegex) {
try {
regexObject = new RegExp(query, regexFlags);
} catch (e) {
// Silent catch.
}
}
if (!regexObject) {
regexObject = createPlainTextSearchRegex(query, regexFlags);
}
return regexObject;
};
export const caseInsensetiveComparator = function(a: string, b: string): number {
a = a.toUpperCase();
b = b.toUpperCase();
if (a === b) {
return 0;
}
return a > b ? 1 : -1;
};
export const hashCode = function(string?: string): number {
if (!string) {
return 0;
}
// Hash algorithm for substrings is described in "Über die Komplexität der Multiplikation in
// eingeschränkten Branchingprogrammmodellen" by Woelfe.
// http://opendatastructures.org/versions/edition-0.1d/ods-java/node33.html#SECTION00832000000000000000
const p = ((1 << 30) * 4 - 5); // prime: 2^32 - 5
const z = 0x5033d967; // 32 bits from random.org
const z2 = 0x59d2f15d; // random odd 32 bit number
let s = 0;
let zi = 1;
for (let i = 0; i < string.length; i++) {
const xi = string.charCodeAt(i) * z2;
s = (s + zi * xi) % p;
zi = (zi * z) % p;
}
s = (s + zi * (p - 1)) % p;
return Math.abs(s | 0);
};
export const compare = (a: string, b: string): number => {
if (a > b) {
return 1;
}
if (a < b) {
return -1;
}
return 0;
};
export const trimMiddle = (str: string, maxLength: number): string => {
if (str.length <= maxLength) {
return String(str);
}
let leftHalf = maxLength >> 1;
let rightHalf = maxLength - leftHalf - 1;
if ((str.codePointAt(str.length - rightHalf - 1) as number) >= 0x10000) {
--rightHalf;
++leftHalf;
}
if (leftHalf > 0 && (str.codePointAt(leftHalf - 1) as number) >= 0x10000) {
--leftHalf;
}
return str.substr(0, leftHalf) + '…' + str.substr(str.length - rightHalf, rightHalf);
};
export const trimEndWithMaxLength = (str: string, maxLength: number): string => {
if (str.length <= maxLength) {
return String(str);
}
return str.substr(0, maxLength - 1) + '…';
};
export const escapeForRegExp = (str: string): string => {
return escapeCharacters(str, SPECIAL_REGEX_CHARACTERS);
};
export const naturalOrderComparator = (a: string, b: string): number => {
const chunk = /^\d+|^\D+/;
let chunka, chunkb, anum, bnum;
while (true) {
if (a) {
if (!b) {
return 1;
}
} else {
if (b) {
return -1;
}
return 0;
}
chunka = (a.match(chunk) as string[])[0];
chunkb = (b.match(chunk) as string[])[0];
anum = !Number.isNaN(Number(chunka));
bnum = !Number.isNaN(Number(chunkb));
if (anum && !bnum) {
return -1;
}
if (bnum && !anum) {
return 1;
}
if (anum && bnum) {
const diff = Number(chunka) - Number(chunkb);
if (diff) {
return diff;
}
if (chunka.length !== chunkb.length) {
if (!Number(chunka) && !Number(chunkb)) { // chunks are strings of all 0s (special case)
return chunka.length - chunkb.length;
}
return chunkb.length - chunka.length;
}
} else if (chunka !== chunkb) {
return (chunka < chunkb) ? -1 : 1;
}
a = a.substring(chunka.length);
b = b.substring(chunkb.length);
}
};
export const base64ToSize = function(content: string|null): number {
if (!content) {
return 0;
}
let size = content.length * 3 / 4;
if (content[content.length - 1] === '=') {
size--;
}
if (content.length > 1 && content[content.length - 2] === '=') {
size--;
}
return size;
};
export const SINGLE_QUOTE = '\'';
export const DOUBLE_QUOTE = '"';
const BACKSLASH = '\\';
export const findUnclosedCssQuote = function(str: string): string {
let unmatchedQuote = '';
for (let i = 0; i < str.length; ++i) {
const char = str[i];
if (char === BACKSLASH) {
i++;
continue;
}
if (char === SINGLE_QUOTE || char === DOUBLE_QUOTE) {
if (unmatchedQuote === char) {
unmatchedQuote = '';
} else if (unmatchedQuote === '') {
unmatchedQuote = char;
}
}
}
return unmatchedQuote;
};
export const createPlainTextSearchRegex = function(query: string, flags?: string): RegExp {
// This should be kept the same as the one in StringUtil.cpp.
let regex = '';
for (let i = 0; i < query.length; ++i) {
const c = query.charAt(i);
if (regexSpecialCharacters().indexOf(c) !== -1) {
regex += '\\';
}
regex += c;
}
return new RegExp(regex, flags || '');
};
class LowerCaseStringTag {
private lowerCaseStringTag: (string|undefined);
}
export type LowerCaseString = string&LowerCaseStringTag;
export const toLowerCaseString = function(input: string): LowerCaseString {
return input.toLowerCase() as LowerCaseString;
};
// Replaces the last ocurrence of parameter `search` with parameter `replacement` in `input`
export const replaceLast = function(input: string, search: string, replacement: string): string {
const replacementStartIndex = input.lastIndexOf(search);
if (replacementStartIndex === -1) {
return input;
}
return input.slice(0, replacementStartIndex) + input.slice(replacementStartIndex).replace(search, replacement);
};
export const stringifyWithPrecision = function stringifyWithPrecision(s: number, precision = 2): string {
if (precision === 0) {
return s.toFixed(0);
}
const string = s.toFixed(precision).replace(/\.?0*$/, '');
return string === '-0' ? '0' : string;
};