153 lines
3.4 KiB
Plaintext
153 lines
3.4 KiB
Plaintext
import { WalkerBase } from './walker.js';
|
|
|
|
/**
|
|
* @typedef { import('estree').Node} Node
|
|
* @typedef { import('./walker.js').WalkerContext} WalkerContext
|
|
* @typedef {(
|
|
* this: WalkerContext,
|
|
* node: Node,
|
|
* parent: Node | null,
|
|
* key: string | number | symbol | null | undefined,
|
|
* index: number | null | undefined
|
|
* ) => Promise<void>} AsyncHandler
|
|
*/
|
|
|
|
export class AsyncWalker extends WalkerBase {
|
|
/**
|
|
*
|
|
* @param {AsyncHandler} [enter]
|
|
* @param {AsyncHandler} [leave]
|
|
*/
|
|
constructor(enter, leave) {
|
|
super();
|
|
|
|
/** @type {boolean} */
|
|
this.should_skip = false;
|
|
|
|
/** @type {boolean} */
|
|
this.should_remove = false;
|
|
|
|
/** @type {Node | null} */
|
|
this.replacement = null;
|
|
|
|
/** @type {WalkerContext} */
|
|
this.context = {
|
|
skip: () => (this.should_skip = true),
|
|
remove: () => (this.should_remove = true),
|
|
replace: (node) => (this.replacement = node)
|
|
};
|
|
|
|
/** @type {AsyncHandler | undefined} */
|
|
this.enter = enter;
|
|
|
|
/** @type {AsyncHandler | undefined} */
|
|
this.leave = leave;
|
|
}
|
|
|
|
/**
|
|
* @template {Node} Parent
|
|
* @param {Node} node
|
|
* @param {Parent | null} parent
|
|
* @param {keyof Parent} [prop]
|
|
* @param {number | null} [index]
|
|
* @returns {Promise<Node | null>}
|
|
*/
|
|
async visit(node, parent, prop, index) {
|
|
if (node) {
|
|
if (this.enter) {
|
|
const _should_skip = this.should_skip;
|
|
const _should_remove = this.should_remove;
|
|
const _replacement = this.replacement;
|
|
this.should_skip = false;
|
|
this.should_remove = false;
|
|
this.replacement = null;
|
|
|
|
await this.enter.call(this.context, node, parent, prop, index);
|
|
|
|
if (this.replacement) {
|
|
node = this.replacement;
|
|
this.replace(parent, prop, index, node);
|
|
}
|
|
|
|
if (this.should_remove) {
|
|
this.remove(parent, prop, index);
|
|
}
|
|
|
|
const skipped = this.should_skip;
|
|
const removed = this.should_remove;
|
|
|
|
this.should_skip = _should_skip;
|
|
this.should_remove = _should_remove;
|
|
this.replacement = _replacement;
|
|
|
|
if (skipped) return node;
|
|
if (removed) return null;
|
|
}
|
|
|
|
/** @type {keyof Node} */
|
|
let key;
|
|
|
|
for (key in node) {
|
|
/** @type {unknown} */
|
|
const value = node[key];
|
|
|
|
if (value && typeof value === 'object') {
|
|
if (Array.isArray(value)) {
|
|
const nodes = /** @type {Array<unknown>} */ (value);
|
|
for (let i = 0; i < nodes.length; i += 1) {
|
|
const item = nodes[i];
|
|
if (isNode(item)) {
|
|
if (!(await this.visit(item, node, key, i))) {
|
|
// removed
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
} else if (isNode(value)) {
|
|
await this.visit(value, node, key, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.leave) {
|
|
const _replacement = this.replacement;
|
|
const _should_remove = this.should_remove;
|
|
this.replacement = null;
|
|
this.should_remove = false;
|
|
|
|
await this.leave.call(this.context, node, parent, prop, index);
|
|
|
|
if (this.replacement) {
|
|
node = this.replacement;
|
|
this.replace(parent, prop, index, node);
|
|
}
|
|
|
|
if (this.should_remove) {
|
|
this.remove(parent, prop, index);
|
|
}
|
|
|
|
const removed = this.should_remove;
|
|
|
|
this.replacement = _replacement;
|
|
this.should_remove = _should_remove;
|
|
|
|
if (removed) return null;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ducktype a node.
|
|
*
|
|
* @param {unknown} value
|
|
* @returns {value is Node}
|
|
*/
|
|
function isNode(value) {
|
|
return (
|
|
value !== null && typeof value === 'object' && 'type' in value && typeof value.type === 'string'
|
|
);
|
|
}
|