[d24f17c] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | const BREAK = Symbol('break visit');
|
---|
| 4 | const SKIP = Symbol('skip children');
|
---|
| 5 | const 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 | */
|
---|
| 34 | function 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 */
|
---|
| 43 | visit.BREAK = BREAK;
|
---|
| 44 | /** Do not visit the children of the current item */
|
---|
| 45 | visit.SKIP = SKIP;
|
---|
| 46 | /** Remove the current item */
|
---|
| 47 | visit.REMOVE = REMOVE;
|
---|
| 48 | /** Find the item at `path` from `cst` as the root */
|
---|
| 49 | visit.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 | */
|
---|
| 66 | visit.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 | };
|
---|
| 74 | function _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 |
|
---|
| 99 | exports.visit = visit;
|
---|