UNPKG

3.99 kBJavaScriptView Raw
1'use strict';
2
3const BREAK = Symbol('break visit');
4const SKIP = Symbol('skip children');
5const REMOVE = Symbol('remove item');
6/**
7 * Apply a visitor to a CST document or item.
8 *
9 * Walks through the tree (depth-first) starting from the root, calling a
10 * `visitor` function with two arguments when entering each item:
11 * - `item`: The current item, which included the following members:
12 * - `start: SourceToken[]` – Source tokens before the key or value,
13 * possibly including its anchor or tag.
14 * - `key?: Token | null` – Set for pair values. May then be `null`, if
15 * the key before the `:` separator is empty.
16 * - `sep?: SourceToken[]` – Source tokens between the key and the value,
17 * which should include the `:` map value indicator if `value` is set.
18 * - `value?: Token` – The value of a sequence item, or of a map pair.
19 * - `path`: The steps from the root to the current node, as an array of
20 * `['key' | 'value', number]` tuples.
21 *
22 * The return value of the visitor may be used to control the traversal:
23 * - `undefined` (default): Do nothing and continue
24 * - `visit.SKIP`: Do not visit the children of this token, continue with
25 * next sibling
26 * - `visit.BREAK`: Terminate traversal completely
27 * - `visit.REMOVE`: Remove the current item, then continue with the next one
28 * - `number`: Set the index of the next step. This is useful especially if
29 * the index of the current token has changed.
30 * - `function`: Define the next visitor for this item. After the original
31 * visitor is called on item entry, next visitors are called after handling
32 * a non-empty `key` and when exiting the item.
33 */
34function visit(cst, visitor) {
35 if ('type' in cst && cst.type === 'document')
36 cst = { start: cst.start, value: cst.value };
37 _visit(Object.freeze([]), cst, visitor);
38}
39// Without the `as symbol` casts, TS declares these in the `visit`
40// namespace using `var`, but then complains about that because
41// `unique symbol` must be `const`.
42/** Terminate visit traversal completely */
43visit.BREAK = BREAK;
44/** Do not visit the children of the current item */
45visit.SKIP = SKIP;
46/** Remove the current item */
47visit.REMOVE = REMOVE;
48/** Find the item at `path` from `cst` as the root */
49visit.itemAtPath = (cst, path) => {
50 let item = cst;
51 for (const [field, index] of path) {
52 const tok = item?.[field];
53 if (tok && 'items' in tok) {
54 item = tok.items[index];
55 }
56 else
57 return undefined;
58 }
59 return item;
60};
61/**
62 * Get the immediate parent collection of the item at `path` from `cst` as the root.
63 *
64 * Throws an error if the collection is not found, which should never happen if the item itself exists.
65 */
66visit.parentCollection = (cst, path) => {
67 const parent = visit.itemAtPath(cst, path.slice(0, -1));
68 const field = path[path.length - 1][0];
69 const coll = parent?.[field];
70 if (coll && 'items' in coll)
71 return coll;
72 throw new Error('Parent collection not found');
73};
74function _visit(path, item, visitor) {
75 let ctrl = visitor(item, path);
76 if (typeof ctrl === 'symbol')
77 return ctrl;
78 for (const field of ['key', 'value']) {
79 const token = item[field];
80 if (token && 'items' in token) {
81 for (let i = 0; i < token.items.length; ++i) {
82 const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor);
83 if (typeof ci === 'number')
84 i = ci - 1;
85 else if (ci === BREAK)
86 return BREAK;
87 else if (ci === REMOVE) {
88 token.items.splice(i, 1);
89 i -= 1;
90 }
91 }
92 if (typeof ctrl === 'function' && field === 'key')
93 ctrl = ctrl(item, path);
94 }
95 }
96 return typeof ctrl === 'function' ? ctrl(item, path) : ctrl;
97}
98
99exports.visit = visit;