You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
236 lines
9.0 KiB
236 lines
9.0 KiB
'use strict'; |
|
|
|
var Node = require('./nodes/Node.js'); |
|
|
|
const BREAK = Symbol('break visit'); |
|
const SKIP = Symbol('skip children'); |
|
const REMOVE = Symbol('remove node'); |
|
/** |
|
* Apply a visitor to an AST node or document. |
|
* |
|
* Walks through the tree (depth-first) starting from `node`, calling a |
|
* `visitor` function with three arguments: |
|
* - `key`: For sequence values and map `Pair`, the node's index in the |
|
* collection. Within a `Pair`, `'key'` or `'value'`, correspondingly. |
|
* `null` for the root node. |
|
* - `node`: The current node. |
|
* - `path`: The ancestry of the current node. |
|
* |
|
* The return value of the visitor may be used to control the traversal: |
|
* - `undefined` (default): Do nothing and continue |
|
* - `visit.SKIP`: Do not visit the children of this node, continue with next |
|
* sibling |
|
* - `visit.BREAK`: Terminate traversal completely |
|
* - `visit.REMOVE`: Remove the current node, then continue with the next one |
|
* - `Node`: Replace the current node, then continue by visiting it |
|
* - `number`: While iterating the items of a sequence or map, set the index |
|
* of the next step. This is useful especially if the index of the current |
|
* node has changed. |
|
* |
|
* If `visitor` is a single function, it will be called with all values |
|
* encountered in the tree, including e.g. `null` values. Alternatively, |
|
* separate visitor functions may be defined for each `Map`, `Pair`, `Seq`, |
|
* `Alias` and `Scalar` node. To define the same visitor function for more than |
|
* one node type, use the `Collection` (map and seq), `Value` (map, seq & scalar) |
|
* and `Node` (alias, map, seq & scalar) targets. Of all these, only the most |
|
* specific defined one will be used for each node. |
|
*/ |
|
function visit(node, visitor) { |
|
const visitor_ = initVisitor(visitor); |
|
if (Node.isDocument(node)) { |
|
const cd = visit_(null, node.contents, visitor_, Object.freeze([node])); |
|
if (cd === REMOVE) |
|
node.contents = null; |
|
} |
|
else |
|
visit_(null, node, visitor_, Object.freeze([])); |
|
} |
|
// Without the `as symbol` casts, TS declares these in the `visit` |
|
// namespace using `var`, but then complains about that because |
|
// `unique symbol` must be `const`. |
|
/** Terminate visit traversal completely */ |
|
visit.BREAK = BREAK; |
|
/** Do not visit the children of the current node */ |
|
visit.SKIP = SKIP; |
|
/** Remove the current node */ |
|
visit.REMOVE = REMOVE; |
|
function visit_(key, node, visitor, path) { |
|
const ctrl = callVisitor(key, node, visitor, path); |
|
if (Node.isNode(ctrl) || Node.isPair(ctrl)) { |
|
replaceNode(key, path, ctrl); |
|
return visit_(key, ctrl, visitor, path); |
|
} |
|
if (typeof ctrl !== 'symbol') { |
|
if (Node.isCollection(node)) { |
|
path = Object.freeze(path.concat(node)); |
|
for (let i = 0; i < node.items.length; ++i) { |
|
const ci = visit_(i, node.items[i], visitor, path); |
|
if (typeof ci === 'number') |
|
i = ci - 1; |
|
else if (ci === BREAK) |
|
return BREAK; |
|
else if (ci === REMOVE) { |
|
node.items.splice(i, 1); |
|
i -= 1; |
|
} |
|
} |
|
} |
|
else if (Node.isPair(node)) { |
|
path = Object.freeze(path.concat(node)); |
|
const ck = visit_('key', node.key, visitor, path); |
|
if (ck === BREAK) |
|
return BREAK; |
|
else if (ck === REMOVE) |
|
node.key = null; |
|
const cv = visit_('value', node.value, visitor, path); |
|
if (cv === BREAK) |
|
return BREAK; |
|
else if (cv === REMOVE) |
|
node.value = null; |
|
} |
|
} |
|
return ctrl; |
|
} |
|
/** |
|
* Apply an async visitor to an AST node or document. |
|
* |
|
* Walks through the tree (depth-first) starting from `node`, calling a |
|
* `visitor` function with three arguments: |
|
* - `key`: For sequence values and map `Pair`, the node's index in the |
|
* collection. Within a `Pair`, `'key'` or `'value'`, correspondingly. |
|
* `null` for the root node. |
|
* - `node`: The current node. |
|
* - `path`: The ancestry of the current node. |
|
* |
|
* The return value of the visitor may be used to control the traversal: |
|
* - `Promise`: Must resolve to one of the following values |
|
* - `undefined` (default): Do nothing and continue |
|
* - `visit.SKIP`: Do not visit the children of this node, continue with next |
|
* sibling |
|
* - `visit.BREAK`: Terminate traversal completely |
|
* - `visit.REMOVE`: Remove the current node, then continue with the next one |
|
* - `Node`: Replace the current node, then continue by visiting it |
|
* - `number`: While iterating the items of a sequence or map, set the index |
|
* of the next step. This is useful especially if the index of the current |
|
* node has changed. |
|
* |
|
* If `visitor` is a single function, it will be called with all values |
|
* encountered in the tree, including e.g. `null` values. Alternatively, |
|
* separate visitor functions may be defined for each `Map`, `Pair`, `Seq`, |
|
* `Alias` and `Scalar` node. To define the same visitor function for more than |
|
* one node type, use the `Collection` (map and seq), `Value` (map, seq & scalar) |
|
* and `Node` (alias, map, seq & scalar) targets. Of all these, only the most |
|
* specific defined one will be used for each node. |
|
*/ |
|
async function visitAsync(node, visitor) { |
|
const visitor_ = initVisitor(visitor); |
|
if (Node.isDocument(node)) { |
|
const cd = await visitAsync_(null, node.contents, visitor_, Object.freeze([node])); |
|
if (cd === REMOVE) |
|
node.contents = null; |
|
} |
|
else |
|
await visitAsync_(null, node, visitor_, Object.freeze([])); |
|
} |
|
// Without the `as symbol` casts, TS declares these in the `visit` |
|
// namespace using `var`, but then complains about that because |
|
// `unique symbol` must be `const`. |
|
/** Terminate visit traversal completely */ |
|
visitAsync.BREAK = BREAK; |
|
/** Do not visit the children of the current node */ |
|
visitAsync.SKIP = SKIP; |
|
/** Remove the current node */ |
|
visitAsync.REMOVE = REMOVE; |
|
async function visitAsync_(key, node, visitor, path) { |
|
const ctrl = await callVisitor(key, node, visitor, path); |
|
if (Node.isNode(ctrl) || Node.isPair(ctrl)) { |
|
replaceNode(key, path, ctrl); |
|
return visitAsync_(key, ctrl, visitor, path); |
|
} |
|
if (typeof ctrl !== 'symbol') { |
|
if (Node.isCollection(node)) { |
|
path = Object.freeze(path.concat(node)); |
|
for (let i = 0; i < node.items.length; ++i) { |
|
const ci = await visitAsync_(i, node.items[i], visitor, path); |
|
if (typeof ci === 'number') |
|
i = ci - 1; |
|
else if (ci === BREAK) |
|
return BREAK; |
|
else if (ci === REMOVE) { |
|
node.items.splice(i, 1); |
|
i -= 1; |
|
} |
|
} |
|
} |
|
else if (Node.isPair(node)) { |
|
path = Object.freeze(path.concat(node)); |
|
const ck = await visitAsync_('key', node.key, visitor, path); |
|
if (ck === BREAK) |
|
return BREAK; |
|
else if (ck === REMOVE) |
|
node.key = null; |
|
const cv = await visitAsync_('value', node.value, visitor, path); |
|
if (cv === BREAK) |
|
return BREAK; |
|
else if (cv === REMOVE) |
|
node.value = null; |
|
} |
|
} |
|
return ctrl; |
|
} |
|
function initVisitor(visitor) { |
|
if (typeof visitor === 'object' && |
|
(visitor.Collection || visitor.Node || visitor.Value)) { |
|
return Object.assign({ |
|
Alias: visitor.Node, |
|
Map: visitor.Node, |
|
Scalar: visitor.Node, |
|
Seq: visitor.Node |
|
}, visitor.Value && { |
|
Map: visitor.Value, |
|
Scalar: visitor.Value, |
|
Seq: visitor.Value |
|
}, visitor.Collection && { |
|
Map: visitor.Collection, |
|
Seq: visitor.Collection |
|
}, visitor); |
|
} |
|
return visitor; |
|
} |
|
function callVisitor(key, node, visitor, path) { |
|
if (typeof visitor === 'function') |
|
return visitor(key, node, path); |
|
if (Node.isMap(node)) |
|
return visitor.Map?.(key, node, path); |
|
if (Node.isSeq(node)) |
|
return visitor.Seq?.(key, node, path); |
|
if (Node.isPair(node)) |
|
return visitor.Pair?.(key, node, path); |
|
if (Node.isScalar(node)) |
|
return visitor.Scalar?.(key, node, path); |
|
if (Node.isAlias(node)) |
|
return visitor.Alias?.(key, node, path); |
|
return undefined; |
|
} |
|
function replaceNode(key, path, node) { |
|
const parent = path[path.length - 1]; |
|
if (Node.isCollection(parent)) { |
|
parent.items[key] = node; |
|
} |
|
else if (Node.isPair(parent)) { |
|
if (key === 'key') |
|
parent.key = node; |
|
else |
|
parent.value = node; |
|
} |
|
else if (Node.isDocument(parent)) { |
|
parent.contents = node; |
|
} |
|
else { |
|
const pt = Node.isAlias(parent) ? 'alias' : 'scalar'; |
|
throw new Error(`Cannot replace node with ${pt} parent`); |
|
} |
|
} |
|
|
|
exports.visit = visit; |
|
exports.visitAsync = visitAsync;
|
|
|