262 lines
9.4 KiB
JavaScript
262 lines
9.4 KiB
JavaScript
const cache = new WeakMap();
|
|
const newline = /(\n|\r\n?|\u2028|\u2029)/g;
|
|
const leadingWhitespace = /^\s*/;
|
|
const nonWhitespace = /\S/;
|
|
const slice = Array.prototype.slice;
|
|
const zero = '0'.charCodeAt(0);
|
|
const nine = '9'.charCodeAt(0);
|
|
const lowerA = 'a'.charCodeAt(0);
|
|
const lowerF = 'f'.charCodeAt(0);
|
|
const upperA = 'A'.charCodeAt(0);
|
|
const upperF = 'F'.charCodeAt(0);
|
|
function dedent(arg) {
|
|
if (typeof arg === 'string') {
|
|
return process([arg])[0];
|
|
}
|
|
if (typeof arg === 'function') {
|
|
return function () {
|
|
const args = slice.call(arguments);
|
|
args[0] = processTemplateStringsArray(args[0]);
|
|
return arg.apply(this, args);
|
|
};
|
|
}
|
|
const strings = processTemplateStringsArray(arg);
|
|
// TODO: This is just `String.cooked`: https://tc39.es/proposal-string-cooked/
|
|
let s = getCooked(strings, 0);
|
|
for (let i = 1; i < strings.length; i++) {
|
|
s += arguments[i] + getCooked(strings, i);
|
|
}
|
|
return s;
|
|
}
|
|
function getCooked(strings, index) {
|
|
const str = strings[index];
|
|
if (str === undefined)
|
|
throw new TypeError(`invalid cooked string at index ${index}`);
|
|
return str;
|
|
}
|
|
function processTemplateStringsArray(strings) {
|
|
const cached = cache.get(strings);
|
|
if (cached)
|
|
return cached;
|
|
const raw = process(strings.raw);
|
|
const cooked = raw.map(cook);
|
|
Object.defineProperty(cooked, 'raw', {
|
|
value: Object.freeze(raw),
|
|
});
|
|
Object.freeze(cooked);
|
|
cache.set(strings, cooked);
|
|
return cooked;
|
|
}
|
|
function process(strings) {
|
|
// splitQuasis is an array of arrays. The inner array is contains text content lines on the
|
|
// even indices, and the newline char that ends the text content line on the odd indices.
|
|
// In the first array, the inner array's 0 index is the opening line of the template literal.
|
|
// In all other arrays, the inner array's 0 index is the continuation of the line directly after a
|
|
// template expression.
|
|
//
|
|
// Eg, in the following case:
|
|
//
|
|
// ```
|
|
// String.dedent`
|
|
// first
|
|
// ${expression} second
|
|
// third
|
|
// `
|
|
// ```
|
|
//
|
|
// We expect the following splitQuasis:
|
|
//
|
|
// ```
|
|
// [
|
|
// ["", "\n", " first", "\n", " "],
|
|
// [" second", "\n", " third", "\n", ""],
|
|
// ]
|
|
// ```
|
|
const splitQuasis = strings.map((quasi) => quasi.split(newline));
|
|
let common;
|
|
for (let i = 0; i < splitQuasis.length; i++) {
|
|
const lines = splitQuasis[i];
|
|
// The first split is the static text starting at the opening line until the first template
|
|
// expression (or the end of the template if there are no expressions).
|
|
const firstSplit = i === 0;
|
|
// The last split is all the static text after the final template expression until the closing
|
|
// line. If there are no template expressions, then the first split is also the last split.
|
|
const lastSplit = i + 1 === splitQuasis.length;
|
|
// The opening line must be empty (it very likely is) and it must not contain a template
|
|
// expression. The opening line's trailing newline char is removed.
|
|
if (firstSplit) {
|
|
// Length > 1 ensures there is a newline, and there is not a template expression.
|
|
if (lines.length === 1 || lines[0].length > 0) {
|
|
throw new Error('invalid content on opening line');
|
|
}
|
|
// Clear the captured newline char.
|
|
lines[1] = '';
|
|
}
|
|
// The closing line may only contain whitespace and must not contain a template expression. The
|
|
// closing line and its preceding newline are removed.
|
|
if (lastSplit) {
|
|
// Length > 1 ensures there is a newline, and there is not a template expression.
|
|
if (lines.length === 1 || nonWhitespace.test(lines[lines.length - 1])) {
|
|
throw new Error('invalid content on closing line');
|
|
}
|
|
// Clear the captured newline char, and the whitespace on the closing line.
|
|
lines[lines.length - 2] = '';
|
|
lines[lines.length - 1] = '';
|
|
}
|
|
// In the first spit, the index 0 is the opening line (which must be empty by now), and in all
|
|
// other splits, its the content trailing the template expression (and so can't be part of
|
|
// leading whitespace).
|
|
// Every odd index is the captured newline char, so we'll skip and only process evens.
|
|
for (let j = 2; j < lines.length; j += 2) {
|
|
const text = lines[j];
|
|
// If we are on the last line of this split, and we are not processing the last split (which
|
|
// is after all template expressions), then this line contains a template expression.
|
|
const lineContainsTemplateExpression = j + 1 === lines.length && !lastSplit;
|
|
// leadingWhitespace is guaranteed to match something, but it could be 0 chars.
|
|
const leading = leadingWhitespace.exec(text)[0];
|
|
// Empty lines do not affect the common indentation, and whitespace only lines are emptied
|
|
// (and also don't affect the comon indentation).
|
|
if (!lineContainsTemplateExpression && leading.length === text.length) {
|
|
lines[j] = '';
|
|
continue;
|
|
}
|
|
common = commonStart(leading, common);
|
|
}
|
|
}
|
|
const min = common ? common.length : 0;
|
|
return splitQuasis.map((lines) => {
|
|
let quasi = lines[0];
|
|
for (let i = 1; i < lines.length; i += 2) {
|
|
const newline = lines[i];
|
|
const text = lines[i + 1];
|
|
quasi += newline + text.slice(min);
|
|
}
|
|
return quasi;
|
|
});
|
|
}
|
|
function commonStart(a, b) {
|
|
if (b === undefined || a === b)
|
|
return a;
|
|
let i = 0;
|
|
for (const len = Math.min(a.length, b.length); i < len; i++) {
|
|
if (a[i] !== b[i])
|
|
break;
|
|
}
|
|
return a.slice(0, i);
|
|
}
|
|
function cook(raw) {
|
|
let out = '';
|
|
let start = 0;
|
|
// We need to find every backslash escape sequence, and cook the escape into a real char.
|
|
let i = 0;
|
|
while ((i = raw.indexOf('\\', i)) > -1) {
|
|
out += raw.slice(start, i);
|
|
// If the backslash is the last char of the string, then it was an invalid sequence.
|
|
// This can't actually happen in a tagged template literal, but could happen if you manually
|
|
// invoked the tag with an array.
|
|
if (++i === raw.length)
|
|
return undefined;
|
|
const next = raw[i++];
|
|
switch (next) {
|
|
// Escaped control codes need to be individually processed.
|
|
case 'b':
|
|
out += '\b';
|
|
break;
|
|
case 't':
|
|
out += '\t';
|
|
break;
|
|
case 'n':
|
|
out += '\n';
|
|
break;
|
|
case 'v':
|
|
out += '\v';
|
|
break;
|
|
case 'f':
|
|
out += '\f';
|
|
break;
|
|
case 'r':
|
|
out += '\r';
|
|
break;
|
|
// Escaped line terminators just skip the char.
|
|
case '\r':
|
|
// Treat `\r\n` as a single terminator.
|
|
if (i < raw.length && raw[i] === '\n')
|
|
++i;
|
|
// fall through
|
|
case '\n':
|
|
case '\u2028':
|
|
case '\u2029':
|
|
break;
|
|
// `\0` is a null control char, but `\0` followed by another digit is an illegal octal escape.
|
|
case '0':
|
|
if (isDigit(raw, i))
|
|
return undefined;
|
|
out += '\0';
|
|
break;
|
|
// Hex escapes must contain 2 hex chars.
|
|
case 'x': {
|
|
const n = parseHex(raw, i, i + 2);
|
|
if (n === -1)
|
|
return undefined;
|
|
i += 2;
|
|
out += String.fromCharCode(n);
|
|
break;
|
|
}
|
|
// Unicode escapes contain either 4 chars, or an unlimited number between `{` and `}`.
|
|
// The hex value must not overflow 0x10ffff.
|
|
case 'u': {
|
|
let n;
|
|
if (i < raw.length && raw[i] === '{') {
|
|
const end = raw.indexOf('}', ++i);
|
|
if (end === -1)
|
|
return undefined;
|
|
n = parseHex(raw, i, end);
|
|
i = end + 1;
|
|
}
|
|
else {
|
|
n = parseHex(raw, i, i + 4);
|
|
i += 4;
|
|
}
|
|
if (n === -1 || n > 0x10ffff)
|
|
return undefined;
|
|
out += String.fromCodePoint(n);
|
|
break;
|
|
}
|
|
default:
|
|
if (isDigit(next, 0))
|
|
return undefined;
|
|
out += next;
|
|
}
|
|
start = i;
|
|
}
|
|
return out + raw.slice(start);
|
|
}
|
|
function isDigit(str, index) {
|
|
const c = str.charCodeAt(index);
|
|
return c >= zero && c <= nine;
|
|
}
|
|
function parseHex(str, index, end) {
|
|
if (end >= str.length)
|
|
return -1;
|
|
let n = 0;
|
|
for (; index < end; index++) {
|
|
const c = hexToInt(str.charCodeAt(index));
|
|
if (c === -1)
|
|
return -1;
|
|
n = n * 16 + c;
|
|
}
|
|
return n;
|
|
}
|
|
function hexToInt(c) {
|
|
if (c >= zero && c <= nine)
|
|
return c - zero;
|
|
if (c >= lowerA && c <= lowerF)
|
|
return c - lowerA + 10;
|
|
if (c >= upperA && c <= upperF)
|
|
return c - upperA + 10;
|
|
return -1;
|
|
}
|
|
|
|
export { dedent as default };
|
|
//# sourceMappingURL=dedent.mjs.map
|