@rethinkhealth/hl7v2-parser 0.2.2 → 0.2.4

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.
Files changed (42) hide show
  1. package/README.md +14 -8
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +325 -534
  5. package/dist/index.js.map +1 -1
  6. package/dist/parser.d.ts +6 -13
  7. package/dist/parser.d.ts.map +1 -1
  8. package/dist/preprocessor.d.ts +26 -0
  9. package/dist/preprocessor.d.ts.map +1 -0
  10. package/dist/processor.d.ts +3 -5
  11. package/dist/processor.d.ts.map +1 -1
  12. package/dist/tokenizer.d.ts +18 -0
  13. package/dist/tokenizer.d.ts.map +1 -0
  14. package/dist/types.d.ts +29 -13
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/utils.d.ts +3 -0
  17. package/dist/utils.d.ts.map +1 -0
  18. package/package.json +6 -5
  19. package/dist/constants.d.ts +0 -11
  20. package/dist/constants.d.ts.map +0 -1
  21. package/dist/pipeline/core/message.d.ts +0 -16
  22. package/dist/pipeline/core/message.d.ts.map +0 -1
  23. package/dist/pipeline/core/parsers/component.d.ts +0 -13
  24. package/dist/pipeline/core/parsers/component.d.ts.map +0 -1
  25. package/dist/pipeline/core/parsers/field.d.ts +0 -13
  26. package/dist/pipeline/core/parsers/field.d.ts.map +0 -1
  27. package/dist/pipeline/core/parsers/segment-msh.d.ts +0 -14
  28. package/dist/pipeline/core/parsers/segment-msh.d.ts.map +0 -1
  29. package/dist/pipeline/core/parsers/segment.d.ts +0 -13
  30. package/dist/pipeline/core/parsers/segment.d.ts.map +0 -1
  31. package/dist/pipeline/core/parsers/subcomponent.d.ts +0 -10
  32. package/dist/pipeline/core/parsers/subcomponent.d.ts.map +0 -1
  33. package/dist/pipeline/core/registry.d.ts +0 -30
  34. package/dist/pipeline/core/registry.d.ts.map +0 -1
  35. package/dist/pipeline/index.d.ts +0 -10
  36. package/dist/pipeline/index.d.ts.map +0 -1
  37. package/dist/pipeline/interfaces.d.ts +0 -109
  38. package/dist/pipeline/interfaces.d.ts.map +0 -1
  39. package/dist/pipeline/utils/index.d.ts +0 -18
  40. package/dist/pipeline/utils/index.d.ts.map +0 -1
  41. package/dist/pipeline/validation/basic-validation.d.ts +0 -17
  42. package/dist/pipeline/validation/basic-validation.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,582 +1,373 @@
1
- // src/constants.ts
2
- var DEFAULT_DELIMITERS = {
3
- field: "|",
4
- component: "^",
5
- subcomponent: "&",
6
- repetition: "~",
7
- escape: "\\",
8
- segment: "\n"
9
- // default to \n (more common in modern systems)
10
- };
11
- var EMPTY_MESSAGE = {
12
- type: "root",
13
- delimiter: DEFAULT_DELIMITERS.segment,
14
- children: [],
15
- position: {
16
- start: { line: 1, column: 1, offset: 0 },
17
- end: { line: 1, column: 1, offset: 0 }
18
- }
19
- };
20
-
21
- // src/pipeline/utils/index.ts
22
- function detectDelimitersFromMSH(raw, segmentDelimiter) {
23
- const detectedSegmentDelimiter = detectSegmentDelimiter(
24
- raw,
25
- segmentDelimiter
26
- );
27
- const firstLineEnd = raw.indexOf(detectedSegmentDelimiter);
28
- const mshSegment = firstLineEnd >= 0 ? raw.slice(0, firstLineEnd) : raw;
29
- if (!mshSegment.startsWith("MSH")) {
30
- return {};
31
- }
32
- const fieldDelimiter = mshSegment[3];
33
- if (!fieldDelimiter) {
34
- return {};
35
- }
36
- const encodingChars = mshSegment.slice(4, 8);
37
- if (encodingChars.length !== 4) {
38
- return {};
39
- }
40
- return {
41
- field: fieldDelimiter,
42
- component: encodingChars[0],
43
- repetition: encodingChars[1],
44
- escape: encodingChars[2],
45
- subcomponent: encodingChars[3],
46
- segment: detectedSegmentDelimiter
1
+ // src/processor.ts
2
+ function createParserCore() {
3
+ const root = { type: "root", children: [] };
4
+ let seg = null;
5
+ let field = null;
6
+ let rep = null;
7
+ let comp = null;
8
+ let currentSub = null;
9
+ let segmentHasContent = false;
10
+ const openSegment = () => {
11
+ seg = { type: "segment", children: [] };
12
+ root.children.push(seg);
13
+ field = null;
14
+ rep = null;
15
+ comp = null;
16
+ currentSub = null;
17
+ segmentHasContent = false;
47
18
  };
48
- }
49
- function splitByString(text, delimiter) {
50
- const result = [];
51
- let lastIndex = 0;
52
- let index = text.indexOf(delimiter);
53
- while (index !== -1) {
54
- result.push({
55
- value: text.slice(lastIndex, index),
56
- start: lastIndex,
57
- end: index
58
- });
59
- lastIndex = index + delimiter.length;
60
- index = text.indexOf(delimiter, lastIndex);
61
- }
62
- result.push({
63
- value: text.slice(lastIndex),
64
- start: lastIndex,
65
- end: text.length
66
- });
67
- return result;
68
- }
69
- function detectSegmentDelimiter(message, fallback = "\n") {
70
- const possibleDelimiters = ["\r\n", "\n", "\r"];
71
- for (const delimiter of possibleDelimiters) {
72
- if (message.includes(delimiter)) {
73
- return delimiter;
19
+ const ensureSegment = () => {
20
+ if (!seg) {
21
+ openSegment();
74
22
  }
75
- }
76
- return fallback;
77
- }
78
-
79
- // src/pipeline/core/parsers/subcomponent.ts
80
- var SubcomponentParser = class {
81
- canParse(input) {
82
- return input.type === "subcomponent";
83
- }
84
- parse(input) {
85
- if (!this.canParse(input)) {
86
- return null;
23
+ };
24
+ const openField = () => {
25
+ ensureSegment();
26
+ field = { type: "field", children: [] };
27
+ seg.children.push(field);
28
+ rep = { type: "field-repetition", children: [] };
29
+ field.children.push(rep);
30
+ comp = { type: "component", children: [] };
31
+ rep.children.push(comp);
32
+ currentSub = null;
33
+ segmentHasContent = true;
34
+ };
35
+ const openRepetition = () => {
36
+ if (!field) {
37
+ openField();
87
38
  }
88
- return {
89
- type: "subcomponent",
90
- index: input.subcomponentIndex,
91
- value: input.text,
92
- position: {
93
- start: { line: input.line, column: input.column, offset: input.start },
94
- end: {
95
- line: input.line,
96
- column: input.column + input.text.length,
97
- offset: input.end
98
- }
99
- }
100
- };
101
- }
102
- };
103
-
104
- // src/pipeline/core/parsers/component.ts
105
- var ComponentParser = class {
106
- subcomponentParser = new SubcomponentParser();
107
- canParse(input) {
108
- return input.type === "component";
109
- }
110
- parse(input) {
111
- if (!this.canParse(input)) {
112
- return null;
39
+ rep = { type: "field-repetition", children: [] };
40
+ field.children.push(rep);
41
+ comp = { type: "component", children: [] };
42
+ rep.children.push(comp);
43
+ currentSub = null;
44
+ segmentHasContent = true;
45
+ };
46
+ const openComponent = () => {
47
+ if (!field) {
48
+ openField();
113
49
  }
114
- const baseNode = {
115
- type: "component",
116
- index: input.componentIndex,
117
- position: {
118
- start: { line: input.line, column: input.column, offset: input.start },
119
- end: {
120
- line: input.line,
121
- column: input.column + input.text.length,
122
- offset: input.end
123
- }
124
- }
125
- };
126
- if (input.text.includes(input.context.delimiters.subcomponent)) {
127
- return {
128
- ...baseNode,
129
- delimiter: input.context.delimiters.subcomponent,
130
- children: this.parseSubcomponents(input)
131
- };
50
+ if (!rep) {
51
+ rep = { type: "field-repetition", children: [] };
52
+ field.children.push(rep);
132
53
  }
133
- return { ...baseNode, value: input.text };
134
- }
135
- parseSubcomponents(input) {
136
- const subcomponents = this.splitByDelimiter(
137
- input.text,
138
- input.context.delimiters.subcomponent
139
- );
140
- const nodes = [];
141
- for (let i = 0; i < subcomponents.length; i++) {
142
- const sub = subcomponents[i];
143
- if (!sub) {
144
- continue;
145
- }
146
- const subNode = this.subcomponentParser.parse({
147
- type: "subcomponent",
148
- text: sub.value,
149
- start: input.start + sub.start,
150
- end: input.start + sub.end,
151
- index: i,
152
- subcomponentIndex: i,
153
- line: input.line,
154
- column: input.column + sub.start,
155
- context: input.context
156
- });
157
- if (subNode) {
158
- nodes.push(subNode);
159
- }
54
+ comp = { type: "component", children: [] };
55
+ rep.children.push(comp);
56
+ currentSub = null;
57
+ segmentHasContent = true;
58
+ };
59
+ const ensureForText = () => {
60
+ if (!field) {
61
+ openField();
160
62
  }
161
- return nodes;
162
- }
163
- splitByDelimiter(text, delimiter) {
164
- const result = [];
165
- let lastIndex = 0;
166
- let index = text.indexOf(delimiter);
167
- while (index !== -1) {
168
- result.push({
169
- value: text.slice(lastIndex, index),
170
- start: lastIndex,
171
- end: index
172
- });
173
- lastIndex = index + delimiter.length;
174
- index = text.indexOf(delimiter, lastIndex);
63
+ if (!rep) {
64
+ openRepetition();
175
65
  }
176
- result.push({
177
- value: text.slice(lastIndex),
178
- start: lastIndex,
179
- end: text.length
180
- });
181
- return result;
182
- }
183
- };
184
-
185
- // src/pipeline/core/parsers/field.ts
186
- var FieldParser = class {
187
- componentParser = new ComponentParser();
188
- canParse(input) {
189
- return input.type === "field";
190
- }
191
- parse(input) {
192
- if (!this.canParse(input)) {
193
- return null;
66
+ if (!comp) {
67
+ openComponent();
194
68
  }
195
- const baseNode = {
196
- type: input.fieldIndex === 0 ? "header" : "field",
197
- index: input.fieldIndex,
198
- position: {
199
- start: { line: input.line, column: input.column, offset: input.start },
200
- end: {
201
- line: input.line,
202
- column: input.column + input.text.length,
203
- offset: input.end
69
+ if (!currentSub) {
70
+ currentSub = { type: "subcomponent", value: "" };
71
+ comp.children.push(currentSub);
72
+ segmentHasContent = true;
73
+ }
74
+ };
75
+ const processToken = (tok) => {
76
+ switch (tok.type) {
77
+ case "SEGMENT_END": {
78
+ dropTrailingEmptyFieldIfPresent();
79
+ seg = null;
80
+ field = null;
81
+ rep = null;
82
+ comp = null;
83
+ currentSub = null;
84
+ segmentHasContent = false;
85
+ return;
86
+ }
87
+ case "FIELD_DELIM": {
88
+ if (!field) {
89
+ openField();
90
+ comp.children.push({ type: "subcomponent", value: "" });
91
+ segmentHasContent = true;
204
92
  }
93
+ openField();
94
+ return;
95
+ }
96
+ case "REPETITION_DELIM": {
97
+ if (!field) {
98
+ openField();
99
+ }
100
+ openRepetition();
101
+ return;
102
+ }
103
+ case "COMPONENT_DELIM": {
104
+ openComponent();
105
+ return;
106
+ }
107
+ case "SUBCOMP_DELIM": {
108
+ if (!comp) {
109
+ openComponent();
110
+ }
111
+ currentSub = { type: "subcomponent", value: "" };
112
+ comp.children.push(currentSub);
113
+ segmentHasContent = true;
114
+ return;
115
+ }
116
+ case "TEXT": {
117
+ const val = tok.value ?? "";
118
+ ensureForText();
119
+ currentSub.value += val;
120
+ segmentHasContent = true;
121
+ return;
122
+ }
123
+ default: {
124
+ throw new Error(`Unexpected token type: ${tok.type}`);
205
125
  }
206
- };
207
- if (input.isEncodingField) {
208
- return { ...baseNode, value: input.text };
209
126
  }
210
- if (input.fieldIndex === 0) {
211
- return { ...baseNode, value: input.text };
127
+ };
128
+ const finalize = () => {
129
+ dropTrailingEmptyFieldIfPresent();
130
+ if (seg && !segmentHasContent) {
131
+ root.children.pop();
212
132
  }
213
- if (input.text.includes(input.context.delimiters.component)) {
214
- return {
215
- ...baseNode,
216
- delimiter: input.context.delimiters.component,
217
- children: this.parseComponents(input)
218
- };
133
+ return root;
134
+ };
135
+ function dropTrailingEmptyFieldIfPresent() {
136
+ if (!seg || seg.children.length === 0) {
137
+ return;
219
138
  }
220
- return { ...baseNode, value: input.text };
221
- }
222
- parseComponents(input) {
223
- const components = this.splitByDelimiter(
224
- input.text,
225
- input.context.delimiters.component
139
+ const lastField = seg.children.at(-1);
140
+ const hasAnySubcomponents = lastField.children.some(
141
+ (r) => r.children.some((c) => c.children.length > 0)
226
142
  );
227
- const nodes = [];
228
- for (let i = 0; i < components.length; i++) {
229
- const comp = components[i];
230
- if (!comp) {
231
- continue;
232
- }
233
- const componentNode = this.componentParser.parse({
234
- type: "component",
235
- text: comp.value,
236
- start: input.start + comp.start,
237
- end: input.start + comp.end,
238
- index: i,
239
- componentIndex: i,
240
- line: input.line,
241
- column: input.column + comp.start,
242
- context: input.context
243
- });
244
- if (componentNode) {
245
- nodes.push(componentNode);
246
- }
143
+ if (!hasAnySubcomponents) {
144
+ seg.children.pop();
247
145
  }
248
- return nodes;
249
146
  }
250
- splitByDelimiter(text, delimiter) {
251
- const result = [];
252
- let lastIndex = 0;
253
- let index = text.indexOf(delimiter);
254
- while (index !== -1) {
255
- result.push({
256
- value: text.slice(lastIndex, index),
257
- start: lastIndex,
258
- end: index
259
- });
260
- lastIndex = index + delimiter.length;
261
- index = text.indexOf(delimiter, lastIndex);
262
- }
263
- result.push({
264
- value: text.slice(lastIndex),
265
- start: lastIndex,
266
- end: text.length
267
- });
268
- return result;
147
+ return { processToken, finalize, root };
148
+ }
149
+ function parseHL7v2FromIterator(tokens) {
150
+ const core = createParserCore();
151
+ for (const tok of tokens) {
152
+ core.processToken(tok);
269
153
  }
270
- };
154
+ return core.finalize();
155
+ }
271
156
 
272
- // src/pipeline/core/parsers/segment.ts
273
- var SegmentParser = class {
274
- segmentType = "DEFAULT";
275
- fieldParser = new FieldParser();
276
- canParse(input) {
277
- return input.type === "segment" && input.text.trim().length > 0;
278
- }
279
- parse(input) {
280
- if (!this.canParse(input)) {
281
- return null;
157
+ // src/tokenizer.ts
158
+ import {
159
+ DEFAULT_DELIMITERS
160
+ } from "@rethinkhealth/hl7v2-utils";
161
+ var HL7v2Tokenizer = class {
162
+ input = "";
163
+ i = 0;
164
+ line = 1;
165
+ col = 1;
166
+ delims;
167
+ // Only-run-once MSH bootstrap at the start of the file
168
+ didMshBootstrap = false;
169
+ pendingBootstrap = null;
170
+ // queue to emit TEXT('MSH'), FIELD_DELIM, TEXT('^~\\&')
171
+ reset(input, opts) {
172
+ this.input = input;
173
+ this.delims = opts.delimiters || DEFAULT_DELIMITERS;
174
+ this.i = 0;
175
+ this.line = 1;
176
+ this.col = 1;
177
+ this.didMshBootstrap = false;
178
+ this.pendingBootstrap = null;
179
+ if (this.input.startsWith("MSH")) {
180
+ const msh = this._slice(0, 3);
181
+ const msh2 = this._slice(4, 8);
182
+ this.pendingBootstrap = [
183
+ { kind: "TEXT", value: msh, advance: msh.length },
184
+ { kind: "FIELD_DELIM", advance: 1 },
185
+ // consume the single field delimiter char at index 3
186
+ { kind: "TEXT", value: msh2, advance: msh2.length }
187
+ ];
282
188
  }
283
- const fields = splitByString(input.text, input.context.delimiters.field);
284
- if (!fields[0] || fields.length === 0) {
189
+ }
190
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: The cognitive complexity of this method is justified because it must handle the HL7v2 MSH segment bootstrap logic and multiple tokenization cases in a single method for performance and maintainability.
191
+ next() {
192
+ const s = this.input;
193
+ const n = s.length;
194
+ if (this.i >= n && !this.pendingBootstrap?.length) {
285
195
  return null;
286
196
  }
287
- const segmentNode = {
288
- type: "segment",
289
- name: fields[0].value,
290
- index: input.index,
291
- delimiter: input.context.delimiters.field,
292
- children: [],
293
- position: {
294
- start: { line: input.line, column: 1, offset: input.start },
295
- end: {
296
- line: input.line,
297
- column: input.text.length + 1,
298
- offset: input.end
197
+ const start = { offset: this.i, line: this.line, column: this.col };
198
+ if (!this.didMshBootstrap && this.pendingBootstrap) {
199
+ const step = this.pendingBootstrap.shift();
200
+ if (step) {
201
+ if (step.kind === "TEXT") {
202
+ const take = Math.min(step.advance, n - this.i);
203
+ const out = step.value.slice(0, take);
204
+ this._fastAdvance(out);
205
+ if (this.pendingBootstrap.length === 0) {
206
+ this.pendingBootstrap = null;
207
+ this.didMshBootstrap = true;
208
+ }
209
+ return this._tok("TEXT", out, start);
299
210
  }
300
- }
301
- };
302
- segmentNode.children = this.parseFields(fields, input);
303
- return segmentNode;
304
- }
305
- parseFields(fields, input) {
306
- const children = [];
307
- for (let i = 0; i < fields.length; i++) {
308
- const field = fields[i];
309
- if (!field) {
310
- continue;
311
- }
312
- const fieldNode = this.fieldParser.parse({
313
- type: "field",
314
- text: field.value,
315
- start: input.start + field.start,
316
- end: input.start + field.end,
317
- index: i,
318
- fieldIndex: i,
319
- line: input.line,
320
- column: field.start + 1,
321
- context: input.context
322
- });
323
- if (fieldNode) {
324
- children.push(fieldNode);
211
+ this._advance(Math.min(step.advance, n - this.i));
212
+ if (this.pendingBootstrap.length === 0) {
213
+ this.pendingBootstrap = null;
214
+ this.didMshBootstrap = true;
215
+ }
216
+ return this._tok("FIELD_DELIM", void 0, start);
325
217
  }
326
218
  }
327
- return children;
328
- }
329
- };
330
-
331
- // src/pipeline/core/parsers/segment-msh.ts
332
- var MSHSegmentParser = class {
333
- segmentType = "MSH";
334
- fieldParser = new FieldParser();
335
- canParse(input) {
336
- return input.type === "segment" && input.segmentType === "MSH";
337
- }
338
- parse(input) {
339
- if (!this.canParse(input)) {
219
+ if (this.i >= n) {
340
220
  return null;
341
221
  }
342
- const fields = this.extractMSHFields(
343
- input.text,
344
- input.context.delimiters.field
345
- );
346
- const segmentNode = {
347
- type: "segment",
348
- name: "MSH",
349
- index: input.index,
350
- delimiter: input.context.delimiters.field,
351
- children: [],
352
- position: {
353
- start: { line: input.line, column: 1, offset: input.start },
354
- end: {
355
- line: input.line,
356
- column: input.text.length + 1,
357
- offset: input.end
358
- }
359
- }
360
- };
361
- segmentNode.children = this.parseFields(fields, input);
362
- return segmentNode;
363
- }
364
- extractMSHFields(segmentText, fieldDelimiter) {
365
- const msh1 = segmentText[3] ?? "";
366
- const msh2 = segmentText.slice(4, 8) ?? "";
367
- const rest = segmentText.slice(9);
368
- const restFields = splitByString(rest, fieldDelimiter);
369
- return [
370
- { value: "MSH", start: 0, end: 3 },
371
- { value: msh1, start: 3, end: 4 },
372
- { value: msh2, start: 4, end: 8, isEncodingField: true },
373
- ...restFields.map((f) => ({
374
- value: f.value ?? "",
375
- start: 9 + (f.start ?? 0),
376
- end: 9 + (f.end ?? 0)
377
- }))
378
- ];
379
- }
380
- parseFields(fields, input) {
381
- const children = [];
382
- for (let i = 0; i < fields.length; i++) {
383
- const field = fields[i];
384
- if (!field) {
385
- continue;
386
- }
387
- const fieldNode = this.fieldParser.parse({
388
- type: "field",
389
- text: field.value,
390
- start: input.start + field.start,
391
- end: input.start + field.end,
392
- index: i,
393
- fieldIndex: i,
394
- line: input.line,
395
- column: field.start + 1,
396
- isEncodingField: field.isEncodingField,
397
- context: input.context
398
- });
399
- if (fieldNode) {
400
- children.push(fieldNode);
401
- }
222
+ const ch = s.charAt(this.i);
223
+ if (ch === this.delims.segment) {
224
+ this._advance(1, true);
225
+ return this._tok("SEGMENT_END", void 0, start);
402
226
  }
403
- return children;
404
- }
405
- };
406
-
407
- // src/pipeline/core/registry.ts
408
- var SegmentParserRegistry = class {
409
- parsers = /* @__PURE__ */ new Map();
410
- defaultParser;
411
- constructor(customParsers) {
412
- this.defaultParser = new SegmentParser();
413
- this.register(new MSHSegmentParser());
414
- if (customParsers) {
415
- for (const [, parser] of customParsers) {
416
- this.register(parser);
417
- }
227
+ if (ch === this.delims.field) {
228
+ this._advance(1);
229
+ return this._tok("FIELD_DELIM", void 0, start);
418
230
  }
419
- }
420
- /**
421
- * Register a segment parser for a specific segment type
422
- */
423
- register(parser) {
424
- this.parsers.set(parser.segmentType, parser);
425
- }
426
- /**
427
- * Get the appropriate parser for a segment type
428
- */
429
- getParser(segmentType) {
430
- return this.parsers.get(segmentType) || this.defaultParser;
431
- }
432
- /**
433
- * Check if a parser is registered for a segment type
434
- */
435
- hasParser(segmentType) {
436
- return this.parsers.has(segmentType);
437
- }
438
- /**
439
- * Unregister a parser for a segment type
440
- */
441
- unregister(segmentType) {
442
- return this.parsers.delete(segmentType);
443
- }
444
- /**
445
- * Get all registered segment types
446
- */
447
- getRegisteredTypes() {
448
- return Array.from(this.parsers.keys());
449
- }
450
- };
451
-
452
- // src/pipeline/core/message.ts
453
- var HL7MessageParser = class {
454
- segmentRegistry;
455
- options;
456
- constructor(options = {}) {
457
- this.options = options;
458
- this.segmentRegistry = new SegmentParserRegistry(options.customParsers);
459
- }
460
- canParse(input) {
461
- return input.type === "message" && input.text.trim().length > 0;
462
- }
463
- parse(input) {
464
- if (!this.canParse(input)) {
465
- return EMPTY_MESSAGE;
231
+ if (ch === this.delims.repetition) {
232
+ this._advance(1);
233
+ return this._tok("REPETITION_DELIM", void 0, start);
466
234
  }
467
- const context = this.createParseContext(input.text);
468
- const segments = splitByString(input.text, context.delimiters.segment);
469
- const messageNode = {
470
- type: "root",
471
- delimiter: context.delimiters.segment,
472
- children: [],
473
- position: {
474
- start: { line: 1, column: 1, offset: 0 },
475
- end: {
476
- line: segments.length,
477
- column: (segments.at(-1)?.value.length ?? 0) + 1,
478
- offset: input.text.length
479
- }
480
- }
481
- };
482
- let currentLine = 1;
483
- for (let i = 0; i < segments.length; i++) {
484
- const seg = segments[i];
485
- const hasContent = seg?.value.trim();
486
- if (!seg) {
487
- currentLine++;
488
- continue;
489
- }
490
- if (!hasContent) {
491
- currentLine++;
492
- continue;
493
- }
494
- const segmentNode = this.parseSegment(seg, currentLine, i, context);
495
- if (segmentNode) {
496
- messageNode.children = messageNode.children || [];
497
- messageNode.children.push(segmentNode);
235
+ if (ch === this.delims.component) {
236
+ this._advance(1);
237
+ return this._tok("COMPONENT_DELIM", void 0, start);
238
+ }
239
+ if (ch === this.delims.subcomponent) {
240
+ this._advance(1);
241
+ return this._tok("SUBCOMP_DELIM", void 0, start);
242
+ }
243
+ let j = this.i;
244
+ const seg = this.delims.segment, fld = this.delims.field, rep = this.delims.repetition, cmp = this.delims.component, sub = this.delims.subcomponent;
245
+ while (j < s.length) {
246
+ const c = s.charAt(j);
247
+ if (c === seg || c === fld || c === rep || c === cmp || c === sub) {
248
+ break;
498
249
  }
499
- currentLine++;
250
+ j++;
251
+ }
252
+ const val = s.slice(this.i, j);
253
+ this._fastAdvance(val);
254
+ return this._tok("TEXT", val, start);
255
+ }
256
+ // ---- helpers ----
257
+ _slice(start, end) {
258
+ return this.input.slice(start, Math.min(end, this.input.length));
259
+ }
260
+ _advance(n, isNewline = false) {
261
+ this.i += n;
262
+ if (isNewline) {
263
+ this.line += 1;
264
+ this.col = 1;
265
+ } else {
266
+ this.col += n;
500
267
  }
501
- return this.applyValidationHooks(messageNode, context);
502
268
  }
503
- createParseContext(rawMessage) {
504
- const baseDelimiters = {
505
- ...DEFAULT_DELIMITERS,
506
- ...this.options.delimiters
507
- };
508
- let activeDelimiters = baseDelimiters;
509
- if (this.options.autoDetectDelimiters !== false) {
510
- const detected = detectDelimitersFromMSH(
511
- rawMessage,
512
- baseDelimiters.segment
513
- );
514
- activeDelimiters = { ...baseDelimiters, ...detected };
269
+ _fastAdvance(chunk) {
270
+ const parts = chunk.split("\r");
271
+ if (parts.length > 1) {
272
+ this.line += parts.length - 1;
273
+ this.col = (parts.at(-1)?.length ?? 0) + 1;
274
+ } else {
275
+ this.col += chunk.length;
515
276
  }
516
- return {
517
- delimiters: activeDelimiters,
518
- options: this.options,
519
- currentLine: 1,
520
- totalOffset: 0
521
- };
277
+ this.i += chunk.length;
522
278
  }
523
- parseSegment(segment, line, index, context) {
524
- const segmentType = segment.value.slice(0, 3);
525
- const parser = this.segmentRegistry.getParser(segmentType);
526
- return parser.parse({
527
- type: "segment",
528
- text: segment.value,
529
- start: segment.start,
530
- end: segment.end,
531
- index,
532
- segmentType,
533
- line,
534
- context
535
- });
279
+ _tok(type, value, start) {
280
+ const end = { offset: this.i, line: this.line, column: this.col };
281
+ return { type, value, position: { start, end } };
536
282
  }
537
- applyValidationHooks(node, context) {
538
- if (!this.options.validationHooks) {
539
- return node;
540
- }
541
- for (const hook of this.options.validationHooks) {
542
- const result = hook.validate(node, context);
543
- if (!result.isValid) {
544
- throw new Error(result.errors?.[0] ?? "Validation error");
283
+ // Iterable protocol (sync)
284
+ [Symbol.iterator]() {
285
+ return {
286
+ next: () => {
287
+ const t = this.next();
288
+ if (t == null) {
289
+ return { done: true, value: void 0 };
290
+ }
291
+ return { done: false, value: t };
545
292
  }
546
- }
547
- return node;
293
+ };
548
294
  }
295
+ // Async iteration support removed to keep the API synchronous.
549
296
  };
550
297
 
551
- // src/parser.ts
552
- function fromHL7v2Pipeline(rawMessage, options = {}) {
553
- const parser = new HL7MessageParser(options);
554
- const result = parser.parse({
555
- type: "message",
556
- text: rawMessage,
557
- start: 0,
558
- end: rawMessage.length,
559
- index: 0,
560
- context: {
561
- delimiters: options.delimiters || {},
562
- options,
563
- currentLine: 1,
564
- totalOffset: 0
565
- }
566
- });
567
- return result || EMPTY_MESSAGE;
298
+ // src/utils.ts
299
+ function* iterateTokenizerSync(t) {
300
+ for (let tok = t.next(); tok; tok = t.next()) {
301
+ yield tok;
302
+ }
568
303
  }
569
304
 
570
- // src/processor.ts
305
+ // src/parser.ts
306
+ function parseHL7v2(input, opts) {
307
+ const tokenizer = new HL7v2Tokenizer();
308
+ tokenizer.reset(input, opts);
309
+ return parseHL7v2FromIterator(iterateTokenizerSync(tokenizer));
310
+ }
571
311
  var hl7v2Parser = function(options = {}) {
572
312
  const self = this;
573
- function parser(value) {
574
- return fromHL7v2Pipeline(value, options);
313
+ self.parser = (value) => parseHL7v2(value, options);
314
+ };
315
+ var parser_default = hl7v2Parser;
316
+
317
+ // src/preprocessor.ts
318
+ var stripBOM = (ctx) => {
319
+ if (ctx.input.charCodeAt(0) === 65279) {
320
+ ctx.input = ctx.input.slice(1);
575
321
  }
576
- self.parser = parser;
322
+ return ctx;
577
323
  };
578
- var processor_default = hl7v2Parser;
324
+ var normalizeNewlines = (ctx) => {
325
+ ctx.input = ctx.input.replace(/\r?\n/g, "\r");
326
+ return ctx;
327
+ };
328
+ var detectDelimiters = (ctx) => {
329
+ if (ctx.input.startsWith("MSH")) {
330
+ const fieldDelim = ctx.input.charAt(3) || "|";
331
+ const enc = ctx.input.slice(4, 8);
332
+ const component = enc.charAt(0) || "^";
333
+ const repetition = enc.charAt(1) || "~";
334
+ const _escape = enc.charAt(2) || "\\";
335
+ const subcomponent = enc.charAt(3) || "&";
336
+ ctx.options.delimiters = {
337
+ field: fieldDelim,
338
+ component,
339
+ repetition,
340
+ escape: _escape,
341
+ subcomponent,
342
+ segment: "\r"
343
+ };
344
+ }
345
+ return ctx;
346
+ };
347
+ var defaultPreprocessors = [
348
+ stripBOM,
349
+ normalizeNewlines,
350
+ detectDelimiters
351
+ ];
352
+ function runPreprocessors(raw, opts = {}, steps = defaultPreprocessors) {
353
+ let ctx = {
354
+ input: raw,
355
+ options: opts,
356
+ metadata: {}
357
+ };
358
+ for (const step of steps) {
359
+ ctx = step(ctx);
360
+ }
361
+ return ctx;
362
+ }
579
363
  export {
580
- processor_default as hl7v2Parser
364
+ HL7v2Tokenizer,
365
+ defaultPreprocessors,
366
+ detectDelimiters,
367
+ parser_default as hl7v2Parser,
368
+ normalizeNewlines,
369
+ parseHL7v2FromIterator,
370
+ runPreprocessors,
371
+ stripBOM
581
372
  };
582
373
  //# sourceMappingURL=index.js.map