-
-
Notifications
You must be signed in to change notification settings - Fork 101
/
cst-visit.ts
86 lines (78 loc) · 3.27 KB
/
cst-visit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import type { CollectionItem, Document } from './cst.js'
const BREAK = Symbol('break visit')
const SKIP = Symbol('skip children')
const REMOVE = Symbol('remove item')
export type Visitor = (
item: CollectionItem,
path: readonly ['key' | 'value', number][]
) => number | symbol | Visitor | void
/**
* 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.
*/
export function visit(cst: Document | CollectionItem, visitor: 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 as symbol
/** Do not visit the children of the current item */
visit.SKIP = SKIP as symbol
/** Remove the current item */
visit.REMOVE = REMOVE as symbol
function _visit(
path: readonly ['key' | 'value', number][],
item: CollectionItem,
visitor: Visitor
): number | symbol | Visitor | void {
let ctrl = visitor(item, path)
if (typeof ctrl === 'symbol') return ctrl
for (const field of ['key', 'value'] as const) {
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
}