391 lines
8.9 KiB
JavaScript
391 lines
8.9 KiB
JavaScript
import * as acorn from 'acorn';
|
|
import { walk } from 'estree-walker';
|
|
import { id, re } from './utils/id.js';
|
|
import { get_comment_handlers } from './utils/comments.js';
|
|
|
|
/** @typedef {import('estree').Expression} Expression */
|
|
/** @typedef {import('estree').Node} Node */
|
|
/** @typedef {import('estree').ObjectExpression} ObjectExpression */
|
|
/** @typedef {import('estree').Property} Property */
|
|
/** @typedef {import('estree').SpreadElement} SpreadElement */
|
|
|
|
/** @typedef {import('./utils/comments').CommentWithLocation} CommentWithLocation */
|
|
|
|
/** @type {Record<string, string>} */
|
|
const sigils = {
|
|
'@': 'AT',
|
|
'#': 'HASH'
|
|
};
|
|
|
|
/** @param {TemplateStringsArray} strings */
|
|
const join = (strings) => {
|
|
let str = strings[0];
|
|
for (let i = 1; i < strings.length; i += 1) {
|
|
str += `_${id}_${i - 1}_${strings[i]}`;
|
|
}
|
|
return str.replace(
|
|
/([@#])(\w+)/g,
|
|
(_m, sigil, name) => `_${id}_${sigils[sigil]}_${name}`
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @param {any[]} array
|
|
* @param {any[]} target
|
|
*/
|
|
const flatten_body = (array, target) => {
|
|
for (let i = 0; i < array.length; i += 1) {
|
|
const statement = array[i];
|
|
if (Array.isArray(statement)) {
|
|
flatten_body(statement, target);
|
|
continue;
|
|
}
|
|
|
|
if (statement.type === 'ExpressionStatement') {
|
|
if (statement.expression === EMPTY) continue;
|
|
|
|
if (Array.isArray(statement.expression)) {
|
|
// TODO this is hacktacular
|
|
let node = statement.expression[0];
|
|
while (Array.isArray(node)) node = node[0];
|
|
if (node) node.leadingComments = statement.leadingComments;
|
|
|
|
flatten_body(statement.expression, target);
|
|
continue;
|
|
}
|
|
|
|
if (/(Expression|Literal)$/.test(statement.expression.type)) {
|
|
target.push(statement);
|
|
continue;
|
|
}
|
|
|
|
if (statement.leadingComments)
|
|
statement.expression.leadingComments = statement.leadingComments;
|
|
if (statement.trailingComments)
|
|
statement.expression.trailingComments = statement.trailingComments;
|
|
|
|
target.push(statement.expression);
|
|
continue;
|
|
}
|
|
|
|
target.push(statement);
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
/**
|
|
* @param {any[]} array
|
|
* @param {any[]} target
|
|
*/
|
|
const flatten_properties = (array, target) => {
|
|
for (let i = 0; i < array.length; i += 1) {
|
|
const property = array[i];
|
|
|
|
if (property.value === EMPTY) continue;
|
|
|
|
if (property.key === property.value && Array.isArray(property.key)) {
|
|
flatten_properties(property.key, target);
|
|
continue;
|
|
}
|
|
|
|
target.push(property);
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
/**
|
|
* @param {any[]} nodes
|
|
* @param {any[]} target
|
|
*/
|
|
const flatten = (nodes, target) => {
|
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
const node = nodes[i];
|
|
|
|
if (node === EMPTY) continue;
|
|
|
|
if (Array.isArray(node)) {
|
|
flatten(node, target);
|
|
continue;
|
|
}
|
|
|
|
target.push(node);
|
|
}
|
|
|
|
return target;
|
|
};
|
|
|
|
const EMPTY = { type: 'Empty' };
|
|
|
|
/**
|
|
*
|
|
* @param {CommentWithLocation[]} comments
|
|
* @param {string} raw
|
|
* @returns {any}
|
|
*/
|
|
const acorn_opts = (comments, raw) => {
|
|
const { onComment } = get_comment_handlers(comments, raw);
|
|
return {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'module',
|
|
allowAwaitOutsideFunction: true,
|
|
allowImportExportEverywhere: true,
|
|
allowReturnOutsideFunction: true,
|
|
onComment
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {string} raw
|
|
* @param {Node} node
|
|
* @param {any[]} values
|
|
* @param {CommentWithLocation[]} comments
|
|
*/
|
|
const inject = (raw, node, values, comments) => {
|
|
comments.forEach((comment) => {
|
|
comment.value = comment.value.replace(re, (m, i) =>
|
|
+i in values ? values[+i] : m
|
|
);
|
|
});
|
|
|
|
const { enter, leave } = get_comment_handlers(comments, raw);
|
|
|
|
return walk(node, {
|
|
enter,
|
|
|
|
/** @param {any} node */
|
|
leave(node) {
|
|
if (node.type === 'Identifier') {
|
|
re.lastIndex = 0;
|
|
const match = re.exec(node.name);
|
|
|
|
if (match) {
|
|
if (match[1]) {
|
|
if (+match[1] in values) {
|
|
let value = values[+match[1]];
|
|
|
|
if (typeof value === 'string') {
|
|
value = {
|
|
type: 'Identifier',
|
|
name: value,
|
|
leadingComments: node.leadingComments,
|
|
trailingComments: node.trailingComments
|
|
};
|
|
} else if (typeof value === 'number') {
|
|
value = {
|
|
type: 'Literal',
|
|
value,
|
|
leadingComments: node.leadingComments,
|
|
trailingComments: node.trailingComments
|
|
};
|
|
}
|
|
|
|
this.replace(value || EMPTY);
|
|
}
|
|
} else {
|
|
node.name = `${match[2] ? `@` : `#`}${match[4]}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.type === 'Literal') {
|
|
if (typeof node.value === 'string') {
|
|
re.lastIndex = 0;
|
|
const new_value = /** @type {string} */ (node.value).replace(
|
|
re,
|
|
(m, i) => (+i in values ? values[+i] : m)
|
|
);
|
|
const has_changed = new_value !== node.value;
|
|
node.value = new_value;
|
|
if (has_changed && node.raw) {
|
|
// preserve the quotes
|
|
node.raw = `${node.raw[0]}${JSON.stringify(node.value).slice(
|
|
1,
|
|
-1
|
|
)}${node.raw[node.raw.length - 1]}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.type === 'TemplateElement') {
|
|
re.lastIndex = 0;
|
|
node.value.raw = /** @type {string} */ (node.value.raw).replace(
|
|
re,
|
|
(m, i) => (+i in values ? values[+i] : m)
|
|
);
|
|
}
|
|
|
|
if (node.type === 'Program' || node.type === 'BlockStatement') {
|
|
node.body = flatten_body(node.body, []);
|
|
}
|
|
|
|
if (node.type === 'ObjectExpression' || node.type === 'ObjectPattern') {
|
|
node.properties = flatten_properties(node.properties, []);
|
|
}
|
|
|
|
if (node.type === 'ArrayExpression' || node.type === 'ArrayPattern') {
|
|
node.elements = flatten(node.elements, []);
|
|
}
|
|
|
|
if (
|
|
node.type === 'FunctionExpression' ||
|
|
node.type === 'FunctionDeclaration' ||
|
|
node.type === 'ArrowFunctionExpression'
|
|
) {
|
|
node.params = flatten(node.params, []);
|
|
}
|
|
|
|
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
|
|
node.arguments = flatten(node.arguments, []);
|
|
}
|
|
|
|
if (
|
|
node.type === 'ImportDeclaration' ||
|
|
node.type === 'ExportNamedDeclaration'
|
|
) {
|
|
node.specifiers = flatten(node.specifiers, []);
|
|
}
|
|
|
|
if (node.type === 'ForStatement') {
|
|
node.init = node.init === EMPTY ? null : node.init;
|
|
node.test = node.test === EMPTY ? null : node.test;
|
|
node.update = node.update === EMPTY ? null : node.update;
|
|
}
|
|
|
|
leave(node);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {TemplateStringsArray} strings
|
|
* @param {any[]} values
|
|
* @returns {Node[]}
|
|
*/
|
|
export function b(strings, ...values) {
|
|
const str = join(strings);
|
|
|
|
/** @type {CommentWithLocation[]} */
|
|
const comments = [];
|
|
|
|
try {
|
|
let ast = /** @type {any} */ (acorn.parse(str, acorn_opts(comments, str)));
|
|
|
|
ast = inject(str, ast, values, comments);
|
|
|
|
return ast.body;
|
|
} catch (err) {
|
|
handle_error(str, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {TemplateStringsArray} strings
|
|
* @param {any[]} values
|
|
* @returns {Expression & { start: Number, end: number }}
|
|
*/
|
|
export function x(strings, ...values) {
|
|
const str = join(strings);
|
|
|
|
/** @type {CommentWithLocation[]} */
|
|
const comments = [];
|
|
|
|
try {
|
|
let expression =
|
|
/** @type {Expression & { start: Number, end: number }} */ (
|
|
acorn.parseExpressionAt(str, 0, acorn_opts(comments, str))
|
|
);
|
|
const match = /\S+/.exec(str.slice(expression.end));
|
|
if (match) {
|
|
throw new Error(`Unexpected token '${match[0]}'`);
|
|
}
|
|
|
|
expression = /** @type {Expression & { start: Number, end: number }} */ (
|
|
inject(str, expression, values, comments)
|
|
);
|
|
|
|
return expression;
|
|
} catch (err) {
|
|
handle_error(str, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {TemplateStringsArray} strings
|
|
* @param {any[]} values
|
|
* @returns {(Property | SpreadElement) & { start: Number, end: number }}
|
|
*/
|
|
export function p(strings, ...values) {
|
|
const str = `{${join(strings)}}`;
|
|
|
|
/** @type {CommentWithLocation[]} */
|
|
const comments = [];
|
|
|
|
try {
|
|
let expression = /** @type {any} */ (
|
|
acorn.parseExpressionAt(str, 0, acorn_opts(comments, str))
|
|
);
|
|
|
|
expression = inject(str, expression, values, comments);
|
|
|
|
return expression.properties[0];
|
|
} catch (err) {
|
|
handle_error(str, err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} str
|
|
* @param {Error} err
|
|
*/
|
|
function handle_error(str, err) {
|
|
// TODO location/code frame
|
|
|
|
re.lastIndex = 0;
|
|
|
|
str = str.replace(re, (m, i, at, hash, name) => {
|
|
if (at) return `@${name}`;
|
|
if (hash) return `#${name}`;
|
|
|
|
return '${...}';
|
|
});
|
|
|
|
console.log(`failed to parse:\n${str}`);
|
|
throw err;
|
|
}
|
|
|
|
export { print } from './print/index.js';
|
|
|
|
/**
|
|
* @param {string} source
|
|
* @param {any} opts
|
|
*/
|
|
export const parse = (source, opts) => {
|
|
/** @type {CommentWithLocation[]} */
|
|
const comments = [];
|
|
const { onComment, enter, leave } = get_comment_handlers(comments, source);
|
|
const ast = /** @type {any} */ (acorn.parse(source, { onComment, ...opts }));
|
|
walk(ast, { enter, leave });
|
|
return ast;
|
|
};
|
|
|
|
/**
|
|
* @param {string} source
|
|
* @param {number} index
|
|
* @param {any} opts
|
|
*/
|
|
export const parseExpressionAt = (source, index, opts) => {
|
|
/** @type {CommentWithLocation[]} */
|
|
const comments = [];
|
|
const { onComment, enter, leave } = get_comment_handlers(comments, source);
|
|
const ast = /** @type {any} */ (
|
|
acorn.parseExpressionAt(source, index, { onComment, ...opts })
|
|
);
|
|
walk(ast, { enter, leave });
|
|
return ast;
|
|
};
|