@cosyte/terminology 0.1.6 → 0.1.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - c688d91: Faster value set membership and translation, and leaner CSV, GEM and UCUM reading. Every answer,
8
+ loaded model, warning and error is unchanged, and no signature changes. One thing is deliberately
9
+ different: within one `loadGems` result, the entries that carry the same five flag digits now share
10
+ one frozen `flags` object rather than each holding its own. Every field reads the same; only
11
+ `entryA.flags === entryB.flags` is now `true` for two such entries. If you told entries apart by
12
+ the identity of their `flags`, compare the entries themselves, or their fields, instead.
13
+
14
+ - `validateCodeInValueSet` builds a loaded release's hierarchy index on the first membership
15
+ question over a filter and reuses it, rather than walking the whole release for every code. A code
16
+ system whose `concepts` you assembled by hand is still indexed afresh on every call, so a change
17
+ you make to it between two calls is read.
18
+ - `translate` finds a loaded ConceptMap's elements through an index of each group by source code,
19
+ built on the first translation through the group, rather than reading every element for each code.
20
+ A map you assembled by hand is still read element by element.
21
+ - `loadCodeSystem` reads a CSV release row by row and builds only the configured columns, so a long
22
+ column you did not configure costs no memory. The strings a loaded model keeps are copied out of
23
+ the release text, so the model does not keep that text alive after the load; the same holds for
24
+ `parseCsv` and `loadGems`.
25
+ - `loadGems` reads each line in place instead of splitting the file into lines and each line into
26
+ fields, and decodes each distinct flag field once per load (the shared `flags` object above).
27
+ - `validateUcum`, `ucumEqual` and `parseUcum` match a unit against the atoms and prefixes that start
28
+ with the same character, from an index of the unit table built once, rather than against the whole
29
+ table.
30
+
31
+ The indexes are kept with the loaded release or map and go when it does. A source coding with no
32
+ `code` is still matched against a loaded map's elements that have none, as before.
33
+
34
+ The package's npm keywords also gain `snomed`, `icd10` and `cpt`, so a search on those terms finds
35
+ it.
36
+
3
37
  ## 0.1.6
4
38
 
5
39
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -446,6 +446,10 @@ function loadUnmapped(raw, path) {
446
446
  assignString(out, raw, "url");
447
447
  return Object.freeze(out);
448
448
  }
449
+ var loadedGroups = /* @__PURE__ */ new WeakSet();
450
+ function isLoadedGroup(group) {
451
+ return loadedGroups.has(group);
452
+ }
449
453
  function loadGroup(raw, path) {
450
454
  if (!isJsonObject(raw)) malformed(path, "group is not an object");
451
455
  const rawElements = getArray(raw, "element") ?? [];
@@ -460,7 +464,9 @@ function loadGroup(raw, path) {
460
464
  if (isJsonObject(raw) && raw["unmapped"] !== void 0) {
461
465
  out.unmapped = loadUnmapped(raw["unmapped"], `${path}.unmapped`);
462
466
  }
463
- return Object.freeze(out);
467
+ const group = Object.freeze(out);
468
+ loadedGroups.add(group);
469
+ return group;
464
470
  }
465
471
  function loadConceptMap(json) {
466
472
  if (!isJsonObject(json)) malformed("", "resource is not a JSON object");
@@ -504,6 +510,24 @@ function groupApplies(group, system) {
504
510
  if (system === void 0) return true;
505
511
  return group.source === void 0 || group.source === system;
506
512
  }
513
+ var elementIndex = /* @__PURE__ */ new WeakMap();
514
+ var NO_ELEMENTS = Object.freeze([]);
515
+ function candidates(group, code) {
516
+ if (typeof code !== "string" || !isLoadedGroup(group)) return group.element;
517
+ let index = elementIndex.get(group);
518
+ if (index === void 0) {
519
+ const built = /* @__PURE__ */ new Map();
520
+ for (const element of group.element) {
521
+ if (element.code === void 0) continue;
522
+ const list = built.get(element.code);
523
+ if (list === void 0) built.set(element.code, [element]);
524
+ else list.push(element);
525
+ }
526
+ index = built;
527
+ elementIndex.set(group, index);
528
+ }
529
+ return index.get(code) ?? NO_ELEMENTS;
530
+ }
507
531
  function provenanceOf(map, group, sourceSystem) {
508
532
  const out = {};
509
533
  if (map.url !== void 0) out.conceptMapUrl = map.url;
@@ -537,7 +561,7 @@ function translate(sourceCoding, map) {
537
561
  for (const group of map.group) {
538
562
  if (!groupApplies(group, system)) continue;
539
563
  if (fallbackGroup === void 0 && group.unmapped !== void 0) fallbackGroup = group;
540
- for (const element of group.element) {
564
+ for (const element of candidates(group, source.code)) {
541
565
  if (element.code !== source.code) continue;
542
566
  sourceCodePresent = true;
543
567
  for (const t of element.target) {
@@ -640,6 +664,12 @@ function frozenMap(source, what) {
640
664
  return Object.freeze(view);
641
665
  }
642
666
 
667
+ // src/common/own-string.ts
668
+ var VIEW_MIN_LENGTH = 13;
669
+ function ownString(s) {
670
+ return s.length < VIEW_MIN_LENGTH ? s : ` ${s}`.slice(1);
671
+ }
672
+
643
673
  // src/codesystem/status.ts
644
674
  var defaultStatusMapper = (raw) => {
645
675
  switch (raw.trim().toUpperCase()) {
@@ -674,7 +704,79 @@ function makeStatus(activity, raw, billable) {
674
704
  }
675
705
 
676
706
  // src/codesystem/csv.ts
677
- function parseCsvRows(content) {
707
+ var QUOTE = 34;
708
+ var COMMA = 44;
709
+ var LINE_FEED = 10;
710
+ var CARRIAGE_RETURN = 13;
711
+ function scanCsv(content, sink) {
712
+ if (typeof content !== "string") return readCsvByCharAt(content, sink);
713
+ const n = content.length;
714
+ const cells = [];
715
+ let column = 0;
716
+ const keeps = (col) => sink.wanted === void 0 || sink.wanted[col] === true;
717
+ let keep = keeps(0);
718
+ let field = "";
719
+ let runStart = 0;
720
+ let inQuotes = false;
721
+ let fieldStart = true;
722
+ let sawAny = false;
723
+ const endField = (end) => {
724
+ if (keep) cells[column] = field + content.slice(runStart, end);
725
+ field = "";
726
+ };
727
+ const endRow = () => {
728
+ sink.row(cells, column + 1);
729
+ column = 0;
730
+ keep = keeps(0);
731
+ };
732
+ for (let i = 0; i < n; i++) {
733
+ const ch = content.charCodeAt(i);
734
+ if (inQuotes) {
735
+ if (ch === QUOTE) {
736
+ if (content.charCodeAt(i + 1) === QUOTE) {
737
+ if (keep) field += content.slice(runStart, i + 1);
738
+ i++;
739
+ } else {
740
+ if (keep) field += content.slice(runStart, i);
741
+ inQuotes = false;
742
+ }
743
+ runStart = i + 1;
744
+ }
745
+ continue;
746
+ }
747
+ if (ch === QUOTE && fieldStart) {
748
+ inQuotes = true;
749
+ fieldStart = false;
750
+ sawAny = true;
751
+ runStart = i + 1;
752
+ } else if (ch === COMMA) {
753
+ endField(i);
754
+ column++;
755
+ keep = keeps(column);
756
+ runStart = i + 1;
757
+ fieldStart = true;
758
+ sawAny = true;
759
+ } else if (ch === LINE_FEED) {
760
+ endField(i);
761
+ endRow();
762
+ runStart = i + 1;
763
+ fieldStart = true;
764
+ sawAny = false;
765
+ } else if (ch === CARRIAGE_RETURN) {
766
+ if (keep) field += content.slice(runStart, i);
767
+ runStart = i + 1;
768
+ } else {
769
+ fieldStart = false;
770
+ sawAny = true;
771
+ }
772
+ }
773
+ if (sawAny) {
774
+ endField(n);
775
+ endRow();
776
+ }
777
+ return inQuotes;
778
+ }
779
+ function readCsvByCharAt(content, sink) {
678
780
  const rows = [];
679
781
  let row = [];
680
782
  let field = "";
@@ -713,20 +815,30 @@ function parseCsvRows(content) {
713
815
  field = "";
714
816
  fieldStart = true;
715
817
  sawAny = false;
716
- } else if (ch === "\r") ; else {
818
+ } else if (ch !== "\r") {
717
819
  field += ch;
718
820
  fieldStart = false;
719
821
  sawAny = true;
720
822
  }
721
823
  }
722
- if (sawAny || field !== "" || row.length > 0) {
824
+ if (sawAny) {
723
825
  row.push(field);
724
826
  rows.push(row);
725
827
  }
726
- return { rows, unterminatedQuote: inQuotes };
828
+ for (const r of rows) sink.row(r, r.length);
829
+ return inQuotes;
727
830
  }
728
831
  function parseCsv(content) {
729
- return parseCsvRows(content).rows;
832
+ const rows = [];
833
+ scanCsv(content, {
834
+ wanted: void 0,
835
+ row: (cells, count) => {
836
+ const row = [];
837
+ for (let c = 0; c < count; c++) row.push(ownString(cells[c]));
838
+ rows.push(row);
839
+ }
840
+ });
841
+ return rows;
730
842
  }
731
843
  var MISSING_COLUMN_MESSAGE = Object.freeze({
732
844
  code: "CSV source: the configured 'code' column is not present in the header row",
@@ -743,47 +855,44 @@ function requireHeader(header, name, role) {
743
855
  }
744
856
  function parseCsvSource(source) {
745
857
  const statusMap = source.statusMap ?? defaultStatusMapper;
746
- const { rows, unterminatedQuote } = parseCsvRows(source.content);
747
- const header = rows[0];
748
- if (header === void 0) {
749
- throw new TerminologyError(
750
- FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
751
- "CSV source: the file has no header row"
752
- );
753
- }
858
+ const content = source.content;
859
+ const concepts = /* @__PURE__ */ new Map();
754
860
  const warnings = [];
755
- if (unterminatedQuote) {
756
- warnings.push(
757
- Object.freeze({
758
- code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
759
- line: rows.length,
760
- detail: "an opened quoted field was not terminated before end-of-input"
761
- })
762
- );
763
- }
764
- const codeIdx = requireHeader(header, source.columns.code, "code");
765
- const displayIdx = requireHeader(header, source.columns.display, "display");
766
- const statusIdx = source.columns.status !== void 0 ? requireHeader(header, source.columns.status, "status") : void 0;
861
+ let rowCount = 0;
862
+ let codeIdx = 0;
863
+ let displayIdx = 0;
864
+ let statusIdx;
767
865
  const propIdx = [];
768
- for (const n of source.columns.properties ?? []) {
769
- propIdx.push({ name: n, index: requireHeader(header, n, "property") });
770
- }
771
- const needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
772
- const concepts = /* @__PURE__ */ new Map();
773
- for (let r = 1; r < rows.length; r++) {
774
- const cells = rows[r] ?? [];
775
- const lineNo = r + 1;
776
- if (cells.length < needCols) {
866
+ let needCols = 0;
867
+ const header = (cells, count) => {
868
+ const names = cells.slice(0, count);
869
+ codeIdx = requireHeader(names, source.columns.code, "code");
870
+ displayIdx = requireHeader(names, source.columns.display, "display");
871
+ statusIdx = source.columns.status !== void 0 ? requireHeader(names, source.columns.status, "status") : void 0;
872
+ for (const n of source.columns.properties ?? []) {
873
+ propIdx.push({ name: n, index: requireHeader(names, n, "property") });
874
+ }
875
+ needCols = Math.max(codeIdx, displayIdx, statusIdx ?? 0, ...propIdx.map((p) => p.index)) + 1;
876
+ const wanted = [];
877
+ wanted[codeIdx] = true;
878
+ wanted[displayIdx] = true;
879
+ if (statusIdx !== void 0) wanted[statusIdx] = true;
880
+ for (const p of propIdx) wanted[p.index] = true;
881
+ sink.wanted = wanted;
882
+ };
883
+ const dataRow = (cells, count, lineNo) => {
884
+ if (count < needCols) {
777
885
  warnings.push(
778
886
  Object.freeze({
779
887
  code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
780
888
  line: lineNo,
781
- detail: `row has ${String(cells.length)} fields, need ${String(needCols)}`
889
+ detail: `row has ${String(count)} fields, need ${String(needCols)}`
782
890
  })
783
891
  );
784
- continue;
892
+ return;
785
893
  }
786
- const code = (cells[codeIdx] ?? "").trim();
894
+ const cell3 = (index) => cells[index];
895
+ const code = cell3(codeIdx).trim();
787
896
  if (code === "") {
788
897
  warnings.push(
789
898
  Object.freeze({
@@ -792,23 +901,47 @@ function parseCsvSource(source) {
792
901
  detail: "row is missing its code"
793
902
  })
794
903
  );
795
- continue;
904
+ return;
796
905
  }
797
- if (concepts.has(code)) continue;
798
- const out = { code, properties: [] };
799
- const display = (cells[displayIdx] ?? "").trim();
800
- if (display !== "") out.display = display;
906
+ if (concepts.has(code)) return;
907
+ const out = { code: ownString(code), properties: [] };
908
+ const display = cell3(displayIdx).trim();
909
+ if (display !== "") out.display = ownString(display);
801
910
  if (statusIdx !== void 0) {
802
- const rawStatus = (cells[statusIdx] ?? "").trim();
911
+ const rawStatus = ownString(cell3(statusIdx).trim());
803
912
  out.status = makeStatus(statusMap(rawStatus), rawStatus);
804
913
  }
805
914
  const props = [];
806
915
  for (const p of propIdx) {
807
- const value = (cells[p.index] ?? "").trim();
808
- if (value !== "") props.push(Object.freeze({ code: p.name, value }));
916
+ const value = cell3(p.index).trim();
917
+ if (value !== "") props.push(Object.freeze({ code: p.name, value: ownString(value) }));
809
918
  }
810
919
  out.properties = Object.freeze(props);
811
- concepts.set(code, Object.freeze(out));
920
+ concepts.set(out.code, Object.freeze(out));
921
+ };
922
+ const sink = {
923
+ wanted: void 0,
924
+ row: (cells, count) => {
925
+ rowCount++;
926
+ if (rowCount === 1) header(cells, count);
927
+ else dataRow(cells, count, rowCount);
928
+ }
929
+ };
930
+ const unterminatedQuote = scanCsv(content, sink);
931
+ if (rowCount === 0) {
932
+ throw new TerminologyError(
933
+ FATAL_CODES.TERM_CODESYSTEM_MALFORMED,
934
+ "CSV source: the file has no header row"
935
+ );
936
+ }
937
+ if (unterminatedQuote) {
938
+ warnings.unshift(
939
+ Object.freeze({
940
+ code: DIAGNOSTIC_CODES.TERM_CSV_MALFORMED,
941
+ line: rowCount,
942
+ detail: "an opened quoted field was not terminated before end-of-input"
943
+ })
944
+ );
812
945
  }
813
946
  return { concepts, warnings };
814
947
  }
@@ -1068,6 +1201,10 @@ function parseRrf(source) {
1068
1201
  }
1069
1202
 
1070
1203
  // src/codesystem/load.ts
1204
+ var loadedConcepts = /* @__PURE__ */ new WeakSet();
1205
+ function isLoadedConcepts(concepts) {
1206
+ return loadedConcepts.has(concepts);
1207
+ }
1071
1208
  function loadCodeSystem(source) {
1072
1209
  let concepts;
1073
1210
  let warnings;
@@ -1106,6 +1243,7 @@ function loadCodeSystem(source) {
1106
1243
  }
1107
1244
  out.count = concepts.size;
1108
1245
  out.concepts = frozenMap(concepts, "a loaded CodeSystem's concepts map");
1246
+ loadedConcepts.add(out.concepts);
1109
1247
  out.warnings = Object.freeze(warnings);
1110
1248
  return Object.freeze(out);
1111
1249
  }
@@ -1308,8 +1446,11 @@ function directParents(concept) {
1308
1446
  return parents;
1309
1447
  }
1310
1448
  function buildSubsumption(cs) {
1449
+ return subsumptionOver(cs.concepts);
1450
+ }
1451
+ function subsumptionOver(concepts) {
1311
1452
  const parentsOf = /* @__PURE__ */ new Map();
1312
- for (const concept of cs.concepts.values()) {
1453
+ for (const concept of concepts.values()) {
1313
1454
  const parents = directParents(concept);
1314
1455
  if (parents.length > 0) parentsOf.set(concept.code, parents);
1315
1456
  }
@@ -1330,6 +1471,17 @@ function buildSubsumption(cs) {
1330
1471
  };
1331
1472
  return { ancestorsOf };
1332
1473
  }
1474
+ var loadedSubsumption = /* @__PURE__ */ new WeakMap();
1475
+ function subsumptionOf(cs) {
1476
+ const concepts = cs.concepts;
1477
+ if (!isLoadedConcepts(concepts)) return subsumptionOver(concepts);
1478
+ let sub = loadedSubsumption.get(concepts);
1479
+ if (sub === void 0) {
1480
+ sub = subsumptionOver(concepts);
1481
+ loadedSubsumption.set(concepts, sub);
1482
+ }
1483
+ return sub;
1484
+ }
1333
1485
  function isA(sub, code, anchor) {
1334
1486
  return code === anchor || sub.ancestorsOf(code).has(anchor);
1335
1487
  }
@@ -1732,7 +1884,7 @@ function matchComponent(target, component, ctx, visited, path, activeOnly = fals
1732
1884
  baseComplete = false;
1733
1885
  } else {
1734
1886
  const concpt = cs.concepts.get(target.code);
1735
- baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, buildSubsumption(cs)).matched;
1887
+ baseMatched = concpt !== void 0 && matchesAllFilters(concpt, filter, subsumptionOf(cs)).matched;
1736
1888
  }
1737
1889
  } else {
1738
1890
  baseMatched = cs.concepts.get(target.code) !== void 0;
@@ -4022,7 +4174,29 @@ function atBoundary(s, pos) {
4022
4174
  function prefixRank(m) {
4023
4175
  return m.prefix ? 1 : 0;
4024
4176
  }
4177
+ function byPreference(a, b) {
4178
+ return b.len - a.len || prefixRank(a) - prefixRank(b);
4179
+ }
4025
4180
  function simpleMatches(essence, s, pos) {
4181
+ if (typeof s !== "string") return simpleMatchesByScan(essence, s, pos);
4182
+ const index = indexOf(essence);
4183
+ const out = [];
4184
+ for (const atom of index.atoms.get(s.charCodeAt(pos)) ?? NONE) {
4185
+ if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
4186
+ }
4187
+ for (const prefix of index.prefixes.get(s.charCodeAt(pos)) ?? NONE) {
4188
+ if (!s.startsWith(prefix.code, pos)) continue;
4189
+ const rem = pos + prefix.code.length;
4190
+ for (const atom of index.metricAtoms.get(s.charCodeAt(rem)) ?? NONE) {
4191
+ if (s.startsWith(atom.code, rem)) {
4192
+ out.push({ prefix, atom, len: prefix.code.length + atom.code.length });
4193
+ }
4194
+ }
4195
+ }
4196
+ out.sort(byPreference);
4197
+ return out;
4198
+ }
4199
+ function simpleMatchesByScan(essence, s, pos) {
4026
4200
  const out = [];
4027
4201
  for (const atom of essence.atoms) {
4028
4202
  if (s.startsWith(atom.code, pos)) out.push({ atom, len: atom.code.length });
@@ -4036,9 +4210,33 @@ function simpleMatches(essence, s, pos) {
4036
4210
  }
4037
4211
  }
4038
4212
  }
4039
- out.sort((a, b) => b.len - a.len || prefixRank(a) - prefixRank(b));
4213
+ out.sort(byPreference);
4040
4214
  return out;
4041
4215
  }
4216
+ var NONE = Object.freeze([]);
4217
+ var firstCharIndexes = /* @__PURE__ */ new WeakMap();
4218
+ function byFirstChar(items) {
4219
+ const out = /* @__PURE__ */ new Map();
4220
+ for (const item of items) {
4221
+ const first = item.code.charCodeAt(0);
4222
+ const list = out.get(first);
4223
+ if (list === void 0) out.set(first, [item]);
4224
+ else list.push(item);
4225
+ }
4226
+ return out;
4227
+ }
4228
+ function indexOf(essence) {
4229
+ let index = firstCharIndexes.get(essence);
4230
+ if (index === void 0) {
4231
+ index = {
4232
+ atoms: byFirstChar(essence.atoms),
4233
+ metricAtoms: byFirstChar(essence.atoms.filter((a) => a.metric)),
4234
+ prefixes: byFirstChar(essence.prefixes)
4235
+ };
4236
+ firstCharIndexes.set(essence, index);
4237
+ }
4238
+ return index;
4239
+ }
4042
4240
  var MAX_PAREN_DEPTH = 100;
4043
4241
  function parseExponent(cur) {
4044
4242
  const { s } = cur;
@@ -4071,8 +4269,8 @@ function parseAnnotation(cur) {
4071
4269
  return { ok: false, reason: "annotation is not closed with '}'" };
4072
4270
  }
4073
4271
  function parseSimpleUnit(cur) {
4074
- const candidates = simpleMatches(cur.essence, cur.s, cur.pos);
4075
- for (const cand of candidates) {
4272
+ const candidates2 = simpleMatches(cur.essence, cur.s, cur.pos);
4273
+ for (const cand of candidates2) {
4076
4274
  const save = cur.pos;
4077
4275
  cur.pos = save + cand.len;
4078
4276
  const exp = parseExponent(cur);
@@ -4298,57 +4496,62 @@ function isNoMapSentinel(target) {
4298
4496
  return target === NO_DX || target === NO_PCS;
4299
4497
  }
4300
4498
  function decodeFlags(raw) {
4301
- if (!/^[0-9]{5}$/.test(raw)) return void 0;
4302
4499
  return Object.freeze({
4303
- approximate: raw[0] === "1",
4304
- noMap: raw[1] === "1",
4305
- combination: raw[2] === "1",
4306
- scenario: Number(raw[3]),
4307
- choiceList: Number(raw[4]),
4500
+ approximate: raw.charCodeAt(0) === DIGIT_ONE,
4501
+ noMap: raw.charCodeAt(1) === DIGIT_ONE,
4502
+ combination: raw.charCodeAt(2) === DIGIT_ONE,
4503
+ scenario: raw.charCodeAt(3) - DIGIT_ZERO,
4504
+ choiceList: raw.charCodeAt(4) - DIGIT_ZERO,
4308
4505
  raw
4309
4506
  });
4310
4507
  }
4508
+ var DIGIT_ZERO = 48;
4509
+ var DIGIT_ONE = 49;
4510
+ var DIGIT_NINE = 57;
4511
+ var SPACE = 32;
4512
+ var NON_ASCII_SPACE = /^\s$/;
4513
+ function isSpace(c) {
4514
+ if (c < 128) return c === SPACE || c >= 9 && c <= 13;
4515
+ return NON_ASCII_SPACE.test(String.fromCharCode(c));
4516
+ }
4311
4517
  function loadGems(source) {
4312
4518
  const entries = /* @__PURE__ */ new Map();
4313
4519
  const warnings = [];
4314
4520
  let count = 0;
4315
- const lines = source.content.split(/\r?\n/);
4316
- for (let i = 0; i < lines.length; i++) {
4317
- const raw = lines[i];
4318
- if (raw === void 0) continue;
4319
- const trimmed = raw.trim();
4320
- if (trimmed === "") continue;
4321
- const fields = trimmed.split(/\s+/);
4322
- if (fields.length !== 3) {
4521
+ const sink = {
4522
+ flags: /* @__PURE__ */ new Map(),
4523
+ malformed: (lineNo, fieldCount) => {
4323
4524
  warnings.push(
4324
4525
  Object.freeze({
4325
4526
  code: "TERM_GEM_MALFORMED_ROW",
4326
- line: i + 1,
4327
- detail: `expected 3 whitespace-delimited fields, found ${String(fields.length)}`
4527
+ line: lineNo,
4528
+ detail: `expected 3 whitespace-delimited fields, found ${String(fieldCount)}`
4328
4529
  })
4329
4530
  );
4330
- continue;
4331
- }
4332
- const [src, tgt, flagStr] = fields;
4333
- const flags = decodeFlags(flagStr);
4334
- if (flags === void 0) {
4335
- warnings.push(
4336
- Object.freeze({
4337
- code: "TERM_GEM_MALFORMED_ROW",
4338
- line: i + 1,
4339
- detail: "flag field is not a 5-digit code"
4340
- })
4531
+ },
4532
+ line: (lineNo, src, tgt, flags) => {
4533
+ if (flags === void 0) {
4534
+ warnings.push(
4535
+ Object.freeze({
4536
+ code: "TERM_GEM_MALFORMED_ROW",
4537
+ line: lineNo,
4538
+ detail: "flag field is not a 5-digit code"
4539
+ })
4540
+ );
4541
+ return;
4542
+ }
4543
+ const frozen = Object.freeze(
4544
+ isNoMapSentinel(tgt) ? { source: src, flags } : { source: src, flags, target: tgt }
4341
4545
  );
4342
- continue;
4546
+ const list = entries.get(src);
4547
+ if (list) list.push(frozen);
4548
+ else entries.set(src, [frozen]);
4549
+ count++;
4343
4550
  }
4344
- const entry = { source: src, flags };
4345
- if (!isNoMapSentinel(tgt)) entry.target = tgt;
4346
- const frozen = Object.freeze(entry);
4347
- const list = entries.get(src);
4348
- if (list) list.push(frozen);
4349
- else entries.set(src, [frozen]);
4350
- count++;
4351
- }
4551
+ };
4552
+ const content = source.content;
4553
+ if (typeof content === "string") readLinesInPlace(content, sink);
4554
+ else readLinesBySplit(content, sink);
4352
4555
  for (const list of entries.values()) Object.freeze(list);
4353
4556
  return Object.freeze({
4354
4557
  direction: source.direction,
@@ -4358,6 +4561,90 @@ function loadGems(source) {
4358
4561
  warnings: Object.freeze(warnings)
4359
4562
  });
4360
4563
  }
4564
+ function sharedFlags(sink, text, from, to) {
4565
+ const key = fiveDigitsAt(text, from, to);
4566
+ if (key === -1) return void 0;
4567
+ let flags = sink.flags.get(key);
4568
+ if (flags === void 0) {
4569
+ flags = decodeFlags(text.slice(from, to));
4570
+ sink.flags.set(key, flags);
4571
+ }
4572
+ return flags;
4573
+ }
4574
+ function fiveDigitsAt(content, from, to) {
4575
+ if (to - from !== 5) return -1;
4576
+ let value = 0;
4577
+ for (let i = from; i < to; i++) {
4578
+ const c = content.charCodeAt(i);
4579
+ if (c < DIGIT_ZERO || c > DIGIT_NINE) return -1;
4580
+ value = value * 10 + (c - DIGIT_ZERO);
4581
+ }
4582
+ return value;
4583
+ }
4584
+ function readLinesInPlace(content, sink) {
4585
+ const n = content.length;
4586
+ let lineNo = 0;
4587
+ for (let start = 0; start <= n; ) {
4588
+ let end = content.indexOf("\n", start);
4589
+ if (end === -1) end = n;
4590
+ lineNo++;
4591
+ let fieldCount = 0;
4592
+ let srcStart = 0;
4593
+ let srcEnd = 0;
4594
+ let tgtStart = 0;
4595
+ let tgtEnd = 0;
4596
+ let flagStart = 0;
4597
+ let flagEnd = 0;
4598
+ for (let i = start; i < end; ) {
4599
+ if (isSpace(content.charCodeAt(i))) {
4600
+ i++;
4601
+ continue;
4602
+ }
4603
+ const from = i;
4604
+ while (i < end && !isSpace(content.charCodeAt(i))) i++;
4605
+ fieldCount++;
4606
+ if (fieldCount === 1) {
4607
+ srcStart = from;
4608
+ srcEnd = i;
4609
+ } else if (fieldCount === 2) {
4610
+ tgtStart = from;
4611
+ tgtEnd = i;
4612
+ } else if (fieldCount === 3) {
4613
+ flagStart = from;
4614
+ flagEnd = i;
4615
+ }
4616
+ }
4617
+ start = end + 1;
4618
+ if (fieldCount === 0) continue;
4619
+ if (fieldCount !== 3) {
4620
+ sink.malformed(lineNo, fieldCount);
4621
+ continue;
4622
+ }
4623
+ const flags = sharedFlags(sink, content, flagStart, flagEnd);
4624
+ sink.line(
4625
+ lineNo,
4626
+ ownString(content.slice(srcStart, srcEnd)),
4627
+ ownString(content.slice(tgtStart, tgtEnd)),
4628
+ flags
4629
+ );
4630
+ }
4631
+ }
4632
+ function readLinesBySplit(content, sink) {
4633
+ const lines = content.split(/\r?\n/);
4634
+ for (let i = 0; i < lines.length; i++) {
4635
+ const raw = lines[i];
4636
+ if (raw === void 0) continue;
4637
+ const trimmed = raw.trim();
4638
+ if (trimmed === "") continue;
4639
+ const fields = trimmed.split(/\s+/);
4640
+ if (fields.length !== 3) {
4641
+ sink.malformed(i + 1, fields.length);
4642
+ continue;
4643
+ }
4644
+ const [src, tgt, flagStr] = fields;
4645
+ sink.line(i + 1, src, tgt, sharedFlags(sink, flagStr, 0, flagStr.length));
4646
+ }
4647
+ }
4361
4648
  function buildCombinations(entries) {
4362
4649
  const combo = entries.filter((e) => e.flags.combination && e.target !== void 0);
4363
4650
  if (combo.length === 0) return void 0;
@@ -5019,7 +5306,7 @@ function approximateMatch(graph, query, options = {}) {
5019
5306
  }
5020
5307
 
5021
5308
  // src/index.ts
5022
- var VERSION = "0.1.6";
5309
+ var VERSION = "0.1.7";
5023
5310
 
5024
5311
  exports.COMPLEX_MAP_SOURCE_SYSTEM = COMPLEX_MAP_SOURCE_SYSTEM;
5025
5312
  exports.DIAGNOSTIC_CODES = DIAGNOSTIC_CODES;