@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 +34 -0
- package/dist/index.cjs +377 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +377 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
818
|
+
} else if (ch !== "\r") {
|
|
717
819
|
field += ch;
|
|
718
820
|
fieldStart = false;
|
|
719
821
|
sawAny = true;
|
|
720
822
|
}
|
|
721
823
|
}
|
|
722
|
-
if (sawAny
|
|
824
|
+
if (sawAny) {
|
|
723
825
|
row.push(field);
|
|
724
826
|
rows.push(row);
|
|
725
827
|
}
|
|
726
|
-
|
|
828
|
+
for (const r of rows) sink.row(r, r.length);
|
|
829
|
+
return inQuotes;
|
|
727
830
|
}
|
|
728
831
|
function parseCsv(content) {
|
|
729
|
-
|
|
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
|
|
747
|
-
const
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
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(
|
|
889
|
+
detail: `row has ${String(count)} fields, need ${String(needCols)}`
|
|
782
890
|
})
|
|
783
891
|
);
|
|
784
|
-
|
|
892
|
+
return;
|
|
785
893
|
}
|
|
786
|
-
const
|
|
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
|
-
|
|
904
|
+
return;
|
|
796
905
|
}
|
|
797
|
-
if (concepts.has(code))
|
|
798
|
-
const out = { code, properties: [] };
|
|
799
|
-
const display = (
|
|
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 = (
|
|
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 = (
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
4075
|
-
for (const cand of
|
|
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
|
|
4304
|
-
noMap: raw
|
|
4305
|
-
combination: raw
|
|
4306
|
-
scenario:
|
|
4307
|
-
choiceList:
|
|
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
|
|
4316
|
-
|
|
4317
|
-
|
|
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:
|
|
4327
|
-
detail: `expected 3 whitespace-delimited fields, found ${String(
|
|
4527
|
+
line: lineNo,
|
|
4528
|
+
detail: `expected 3 whitespace-delimited fields, found ${String(fieldCount)}`
|
|
4328
4529
|
})
|
|
4329
4530
|
);
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
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
|
-
|
|
4546
|
+
const list = entries.get(src);
|
|
4547
|
+
if (list) list.push(frozen);
|
|
4548
|
+
else entries.set(src, [frozen]);
|
|
4549
|
+
count++;
|
|
4343
4550
|
}
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
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.
|
|
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;
|