@glion/util-visit 0.14.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Rethink Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,486 @@
1
+ # @glion/util-visit
2
+
3
+ ## Introduction
4
+
5
+ This package provides a lightweight, type-safe visitor pattern for traversing [HL7v2 AST][hl7v2-ast] trees. Built on top of the battle-tested [unist-util-visit-parents][], it adds HL7v2-specific context while delegating core traversal to a proven implementation.
6
+
7
+ ### What is this?
8
+
9
+ `hl7v2-util-visit` enables you to walk through any HL7v2 AST tree — from the root message down to individual subcomponents — with full context about where you are in the hierarchy. The visitor pattern:
10
+
11
+ - **Works from any starting node** (Root, Segment, Field, Component, etc.)
12
+ - **Tracks ancestors** from traversal root to current node
13
+ - **Provides visit info** (index, sequence, depth, metadata)
14
+ - **Efficient O(n) traversal** — Pre-computed index map for O(1) lookups
15
+ - **Battle-tested core** — Delegates traversal to `unist-util-visit-parents`
16
+
17
+ ### When should I use this?
18
+
19
+ Use `hl7v2-util-visit` when you need to:
20
+
21
+ - Validate HL7v2 message structure
22
+ - Transform or annotate AST nodes
23
+ - Extract specific fields with context
24
+ - Analyze message patterns across the tree
25
+ - Implement custom processing rules that need parent/ancestor awareness
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ npm install @glion/util-visit
31
+ ```
32
+
33
+ ## Use
34
+
35
+ ```typescript
36
+ import { visit, EXIT, SKIP } from "@glion/util-visit";
37
+ import { parse } from "@glion/parser";
38
+
39
+ const message = parse("MSH|^~\\&|...\rPID|...");
40
+
41
+ // Visit all segments
42
+ visit(message, "segment", (node, ancestors, info) => {
43
+ console.log(`Segment: ${info.metadata?.header} at depth ${info.depth}`);
44
+ });
45
+ // => Segment: MSH at depth 2
46
+ // => Segment: PID at depth 2
47
+
48
+ // Find fields with parent context
49
+ visit(message, "field", (node, ancestors, info) => {
50
+ const segment = ancestors.find((n) => n.type === "segment");
51
+ console.log(`Field ${info.sequence} in segment`);
52
+ });
53
+
54
+ // Skip processing of sensitive segments
55
+ visit(message, (node, ancestors, info) => {
56
+ if (node.type === "segment" && info.metadata?.header === "NTE") {
57
+ return SKIP; // Skip NTE segment children
58
+ }
59
+ });
60
+ ```
61
+
62
+ ## API
63
+
64
+ ### `visit(tree, visitor)`
65
+
66
+ ### `visit(tree, test, visitor)`
67
+
68
+ Visit nodes in an HL7v2 AST tree.
69
+
70
+ #### Parameters
71
+
72
+ - **`tree`** (`Nodes`) — Tree to traverse (can be any node type, not just Root)
73
+ - **`test`** (`string | Partial<Nodes> | Test`) — Optional filter:
74
+ - `string`: Match nodes by type (e.g., `'segment'`)
75
+ - `Partial<Nodes>`: Match nodes with matching properties (e.g., `{ name: 'PATIENT_GROUP' }`)
76
+ - `Test`: Custom function `(node, ancestors) => boolean`
77
+ - **`visitor`** (`Visitor`) — Function called for each matching node
78
+
79
+ #### Returns
80
+
81
+ `void`
82
+
83
+ #### Important: Test vs Visitor Functions
84
+
85
+ **If you pass a function as the second argument, it is always treated as a Visitor, never as a Test.**
86
+
87
+ ```typescript
88
+ // WRONG - testFn will be treated as a visitor, not a test
89
+ visit(ast, (node) => node.type === 'segment', ...); // Missing visitor!
90
+
91
+ // CORRECT - Explicit 3-argument form
92
+ visit(ast, (node) => node.type === 'segment', (node, ancestors, info) => {
93
+ console.log('Visiting segment');
94
+ });
95
+
96
+ // CORRECT - Use string or object for simple tests
97
+ visit(ast, 'segment', (node, ancestors, info) => {
98
+ console.log('Visiting segment');
99
+ });
100
+ ```
101
+
102
+ #### Visitor Function
103
+
104
+ ```typescript
105
+ type Visitor<T extends Nodes = Nodes> = (
106
+ node: T,
107
+ ancestors: Nodes[],
108
+ info: VisitInfo
109
+ ) => VisitorResult;
110
+ ```
111
+
112
+ The visitor receives:
113
+
114
+ - **`node`** — Current AST node
115
+ - **`ancestors`** — Array of ancestor nodes from root to parent (not including current node)
116
+ - **`info`** — Visit information with index, sequence, depth, and metadata
117
+
118
+ The visitor can return:
119
+
120
+ - `undefined` or `void` — Continue traversal normally
121
+ - `SKIP` — Skip children of current node
122
+ - `EXIT` — Stop traversal immediately
123
+
124
+ ### VisitInfo
125
+
126
+ ```typescript
127
+ interface VisitInfo {
128
+ /** 0-based index among siblings */
129
+ index: number;
130
+
131
+ /** 1-based sequence (HL7v2 convention). For segment-header: 0 */
132
+ sequence: number;
133
+
134
+ /** 1-based depth in tree (root = 1) */
135
+ depth: number;
136
+
137
+ /** Metadata (e.g., { header: "MSH" } or { name: "PATIENT" }) */
138
+ metadata: Record<string, unknown> | undefined;
139
+ }
140
+ ```
141
+
142
+ **Important**: `index` and `sequence` represent the node's **position in the tree**, not its position among filtered results. For example:
143
+
144
+ ```typescript
145
+ // Structure: MSH segment with fields at positions 1, 2, 3, 4
146
+ // Filter matches only field 3
147
+
148
+ visit(
149
+ ast,
150
+ (n) => n.type === "field" && hasContent(n),
151
+ (node, ancestors, info) => {
152
+ console.log(info.sequence); // => 3 (position in segment, not "1st match")
153
+ }
154
+ );
155
+ ```
156
+
157
+ This is correct because HL7v2 paths like `PID.3` refer to tree positions, not filtered positions.
158
+
159
+ ### Automatic Metadata Extraction
160
+
161
+ The `metadata` field is populated automatically with common metadata:
162
+
163
+ | Node Type | Metadata Key | Description |
164
+ | --------- | ------------ | ------------------------------------------- |
165
+ | `segment` | `header` | Segment identifier (e.g., `"MSH"`, `"PID"`) |
166
+ | `group` | `name` | Group name (e.g., `"PATIENT_GROUP"`) |
167
+
168
+ ### Exports
169
+
170
+ ```typescript
171
+ import {
172
+ visit, // Main traversal function
173
+ EXIT, // Return to stop traversal
174
+ SKIP, // Return to skip children
175
+ } from "@glion/util-visit";
176
+
177
+ import type {
178
+ VisitInfo, // { index, sequence, depth, metadata }
179
+ Visitor, // (node, ancestors, info) => VisitorResult
180
+ VisitorResult, // Return type from visitor
181
+ Test, // Filter predicate
182
+ Predicate, // (node, ancestors) => boolean
183
+ } from "@glion/util-visit";
184
+ ```
185
+
186
+ ## Examples
187
+
188
+ ### Filter by Node Type
189
+
190
+ ```typescript
191
+ visit(ast, "segment", (node, ancestors, info) => {
192
+ console.log(`Found segment: ${info.metadata?.header}`);
193
+ });
194
+ ```
195
+
196
+ ### Filter by Properties
197
+
198
+ ```typescript
199
+ visit(ast, { name: "PATIENT_GROUP" }, (node, ancestors, info) => {
200
+ console.log("Inside PATIENT_GROUP");
201
+ });
202
+ ```
203
+
204
+ ### Custom Test Function
205
+
206
+ ```typescript
207
+ // Visit fields in MSH segment only
208
+ visit(
209
+ ast,
210
+ (node, ancestors) => {
211
+ const parent = ancestors.at(-1);
212
+ return node.type === "field" && parent?.type === "segment";
213
+ },
214
+ (node, ancestors, info) => {
215
+ console.log(`Field at sequence ${info.sequence}`);
216
+ }
217
+ );
218
+ ```
219
+
220
+ ### Access Parent and Ancestors
221
+
222
+ ```typescript
223
+ visit(ast, "component", (node, ancestors, info) => {
224
+ // Get immediate parent
225
+ const parent = ancestors.at(-1);
226
+
227
+ // Find closest segment ancestor
228
+ const segment = ancestors.findLast((n) => n.type === "segment");
229
+
230
+ console.log(`Component at depth ${info.depth}`);
231
+ });
232
+ ```
233
+
234
+ ### Control Flow: Skip Children
235
+
236
+ ```typescript
237
+ visit(ast, (node, ancestors, info) => {
238
+ if (node.type === "segment" && info.metadata?.header === "OBX") {
239
+ return SKIP; // Don't process OBX segment children
240
+ }
241
+ });
242
+ ```
243
+
244
+ ### Control Flow: Exit Early
245
+
246
+ ```typescript
247
+ import { EXIT } from '@glion/util-visit';
248
+
249
+ let found = false;
250
+ visit(ast, 'field', (node, ancestors, info) => {
251
+ if (/* some condition */) {
252
+ found = true;
253
+ return EXIT; // Stop traversal completely
254
+ }
255
+ });
256
+ ```
257
+
258
+ ### Start from Any Node
259
+
260
+ ```typescript
261
+ import { s, f, c } from "@glion/builder";
262
+
263
+ // Create a standalone segment
264
+ const segment = s("PID", f(c("value1")), f(c("value2")));
265
+
266
+ // Traverse from segment (not root)
267
+ visit(segment, "field", (node, ancestors, info) => {
268
+ console.log(`Field at sequence ${info.sequence}`);
269
+ });
270
+ ```
271
+
272
+ ### Track Nesting Levels
273
+
274
+ ```typescript
275
+ visit(ast, (node, ancestors, info) => {
276
+ const indent = " ".repeat(info.depth - 1);
277
+ console.log(`${indent}${node.type} [${info.sequence}]`);
278
+ });
279
+ // Output:
280
+ // root [1]
281
+ // segment [1]
282
+ // segment-header [0]
283
+ // field [1]
284
+ // field-repetition [1]
285
+ // component [1]
286
+ ```
287
+
288
+ ### Group Hierarchy Navigation
289
+
290
+ ```typescript
291
+ visit(ast, "segment", (node, ancestors, info) => {
292
+ // Get all parent groups
293
+ const groups = ancestors
294
+ .filter((n) => n.type === "group")
295
+ .map((n) => (n as any).name)
296
+ .filter((name): name is string => typeof name === "string");
297
+
298
+ console.log(`${info.metadata?.header} is in groups: ${groups.join(" > ")}`);
299
+ });
300
+ // => PID is in groups: PATIENT_GROUP
301
+ ```
302
+
303
+ ## Real-World Use Cases
304
+
305
+ ### Validate Required Fields
306
+
307
+ ```typescript
308
+ function validateRequiredFields(ast: Root): string[] {
309
+ const errors: string[] = [];
310
+
311
+ visit(ast, "segment", (node, ancestors, info) => {
312
+ const segment = node as Segment;
313
+ const header = info.metadata?.header;
314
+
315
+ // MSH segment must have at least 12 fields
316
+ if (header === "MSH" && segment.children.length < 12) {
317
+ errors.push(`MSH segment missing required fields`);
318
+ }
319
+
320
+ // PID segment must have patient ID (PID.3)
321
+ if (header === "PID") {
322
+ const patientId = segment.children[3];
323
+ if (!patientId || patientId.children.length === 0) {
324
+ errors.push("PID segment missing required Patient ID (PID.3)");
325
+ }
326
+ }
327
+ });
328
+
329
+ return errors;
330
+ }
331
+ ```
332
+
333
+ ### Extract Specific Data with Context
334
+
335
+ ```typescript
336
+ interface PatientName {
337
+ name: string;
338
+ sequence: number;
339
+ inGroup?: string;
340
+ }
341
+
342
+ function extractPatientNames(ast: Root): PatientName[] {
343
+ const names: PatientName[] = [];
344
+
345
+ visit(ast, "segment", (node, ancestors, info) => {
346
+ if (info.metadata?.header !== "PID") return;
347
+
348
+ const segment = node as Segment;
349
+ const nameField = segment.children[5];
350
+ if (nameField?.children[0]?.children[0]) {
351
+ const nameComponent = nameField.children[0].children[0];
352
+ const name = (nameComponent.children[0] as Subcomponent)?.value || "";
353
+
354
+ const groupAncestor = ancestors.find((n) => n.type === "group");
355
+
356
+ names.push({
357
+ name,
358
+ sequence: info.sequence,
359
+ inGroup: groupAncestor ? (groupAncestor as any).name : undefined,
360
+ });
361
+ }
362
+ });
363
+
364
+ return names;
365
+ }
366
+ ```
367
+
368
+ ### Message Structure Analysis
369
+
370
+ ```typescript
371
+ interface MessageStructure {
372
+ segmentCount: number;
373
+ groupCount: number;
374
+ maxDepth: number;
375
+ segmentTypes: Record<string, number>;
376
+ }
377
+
378
+ function analyzeStructure(ast: Root): MessageStructure {
379
+ const structure: MessageStructure = {
380
+ segmentCount: 0,
381
+ groupCount: 0,
382
+ maxDepth: 0,
383
+ segmentTypes: {},
384
+ };
385
+
386
+ visit(ast, (node, ancestors, info) => {
387
+ structure.maxDepth = Math.max(structure.maxDepth, info.depth);
388
+
389
+ if (node.type === "segment") {
390
+ structure.segmentCount++;
391
+ const header = info.metadata?.header as string;
392
+ if (header) {
393
+ structure.segmentTypes[header] =
394
+ (structure.segmentTypes[header] || 0) + 1;
395
+ }
396
+ }
397
+
398
+ if (node.type === "group") {
399
+ structure.groupCount++;
400
+ }
401
+ });
402
+
403
+ return structure;
404
+ }
405
+ ```
406
+
407
+ ### Find First Match and Exit
408
+
409
+ ```typescript
410
+ function findFirstObservation(ast: Root, targetCode: string): string | null {
411
+ let result: string | null = null;
412
+
413
+ visit(ast, "segment", (node, ancestors, info) => {
414
+ if (info.metadata?.header !== "OBX") return;
415
+
416
+ const segment = node as Segment;
417
+ const identifierField = segment.children[3];
418
+ const code = identifierField?.children[0]?.children[0]?.children[0]?.value;
419
+
420
+ if (code === targetCode) {
421
+ const valueField = segment.children[5];
422
+ result = valueField?.children[0]?.children[0]?.children[0]?.value || null;
423
+ return EXIT; // Stop traversal
424
+ }
425
+ });
426
+
427
+ return result;
428
+ }
429
+ ```
430
+
431
+ ## Architecture
432
+
433
+ This library is a thin wrapper around [unist-util-visit-parents][] that adds HL7v2-specific features:
434
+
435
+ 1. **Core Traversal**: Delegates to `unist-util-visit-parents` (battle-tested with 50M+ weekly downloads)
436
+ 2. **Index Optimization**: Pre-computes node indices for O(1) lookups (avoids O(n²) `indexOf` calls)
437
+ 3. **HL7v2 Context**: Adds `VisitInfo` with sequence numbers, depth tracking, and metadata extraction
438
+ 4. **Domain Conventions**: Implements HL7v2-specific indexing (segment-header sequence = 0, fields = 1,2,3...)
439
+
440
+ This design gives us:
441
+
442
+ - ✅ Proven traversal logic (EXIT/SKIP handling, edge cases)
443
+ - ✅ Mutation support (inherited from unist)
444
+ - ✅ Reverse traversal support (pass `reverse: true` to underlying library)
445
+ - ✅ Small codebase (~120 lines vs previous ~180 lines)
446
+ - ✅ Ecosystem compatibility (works with other unist utilities)
447
+
448
+ ## Performance Characteristics
449
+
450
+ - **O(n) traversal** — Single pass through all nodes
451
+ - **O(n) index pre-computation** — One-time upfront cost
452
+ - **O(1) index lookups** — Avoids O(n²) `indexOf()` anti-pattern
453
+ - **O(d) ancestor construction** where d = depth (typically < 10 for HL7v2)
454
+ - **Minimal allocations** — Metadata extracted once per node
455
+
456
+ ## Types
457
+
458
+ ```typescript
459
+ export type {
460
+ VisitInfo, // { index, sequence, depth, metadata }
461
+ Visitor, // (node, ancestors, info) => VisitorResult
462
+ VisitorResult, // void | false | 'skip' | ActionTuple
463
+ Test, // string | Partial | predicate | null
464
+ Predicate, // (node, ancestors) => boolean
465
+ } from "@glion/util-visit";
466
+ ```
467
+
468
+ ## Contributing
469
+
470
+ We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
471
+
472
+ ## Code of Conduct
473
+
474
+ To ensure a welcoming and positive environment, we have a [Code of Conduct][github-code-of-conduct] that all contributors and participants are expected to adhere to.
475
+
476
+ ## License
477
+
478
+ Copyright 2025 Rethink Health, SUARL. All rights reserved.
479
+
480
+ This program is licensed to you under the terms of the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE][github-license] file for details.
481
+
482
+ [github-code-of-conduct]: https://github.com/rethinkhealth/glion/blob/main/CODE_OF_CONDUCT.md
483
+ [github-license]: https://github.com/rethinkhealth/glion/blob/main/LICENSE
484
+ [github-contributing]: https://github.com/rethinkhealth/glion/blob/main/CONTRIBUTING.md
485
+ [hl7v2-ast]: https://github.com/rethinkhealth/glion/tree/main/packages/hl7v2-ast
486
+ [unist-util-visit-parents]: https://github.com/syntax-tree/unist-util-visit-parents
@@ -0,0 +1,17 @@
1
+ /**
2
+ * HL7v2 AST Visitor
3
+ *
4
+ * Wraps unist-util-visit-parents to add HL7v2-specific context
5
+ * (index, sequence, depth, metadata) while delegating core traversal.
6
+ */
7
+ import type { Nodes } from "@glion/ast";
8
+ import type { Test, Visitor } from "./types";
9
+ export type { VisitorResult } from "unist-util-visit-parents";
10
+ export { EXIT, SKIP } from "unist-util-visit-parents";
11
+ export type { Predicate, Test, VisitInfo, Visitor } from "./types";
12
+ export declare function visit(tree: Nodes, visitor: Visitor): void;
13
+ export declare function visit<Type extends Nodes["type"]>(tree: Nodes, test: Type, visitor: Visitor<Extract<Nodes, {
14
+ type: Type;
15
+ }>>): void;
16
+ export declare function visit<T extends Nodes>(tree: Nodes, test: Test<T>, visitor: Visitor<T>): void;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
1
+ import { EXIT, SKIP, visitParents } from "unist-util-visit-parents";
2
+ //#region src/utils.ts
3
+ /**
4
+ * Create test predicate from various input types.
5
+ *
6
+ * Assumptions:
7
+ *
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
13
+ *
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
17
+ */
18
+ function createTest(test) {
19
+ if (test === null) return () => true;
20
+ if (typeof test === "string") return (node) => node.type === test;
21
+ 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
+ };
33
+ }
34
+ //#endregion
35
+ //#region src/index.ts
36
+ /**
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
+ * Visit nodes in an HL7 AST tree.
59
+ * Wraps unist-util-visit-parents to add HL7v2-specific context.
60
+ *
61
+ * @param tree - The tree to traverse (can be any node type, not just Root)
62
+ * @param test - Optional test to filter nodes (type string, partial match, or
63
+ * Test function)
64
+ * @param visitor - Function called for each matching node
65
+ *
66
+ * **Important**: If you pass a function as the second argument, it is always
67
+ * treated as a Visitor, never as a Test. To use a Test function, you MUST
68
+ * provide both the test and visitor parameters: `visit(tree, testFn,
69
+ * visitorFn)`.
70
+ */
71
+ function visit(tree, arg2, arg3) {
72
+ let test = null;
73
+ let visitor;
74
+ if (arg3 === void 0) visitor = arg2;
75
+ else {
76
+ test = arg2;
77
+ visitor = arg3;
78
+ }
79
+ const predicate = createTest(test);
80
+ const indexMap = buildIndexMap(tree);
81
+ visitParents(tree, (node, ancestors) => {
82
+ if (!predicate(node, ancestors)) return;
83
+ const parent = ancestors.at(-1);
84
+ const childIndex = parent ? indexMap.get(node) ?? 0 : 0;
85
+ const info = {
86
+ 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
90
+ };
91
+ return visitor(node, ancestors, info);
92
+ });
93
+ }
94
+ //#endregion
95
+ export { EXIT, SKIP, visit };
96
+
97
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,43 @@
1
+ import type { Nodes } from "@glion/ast";
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
+ /** 0-based index among siblings */
9
+ index: number;
10
+ /** 1-based sequence (HL7v2 convention) */
11
+ sequence: number;
12
+ /** 1-based depth in tree (root = 1) */
13
+ depth: number;
14
+ /** Metadata extracted from node (e.g., { name: "MSH" } or { name: "PATIENT" }) */
15
+ metadata: Record<string, unknown> | undefined;
16
+ }
17
+ /**
18
+ * A function called for each node during traversal.
19
+ * Extends unist's visitor signature with HL7v2-specific context.
20
+ *
21
+ * @param node - Current node being visited
22
+ * @param ancestors - Array of ancestor nodes from root to parent
23
+ * @param info - HL7v2-specific visit info (index, sequence, depth, metadata)
24
+ * @returns VisitorResult to control traversal (EXIT, SKIP, etc.)
25
+ */
26
+ export type Visitor<T extends Nodes = Nodes> = (node: T, ancestors: Nodes[], info: VisitInfo) => VisitorResult;
27
+ /**
28
+ * Predicate function to test if a node matches filter criteria.
29
+ * Compatible with unist-util-is predicates but uses our Nodes type.
30
+ */
31
+ export type Predicate = (node: Nodes, ancestors: Nodes[]) => boolean;
32
+ /**
33
+ * Filter criteria to determine which nodes to visit.
34
+ * Based on unist-util-is Test but specialized for HL7v2 AST.
35
+ *
36
+ * Can be:
37
+ * - string: matches `node.type`
38
+ * - object: matches properties (partial match)
39
+ * - function: predicate returning boolean or type guard
40
+ * - null: matches everything
41
+ */
42
+ export type Test<T extends Nodes = Nodes> = string | Partial<T> | ((node: Nodes, ancestors: Nodes[]) => node is T) | ((node: Nodes, ancestors: Nodes[]) => boolean) | null;
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +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;;;;;;;;;GASG;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"}
@@ -0,0 +1,19 @@
1
+ import type { Nodes } from "@glion/ast";
2
+ import type { Predicate, Test } from "./types";
3
+ /**
4
+ * Create test predicate from various input types.
5
+ *
6
+ * Assumptions:
7
+ *
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
13
+ *
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
17
+ */
18
+ export declare function createTest(test: Test<Nodes>): Predicate;
19
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@glion/util-visit",
3
+ "version": "0.14.1",
4
+ "description": "An AST visitor for HL7v2 messages",
5
+ "keywords": [
6
+ "health",
7
+ "healthcare",
8
+ "hl7",
9
+ "hl7v2",
10
+ "nodejs",
11
+ "typescript"
12
+ ],
13
+ "homepage": "https://glion.dev",
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "Melek Somai",
17
+ "email": "melek@rethinkhealth.io"
18
+ },
19
+ "repository": "rethinkhealth/glion.git",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": "./dist/index.js"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "unist-util-visit-parents": "6.0.2"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.5.0",
37
+ "@vitest/coverage-v8": "4.1.2",
38
+ "tsdown": "0.21.7",
39
+ "typescript": "^5.9.3",
40
+ "vitest": "4.1.2",
41
+ "@glion/ast": "0.14.1",
42
+ "@glion/parser": "0.14.1",
43
+ "@glion/builder": "0.14.1",
44
+ "@glion/testing": "0.0.2",
45
+ "@glion/config": "0.14.1",
46
+ "@glion/tsconfig": "0.0.1"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown && tsc --emitDeclarationOnly",
53
+ "check-types": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:watch": "vitest"
57
+ }
58
+ }