Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cst): Add visitor & stringifier + utilities
Adds: - `CST.isCollection(token)` - `CST.isScalar(token)` - `CST.stringify(cst)` - `CST.visit(cst, visitor)`
- Loading branch information
Showing
4 changed files
with
168 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import type { CollectionItem, Token } from './cst.js' | ||
|
||
/** | ||
* Stringify a CST document, token, or collection item | ||
* | ||
* Fair warning: This applies no validation whatsoever, and | ||
* simply concatenates the sources in their logical order. | ||
*/ | ||
export const stringify = (cst: Token | CollectionItem) => | ||
'type' in cst ? stringifyToken(cst) : stringifyItem(cst) | ||
|
||
function stringifyToken(token: Token) { | ||
switch (token.type) { | ||
case 'block-scalar': { | ||
let res = '' | ||
for (const tok of token.props) res += stringifyToken(tok) | ||
return res + token.source | ||
} | ||
case 'block-map': | ||
case 'block-seq': { | ||
let res = '' | ||
for (const item of token.items) res += stringifyItem(item) | ||
return res | ||
} | ||
case 'flow-collection': { | ||
let res = token.start.source | ||
for (const item of token.items) res += stringifyItem(item) | ||
for (const st of token.end) res += st.source | ||
return res | ||
} | ||
case 'document': { | ||
let res = stringifyItem(token) | ||
if (token.end) for (const st of token.end) res += st.source | ||
return res | ||
} | ||
default: { | ||
let res = token.source | ||
if ('end' in token && token.end) | ||
for (const st of token.end) res += st.source | ||
return res | ||
} | ||
} | ||
} | ||
|
||
function stringifyItem({ start, key, sep, value }: CollectionItem) { | ||
let res = '' | ||
for (const st of start) res += st.source | ||
if (key) res += stringifyToken(key) | ||
if (sep) for (const st of sep) res += st.source | ||
if (value) res += stringifyToken(value) | ||
return res | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters