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.
97 lines
3.9 KiB
97 lines
3.9 KiB
const BREAK = Symbol('break visit'); |
|
const SKIP = Symbol('skip children'); |
|
const REMOVE = Symbol('remove item'); |
|
/** |
|
* Apply a visitor to a CST document or item. |
|
* |
|
* Walks through the tree (depth-first) starting from the root, calling a |
|
* `visitor` function with two arguments when entering each item: |
|
* - `item`: The current item, which included the following members: |
|
* - `start: SourceToken[]` – Source tokens before the key or value, |
|
* possibly including its anchor or tag. |
|
* - `key?: Token | null` – Set for pair values. May then be `null`, if |
|
* the key before the `:` separator is empty. |
|
* - `sep?: SourceToken[]` – Source tokens between the key and the value, |
|
* which should include the `:` map value indicator if `value` is set. |
|
* - `value?: Token` – The value of a sequence item, or of a map pair. |
|
* - `path`: The steps from the root to the current node, as an array of |
|
* `['key' | 'value', number]` tuples. |
|
* |
|
* 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 token, continue with |
|
* next sibling |
|
* - `visit.BREAK`: Terminate traversal completely |
|
* - `visit.REMOVE`: Remove the current item, then continue with the next one |
|
* - `number`: Set the index of the next step. This is useful especially if |
|
* the index of the current token has changed. |
|
* - `function`: Define the next visitor for this item. After the original |
|
* visitor is called on item entry, next visitors are called after handling |
|
* a non-empty `key` and when exiting the item. |
|
*/ |
|
function visit(cst, visitor) { |
|
if ('type' in cst && cst.type === 'document') |
|
cst = { start: cst.start, value: cst.value }; |
|
_visit(Object.freeze([]), cst, visitor); |
|
} |
|
// 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 item */ |
|
visit.SKIP = SKIP; |
|
/** Remove the current item */ |
|
visit.REMOVE = REMOVE; |
|
/** Find the item at `path` from `cst` as the root */ |
|
visit.itemAtPath = (cst, path) => { |
|
let item = cst; |
|
for (const [field, index] of path) { |
|
const tok = item?.[field]; |
|
if (tok && 'items' in tok) { |
|
item = tok.items[index]; |
|
} |
|
else |
|
return undefined; |
|
} |
|
return item; |
|
}; |
|
/** |
|
* Get the immediate parent collection of the item at `path` from `cst` as the root. |
|
* |
|
* Throws an error if the collection is not found, which should never happen if the item itself exists. |
|
*/ |
|
visit.parentCollection = (cst, path) => { |
|
const parent = visit.itemAtPath(cst, path.slice(0, -1)); |
|
const field = path[path.length - 1][0]; |
|
const coll = parent?.[field]; |
|
if (coll && 'items' in coll) |
|
return coll; |
|
throw new Error('Parent collection not found'); |
|
}; |
|
function _visit(path, item, visitor) { |
|
let ctrl = visitor(item, path); |
|
if (typeof ctrl === 'symbol') |
|
return ctrl; |
|
for (const field of ['key', 'value']) { |
|
const token = item[field]; |
|
if (token && 'items' in token) { |
|
for (let i = 0; i < token.items.length; ++i) { |
|
const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor); |
|
if (typeof ci === 'number') |
|
i = ci - 1; |
|
else if (ci === BREAK) |
|
return BREAK; |
|
else if (ci === REMOVE) { |
|
token.items.splice(i, 1); |
|
i -= 1; |
|
} |
|
} |
|
if (typeof ctrl === 'function' && field === 'key') |
|
ctrl = ctrl(item, path); |
|
} |
|
} |
|
return typeof ctrl === 'function' ? ctrl(item, path) : ctrl; |
|
} |
|
|
|
export { visit };
|
|
|