cst-visit.js 3.9 KB

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