@glion/util-visit 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Visitor for traversing HL7v2 AST trees with ancestor context, depth, and HL7v2-a
4
4
 
5
5
  ## What it does
6
6
 
7
- Walks an HL7v2 AST from any starting node — Root, Segment, Field, Component, or Subcomponent — calling a visitor function for each matching node. Each call receives the node, its ancestors (root-to-parent), and a `VisitInfo` record with index, HL7v2 sequence number, depth, and extracted metadata. Supports filtering by type string, property object, or predicate, and exposes `SKIP`/`EXIT` control actions. Delegates core traversal to `unist-util-visit-parents` and pre-computes sibling indices for O(1) lookups.
7
+ Walks an HL7v2 AST from any starting node — Root, Segment, Field, Component, or Subcomponent — calling a visitor function for each matching node. Each call receives the node, its ancestors (root-to-parent), and a `VisitInfo` record with index, HL7v2 sequence number, depth, and extracted metadata. Supports filtering by type string, property object, or predicate, and exposes `SKIP`/`EXIT` control actions. Delegates core traversal to `unist-util-visit-parents`.
8
8
 
9
9
  ## Install
10
10
 
@@ -52,7 +52,7 @@ Visit nodes in an HL7v2 AST tree.
52
52
  - `tree` (`Nodes`) — Tree to traverse. Can be any node type, not just `Root`.
53
53
  - `test` (`string | Partial<Nodes> | Test`, optional) — Filter:
54
54
  - `string` — Match nodes by type (e.g., `"segment"`).
55
- - `Partial<Nodes>` — Match nodes with matching properties (e.g., `{ name: "PATIENT_GROUP" }`).
55
+ - `Partial<Nodes>` — Match nodes whose own properties equal (`===`) each value (e.g., `{ name: "PATIENT_GROUP" }`). A property the node does not own reads as `undefined`: an `undefined` value matches an absent property, and a key naming an inherited member (`constructor`, `toString`) matches no node.
56
56
  - `Test` — Custom function `(node, ancestors) => boolean`.
57
57
  - `visitor` (`Visitor`) — Function called for each matching node.
58
58
 
@@ -105,16 +105,16 @@ The visitor can return:
105
105
 
106
106
  ```typescript
107
107
  interface VisitInfo {
108
- /** 0-based index among siblings */
108
+ /** 0-based index among the parent's children when the node is visited */
109
109
  index: number;
110
110
 
111
- /** 1-based sequence (HL7v2 convention). For segment-header: 0 */
111
+ /** `index` + 1, the HL7v2 sequence number */
112
112
  sequence: number;
113
113
 
114
114
  /** 1-based depth in tree (root = 1) */
115
115
  depth: number;
116
116
 
117
- /** Metadata (e.g., { header: "MSH" } or { name: "PATIENT" }) */
117
+ /** Metadata (e.g., { name: "MSH" } or { name: "PATIENT" }) */
118
118
  metadata: Record<string, unknown> | undefined;
119
119
  }
120
120
  ```
@@ -135,6 +135,17 @@ visit(
135
135
 
136
136
  This is correct because HL7v2 paths like `PID.3` refer to tree positions, not filtered positions.
137
137
 
138
+ The position is read when the node is visited, so it reflects siblings a visitor inserted or removed earlier in the same visit. A visitor that removes the current node can return `info.index` to continue with the sibling that moved into its place:
139
+
140
+ ```typescript
141
+ visit(segment, "field", (node, ancestors, info) => {
142
+ if (isEmptyNode(node)) {
143
+ ancestors.at(-1).children.splice(info.index, 1);
144
+ return info.index;
145
+ }
146
+ });
147
+ ```
148
+
138
149
  ### Automatic metadata extraction
139
150
 
140
151
  The `metadata` field is populated automatically:
@@ -319,12 +330,14 @@ function extractPatientNames(ast: Root): PatientName[] {
319
330
  if (nameField?.children[0]?.children[0]) {
320
331
  const nameComponent = nameField.children[0].children[0];
321
332
  const name = (nameComponent.children[0] as Subcomponent)?.value || "";
322
- const groupAncestor = ancestors.find((n) => n.type === "group");
333
+ const groupAncestor = ancestors.find(
334
+ (n): n is Group => n.type === "group"
335
+ );
323
336
 
324
337
  names.push({
325
338
  name,
326
339
  sequence: info.sequence,
327
- inGroup: groupAncestor ? (groupAncestor as any).name : undefined,
340
+ inGroup: groupAncestor?.name,
328
341
  });
329
342
  }
330
343
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,OAAO,KAAK,EAAE,IAAI,EAAa,OAAO,EAAE,MAAM,SAAS,CAAC;AAGxD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAG9D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEtD,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AA8BnE,wBAAgB,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;AAC3D,wBAAgB,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,MAAM,CAAC,EAC9C,IAAI,EAAE,KAAK,EACX,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,GAC/C,IAAI,CAAC;AACR,wBAAgB,KAAK,CAAC,CAAC,SAAS,KAAK,EACnC,IAAI,EAAE,KAAK,EACX,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACb,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAClB,IAAI,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,OAAO,KAAK,EAAE,IAAI,EAAa,OAAO,EAAE,MAAM,SAAS,CAAC;AAGxD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEtD,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGnE,wBAAgB,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;AAC3D,wBAAgB,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,MAAM,CAAC,EAC9C,IAAI,EAAE,KAAK,EACX,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,GAC/C,IAAI,CAAC;AACR,wBAAgB,KAAK,CAAC,CAAC,SAAS,KAAK,EACnC,IAAI,EAAE,KAAK,EACX,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACb,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAClB,IAAI,CAAC"}
package/dist/index.js CHANGED
@@ -1,60 +1,51 @@
1
1
  import { EXIT, SKIP, visitParents } from "unist-util-visit-parents";
2
2
  //#region src/utils.ts
3
3
  /**
4
- * Create test predicate from various input types.
4
+ * The predicate a `visit` test describes.
5
5
  *
6
- * Assumptions:
6
+ * - `null` matches every node.
7
+ * - A string matches nodes whose `type` equals it.
8
+ * - A function is the predicate.
9
+ * - An object matches nodes whose own property equals (`===`) each of its values.
10
+ * A property the node does not own reads as `undefined`, so an `undefined`
11
+ * value matches an absent property, and a key that names an inherited member
12
+ * (`constructor`, `toString`, an own `__proto__`) matches no node. The
13
+ * object's entries are read once, when the predicate is created.
7
14
  *
8
- * - Null test matches all nodes
9
- * - String test matches by node.type property
10
- * - Object test uses strict equality (===) for property matching
11
- * - Explicit undefined values in test object check for property absence
12
- * - Dangerous keys (**proto**, constructor, prototype) are filtered for security
15
+ * @example
16
+ * ```typescript
17
+ * const msh = s("MSH", f("|"));
13
18
  *
14
- * @param test - Filter criteria: null (all), string (type), object
15
- * (properties), or function
16
- * @returns Predicate function that returns true if node matches test criteria
19
+ * createTest(null)(msh, []); // true
20
+ * createTest("segment")(msh, []); // true
21
+ * createTest("field")(msh, []); // false
22
+ *
23
+ * createTest({ type: "segment", name: "MSH" })(msh, []); // true
24
+ * createTest({ name: "PID" })(msh, []); // false
25
+ *
26
+ * // `undefined` matches a property the node does not have
27
+ * createTest({ name: undefined })(f("|"), []); // true
28
+ *
29
+ * // An inherited member is never matched
30
+ * createTest({ constructor: Object })(msh, []); // false
31
+ *
32
+ * // A function receives the ancestors as well
33
+ * const inGroup = createTest((_node, ancestors) => ancestors.length > 1);
34
+ * ```;
35
+ *
36
+ * @param test - `null`, a node type, a property object, or a predicate.
37
+ * @returns The predicate.
17
38
  */
18
39
  function createTest(test) {
19
40
  if (test === null) return () => true;
20
41
  if (typeof test === "string") return (node) => node.type === test;
21
42
  if (typeof test === "function") return test;
22
- return (node) => {
23
- for (const key of Object.keys(test)) {
24
- if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
25
- const testValue = test[key];
26
- const nodeValue = node[key];
27
- if (testValue === void 0) {
28
- if (Object.hasOwn(node, key) && nodeValue !== void 0) return false;
29
- } else if (nodeValue !== testValue) return false;
30
- }
31
- return true;
32
- };
43
+ const entries = Object.entries(test);
44
+ return (node) => entries.every(([key, value]) => (Object.hasOwn(node, key) ? Reflect.get(node, key) : void 0) === value);
33
45
  }
34
46
  //#endregion
35
47
  //#region src/index.ts
36
48
  /**
37
- * Build index map for O(1) child index lookups.
38
- * Pre-computes the index of each node within its parent's children array.
39
- *
40
- * @param tree - Root of tree to index
41
- * @returns WeakMap mapping each node to its index in parent.children
42
- */
43
- function buildIndexMap(tree) {
44
- const map = /* @__PURE__ */ new WeakMap();
45
- function traverse(node) {
46
- if ("children" in node && Array.isArray(node.children)) for (let i = 0; i < node.children.length; i++) {
47
- const child = node.children[i];
48
- if (child) {
49
- map.set(child, i);
50
- traverse(child);
51
- }
52
- }
53
- }
54
- traverse(tree);
55
- return map;
56
- }
57
- /**
58
49
  * Visit nodes in an HL7 AST tree.
59
50
  * Wraps unist-util-visit-parents to add HL7v2-specific context.
60
51
  *
@@ -77,16 +68,18 @@ function visit(tree, arg2, arg3) {
77
68
  visitor = arg3;
78
69
  }
79
70
  const predicate = createTest(test);
80
- const indexMap = buildIndexMap(tree);
81
- visitParents(tree, (node, ancestors) => {
71
+ let currentIndex = 0;
72
+ const captureIndex = (_node, index) => {
73
+ currentIndex = index ?? 0;
74
+ return true;
75
+ };
76
+ visitParents(tree, captureIndex, (node, ancestors) => {
82
77
  if (!predicate(node, ancestors)) return;
83
- const parent = ancestors.at(-1);
84
- const childIndex = parent ? indexMap.get(node) ?? 0 : 0;
85
78
  const info = {
86
79
  depth: ancestors.length + 1,
87
- index: parent ? childIndex : 0,
88
- metadata: "name" in node && typeof node.name === "string" ? { name: node.name } : void 0,
89
- sequence: parent ? childIndex + 1 : 1
80
+ index: currentIndex,
81
+ metadata: "name" in node ? { name: node.name } : void 0,
82
+ sequence: currentIndex + 1
90
83
  };
91
84
  return visitor(node, ancestors, info);
92
85
  });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import type { Nodes } from \"@glion/ast\";\n\nimport type { Predicate, Test } from \"./types\";\n\n/**\n * Create test predicate from various input types.\n *\n * Assumptions:\n *\n * - Null test matches all nodes\n * - String test matches by node.type property\n * - Object test uses strict equality (===) for property matching\n * - Explicit undefined values in test object check for property absence\n * - Dangerous keys (**proto**, constructor, prototype) are filtered for security\n *\n * @param test - Filter criteria: null (all), string (type), object\n * (properties), or function\n * @returns Predicate function that returns true if node matches test criteria\n */\nexport function createTest(test: Test<Nodes>): Predicate {\n if (test === null) {\n return () => true;\n }\n if (typeof test === \"string\") {\n return (node) => node.type === test;\n }\n if (typeof test === \"function\") {\n return test;\n }\n // Object property matching\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Property matching requires checking multiple conditions\n return (node) => {\n for (const key of Object.keys(test)) {\n // Guard against prototype pollution\n if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n continue;\n }\n\n const testValue = test[key as keyof typeof test];\n // oxlint-disable-next-line typescript/no-explicit-any\n const nodeValue = (node as any)[key];\n\n // If test has explicit undefined, check property doesn't exist or is undefined\n if (testValue === undefined) {\n if (Object.hasOwn(node, key) && nodeValue !== undefined) {\n return false;\n }\n } else if (nodeValue !== testValue) {\n // For non-undefined values, strict equality check\n return false;\n }\n }\n return true;\n };\n}\n","/**\n * HL7v2 AST Visitor\n *\n * Wraps unist-util-visit-parents to add HL7v2-specific context\n * (index, sequence, depth, metadata) while delegating core traversal.\n */\n\nimport type { Nodes } from \"@glion/ast\";\nimport { visitParents } from \"unist-util-visit-parents\";\n\nimport type { Test, VisitInfo, Visitor } from \"./types\";\nimport { createTest } from \"./utils\";\n\nexport type { VisitorResult } from \"unist-util-visit-parents\";\n\n// biome-ignore lint/performance/noBarrelFile: fine\nexport { EXIT, SKIP } from \"unist-util-visit-parents\";\n\nexport type { Predicate, Test, VisitInfo, Visitor } from \"./types\";\n\n/**\n * Build index map for O(1) child index lookups.\n * Pre-computes the index of each node within its parent's children array.\n *\n * @param tree - Root of tree to index\n * @returns WeakMap mapping each node to its index in parent.children\n */\nfunction buildIndexMap(tree: Nodes): WeakMap<Nodes, number> {\n const map = new WeakMap<Nodes, number>();\n\n function traverse(node: Nodes): void {\n if (\"children\" in node && Array.isArray(node.children)) {\n // oxlint-disable-next-line no-plusplus\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n if (child) {\n map.set(child, i);\n traverse(child);\n }\n }\n }\n }\n\n traverse(tree);\n return map;\n}\n\n// Overload signatures\nexport function visit(tree: Nodes, visitor: Visitor): void;\nexport function visit<Type extends Nodes[\"type\"]>(\n tree: Nodes,\n test: Type,\n visitor: Visitor<Extract<Nodes, { type: Type }>>\n): void;\nexport function visit<T extends Nodes>(\n tree: Nodes,\n test: Test<T>,\n visitor: Visitor<T>\n): void;\n\n/**\n * Visit nodes in an HL7 AST tree.\n * Wraps unist-util-visit-parents to add HL7v2-specific context.\n *\n * @param tree - The tree to traverse (can be any node type, not just Root)\n * @param test - Optional test to filter nodes (type string, partial match, or\n * Test function)\n * @param visitor - Function called for each matching node\n *\n * **Important**: If you pass a function as the second argument, it is always\n * treated as a Visitor, never as a Test. To use a Test function, you MUST\n * provide both the test and visitor parameters: `visit(tree, testFn,\n * visitorFn)`.\n */\nexport function visit<T extends Nodes>(\n tree: Nodes,\n arg2: Visitor<T> | Test<T>,\n arg3?: Visitor<T>\n): void {\n let test: Test<T> = null;\n let visitor: Visitor<T>;\n\n if (arg3 === undefined) {\n visitor = arg2 as Visitor<T>;\n } else {\n test = arg2 as Test<T>;\n visitor = arg3;\n }\n\n const predicate = createTest(test as Test<Nodes>);\n\n // Pre-compute index map for O(1) lookups\n const indexMap = buildIndexMap(tree);\n\n // Delegate traversal to unist-util-visit-parents\n visitParents(tree, (node, ancestors) => {\n // Only call visitor if node matches test\n if (!predicate(node, ancestors)) {\n return; // Continue traversal but skip visitor\n }\n\n // Compute HL7v2-specific context\n const parent = ancestors.at(-1);\n\n // For root node (no parent), use defaults\n // For children, look up their index in the parent's children array\n const childIndex = parent ? (indexMap.get(node) ?? 0) : 0;\n\n const info: VisitInfo = {\n depth: ancestors.length + 1,\n index: parent ? childIndex : 0,\n metadata:\n \"name\" in node && typeof node.name === \"string\"\n ? { name: node.name }\n : undefined,\n sequence: parent ? childIndex + 1 : 1,\n };\n\n // Call user visitor with augmented signature\n return visitor(node as T, ancestors, info);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,SAAgB,WAAW,MAA8B;AACvD,KAAI,SAAS,KACX,cAAa;AAEf,KAAI,OAAO,SAAS,SAClB,SAAQ,SAAS,KAAK,SAAS;AAEjC,KAAI,OAAO,SAAS,WAClB,QAAO;AAIT,SAAQ,SAAS;AACf,OAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AAEnC,OAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,YAC1D;GAGF,MAAM,YAAY,KAAK;GAEvB,MAAM,YAAa,KAAa;AAGhC,OAAI,cAAc,KAAA;QACZ,OAAO,OAAO,MAAM,IAAI,IAAI,cAAc,KAAA,EAC5C,QAAO;cAEA,cAAc,UAEvB,QAAO;;AAGX,SAAO;;;;;;;;;;;;ACzBX,SAAS,cAAc,MAAqC;CAC1D,MAAM,sBAAM,IAAI,SAAwB;CAExC,SAAS,SAAS,MAAmB;AACnC,MAAI,cAAc,QAAQ,MAAM,QAAQ,KAAK,SAAS,CAEpD,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,QAAQ,KAAK;GAC7C,MAAM,QAAQ,KAAK,SAAS;AAC5B,OAAI,OAAO;AACT,QAAI,IAAI,OAAO,EAAE;AACjB,aAAS,MAAM;;;;AAMvB,UAAS,KAAK;AACd,QAAO;;;;;;;;;;;;;;;;AA8BT,SAAgB,MACd,MACA,MACA,MACM;CACN,IAAI,OAAgB;CACpB,IAAI;AAEJ,KAAI,SAAS,KAAA,EACX,WAAU;MACL;AACL,SAAO;AACP,YAAU;;CAGZ,MAAM,YAAY,WAAW,KAAoB;CAGjD,MAAM,WAAW,cAAc,KAAK;AAGpC,cAAa,OAAO,MAAM,cAAc;AAEtC,MAAI,CAAC,UAAU,MAAM,UAAU,CAC7B;EAIF,MAAM,SAAS,UAAU,GAAG,GAAG;EAI/B,MAAM,aAAa,SAAU,SAAS,IAAI,KAAK,IAAI,IAAK;EAExD,MAAM,OAAkB;GACtB,OAAO,UAAU,SAAS;GAC1B,OAAO,SAAS,aAAa;GAC7B,UACE,UAAU,QAAQ,OAAO,KAAK,SAAS,WACnC,EAAE,MAAM,KAAK,MAAM,GACnB,KAAA;GACN,UAAU,SAAS,aAAa,IAAI;GACrC;AAGD,SAAO,QAAQ,MAAW,WAAW,KAAK;GAC1C"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import type { Nodes } from \"@glion/ast\";\n\nimport type { Predicate, Test } from \"./types\";\n\n/**\n * The predicate a `visit` test describes.\n *\n * - `null` matches every node.\n * - A string matches nodes whose `type` equals it.\n * - A function is the predicate.\n * - An object matches nodes whose own property equals (`===`) each of its values.\n * A property the node does not own reads as `undefined`, so an `undefined`\n * value matches an absent property, and a key that names an inherited member\n * (`constructor`, `toString`, an own `__proto__`) matches no node. The\n * object's entries are read once, when the predicate is created.\n *\n * @example\n * ```typescript\n * const msh = s(\"MSH\", f(\"|\"));\n *\n * createTest(null)(msh, []); // true\n * createTest(\"segment\")(msh, []); // true\n * createTest(\"field\")(msh, []); // false\n *\n * createTest({ type: \"segment\", name: \"MSH\" })(msh, []); // true\n * createTest({ name: \"PID\" })(msh, []); // false\n *\n * // `undefined` matches a property the node does not have\n * createTest({ name: undefined })(f(\"|\"), []); // true\n *\n * // An inherited member is never matched\n * createTest({ constructor: Object })(msh, []); // false\n *\n * // A function receives the ancestors as well\n * const inGroup = createTest((_node, ancestors) => ancestors.length > 1);\n * ```;\n *\n * @param test - `null`, a node type, a property object, or a predicate.\n * @returns The predicate.\n */\nexport function createTest(test: Test<Nodes>): Predicate {\n if (test === null) {\n return () => true;\n }\n if (typeof test === \"string\") {\n return (node) => node.type === test;\n }\n if (typeof test === \"function\") {\n // Used as is: `visit` calls it with `(node, ancestors)`, so it may match on\n // a node's position in the tree as well as on the node itself.\n return test;\n }\n\n // Read once here rather than per visited node: a visit calls the predicate\n // for every node, and a test object changed during the visit does not change\n // which nodes match.\n const entries = Object.entries(test);\n\n return (node) =>\n entries.every(\n ([key, value]) =>\n // `Object.hasOwn` limits the match to the node's own properties. Without\n // it, `node[key]` also finds inherited members, so `{ constructor: x }`\n // or `{ toString: x }` would be compared against `Object.prototype`.\n // `Reflect.get(node, key)` is `node[key]`; it compiles for an arbitrary\n // string key, which `Nodes` (no index signature) does not allow.\n (Object.hasOwn(node, key) ? Reflect.get(node, key) : undefined) ===\n value\n );\n}\n","/**\n * HL7v2 AST Visitor\n *\n * Wraps unist-util-visit-parents to add HL7v2-specific context\n * (index, sequence, depth, metadata) while delegating core traversal.\n */\n\nimport type { Nodes } from \"@glion/ast\";\nimport { visitParents } from \"unist-util-visit-parents\";\n\nimport type { Test, VisitInfo, Visitor } from \"./types\";\nimport { createTest } from \"./utils\";\n\nexport type { VisitorResult } from \"unist-util-visit-parents\";\n\nexport { EXIT, SKIP } from \"unist-util-visit-parents\";\n\nexport type { Predicate, Test, VisitInfo, Visitor } from \"./types\";\n\n// Overload signatures\nexport function visit(tree: Nodes, visitor: Visitor): void;\nexport function visit<Type extends Nodes[\"type\"]>(\n tree: Nodes,\n test: Type,\n visitor: Visitor<Extract<Nodes, { type: Type }>>\n): void;\nexport function visit<T extends Nodes>(\n tree: Nodes,\n test: Test<T>,\n visitor: Visitor<T>\n): void;\n\n/**\n * Visit nodes in an HL7 AST tree.\n * Wraps unist-util-visit-parents to add HL7v2-specific context.\n *\n * @param tree - The tree to traverse (can be any node type, not just Root)\n * @param test - Optional test to filter nodes (type string, partial match, or\n * Test function)\n * @param visitor - Function called for each matching node\n *\n * **Important**: If you pass a function as the second argument, it is always\n * treated as a Visitor, never as a Test. To use a Test function, you MUST\n * provide both the test and visitor parameters: `visit(tree, testFn,\n * visitorFn)`.\n */\nexport function visit<T extends Nodes>(\n tree: Nodes,\n arg2: Visitor<T> | Test<T>,\n arg3?: Visitor<T>\n): void {\n let test: Test<T> = null;\n let visitor: Visitor<T>;\n\n if (arg3 === undefined) {\n visitor = arg2 as Visitor<T>;\n } else {\n test = arg2 as Test<T>;\n visitor = arg3;\n }\n\n const predicate = createTest(test as Test<Nodes>);\n\n // visit-parents passes a node's index among its parent's children only to\n // its test, `test(node, index, parent)`, never to its visitor. It calls the\n // test and, when the test passes, the visitor on that same node with nothing\n // in between (unist-util-visit-parents 6.0.2, lib/index.js; the version is\n // pinned). `captureIndex` is that test: it stores the index for the visitor\n // to read and passes every node. The index is the node's position when it\n // is visited, so it reflects siblings a visitor inserted or removed earlier.\n let currentIndex = 0;\n const captureIndex = (_node: unknown, index?: number): boolean => {\n // The node `visit` starts from has no parent, so it has no index.\n currentIndex = index ?? 0;\n return true;\n };\n\n visitParents(tree, captureIndex, (node, ancestors) => {\n // The caller's test runs here, not in `captureIndex`: it may read\n // `ancestors`, which visit-parents gives only to the visitor.\n if (!predicate(node, ancestors)) {\n // Returning nothing means \"continue\": visit-parents still walks into\n // this node's children, so `visit(tree, \"field\", …)` reaches the fields\n // of segments it does not match.\n return;\n }\n\n const info: VisitInfo = {\n depth: ancestors.length + 1,\n index: currentIndex,\n metadata: \"name\" in node ? { name: node.name } : undefined,\n sequence: currentIndex + 1,\n };\n\n // visit-parents reads the visitor's return value to decide what to visit\n // next, so the caller's result is passed through unchanged:\n // - nothing or CONTINUE: walk into this node's children;\n // - SKIP: skip this node's children;\n // - EXIT: stop the walk;\n // - a number: continue at that index among this node's siblings, as after\n // removing the current node with `splice(info.index, 1)`;\n // - [action, index]: both.\n // Calling the visitor without returning its result would turn every one\n // of these into \"continue\".\n return visitor(node as T, ancestors, info);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,WAAW,MAA8B;AACvD,KAAI,SAAS,KACX,cAAa;AAEf,KAAI,OAAO,SAAS,SAClB,SAAQ,SAAS,KAAK,SAAS;AAEjC,KAAI,OAAO,SAAS,WAGlB,QAAO;CAMT,MAAM,UAAU,OAAO,QAAQ,KAAK;AAEpC,SAAQ,SACN,QAAQ,OACL,CAAC,KAAK,YAMJ,OAAO,OAAO,MAAM,IAAI,GAAG,QAAQ,IAAI,MAAM,IAAI,GAAG,KAAA,OACrD,MACH;;;;;;;;;;;;;;;;;;ACtBL,SAAgB,MACd,MACA,MACA,MACM;CACN,IAAI,OAAgB;CACpB,IAAI;AAEJ,KAAI,SAAS,KAAA,EACX,WAAU;MACL;AACL,SAAO;AACP,YAAU;;CAGZ,MAAM,YAAY,WAAW,KAAoB;CASjD,IAAI,eAAe;CACnB,MAAM,gBAAgB,OAAgB,UAA4B;AAEhE,iBAAe,SAAS;AACxB,SAAO;;AAGT,cAAa,MAAM,eAAe,MAAM,cAAc;AAGpD,MAAI,CAAC,UAAU,MAAM,UAAU,CAI7B;EAGF,MAAM,OAAkB;GACtB,OAAO,UAAU,SAAS;GAC1B,OAAO;GACP,UAAU,UAAU,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,KAAA;GACjD,UAAU,eAAe;GAC1B;AAYD,SAAO,QAAQ,MAAW,WAAW,KAAK;GAC1C"}
package/dist/types.d.ts CHANGED
@@ -5,9 +5,13 @@ import type { VisitorResult } from "unist-util-visit-parents";
5
5
  * This is HL7v2-specific context not provided by unist-util-visit-parents.
6
6
  */
7
7
  export interface VisitInfo {
8
- /** 0-based index among siblings */
8
+ /**
9
+ * 0-based index among the parent's children when the node is visited, or 0
10
+ * for the node the visit starts from. Reflects changes a visitor made to
11
+ * those children earlier in the same visit.
12
+ */
9
13
  index: number;
10
- /** 1-based sequence (HL7v2 convention) */
14
+ /** `index` + 1, the HL7v2 sequence number. */
11
15
  sequence: number;
12
16
  /** 1-based depth in tree (root = 1) */
13
17
  depth: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IAEd,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IAEjB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IAEd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI,CAC7C,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,KAAK,EAAE,EAClB,IAAI,EAAE,SAAS,KACZ,aAAa,CAAC;AAEnB;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAErE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IACpC,MAAM,GACN,OAAO,CAAC,CAAC,CAAC,GACV,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,GAChD,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAC9C,IAAI,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IAEjB,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IAEd,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI,CAC7C,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,KAAK,EAAE,EAClB,IAAI,EAAE,SAAS,KACZ,aAAa,CAAC;AAEnB;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAErE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IACpC,MAAM,GACN,OAAO,CAAC,CAAC,CAAC,GACV,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,IAAI,IAAI,CAAC,CAAC,GAChD,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,GAC9C,IAAI,CAAC"}
package/dist/utils.d.ts CHANGED
@@ -1,19 +1,40 @@
1
1
  import type { Nodes } from "@glion/ast";
2
2
  import type { Predicate, Test } from "./types";
3
3
  /**
4
- * Create test predicate from various input types.
4
+ * The predicate a `visit` test describes.
5
5
  *
6
- * Assumptions:
6
+ * - `null` matches every node.
7
+ * - A string matches nodes whose `type` equals it.
8
+ * - A function is the predicate.
9
+ * - An object matches nodes whose own property equals (`===`) each of its values.
10
+ * A property the node does not own reads as `undefined`, so an `undefined`
11
+ * value matches an absent property, and a key that names an inherited member
12
+ * (`constructor`, `toString`, an own `__proto__`) matches no node. The
13
+ * object's entries are read once, when the predicate is created.
7
14
  *
8
- * - Null test matches all nodes
9
- * - String test matches by node.type property
10
- * - Object test uses strict equality (===) for property matching
11
- * - Explicit undefined values in test object check for property absence
12
- * - Dangerous keys (**proto**, constructor, prototype) are filtered for security
15
+ * @example
16
+ * ```typescript
17
+ * const msh = s("MSH", f("|"));
13
18
  *
14
- * @param test - Filter criteria: null (all), string (type), object
15
- * (properties), or function
16
- * @returns Predicate function that returns true if node matches test criteria
19
+ * createTest(null)(msh, []); // true
20
+ * createTest("segment")(msh, []); // true
21
+ * createTest("field")(msh, []); // false
22
+ *
23
+ * createTest({ type: "segment", name: "MSH" })(msh, []); // true
24
+ * createTest({ name: "PID" })(msh, []); // false
25
+ *
26
+ * // `undefined` matches a property the node does not have
27
+ * createTest({ name: undefined })(f("|"), []); // true
28
+ *
29
+ * // An inherited member is never matched
30
+ * createTest({ constructor: Object })(msh, []); // false
31
+ *
32
+ * // A function receives the ancestors as well
33
+ * const inGroup = createTest((_node, ancestors) => ancestors.length > 1);
34
+ * ```;
35
+ *
36
+ * @param test - `null`, a node type, a property object, or a predicate.
37
+ * @returns The predicate.
17
38
  */
18
39
  export declare function createTest(test: Test<Nodes>): Predicate;
19
40
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CAmCvD"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,SAAS,CA6BvD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/util-visit",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "An AST visitor for HL7v2 messages",
5
5
  "keywords": [
6
6
  "health",
@@ -37,18 +37,20 @@
37
37
  "unist-util-visit-parents": "6.0.2"
38
38
  },
39
39
  "devDependencies": {
40
+ "@stryker-mutator/core": "10.0.0",
41
+ "@stryker-mutator/vitest-runner": "10.0.0",
40
42
  "@types/node": "25.6.0",
41
43
  "@vitest/coverage-v8": "4.1.11",
42
44
  "publint": "0.3.18",
43
45
  "tsdown": "0.21.10",
44
46
  "typescript": "6.0.3",
45
47
  "vitest": "4.1.11",
46
- "@glion/ast": "0.22.0",
47
- "@glion/builder": "0.22.0",
48
- "@glion/parser": "0.22.0",
48
+ "@glion/ast": "0.24.0",
49
+ "@glion/builder": "0.24.0",
50
+ "@glion/check-readme": "0.18.0",
51
+ "@glion/parser": "0.24.0",
49
52
  "@glion/testing": "0.18.0",
50
- "@glion/tsconfig": "0.18.0",
51
- "@glion/check-readme": "0.18.0"
53
+ "@glion/tsconfig": "0.18.0"
52
54
  },
53
55
  "engines": {
54
56
  "node": ">=22"
@@ -57,6 +59,7 @@
57
59
  "build": "tsdown && tsc --emitDeclarationOnly",
58
60
  "check-types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json",
59
61
  "check:readme": "glion-check-readme",
62
+ "mutate": "stryker run",
60
63
  "test": "vitest run",
61
64
  "test:coverage": "vitest run --coverage",
62
65
  "test:watch": "vitest",