@glion/util-query 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/dist/index.js ADDED
@@ -0,0 +1,778 @@
1
+ import { DEFAULT_DELIMITERS } from "@glion/utils";
2
+ //#region src/parse.ts
3
+ /**
4
+ * Regular expression for canonical HL7 paths.
5
+ *
6
+ * Format:
7
+ * [GROUP[N]-]...[GROUP[N]-]NAME[N][-FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]]
8
+ *
9
+ * **Design Philosophy**: Groups vs Segments are distinguished by POSITION and
10
+ * USAGE, not name length:
11
+ *
12
+ * - **Navigation (Groups)**: Names appearing as prefixes (before the final name)
13
+ * are treated as group navigation: `ORDER-ORC`, `ORDER-TIMING-TQ1`
14
+ * - **Target (Segments)**: The final name in the path is the query target.
15
+ * Whether it's a segment or group is determined by:
16
+ *
17
+ * 1. Field access (-.digit): Definitively a segment → `ORC-1`, `ORDER-ORC-1`
18
+ * 2. No field access: Query the AST to see what exists → `ORC`, `ORDER`
19
+ *
20
+ * This approach:
21
+ *
22
+ * - Makes no assumptions about name length (works with standard 3-char segments
23
+ * AND edge cases)
24
+ * - Lets the path structure indicate intent (field access = segment internals)
25
+ * - Allows the AST to be the source of truth for what exists
26
+ *
27
+ * Examples:
28
+ *
29
+ * - `MSH-3` → MSH is segment (has field access)
30
+ * - `ORDER-ORC` → ORDER is navigation, ORC is target (could be segment or nested
31
+ * group)
32
+ * - `ORDER-ORC-1` → ORDER is navigation, ORC is segment (has field access)
33
+ */
34
+ const PATH_REGEX = /^((?:[A-Z][A-Z0-9]*(?:\[\d+\])?-)*)([A-Z][A-Z0-9]*)(?:\[(\d+)\])?(?:-(\d+)(?:(?:\[(\d+)\])?(?:\.(\d+)(?:\.(\d+))?)?)?)?$/;
35
+ /**
36
+ * Cache for parsed paths to avoid re-parsing the same path multiple times.
37
+ * No eviction — the realistic set of unique paths in an application is
38
+ * small (50-100 entries) and the memory cost is negligible.
39
+ */
40
+ const parseCache = /* @__PURE__ */ new Map();
41
+ /**
42
+ * Pre-computed PathParts for frequently accessed MSH fields.
43
+ *
44
+ * Multiple plugins and lint rules read MSH-9 (message type) and MSH-12
45
+ * (version) on every message. Skipping regex parsing and cache lookups
46
+ * for these paths eliminates ~100ns per call.
47
+ */
48
+ const HOT_PATHS = {
49
+ "MSH-3": {
50
+ segment: { name: "MSH" },
51
+ field: 3
52
+ },
53
+ "MSH-4": {
54
+ segment: { name: "MSH" },
55
+ field: 4
56
+ },
57
+ "MSH-5": {
58
+ segment: { name: "MSH" },
59
+ field: 5
60
+ },
61
+ "MSH-6": {
62
+ segment: { name: "MSH" },
63
+ field: 6
64
+ },
65
+ "MSH-9": {
66
+ segment: { name: "MSH" },
67
+ field: 9
68
+ },
69
+ "MSH-9.1": {
70
+ segment: { name: "MSH" },
71
+ field: 9,
72
+ component: 1
73
+ },
74
+ "MSH-9.2": {
75
+ segment: { name: "MSH" },
76
+ field: 9,
77
+ component: 2
78
+ },
79
+ "MSH-9.3": {
80
+ segment: { name: "MSH" },
81
+ field: 9,
82
+ component: 3
83
+ },
84
+ "MSH-10": {
85
+ segment: { name: "MSH" },
86
+ field: 10
87
+ },
88
+ "MSH-12": {
89
+ segment: { name: "MSH" },
90
+ field: 12
91
+ }
92
+ };
93
+ /**
94
+ * Parse an HL7 path string into structured parts.
95
+ * Results are memoized for performance. Common MSH paths use
96
+ * pre-computed constants to skip parsing entirely.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * parse("PID-5[1].2.1");
101
+ * // {
102
+ * // segment: { name: 'PID' },
103
+ * // field: 5,
104
+ * // repetition: 1,
105
+ * // component: 2,
106
+ * // subcomponent: 1
107
+ * // }
108
+ * ```;
109
+ *
110
+ * @param path - HL7 path string to parse
111
+ * @returns Structured path components
112
+ */
113
+ function parse(path) {
114
+ const hot = HOT_PATHS[path];
115
+ if (hot) return hot;
116
+ const cached = parseCache.get(path);
117
+ if (cached) return cached;
118
+ const result = parseImpl(path);
119
+ parseCache.set(path, result);
120
+ return result;
121
+ }
122
+ /**
123
+ * Internal implementation of path parsing.
124
+ */
125
+ function parseImpl(path) {
126
+ if (!path || typeof path !== "string") throw new Error(`Path must be a non-empty string, got: ${path}`);
127
+ if (path.trim() !== path) throw new Error(`Path cannot contain leading/trailing spaces: "${path}"`);
128
+ const match = PATH_REGEX.exec(path);
129
+ if (!match) throw new Error(`Invalid HL7 path format: "${path}". Expected [GROUP[N]-]...NAME[N][-FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]].`);
130
+ const [, groupsRaw, segmentName, segmentRep, fieldStr, repetitionStr, componentStr, subcomponentStr] = match;
131
+ if (!segmentName) throw new Error(`Path must include a segment identifier: "${path}"`);
132
+ const parts = { segment: { name: segmentName } };
133
+ if (segmentRep) {
134
+ const repetition = Number.parseInt(segmentRep, 10);
135
+ if (repetition < 1) throw new Error(`Segment repetition must be ≥1, got: ${segmentRep} in "${path}"`);
136
+ parts.segment.repetition = repetition;
137
+ }
138
+ if (groupsRaw) {
139
+ const groups = [];
140
+ for (const groupMatch of groupsRaw.matchAll(/([A-Z][A-Z0-9]*)(?:\[(\d+)\])?-/g)) {
141
+ const name = groupMatch[1];
142
+ if (!name) continue;
143
+ const repStr = groupMatch[2];
144
+ const group = { name };
145
+ if (repStr) {
146
+ const repetition = Number.parseInt(repStr, 10);
147
+ if (repetition < 1) throw new Error(`Group repetition must be ≥1, got: ${repStr} in "${path}"`);
148
+ group.repetition = repetition;
149
+ }
150
+ groups.push(group);
151
+ }
152
+ if (groups.length > 0) parts.groups = groups;
153
+ }
154
+ if (fieldStr) {
155
+ const field = Number.parseInt(fieldStr, 10);
156
+ if (field < 1) throw new Error(`Field number must be ≥1, got: ${fieldStr}`);
157
+ parts.field = field;
158
+ }
159
+ if (repetitionStr) {
160
+ const repetition = Number.parseInt(repetitionStr, 10);
161
+ if (repetition < 1) throw new Error(`Repetition number must be ≥1, got: ${repetitionStr}`);
162
+ parts.repetition = repetition;
163
+ }
164
+ if (componentStr) {
165
+ const component = Number.parseInt(componentStr, 10);
166
+ if (component < 1) throw new Error(`Component number must be ≥1, got: ${componentStr}`);
167
+ parts.component = component;
168
+ }
169
+ if (subcomponentStr) {
170
+ const subcomponent = Number.parseInt(subcomponentStr, 10);
171
+ if (subcomponent < 1) throw new Error(`Subcomponent number must be ≥1, got: ${subcomponentStr}`);
172
+ parts.subcomponent = subcomponent;
173
+ }
174
+ return parts;
175
+ }
176
+ //#endregion
177
+ //#region src/utils.ts
178
+ function locateSegmentOrGroup(root, parts) {
179
+ const ancestors = [root];
180
+ const scope = followGroups(root, parts.groups ?? [], ancestors);
181
+ if (!scope) return null;
182
+ const targetName = parts.segment.name;
183
+ const targetRepetition = parts.segment.repetition;
184
+ const matchedSegments = collectSegments(scope, targetName, ancestors);
185
+ if (matchedSegments.length > 0) {
186
+ const match = matchedSegments[(targetRepetition ?? 1) - 1];
187
+ if (match) return [match.segment, match.ancestors];
188
+ }
189
+ const matchedGroups = collectGroups(scope, targetName);
190
+ if (matchedGroups.length > 0) {
191
+ const group = matchedGroups[(targetRepetition ?? 1) - 1];
192
+ if (group) return [group, ancestors];
193
+ }
194
+ return null;
195
+ }
196
+ function locateSegment(root, parts) {
197
+ const ancestors = [root];
198
+ const scope = followGroups(root, parts.groups ?? [], ancestors);
199
+ if (!scope) return null;
200
+ const matchedSegments = collectSegments(scope, parts.segment.name, ancestors);
201
+ if (matchedSegments.length === 0) return null;
202
+ const match = matchedSegments[(parts.segment.repetition ?? 1) - 1];
203
+ if (!match) return null;
204
+ return [match.segment, match.ancestors];
205
+ }
206
+ function followGroups(root, groups, ancestorChain) {
207
+ let children = filterSegmentsAndGroups(root.children);
208
+ for (const locator of groups) {
209
+ const matchedGroups = children.filter((node) => node.type === "group" && node.name === locator.name);
210
+ if (matchedGroups.length === 0) return null;
211
+ const selected = matchedGroups[(locator.repetition ?? 1) - 1];
212
+ if (!selected) return null;
213
+ ancestorChain.push(selected);
214
+ children = filterSegmentsAndGroups(selected.children);
215
+ }
216
+ return children;
217
+ }
218
+ /**
219
+ * Collect all possible scopes when groups can match multiple instances.
220
+ * Used by selectAll to find nodes across all matching groups.
221
+ */
222
+ function collectAllScopes(root, groups) {
223
+ if (groups.length === 0) return [{
224
+ ancestors: [root],
225
+ scope: filterSegmentsAndGroups(root.children)
226
+ }];
227
+ const results = [];
228
+ const firstLocator = groups[0];
229
+ const remainingGroups = groups.slice(1);
230
+ if (!firstLocator) return [{
231
+ ancestors: [root],
232
+ scope: filterSegmentsAndGroups(root.children)
233
+ }];
234
+ const matchingGroups = filterSegmentsAndGroups(root.children).filter((node) => node.type === "group" && node.name === firstLocator.name);
235
+ if (firstLocator.repetition !== void 0) {
236
+ const selected = matchingGroups[firstLocator.repetition - 1];
237
+ if (!selected) return [];
238
+ const ancestors = [root, selected];
239
+ return collectAllScopesRecursive(selected.children, remainingGroups, ancestors);
240
+ }
241
+ for (const group of matchingGroups) {
242
+ const ancestors = [root, group];
243
+ const subScopes = collectAllScopesRecursive(group.children, remainingGroups, ancestors);
244
+ results.push(...subScopes);
245
+ }
246
+ return results;
247
+ }
248
+ function collectAllScopesRecursive(children, groups, ancestors) {
249
+ if (groups.length === 0) return [{
250
+ ancestors,
251
+ scope: filterSegmentsAndGroups(children)
252
+ }];
253
+ const results = [];
254
+ const firstLocator = groups[0];
255
+ const remainingGroups = groups.slice(1);
256
+ if (!firstLocator) return [{
257
+ ancestors,
258
+ scope: filterSegmentsAndGroups(children)
259
+ }];
260
+ const matchingGroups = filterSegmentsAndGroups(children).filter((node) => node.type === "group" && node.name === firstLocator.name);
261
+ if (firstLocator.repetition !== void 0) {
262
+ const selected = matchingGroups[firstLocator.repetition - 1];
263
+ if (!selected) return [];
264
+ const newAncestors = [...ancestors, selected];
265
+ return collectAllScopesRecursive(selected.children, remainingGroups, newAncestors);
266
+ }
267
+ for (const group of matchingGroups) {
268
+ const newAncestors = [...ancestors, group];
269
+ const subScopes = collectAllScopesRecursive(group.children, remainingGroups, newAncestors);
270
+ results.push(...subScopes);
271
+ }
272
+ return results;
273
+ }
274
+ function collectSegments(nodes, targetSegmentName, ancestors) {
275
+ const result = [];
276
+ for (const node of nodes) {
277
+ if (node.type === "segment") {
278
+ if (getSegmentName(node) === targetSegmentName) result.push({
279
+ ancestors: [...ancestors],
280
+ segment: node
281
+ });
282
+ }
283
+ if (node.type === "group") {
284
+ const nextAncestors = [...ancestors, node];
285
+ result.push(...collectSegments(filterSegmentsAndGroups(node.children), targetSegmentName, nextAncestors));
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+ function collectGroups(nodes, targetGroupName) {
291
+ const result = [];
292
+ for (const node of nodes) if (node.type === "group" && node.name === targetGroupName) result.push(node);
293
+ return result;
294
+ }
295
+ function locateField(segment, fieldNum, parentAncestors) {
296
+ const field = getFields(segment)[fieldNum - 1];
297
+ if (!field) return null;
298
+ return [field, [...parentAncestors, segment]];
299
+ }
300
+ function locateRepetition(field, repetition, parentAncestors) {
301
+ if (repetition < 1) return null;
302
+ const node = field.children?.[repetition - 1];
303
+ if (!node) return null;
304
+ return [node, [...parentAncestors, field]];
305
+ }
306
+ function locateComponent(repetition, componentNum, parentAncestors) {
307
+ if (componentNum < 1) return null;
308
+ const component = repetition.children?.[componentNum - 1];
309
+ if (!component) return null;
310
+ return [component, [...parentAncestors, repetition]];
311
+ }
312
+ function locateSubcomponent(component, subcomponentNum, parentAncestors) {
313
+ if (subcomponentNum < 1) return null;
314
+ const node = component.children?.[subcomponentNum - 1];
315
+ if (!node) return null;
316
+ return [node, [...parentAncestors, component]];
317
+ }
318
+ function getSegmentName(segment) {
319
+ return segment.name;
320
+ }
321
+ function getFields(segment) {
322
+ return segment.children;
323
+ }
324
+ function filterSegmentsAndGroups(nodes) {
325
+ return nodes.filter((node) => node.type === "segment" || node.type === "group");
326
+ }
327
+ //#endregion
328
+ //#region src/select.ts
329
+ /**
330
+ * Select the first node at the given path and return it with its ancestor
331
+ * chain.
332
+ *
333
+ * This function finds the first matching node for the given path. For paths
334
+ * that can match multiple nodes (e.g., "PID" when multiple PID segments exist),
335
+ * use {@link selectAll} to retrieve all matches.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const result = select(root, "PID-5");
340
+ * if (result) {
341
+ * result.node; // Type: Field
342
+ * result.ancestors; // [Root, Segment]
343
+ * }
344
+ * ```;
345
+ *
346
+ * @param root - The root node to search from
347
+ * @param path - The HL7 path string
348
+ * @returns Object containing the node and its ancestors, or null if not found
349
+ */
350
+ function select(root, path) {
351
+ const parts = parse(path);
352
+ if (parts.field === void 0) {
353
+ const result = locateSegmentOrGroup(root, parts);
354
+ if (!result) return null;
355
+ return {
356
+ ancestors: result[1],
357
+ node: result[0]
358
+ };
359
+ }
360
+ const segmentResult = locateSegment(root, parts);
361
+ if (!segmentResult) return null;
362
+ const [segment, segmentAncestors] = segmentResult;
363
+ const fieldResult = locateField(segment, parts.field, segmentAncestors);
364
+ if (!fieldResult) return null;
365
+ const [field, fieldAncestors] = fieldResult;
366
+ if (parts.component === void 0 && parts.subcomponent === void 0 && parts.repetition === void 0) return {
367
+ ancestors: fieldAncestors,
368
+ node: field
369
+ };
370
+ const repetitionResult = locateRepetition(field, parts.repetition ?? 1, fieldAncestors);
371
+ if (!repetitionResult) return null;
372
+ const [repetitionNode, repetitionAncestors] = repetitionResult;
373
+ if (parts.component === void 0) return {
374
+ ancestors: repetitionAncestors,
375
+ node: repetitionNode
376
+ };
377
+ const componentResult = locateComponent(repetitionNode, parts.component, repetitionAncestors);
378
+ if (!componentResult) return null;
379
+ const [componentNode, componentAncestors] = componentResult;
380
+ if (parts.subcomponent === void 0) return {
381
+ ancestors: componentAncestors,
382
+ node: componentNode
383
+ };
384
+ const subResult = locateSubcomponent(componentNode, parts.subcomponent, componentAncestors);
385
+ if (!subResult) return null;
386
+ return {
387
+ ancestors: subResult[1],
388
+ node: subResult[0]
389
+ };
390
+ }
391
+ /**
392
+ * Select all nodes that match the given path.
393
+ *
394
+ * This function returns all matching nodes for paths that can match multiple
395
+ * elements. For example, "PID" will return all PID segments in the message, and
396
+ * "OBX-5" will return all observation values.
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * // Get all OBX segments
401
+ * const results = selectAll(root, "OBX");
402
+ * for (const { node } of results) {
403
+ * console.log(node.type); // 'segment'
404
+ * }
405
+ *
406
+ * // Get all patient name fields
407
+ * const names = selectAll(root, "PID-5");
408
+ * ```;
409
+ *
410
+ * @param root - The root node to search from
411
+ * @param path - The HL7 path string (must not include repetition index)
412
+ * @returns Array of objects containing nodes and their ancestor chains
413
+ */
414
+ function selectAll(root, path) {
415
+ const parts = parse(path);
416
+ if (parts.segment.repetition !== void 0 || parts.repetition !== void 0) {
417
+ const result = select(root, path);
418
+ return result ? [result] : [];
419
+ }
420
+ const results = [];
421
+ const scopes = collectAllScopes(root, parts.groups ?? []);
422
+ const segments = [];
423
+ const includeGroups = parts.field === void 0;
424
+ const groups = [];
425
+ for (const { scope, ancestors } of scopes) {
426
+ const matchingSegments = collectSegments(scope, parts.segment.name, ancestors);
427
+ segments.push(...matchingSegments);
428
+ if (includeGroups) {
429
+ const matchingGroups = collectGroups(scope, parts.segment.name);
430
+ for (const group of matchingGroups) groups.push({
431
+ ancestors: [...ancestors],
432
+ group
433
+ });
434
+ }
435
+ }
436
+ if (parts.field === void 0) {
437
+ for (const { segment, ancestors } of segments) results.push({
438
+ ancestors,
439
+ node: segment
440
+ });
441
+ for (const { group, ancestors } of groups) results.push({
442
+ ancestors,
443
+ node: group
444
+ });
445
+ return results;
446
+ }
447
+ for (const { segment, ancestors: segmentAncestors } of segments) {
448
+ const fieldResult = locateField(segment, parts.field, segmentAncestors);
449
+ if (!fieldResult) continue;
450
+ const [field, fieldAncestors] = fieldResult;
451
+ if (parts.component === void 0 && parts.subcomponent === void 0 && parts.repetition === void 0) {
452
+ results.push({
453
+ ancestors: fieldAncestors,
454
+ node: field
455
+ });
456
+ continue;
457
+ }
458
+ if (!field.children) continue;
459
+ for (const repetitionNode of field.children) {
460
+ const repetitionAncestors = [...fieldAncestors, field];
461
+ if (parts.component === void 0) {
462
+ results.push({
463
+ ancestors: repetitionAncestors,
464
+ node: repetitionNode
465
+ });
466
+ continue;
467
+ }
468
+ const componentResult = locateComponent(repetitionNode, parts.component, repetitionAncestors);
469
+ if (!componentResult) continue;
470
+ const [componentNode, componentAncestors] = componentResult;
471
+ if (parts.subcomponent === void 0) {
472
+ results.push({
473
+ ancestors: componentAncestors,
474
+ node: componentNode
475
+ });
476
+ continue;
477
+ }
478
+ const subResult = locateSubcomponent(componentNode, parts.subcomponent, componentAncestors);
479
+ if (subResult) results.push({
480
+ ancestors: subResult[1],
481
+ node: subResult[0]
482
+ });
483
+ }
484
+ }
485
+ return results;
486
+ }
487
+ //#endregion
488
+ //#region src/value.ts
489
+ /**
490
+ * Get the string value at the path, drilling down through single-child nodes
491
+ * when needed. Also returns the node and ancestor chain for context.
492
+ *
493
+ * This is a convenience function that automatically traverses through container
494
+ * nodes (Field → FieldRepetition → Component → Subcomponent) to extract the
495
+ * primary string value. When a container has multiple children, the first child
496
+ * is selected (e.g., `value(tree, "MSH-12")` returns the first component of a
497
+ * composite VID field).
498
+ *
499
+ * When experimental.emptyMode is "empty", nodes with no children will return
500
+ * null to differentiate between empty strings ("") and missing values.
501
+ *
502
+ * @example
503
+ * ```typescript
504
+ * const result = value(root, "PID-5.1");
505
+ * if (result) {
506
+ * console.log(result.value); // string | null
507
+ * console.log(result.node.type); // 'subcomponent'
508
+ * console.log(result.ancestors); // [Root, Segment, Field, ...]
509
+ * }
510
+ * ```;
511
+ *
512
+ * @param root - The root node to search from
513
+ * @param path - The HL7 path string
514
+ * @returns Object containing the value, node, and ancestors, or null if not
515
+ * found
516
+ */
517
+ function value(root, path) {
518
+ const selectResult = select(root, path);
519
+ if (!selectResult) return null;
520
+ let { node } = selectResult;
521
+ let { ancestors } = selectResult;
522
+ while (node.type !== "subcomponent") {
523
+ if (node.type === "segment" || node.type === "group" || node.type === "root") return null;
524
+ if (!("children" in node && node.children)) return null;
525
+ if (node.children.length === 0) return {
526
+ ancestors: [...ancestors],
527
+ node,
528
+ value: null
529
+ };
530
+ const next = node.children[0];
531
+ if (!next) return null;
532
+ ancestors = [...ancestors, node];
533
+ node = next;
534
+ }
535
+ return {
536
+ ancestors,
537
+ node,
538
+ value: node.value
539
+ };
540
+ }
541
+ //#endregion
542
+ //#region src/delimiters.ts
543
+ /** Keys of MSH-2 encoding characters in positional order. */
544
+ const MSH2_KEYS = [
545
+ "component",
546
+ "repetition",
547
+ "escape",
548
+ "subcomponent"
549
+ ];
550
+ /**
551
+ * Extract delimiters from a Root AST by reading MSH-1 and MSH-2.
552
+ *
553
+ * MSH-1 contains the field separator character (e.g., `|`).
554
+ * MSH-2 contains up to 4 encoding characters: component, repetition,
555
+ * escape, and subcomponent (e.g., `^~\&`).
556
+ *
557
+ * Falls back to DEFAULT_DELIMITERS for any values that cannot be derived.
558
+ *
559
+ * @param root - The Root AST node to extract delimiters from.
560
+ * @returns The resolved Delimiters object.
561
+ */
562
+ function delimiters(root) {
563
+ const result = { ...DEFAULT_DELIMITERS };
564
+ const msh1 = value(root, "MSH-1")?.value;
565
+ if (msh1 && msh1.length === 1) result.field = msh1;
566
+ const msh2 = value(root, "MSH-2")?.value;
567
+ if (msh2) {
568
+ const chars = [...msh2];
569
+ for (const [i, key] of MSH2_KEYS.entries()) {
570
+ const ch = chars[i];
571
+ if (ch) result[key] = ch;
572
+ }
573
+ }
574
+ return result;
575
+ }
576
+ //#endregion
577
+ //#region src/format.ts
578
+ /**
579
+ * Format a node and its ancestor chain into a canonical HL7v2 path string.
580
+ *
581
+ * This is the inverse of `select`: given a node and the ancestors
582
+ * returned by `visit` or `select`, it produces the path string that
583
+ * would select that node.
584
+ *
585
+ * @example
586
+ * ```typescript
587
+ * visit(tree, "field", (node, ancestors) => {
588
+ * format(node, ancestors); // "PID-5"
589
+ * });
590
+ *
591
+ * const result = select(root, "PID-5.2");
592
+ * format(result.node, result.ancestors); // "PID-5.2"
593
+ * ```;
594
+ *
595
+ * @param node - The target AST node
596
+ * @param ancestors - Ancestor chain from root to parent (as provided by
597
+ * visit/select)
598
+ * @returns Canonical path string (e.g., "PID-5.2", "ORDER-ORC-1"), or `null` if the
599
+ * ancestor chain contains no segment or group
600
+ */
601
+ function format(node, ancestors) {
602
+ let result = null;
603
+ const chain = [...ancestors, node];
604
+ for (const [i, current] of chain.entries()) {
605
+ const parent = chain[i - 1];
606
+ switch (current.type) {
607
+ case "root": break;
608
+ case "group":
609
+ case "segment": {
610
+ const pos = positionOf(current, parent);
611
+ result = (result ?? "") + current.name;
612
+ if (pos > 1) result += `[${pos}]`;
613
+ if (current.type === "group" && current !== node) result += "-";
614
+ break;
615
+ }
616
+ case "field":
617
+ result += `-${positionOf(current, parent)}`;
618
+ break;
619
+ case "field-repetition": {
620
+ const pos = positionOf(current, parent);
621
+ if (pos > 1 || current === node) result += `[${pos}]`;
622
+ break;
623
+ }
624
+ case "component":
625
+ result += `.${positionOf(current, parent)}`;
626
+ break;
627
+ case "subcomponent":
628
+ result += `.${positionOf(current, parent)}`;
629
+ break;
630
+ }
631
+ }
632
+ return result;
633
+ }
634
+ /**
635
+ * Compute the 1-based position of a node among its parent's children.
636
+ *
637
+ * For named nodes (segments, groups), counts only same-name siblings
638
+ * because segments are identified by name: the 2nd PID in [MSH, PID, OBX, PID]
639
+ * is PID[2], not PID[4].
640
+ *
641
+ * For positional nodes (fields, repetitions, components, subcomponents),
642
+ * uses the array index directly.
643
+ */
644
+ function positionOf(node, parent) {
645
+ if (!parent || !("children" in parent)) return 1;
646
+ const children = parent.children;
647
+ if ("name" in node) {
648
+ const name = node.name;
649
+ const { type } = node;
650
+ let count = 0;
651
+ for (const child of children) {
652
+ if (child === node) return count + 1;
653
+ if (child.type === type && "name" in child && child.name === name) count++;
654
+ }
655
+ throw new Error("format(): node not found in parent's children array");
656
+ }
657
+ const idx = children.indexOf(node);
658
+ if (idx === -1) throw new Error("format(): node not found in parent's children array");
659
+ return idx + 1;
660
+ }
661
+ //#endregion
662
+ //#region src/matches.ts
663
+ /**
664
+ * Check if a node exists at the given path.
665
+ *
666
+ * This is a convenience function equivalent to `select(root, path) !== null`,
667
+ * but more semantically clear for existence checks.
668
+ *
669
+ * @example
670
+ * ```typescript
671
+ * if (matches(root, "PID-5")) {
672
+ * console.log("Patient has a name field");
673
+ * }
674
+ *
675
+ * if (!matches(root, "OBX-5")) {
676
+ * throw new Error("Missing observation value");
677
+ * }
678
+ * ```;
679
+ *
680
+ * @param root - The root node to search from
681
+ * @param path - The HL7 path string
682
+ * @returns True if the path resolves to a node, false otherwise
683
+ */
684
+ function matches(root, path) {
685
+ return select(root, path) !== null;
686
+ }
687
+ //#endregion
688
+ //#region src/set.ts
689
+ /**
690
+ * Set a string value at the given HL7 path, creating all missing intermediate
691
+ * nodes as needed (fields, repetitions, components, subcomponents).
692
+ *
693
+ * This is the write counterpart to {@link value}. It handles the full HL7v2
694
+ * path syntax including group navigation and segment repetitions.
695
+ *
696
+ * Padding nodes are created with empty children arrays — they represent
697
+ * structurally absent nodes, not empty values. Only the target node gets
698
+ * a fully populated chain down to the subcomponent.
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * // Update existing value
703
+ * set(tree, "MSH-12", "2.7.1");
704
+ *
705
+ * // Create missing MSH-9.3 (fills gaps with empty components)
706
+ * set(tree, "MSH-9.3", "ADT_A01");
707
+ *
708
+ * // Set a specific repetition
709
+ * set(tree, "PID-3[2]", "ID-002");
710
+ *
711
+ * // Set a subcomponent
712
+ * set(tree, "PID-5.1.2", "suffix");
713
+ * ```;
714
+ *
715
+ * @param root - The root node to modify
716
+ * @param path - The HL7 path string (must include at least a field)
717
+ * @param val - The string value to set
718
+ * @throws When the path has no field specifier (segment-only paths)
719
+ * @throws When the target segment does not exist in the tree
720
+ */
721
+ function set(root, path, val) {
722
+ const parts = parse(path);
723
+ if (parts.field === void 0) throw new Error(`Cannot set value on segment-only path: "${path}"`);
724
+ const segmentResult = locateSegment(root, parts);
725
+ if (!segmentResult) throw new Error(`Segment "${parts.segment.name}" not found in tree`);
726
+ const [segment] = segmentResult;
727
+ const fieldIndex = parts.field - 1;
728
+ padArray(segment.children, fieldIndex + 1, stubField);
729
+ const field = segment.children[fieldIndex];
730
+ const repIndex = (parts.repetition ?? 1) - 1;
731
+ padArray(field.children, repIndex + 1, stubRepetition);
732
+ const repetition = field.children[repIndex];
733
+ const compIndex = (parts.component ?? 1) - 1;
734
+ padArray(repetition.children, compIndex + 1, stubComponent);
735
+ const component = repetition.children[compIndex];
736
+ const subIndex = (parts.subcomponent ?? 1) - 1;
737
+ padArray(component.children, subIndex + 1, stubSubcomponent);
738
+ component.children[subIndex].value = val;
739
+ }
740
+ /**
741
+ * Pad an array to a target length using a factory function.
742
+ */
743
+ function padArray(arr, targetLength, factory) {
744
+ while (arr.length < targetLength) arr.push(factory());
745
+ }
746
+ /**
747
+ * Stub nodes for padding — created with empty children arrays to represent
748
+ * structurally absent nodes. These are distinguishable from nodes with empty
749
+ * values (which have a subcomponent with value "").
750
+ */
751
+ function stubField() {
752
+ return {
753
+ type: "field",
754
+ children: []
755
+ };
756
+ }
757
+ function stubRepetition() {
758
+ return {
759
+ type: "field-repetition",
760
+ children: []
761
+ };
762
+ }
763
+ function stubComponent() {
764
+ return {
765
+ type: "component",
766
+ children: []
767
+ };
768
+ }
769
+ function stubSubcomponent() {
770
+ return {
771
+ type: "subcomponent",
772
+ value: ""
773
+ };
774
+ }
775
+ //#endregion
776
+ export { delimiters, format, matches, parse, select, selectAll, set, value };
777
+
778
+ //# sourceMappingURL=index.js.map