@glion/util-visit 0.25.0 → 0.27.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Rethink Health
3
+ Copyright (c) 2023 Rethink Health, SUARL
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @glion/util-visit
2
2
 
3
- Visitor for traversing HL7v2 AST trees with ancestor context, depth, and HL7v2-aware sequence numbers.
3
+ Visitor for traversing HL7v2 AST trees with ancestor context.
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`.
7
+ Walks an HL7v2 AST from any starting node — Root, Group, Segment, Field, Component, or Subcomponent — calling a visitor function for each matching node with the node and its ancestors, from the node the walk starts at to the node's parent. Supports filtering by type string, property object, or predicate, and exposes `SKIP`/`EXIT` control actions. Delegates traversal to `unist-util-visit-parents`, with types from `@glion/ast`.
8
8
 
9
9
  ## Install
10
10
 
@@ -15,25 +15,21 @@ npm install @glion/util-visit
15
15
  ## Use
16
16
 
17
17
  ```typescript
18
- import { visit, EXIT, SKIP } from "@glion/util-visit";
19
- import { parse } from "@glion/parser";
18
+ import { parseHL7v2 } from "@glion/parser";
19
+ import { SKIP, visit } from "@glion/util-visit";
20
20
 
21
- const message = parse("MSH|^~\\&|...\rPID|...");
22
-
23
- // Visit all segments
24
- visit(message, "segment", (node, ancestors, info) => {
25
- console.log(`Segment: ${info.metadata?.header} at depth ${info.depth}`);
26
- });
21
+ const tree = parseHL7v2(
22
+ "MSH|^~\\&|LAB|FAC|EMR|RFAC|20250601120000||ORU^R01^ORU_R01|MSG00001|P|2.5\rPID|1||12345\rNTE|1||note"
23
+ );
27
24
 
28
- // Find fields with parent context
29
- visit(message, "field", (node, ancestors, info) => {
30
- const segment = ancestors.find((n) => n.type === "segment");
31
- console.log(`Field ${info.sequence} in segment`);
25
+ // Visit every segment
26
+ visit(tree, "segment", (segment) => {
27
+ console.log(segment.name); // "MSH", "PID", "NTE"
32
28
  });
33
29
 
34
- // Skip processing of sensitive segments
35
- visit(message, (node, ancestors, info) => {
36
- if (node.type === "segment" && info.metadata?.header === "NTE") {
30
+ // Skip the fields of NTE segments
31
+ visit(tree, (node) => {
32
+ if (node.type === "segment" && node.name === "NTE") {
37
33
  return SKIP;
38
34
  }
39
35
  });
@@ -45,14 +41,14 @@ visit(message, (node, ancestors, info) => {
45
41
 
46
42
  ### `visit(tree, test, visitor)`
47
43
 
48
- Visit nodes in an HL7v2 AST tree.
44
+ Visit nodes in an HL7v2 AST tree, in document order.
49
45
 
50
46
  #### Parameters
51
47
 
52
48
  - `tree` (`Nodes`) — Tree to traverse. Can be any node type, not just `Root`.
53
49
  - `test` (`string | Partial<Nodes> | Test`, optional) — Filter:
54
50
  - `string` — Match nodes by type (e.g., `"segment"`).
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.
51
+ - `Partial<Nodes>` — Match nodes whose own properties equal (`===`) each value (e.g., `{ id: "PATIENT" }`). 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
52
  - `Test` — Custom function `(node, ancestors) => boolean`.
57
53
  - `visitor` (`Visitor`) — Function called for each matching node.
58
54
 
@@ -60,23 +56,18 @@ Visit nodes in an HL7v2 AST tree.
60
56
 
61
57
  `void`.
62
58
 
63
- #### Important: test vs visitor functions
59
+ #### Test vs visitor functions
64
60
 
65
- **If you pass a function as the second argument, it is always treated as a Visitor, never as a Test.**
61
+ A function passed as the second argument is always the visitor, never a test. A test function needs the three-argument form:
66
62
 
67
63
  ```typescript
68
- // WRONG — testFn will be treated as a visitor, not a test
69
- visit(ast, (node) => node.type === 'segment', ...); // Missing visitor!
70
-
71
- // CORRECT — Explicit 3-argument form
72
- visit(ast, (node) => node.type === 'segment', (node, ancestors, info) => {
73
- console.log('Visiting segment');
74
- });
75
-
76
- // CORRECT — Use string or object for simple tests
77
- visit(ast, 'segment', (node, ancestors, info) => {
78
- console.log('Visiting segment');
79
- });
64
+ visit(
65
+ tree,
66
+ (node) => node.type === "segment",
67
+ (segment) => {
68
+ console.log(segment.type); // "segment"
69
+ }
70
+ );
80
71
  ```
81
72
 
82
73
  #### Visitor function
@@ -84,290 +75,106 @@ visit(ast, 'segment', (node, ancestors, info) => {
84
75
  ```typescript
85
76
  type Visitor<T extends Nodes = Nodes> = (
86
77
  node: T,
87
- ancestors: Nodes[],
88
- info: VisitInfo
78
+ ancestors: Nodes[]
89
79
  ) => VisitorResult;
90
80
  ```
91
81
 
92
82
  The visitor receives:
93
83
 
94
84
  - `node` — Current AST node.
95
- - `ancestors` — Array of ancestor nodes from root to parent (not including current node).
96
- - `info` — Visit information with index, sequence, depth, and metadata.
85
+ - `ancestors` — Ancestor nodes from the node the walk starts at to the parent, not including the current node. Empty for the starting node.
97
86
 
98
87
  The visitor can return:
99
88
 
100
- - `undefined` or `void` — Continue traversal normally.
101
- - `SKIP` — Skip children of current node.
102
- - `EXIT` — Stop traversal immediately.
103
-
104
- ### `VisitInfo`
105
-
106
- ```typescript
107
- interface VisitInfo {
108
- /** 0-based index among the parent's children when the node is visited */
109
- index: number;
110
-
111
- /** `index` + 1, the HL7v2 sequence number */
112
- sequence: number;
113
-
114
- /** 1-based depth in tree (root = 1) */
115
- depth: number;
116
-
117
- /** Metadata (e.g., { name: "MSH" } or { name: "PATIENT" }) */
118
- metadata: Record<string, unknown> | undefined;
119
- }
120
- ```
121
-
122
- `index` and `sequence` represent the node's **position in the tree**, not its position among filtered results:
89
+ - `undefined` or `CONTINUE` — Continue traversal, into the node's children.
90
+ - `SKIP` — Skip the children of the current node.
91
+ - `EXIT` — Stop traversal.
92
+ - An index — Continue at that index among the current node's siblings.
93
+ - `[action, index]` — Both.
123
94
 
124
- ```typescript
125
- // Structure: MSH segment with fields at positions 1, 2, 3, 4
126
- // Filter matches only field 3
127
- visit(
128
- ast,
129
- (n) => n.type === "field" && hasContent(n),
130
- (node, ancestors, info) => {
131
- console.log(info.sequence); // 3 (position in segment, not "1st match")
132
- }
133
- );
134
- ```
135
-
136
- This is correct because HL7v2 paths like `PID.3` refer to tree positions, not filtered positions.
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:
95
+ A visitor that removes the current node returns its former index to continue with the sibling that moved into its place:
139
96
 
140
97
  ```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
- }
98
+ visit(tree, { type: "segment", name: "NTE" }, (segment, ancestors) => {
99
+ const parent = ancestors.at(-1) as Root | Group;
100
+ const index = parent.children.indexOf(segment);
101
+ parent.children.splice(index, 1);
102
+ return index;
146
103
  });
147
104
  ```
148
105
 
149
- ### Automatic metadata extraction
150
-
151
- The `metadata` field is populated automatically:
152
-
153
- | Node type | Metadata key | Description |
154
- | --------- | ------------ | ------------------------------------------- |
155
- | `segment` | `header` | Segment identifier (e.g., `"MSH"`, `"PID"`) |
156
- | `group` | `name` | Group name (e.g., `"PATIENT_GROUP"`) |
106
+ `indexOf` scans the parent's children, so removing many children of one parent this way costs time in the square of their number. To remove many, filter `parent.children` once from the parent's visitor instead.
157
107
 
158
108
  ### Exports
159
109
 
160
110
  ```typescript
161
111
  import {
162
- visit, // Main traversal function
112
+ visit, // Traversal function
163
113
  EXIT, // Return to stop traversal
164
114
  SKIP, // Return to skip children
165
115
  } from "@glion/util-visit";
166
116
 
167
117
  import type {
168
- VisitInfo, // { index, sequence, depth, metadata }
169
- Visitor, // (node, ancestors, info) => VisitorResult
170
- VisitorResult, // Return type from visitor
171
- Test, // Filter predicate
118
+ Visitor, // (node, ancestors) => VisitorResult
119
+ VisitorResult, // Return type of a visitor
120
+ Test, // Filter: type string, property object, predicate, or null
172
121
  Predicate, // (node, ancestors) => boolean
173
122
  } from "@glion/util-visit";
174
123
  ```
175
124
 
176
125
  ## Examples
177
126
 
178
- ### Filter by node type
179
-
180
- ```typescript
181
- visit(ast, "segment", (node, ancestors, info) => {
182
- console.log(`Found segment: ${info.metadata?.header}`);
183
- });
184
- ```
185
-
186
127
  ### Filter by properties
187
128
 
188
129
  ```typescript
189
- visit(ast, { name: "PATIENT_GROUP" }, (node, ancestors, info) => {
190
- console.log("Inside PATIENT_GROUP");
130
+ // A group's `id` is its key in paths; its `name` is the title for display
131
+ visit(tree, { type: "group", id: "PATIENT" }, (group) => {
132
+ console.log(group.name); // "Patient"
191
133
  });
192
134
  ```
193
135
 
194
- ### Custom test function
136
+ ### Read the ancestors
195
137
 
196
138
  ```typescript
197
- // Visit fields in MSH segment only
198
- visit(
199
- ast,
200
- (node, ancestors) => {
201
- const parent = ancestors.at(-1);
202
- return node.type === "field" && parent?.type === "segment";
203
- },
204
- (node, ancestors, info) => {
205
- console.log(`Field at sequence ${info.sequence}`);
206
- }
207
- );
208
- ```
209
-
210
- ### Access parent and ancestors
211
-
212
- ```typescript
213
- visit(ast, "component", (node, ancestors, info) => {
214
- const parent = ancestors.at(-1);
215
- const segment = ancestors.findLast((n) => n.type === "segment");
216
- console.log(`Component at depth ${info.depth}`);
139
+ visit(tree, "component", (component, ancestors) => {
140
+ const parent = ancestors.at(-1); // the field repetition
141
+ const segment = ancestors.findLast((node) => node.type === "segment");
217
142
  });
218
143
  ```
219
144
 
220
- ### Control flow: skip children
145
+ ### Read the segment groups above a segment
221
146
 
222
147
  ```typescript
223
- visit(ast, (node, ancestors, info) => {
224
- if (node.type === "segment" && info.metadata?.header === "OBX") {
225
- return SKIP; // Don't process OBX segment children
226
- }
148
+ visit(tree, "segment", (segment, ancestors) => {
149
+ const groups = ancestors
150
+ .filter((node): node is Group => node.type === "group")
151
+ .map((group) => group.id);
152
+ console.log(`${segment.name}: ${groups.join(" > ")}`); // "PID: PATIENT_RESULT > PATIENT"
227
153
  });
228
154
  ```
229
155
 
230
- ### Control flow: exit early
156
+ ### Stop at the first match
231
157
 
232
158
  ```typescript
233
159
  import { EXIT } from "@glion/util-visit";
234
160
 
235
- let found = false;
236
- visit(ast, "field", (node, ancestors, info) => {
237
- if (/* some condition */) {
238
- found = true;
239
- return EXIT; // Stop traversal completely
240
- }
161
+ let first: Segment | undefined;
162
+ visit(tree, { type: "segment", name: "OBX" }, (segment) => {
163
+ first = segment as Segment;
164
+ return EXIT;
241
165
  });
242
166
  ```
243
167
 
244
168
  ### Start from any node
245
169
 
246
170
  ```typescript
247
- import { s, f, c } from "@glion/builder";
171
+ import { c, f, s } from "@glion/builder";
248
172
 
249
- // Create a standalone segment
250
173
  const segment = s("PID", f(c("value1")), f(c("value2")));
251
174
 
252
- // Traverse from segment (not root)
253
- visit(segment, "field", (node, ancestors, info) => {
254
- console.log(`Field at sequence ${info.sequence}`);
255
- });
256
- ```
257
-
258
- ### Track nesting levels
259
-
260
- ```typescript
261
- visit(ast, (node, ancestors, info) => {
262
- const indent = " ".repeat(info.depth - 1);
263
- console.log(`${indent}${node.type} [${info.sequence}]`);
175
+ visit(segment, "field", (field, ancestors) => {
176
+ console.log(ancestors.length); // 1: the segment
264
177
  });
265
- // root [1]
266
- // segment [1]
267
- // segment-header [0]
268
- // field [1]
269
- // field-repetition [1]
270
- // component [1]
271
- ```
272
-
273
- ### Group hierarchy navigation
274
-
275
- ```typescript
276
- visit(ast, "segment", (node, ancestors, info) => {
277
- const groups = ancestors
278
- .filter((n) => n.type === "group")
279
- .map((n) => (n as any).name)
280
- .filter((name): name is string => typeof name === "string");
281
-
282
- console.log(`${info.metadata?.header} is in groups: ${groups.join(" > ")}`);
283
- });
284
- // PID is in groups: PATIENT_GROUP
285
- ```
286
-
287
- ### Validate required fields
288
-
289
- ```typescript
290
- function validateRequiredFields(ast: Root): string[] {
291
- const errors: string[] = [];
292
-
293
- visit(ast, "segment", (node, ancestors, info) => {
294
- const segment = node as Segment;
295
- const header = info.metadata?.header;
296
-
297
- if (header === "MSH" && segment.children.length < 12) {
298
- errors.push("MSH segment missing required fields");
299
- }
300
-
301
- if (header === "PID") {
302
- const patientId = segment.children[3];
303
- if (!patientId || patientId.children.length === 0) {
304
- errors.push("PID segment missing required Patient ID (PID.3)");
305
- }
306
- }
307
- });
308
-
309
- return errors;
310
- }
311
- ```
312
-
313
- ### Extract data with context
314
-
315
- ```typescript
316
- interface PatientName {
317
- name: string;
318
- sequence: number;
319
- inGroup?: string;
320
- }
321
-
322
- function extractPatientNames(ast: Root): PatientName[] {
323
- const names: PatientName[] = [];
324
-
325
- visit(ast, "segment", (node, ancestors, info) => {
326
- if (info.metadata?.header !== "PID") return;
327
-
328
- const segment = node as Segment;
329
- const nameField = segment.children[5];
330
- if (nameField?.children[0]?.children[0]) {
331
- const nameComponent = nameField.children[0].children[0];
332
- const name = (nameComponent.children[0] as Subcomponent)?.value || "";
333
- const groupAncestor = ancestors.find(
334
- (n): n is Group => n.type === "group"
335
- );
336
-
337
- names.push({
338
- name,
339
- sequence: info.sequence,
340
- inGroup: groupAncestor?.name,
341
- });
342
- }
343
- });
344
-
345
- return names;
346
- }
347
- ```
348
-
349
- ### Find first match and exit
350
-
351
- ```typescript
352
- function findFirstObservation(ast: Root, targetCode: string): string | null {
353
- let result: string | null = null;
354
-
355
- visit(ast, "segment", (node, ancestors, info) => {
356
- if (info.metadata?.header !== "OBX") return;
357
-
358
- const segment = node as Segment;
359
- const identifierField = segment.children[3];
360
- const code = identifierField?.children[0]?.children[0]?.children[0]?.value;
361
-
362
- if (code === targetCode) {
363
- const valueField = segment.children[5];
364
- result = valueField?.children[0]?.children[0]?.children[0]?.value || null;
365
- return EXIT;
366
- }
367
- });
368
-
369
- return result;
370
- }
371
178
  ```
372
179
 
373
180
  ## Part of Glion
package/dist/index.d.ts CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * HL7v2 AST Visitor
2
+ * HL7v2 AST visitor over `unist-util-visit-parents`, typed for `@glion/ast`.
3
3
  *
4
- * Wraps unist-util-visit-parents to add HL7v2-specific context
5
- * (index, sequence, depth, metadata) while delegating core traversal.
4
+ * @module
6
5
  */
7
6
  import type { Nodes } from "@glion/ast";
8
7
  import type { Test, Visitor } from "./types";
9
8
  export type { VisitorResult } from "unist-util-visit-parents";
10
9
  export { EXIT, SKIP } from "unist-util-visit-parents";
11
- export type { Predicate, Test, VisitInfo, Visitor } from "./types";
10
+ export type { Predicate, Test, Visitor } from "./types";
12
11
  export declare function visit(tree: Nodes, visitor: Visitor): void;
13
12
  export declare function visit<Type extends Nodes["type"]>(tree: Nodes, test: Type, visitor: Visitor<Extract<Nodes, {
14
13
  type: Type;
@@ -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;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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAG7C,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAEtD,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGxD,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
@@ -47,7 +47,6 @@ function createTest(test) {
47
47
  //#region src/index.ts
48
48
  /**
49
49
  * Visit nodes in an HL7 AST tree.
50
- * Wraps unist-util-visit-parents to add HL7v2-specific context.
51
50
  *
52
51
  * @param tree - The tree to traverse (can be any node type, not just Root)
53
52
  * @param test - Optional test to filter nodes (type string, partial match, or
@@ -68,20 +67,9 @@ function visit(tree, arg2, arg3) {
68
67
  visitor = arg3;
69
68
  }
70
69
  const predicate = createTest(test);
71
- let currentIndex = 0;
72
- const captureIndex = (_node, index) => {
73
- currentIndex = index ?? 0;
74
- return true;
75
- };
76
- visitParents(tree, captureIndex, (node, ancestors) => {
70
+ visitParents(tree, (node, ancestors) => {
77
71
  if (!predicate(node, ancestors)) return;
78
- const info = {
79
- depth: ancestors.length + 1,
80
- index: currentIndex,
81
- metadata: "name" in node ? { name: node.name } : void 0,
82
- sequence: currentIndex + 1
83
- };
84
- return visitor(node, ancestors, info);
72
+ return visitor(node, ancestors);
85
73
  });
86
74
  }
87
75
  //#endregion
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 * 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"}
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 over `unist-util-visit-parents`, typed for `@glion/ast`.\n *\n * @module\n */\n\nimport type { Nodes } from \"@glion/ast\";\nimport { visitParents } from \"unist-util-visit-parents\";\n\nimport type { Test, 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, 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 *\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 // The caller's test runs in the visitor, not as visit-parents' test: it may\n // read `ancestors`, which visit-parents gives only to the visitor.\n visitParents(tree, (node, ancestors) => {\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 // visit-parents reads the visitor's return value to decide what to visit\n // next (CONTINUE, SKIP, EXIT, an index, or both), so the caller's result\n // is passed through unchanged.\n return visitor(node as T, ancestors);\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;;;;;;;;;;;;;;;;;ACxBL,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;AAIjD,cAAa,OAAO,MAAM,cAAc;AACtC,MAAI,CAAC,UAAU,MAAM,UAAU,CAI7B;AAMF,SAAO,QAAQ,MAAW,UAAU;GACpC"}
package/dist/types.d.ts CHANGED
@@ -1,33 +1,13 @@
1
1
  import type { Nodes } from "@glion/ast";
2
2
  import type { VisitorResult } from "unist-util-visit-parents";
3
- /**
4
- * Visit information computed for each node.
5
- * This is HL7v2-specific context not provided by unist-util-visit-parents.
6
- */
7
- export interface VisitInfo {
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
- */
13
- index: number;
14
- /** `index` + 1, the HL7v2 sequence number. */
15
- sequence: number;
16
- /** 1-based depth in tree (root = 1) */
17
- depth: number;
18
- /** Metadata extracted from node (e.g., { name: "MSH" } or { name: "PATIENT" }) */
19
- metadata: Record<string, unknown> | undefined;
20
- }
21
3
  /**
22
4
  * A function called for each node during traversal.
23
- * Extends unist's visitor signature with HL7v2-specific context.
24
5
  *
25
6
  * @param node - Current node being visited
26
7
  * @param ancestors - Array of ancestor nodes from root to parent
27
- * @param info - HL7v2-specific visit info (index, sequence, depth, metadata)
28
8
  * @returns VisitorResult to control traversal (EXIT, SKIP, etc.)
29
9
  */
30
- export type Visitor<T extends Nodes = Nodes> = (node: T, ancestors: Nodes[], info: VisitInfo) => VisitorResult;
10
+ export type Visitor<T extends Nodes = Nodes> = (node: T, ancestors: Nodes[]) => VisitorResult;
31
11
  /**
32
12
  * Predicate function to test if a node matches filter criteria.
33
13
  * Compatible with unist-util-is predicates but uses our Nodes type.
@@ -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;;;;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"}
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;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI,CAC7C,IAAI,EAAE,CAAC,EACP,SAAS,EAAE,KAAK,EAAE,KACf,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/util-visit",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "An AST visitor for HL7v2 messages",
5
5
  "keywords": [
6
6
  "health",
@@ -45,10 +45,10 @@
45
45
  "tsdown": "0.21.10",
46
46
  "typescript": "6.0.3",
47
47
  "vitest": "4.1.11",
48
- "@glion/ast": "0.25.0",
49
- "@glion/builder": "0.25.0",
48
+ "@glion/ast": "0.27.0",
49
+ "@glion/builder": "0.27.0",
50
50
  "@glion/check-readme": "0.18.0",
51
- "@glion/parser": "0.25.0",
51
+ "@glion/parser": "0.27.0",
52
52
  "@glion/testing": "0.18.0",
53
53
  "@glion/tsconfig": "0.18.0"
54
54
  },