cst-visit.js 3.9 KB

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