ezmedicationinput 0.1.9 → 0.1.11
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/README.md +100 -0
- package/dist/fhir.js +28 -7
- package/dist/i18n.js +65 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +89 -28
- package/dist/internal-types.d.ts +8 -1
- package/dist/maps.d.ts +8 -1
- package/dist/maps.js +313 -5
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +372 -18
- package/dist/types.d.ts +83 -0
- package/package.json +1 -1
package/dist/parser.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
exports.tokenize = tokenize;
|
|
4
13
|
exports.parseInternal = parseInternal;
|
|
14
|
+
exports.applySiteCoding = applySiteCoding;
|
|
15
|
+
exports.applySiteCodingAsync = applySiteCodingAsync;
|
|
5
16
|
const maps_1 = require("./maps");
|
|
6
17
|
const context_1 = require("./context");
|
|
7
18
|
const safety_1 = require("./safety");
|
|
8
19
|
const types_1 = require("./types");
|
|
9
20
|
const object_1 = require("./utils/object");
|
|
10
21
|
const array_1 = require("./utils/array");
|
|
22
|
+
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
11
23
|
const BODY_SITE_HINTS = new Set([
|
|
12
24
|
"left",
|
|
13
25
|
"right",
|
|
@@ -66,10 +78,14 @@ const BODY_SITE_HINTS = new Set([
|
|
|
66
78
|
"veins",
|
|
67
79
|
"vagina",
|
|
68
80
|
"vaginal",
|
|
81
|
+
"penis",
|
|
82
|
+
"penile",
|
|
69
83
|
"rectum",
|
|
70
84
|
"rectal",
|
|
71
85
|
"anus",
|
|
72
|
-
"perineum"
|
|
86
|
+
"perineum",
|
|
87
|
+
"temple",
|
|
88
|
+
"temples"
|
|
73
89
|
]);
|
|
74
90
|
const SITE_CONNECTORS = new Set(["to", "in", "into", "on", "onto", "at"]);
|
|
75
91
|
const SITE_FILLER_WORDS = new Set([
|
|
@@ -241,7 +257,7 @@ const OPHTHALMIC_CONTEXT_TOKENS = new Set([
|
|
|
241
257
|
"be"
|
|
242
258
|
]);
|
|
243
259
|
function normalizeTokenLower(token) {
|
|
244
|
-
return token.lower.replace(
|
|
260
|
+
return token.lower.replace(/[.{}]/g, "");
|
|
245
261
|
}
|
|
246
262
|
function hasOphthalmicContextHint(tokens, index) {
|
|
247
263
|
for (let offset = -3; offset <= 3; offset++) {
|
|
@@ -605,6 +621,68 @@ function tokenize(input) {
|
|
|
605
621
|
}
|
|
606
622
|
return tokens;
|
|
607
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Locates the span of the detected site tokens within the caller's original
|
|
626
|
+
* input so downstream consumers can highlight or replace the exact substring.
|
|
627
|
+
*/
|
|
628
|
+
function computeSiteTextRange(input, tokens, indices) {
|
|
629
|
+
if (!indices.length) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
const lowerInput = input.toLowerCase();
|
|
633
|
+
let searchStart = 0;
|
|
634
|
+
let rangeStart;
|
|
635
|
+
let rangeEnd;
|
|
636
|
+
for (const tokenIndex of indices) {
|
|
637
|
+
const token = tokens[tokenIndex];
|
|
638
|
+
if (!token) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const segment = token.original.trim();
|
|
642
|
+
if (!segment) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const lowerSegment = segment.toLowerCase();
|
|
646
|
+
const foundIndex = lowerInput.indexOf(lowerSegment, searchStart);
|
|
647
|
+
if (foundIndex === -1) {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
const segmentEnd = foundIndex + lowerSegment.length;
|
|
651
|
+
if (rangeStart === undefined) {
|
|
652
|
+
rangeStart = foundIndex;
|
|
653
|
+
}
|
|
654
|
+
rangeEnd = segmentEnd;
|
|
655
|
+
searchStart = segmentEnd;
|
|
656
|
+
}
|
|
657
|
+
if (rangeStart === undefined || rangeEnd === undefined) {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
return { start: rangeStart, end: rangeEnd };
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Prefers highlighting the sanitized site text when it can be located directly
|
|
664
|
+
* in the original input; otherwise falls back to the broader token-derived
|
|
665
|
+
* range.
|
|
666
|
+
*/
|
|
667
|
+
function refineSiteRange(input, sanitized, tokenRange) {
|
|
668
|
+
if (!input) {
|
|
669
|
+
return tokenRange;
|
|
670
|
+
}
|
|
671
|
+
const trimmed = sanitized.trim();
|
|
672
|
+
if (!trimmed) {
|
|
673
|
+
return tokenRange;
|
|
674
|
+
}
|
|
675
|
+
const lowerInput = input.toLowerCase();
|
|
676
|
+
const lowerSanitized = trimmed.toLowerCase();
|
|
677
|
+
let startIndex = tokenRange ? lowerInput.indexOf(lowerSanitized, tokenRange.start) : -1;
|
|
678
|
+
if (startIndex === -1) {
|
|
679
|
+
startIndex = lowerInput.indexOf(lowerSanitized);
|
|
680
|
+
}
|
|
681
|
+
if (startIndex === -1) {
|
|
682
|
+
return tokenRange;
|
|
683
|
+
}
|
|
684
|
+
return { start: startIndex, end: startIndex + lowerSanitized.length };
|
|
685
|
+
}
|
|
608
686
|
function splitToken(token) {
|
|
609
687
|
if (/^[0-9]+(?:\.[0-9]+)?$/.test(token)) {
|
|
610
688
|
return [token];
|
|
@@ -1169,7 +1247,8 @@ function parseInternal(input, options) {
|
|
|
1169
1247
|
dayOfWeek: [],
|
|
1170
1248
|
when: [],
|
|
1171
1249
|
warnings: [],
|
|
1172
|
-
siteTokenIndices: new Set()
|
|
1250
|
+
siteTokenIndices: new Set(),
|
|
1251
|
+
siteLookups: []
|
|
1173
1252
|
};
|
|
1174
1253
|
const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
|
|
1175
1254
|
const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
|
|
@@ -1630,7 +1709,8 @@ function parseInternal(input, options) {
|
|
|
1630
1709
|
const leftoverTokens = tokens.filter((t) => !internal.consumed.has(t.index));
|
|
1631
1710
|
const siteCandidateIndices = new Set();
|
|
1632
1711
|
for (const token of leftoverTokens) {
|
|
1633
|
-
|
|
1712
|
+
const normalized = normalizeTokenLower(token);
|
|
1713
|
+
if (BODY_SITE_HINTS.has(normalized)) {
|
|
1634
1714
|
siteCandidateIndices.add(token.index);
|
|
1635
1715
|
}
|
|
1636
1716
|
}
|
|
@@ -1646,7 +1726,7 @@ function parseInternal(input, options) {
|
|
|
1646
1726
|
if (!token) {
|
|
1647
1727
|
break;
|
|
1648
1728
|
}
|
|
1649
|
-
const lower = token
|
|
1729
|
+
const lower = normalizeTokenLower(token);
|
|
1650
1730
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1651
1731
|
BODY_SITE_HINTS.has(lower) ||
|
|
1652
1732
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
@@ -1662,7 +1742,7 @@ function parseInternal(input, options) {
|
|
|
1662
1742
|
if (!token) {
|
|
1663
1743
|
break;
|
|
1664
1744
|
}
|
|
1665
|
-
const lower = token
|
|
1745
|
+
const lower = normalizeTokenLower(token);
|
|
1666
1746
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1667
1747
|
BODY_SITE_HINTS.has(lower) ||
|
|
1668
1748
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
@@ -1680,8 +1760,10 @@ function parseInternal(input, options) {
|
|
|
1680
1760
|
if (!token) {
|
|
1681
1761
|
continue;
|
|
1682
1762
|
}
|
|
1683
|
-
const lower = token
|
|
1684
|
-
|
|
1763
|
+
const lower = normalizeTokenLower(token);
|
|
1764
|
+
const trimmed = token.original.trim();
|
|
1765
|
+
const isBraceToken = trimmed.length > 0 && /^[{}]+$/.test(trimmed);
|
|
1766
|
+
if (!isBraceToken && !SITE_CONNECTORS.has(lower) && !SITE_FILLER_WORDS.has(lower)) {
|
|
1685
1767
|
displayWords.push(token.original);
|
|
1686
1768
|
}
|
|
1687
1769
|
mark(internal.consumed, token);
|
|
@@ -1691,16 +1773,45 @@ function parseInternal(input, options) {
|
|
|
1691
1773
|
.join(" ")
|
|
1692
1774
|
.trim();
|
|
1693
1775
|
if (normalizedSite) {
|
|
1694
|
-
const
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1776
|
+
const tokenRange = computeSiteTextRange(internal.input, tokens, sortedIndices);
|
|
1777
|
+
let sanitized = normalizedSite;
|
|
1778
|
+
let isProbe = false;
|
|
1779
|
+
const probeMatch = sanitized.match(/^\{(.+)}$/);
|
|
1780
|
+
if (probeMatch) {
|
|
1781
|
+
// `{site}` placeholders flag interactive lookups so consumers can prompt
|
|
1782
|
+
// for a coded selection even when the parser cannot resolve the entry.
|
|
1783
|
+
isProbe = true;
|
|
1784
|
+
sanitized = probeMatch[1];
|
|
1785
|
+
}
|
|
1786
|
+
// Remove stray braces and normalize whitespace so lookups and downstream
|
|
1787
|
+
// displays operate on a clean phrase.
|
|
1788
|
+
sanitized = sanitized.replace(/[{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
1789
|
+
const range = refineSiteRange(internal.input, sanitized, tokenRange);
|
|
1790
|
+
const sourceText = range ? internal.input.slice(range.start, range.end) : undefined;
|
|
1791
|
+
internal.siteLookupRequest = {
|
|
1792
|
+
originalText: normalizedSite,
|
|
1793
|
+
text: sanitized,
|
|
1794
|
+
normalized: sanitized.toLowerCase(),
|
|
1795
|
+
canonical: sanitized ? (0, maps_1.normalizeBodySiteKey)(sanitized) : "",
|
|
1796
|
+
isProbe,
|
|
1797
|
+
inputText: internal.input,
|
|
1798
|
+
sourceText,
|
|
1799
|
+
range
|
|
1800
|
+
};
|
|
1801
|
+
if (sanitized) {
|
|
1802
|
+
const normalizedLower = sanitized.toLowerCase();
|
|
1803
|
+
const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
|
|
1804
|
+
const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
|
|
1805
|
+
const hasNonSiteWords = siteWords.some((word) => !BODY_SITE_HINTS.has(word));
|
|
1806
|
+
const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
|
|
1807
|
+
const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
|
|
1808
|
+
if (!appliedRouteDescriptor) {
|
|
1809
|
+
// Preserve the clean site text for FHIR output and resolver context
|
|
1810
|
+
// whenever we keep the original phrase.
|
|
1811
|
+
internal.siteText = sanitized;
|
|
1812
|
+
if (!internal.siteSource) {
|
|
1813
|
+
internal.siteSource = "text";
|
|
1814
|
+
}
|
|
1704
1815
|
}
|
|
1705
1816
|
}
|
|
1706
1817
|
}
|
|
@@ -1734,6 +1845,249 @@ function parseInternal(input, options) {
|
|
|
1734
1845
|
}
|
|
1735
1846
|
return internal;
|
|
1736
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Resolves parsed site text against SNOMED dictionaries and synchronous
|
|
1850
|
+
* callbacks, applying the best match to the in-progress parse result.
|
|
1851
|
+
*/
|
|
1852
|
+
function applySiteCoding(internal, options) {
|
|
1853
|
+
runSiteCodingResolutionSync(internal, options);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Asynchronous counterpart to {@link applySiteCoding} that awaits resolver and
|
|
1857
|
+
* suggestion callbacks so remote terminology services can be used.
|
|
1858
|
+
*/
|
|
1859
|
+
function applySiteCodingAsync(internal, options) {
|
|
1860
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1861
|
+
yield runSiteCodingResolutionAsync(internal, options);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Attempts to resolve site codings using built-in dictionaries followed by any
|
|
1866
|
+
* provided synchronous resolvers. Suggestions are collected when resolution
|
|
1867
|
+
* fails or a `{probe}` placeholder requested an interactive lookup.
|
|
1868
|
+
*/
|
|
1869
|
+
function runSiteCodingResolutionSync(internal, options) {
|
|
1870
|
+
internal.siteLookups = [];
|
|
1871
|
+
const request = internal.siteLookupRequest;
|
|
1872
|
+
if (!request) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const canonical = request.canonical;
|
|
1876
|
+
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
1877
|
+
let resolution = customDefinition;
|
|
1878
|
+
if (!resolution) {
|
|
1879
|
+
// Allow synchronous resolver callbacks to claim the site.
|
|
1880
|
+
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
1881
|
+
const result = resolver(request);
|
|
1882
|
+
if (isPromise(result)) {
|
|
1883
|
+
throw new Error("Site code resolver returned a Promise; use parseSigAsync for asynchronous site resolution.");
|
|
1884
|
+
}
|
|
1885
|
+
if (result) {
|
|
1886
|
+
resolution = result;
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
|
|
1892
|
+
if (!resolution && defaultDefinition) {
|
|
1893
|
+
// Fall back to bundled SNOMED lookups when no overrides claim the site.
|
|
1894
|
+
resolution = defaultDefinition;
|
|
1895
|
+
}
|
|
1896
|
+
if (resolution) {
|
|
1897
|
+
applySiteDefinition(internal, resolution);
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
internal.siteCoding = undefined;
|
|
1901
|
+
}
|
|
1902
|
+
const needsSuggestions = request.isProbe || !resolution;
|
|
1903
|
+
if (!needsSuggestions) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const suggestionMap = new Map();
|
|
1907
|
+
if (customDefinition) {
|
|
1908
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
1909
|
+
}
|
|
1910
|
+
if (defaultDefinition) {
|
|
1911
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
|
|
1912
|
+
}
|
|
1913
|
+
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
|
|
1914
|
+
// Aggregates resolver suggestions while guarding against accidental async
|
|
1915
|
+
// usage, mirroring the behavior of site resolvers.
|
|
1916
|
+
const result = resolver(request);
|
|
1917
|
+
if (isPromise(result)) {
|
|
1918
|
+
throw new Error("Site code suggestion resolver returned a Promise; use parseSigAsync for asynchronous site suggestions.");
|
|
1919
|
+
}
|
|
1920
|
+
collectSuggestionResult(suggestionMap, result);
|
|
1921
|
+
}
|
|
1922
|
+
const suggestions = Array.from(suggestionMap.values());
|
|
1923
|
+
if (suggestions.length || request.isProbe) {
|
|
1924
|
+
internal.siteLookups.push({ request, suggestions });
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Async version of {@link runSiteCodingResolutionSync} that awaits resolver
|
|
1929
|
+
* results and suggestion providers, enabling remote terminology services.
|
|
1930
|
+
*/
|
|
1931
|
+
function runSiteCodingResolutionAsync(internal, options) {
|
|
1932
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1933
|
+
internal.siteLookups = [];
|
|
1934
|
+
const request = internal.siteLookupRequest;
|
|
1935
|
+
if (!request) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const canonical = request.canonical;
|
|
1939
|
+
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
1940
|
+
let resolution = customDefinition;
|
|
1941
|
+
if (!resolution) {
|
|
1942
|
+
// Await asynchronous resolver callbacks (e.g., HTTP terminology services).
|
|
1943
|
+
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
1944
|
+
const result = yield resolver(request);
|
|
1945
|
+
if (result) {
|
|
1946
|
+
resolution = result;
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
const defaultDefinition = canonical ? maps_1.DEFAULT_BODY_SITE_SNOMED[canonical] : undefined;
|
|
1952
|
+
if (!resolution && defaultDefinition) {
|
|
1953
|
+
resolution = defaultDefinition;
|
|
1954
|
+
}
|
|
1955
|
+
if (resolution) {
|
|
1956
|
+
applySiteDefinition(internal, resolution);
|
|
1957
|
+
}
|
|
1958
|
+
else {
|
|
1959
|
+
internal.siteCoding = undefined;
|
|
1960
|
+
}
|
|
1961
|
+
const needsSuggestions = request.isProbe || !resolution;
|
|
1962
|
+
if (!needsSuggestions) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
const suggestionMap = new Map();
|
|
1966
|
+
if (customDefinition) {
|
|
1967
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
1968
|
+
}
|
|
1969
|
+
if (defaultDefinition) {
|
|
1970
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(defaultDefinition));
|
|
1971
|
+
}
|
|
1972
|
+
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeSuggestionResolvers)) {
|
|
1973
|
+
// Async suggestion providers are awaited, allowing UI workflows to fetch
|
|
1974
|
+
// candidate codes on demand.
|
|
1975
|
+
const result = yield resolver(request);
|
|
1976
|
+
collectSuggestionResult(suggestionMap, result);
|
|
1977
|
+
}
|
|
1978
|
+
const suggestions = Array.from(suggestionMap.values());
|
|
1979
|
+
if (suggestions.length || request.isProbe) {
|
|
1980
|
+
internal.siteLookups.push({ request, suggestions });
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Looks up a body-site definition in a caller-provided map, honoring both
|
|
1986
|
+
* direct keys and entries that normalize to the same canonical phrase.
|
|
1987
|
+
*/
|
|
1988
|
+
function lookupBodySiteDefinition(map, canonical) {
|
|
1989
|
+
if (!map) {
|
|
1990
|
+
return undefined;
|
|
1991
|
+
}
|
|
1992
|
+
const direct = map[canonical];
|
|
1993
|
+
if (direct) {
|
|
1994
|
+
return direct;
|
|
1995
|
+
}
|
|
1996
|
+
for (const [key, definition] of (0, object_1.objectEntries)(map)) {
|
|
1997
|
+
if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
|
|
1998
|
+
return definition;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return undefined;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Applies the selected body-site definition onto the parser state, defaulting
|
|
2005
|
+
* the coding system to SNOMED CT when the definition omits one.
|
|
2006
|
+
*/
|
|
2007
|
+
function applySiteDefinition(internal, definition) {
|
|
2008
|
+
var _a;
|
|
2009
|
+
const coding = definition.coding;
|
|
2010
|
+
internal.siteCoding = {
|
|
2011
|
+
code: coding.code,
|
|
2012
|
+
display: coding.display,
|
|
2013
|
+
system: (_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
|
|
2014
|
+
};
|
|
2015
|
+
if (definition.text) {
|
|
2016
|
+
internal.siteText = definition.text;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Converts a body-site definition into a suggestion payload so all suggestion
|
|
2021
|
+
* sources share consistent structure.
|
|
2022
|
+
*/
|
|
2023
|
+
function definitionToSuggestion(definition) {
|
|
2024
|
+
var _a;
|
|
2025
|
+
return {
|
|
2026
|
+
coding: {
|
|
2027
|
+
code: definition.coding.code,
|
|
2028
|
+
display: definition.coding.display,
|
|
2029
|
+
system: (_a = definition.coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM
|
|
2030
|
+
},
|
|
2031
|
+
text: definition.text
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Inserts a suggestion into a deduplicated map keyed by system and code.
|
|
2036
|
+
*/
|
|
2037
|
+
function addSuggestionToMap(map, suggestion) {
|
|
2038
|
+
var _a, _b;
|
|
2039
|
+
if (!suggestion) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const coding = suggestion.coding;
|
|
2043
|
+
if (!(coding === null || coding === void 0 ? void 0 : coding.code)) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const key = `${(_a = coding.system) !== null && _a !== void 0 ? _a : SNOMED_SYSTEM}|${coding.code}`;
|
|
2047
|
+
if (!map.has(key)) {
|
|
2048
|
+
map.set(key, {
|
|
2049
|
+
coding: {
|
|
2050
|
+
code: coding.code,
|
|
2051
|
+
display: coding.display,
|
|
2052
|
+
system: (_b = coding.system) !== null && _b !== void 0 ? _b : SNOMED_SYSTEM
|
|
2053
|
+
},
|
|
2054
|
+
text: suggestion.text
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Normalizes resolver outputs into a consistent array before merging them into
|
|
2060
|
+
* the suggestion map.
|
|
2061
|
+
*/
|
|
2062
|
+
function collectSuggestionResult(map, result) {
|
|
2063
|
+
if (!result) {
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const suggestions = Array.isArray(result)
|
|
2067
|
+
? result
|
|
2068
|
+
: typeof result === "object" && "suggestions" in result
|
|
2069
|
+
? result.suggestions
|
|
2070
|
+
: [result];
|
|
2071
|
+
for (const suggestion of suggestions) {
|
|
2072
|
+
addSuggestionToMap(map, suggestion);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Wraps scalar or array configuration into an array to simplify iteration.
|
|
2077
|
+
*/
|
|
2078
|
+
function toArray(value) {
|
|
2079
|
+
if (!value) {
|
|
2080
|
+
return [];
|
|
2081
|
+
}
|
|
2082
|
+
return Array.isArray(value) ? value : [value];
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Detects thenables without relying on `instanceof Promise`, which can break
|
|
2086
|
+
* across execution contexts.
|
|
2087
|
+
*/
|
|
2088
|
+
function isPromise(value) {
|
|
2089
|
+
return !!value && typeof value.then === "function";
|
|
2090
|
+
}
|
|
1737
2091
|
function normalizeUnit(token, options) {
|
|
1738
2092
|
var _a;
|
|
1739
2093
|
const override = enforceHouseholdUnitPolicy((_a = options === null || options === void 0 ? void 0 : options.unitMap) === null || _a === void 0 ? void 0 : _a[token], options);
|
package/dist/types.d.ts
CHANGED
|
@@ -289,6 +289,64 @@ export interface FormatOptions {
|
|
|
289
289
|
locale?: "en" | "th" | string;
|
|
290
290
|
i18n?: SigTranslationConfig;
|
|
291
291
|
}
|
|
292
|
+
export interface BodySiteCode {
|
|
293
|
+
code: string;
|
|
294
|
+
display?: string;
|
|
295
|
+
system?: string;
|
|
296
|
+
}
|
|
297
|
+
export interface BodySiteDefinition {
|
|
298
|
+
coding: BodySiteCode;
|
|
299
|
+
text?: string;
|
|
300
|
+
}
|
|
301
|
+
export interface TextRange {
|
|
302
|
+
/** Inclusive start index of the matched substring within the original input. */
|
|
303
|
+
start: number;
|
|
304
|
+
/** Exclusive end index of the matched substring within the original input. */
|
|
305
|
+
end: number;
|
|
306
|
+
}
|
|
307
|
+
export interface SiteCodeLookupRequest {
|
|
308
|
+
/** Original site text preserved for debugging or auditing. */
|
|
309
|
+
originalText: string;
|
|
310
|
+
/**
|
|
311
|
+
* Sanitized site text used for human-readable output. Connectors and braces
|
|
312
|
+
* are stripped but casing is preserved.
|
|
313
|
+
*/
|
|
314
|
+
text: string;
|
|
315
|
+
/** Lower-case variant of the text for case-insensitive lookups. */
|
|
316
|
+
normalized: string;
|
|
317
|
+
/** Canonical key generated by trimming and collapsing whitespace. */
|
|
318
|
+
canonical: string;
|
|
319
|
+
/** Indicates the text was wrapped in `{}` to request interactive lookup. */
|
|
320
|
+
isProbe: boolean;
|
|
321
|
+
/** Full original input string provided to the parser. */
|
|
322
|
+
inputText: string;
|
|
323
|
+
/**
|
|
324
|
+
* Substring captured directly from the original input, preserving spacing and
|
|
325
|
+
* casing. Undefined when a reliable slice cannot be determined.
|
|
326
|
+
*/
|
|
327
|
+
sourceText?: string;
|
|
328
|
+
/** Location of {@link sourceText} relative to the original input. */
|
|
329
|
+
range?: TextRange;
|
|
330
|
+
}
|
|
331
|
+
export interface SiteCodeResolution extends BodySiteDefinition {
|
|
332
|
+
}
|
|
333
|
+
export interface SiteCodeSuggestion {
|
|
334
|
+
coding: BodySiteCode;
|
|
335
|
+
text?: string;
|
|
336
|
+
}
|
|
337
|
+
export interface SiteCodeSuggestionsResult {
|
|
338
|
+
suggestions: SiteCodeSuggestion[];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Site code resolvers can perform deterministic lookups or remote queries with
|
|
342
|
+
* access to the original sig text and extracted site range.
|
|
343
|
+
*/
|
|
344
|
+
export type SiteCodeResolver = (request: SiteCodeLookupRequest) => SiteCodeResolution | null | undefined | Promise<SiteCodeResolution | null | undefined>;
|
|
345
|
+
/**
|
|
346
|
+
* Suggestion providers receive the same context as resolvers, including the
|
|
347
|
+
* caller's full input and the character range of the detected site phrase.
|
|
348
|
+
*/
|
|
349
|
+
export type SiteCodeSuggestionResolver = (request: SiteCodeLookupRequest) => SiteCodeSuggestionsResult | SiteCodeSuggestion[] | SiteCodeSuggestion | null | undefined | Promise<SiteCodeSuggestionsResult | SiteCodeSuggestion[] | SiteCodeSuggestion | null | undefined>;
|
|
292
350
|
export interface ParseOptions extends FormatOptions {
|
|
293
351
|
/**
|
|
294
352
|
* Optional medication context that assists with default unit inference.
|
|
@@ -326,6 +384,23 @@ export interface ParseOptions extends FormatOptions {
|
|
|
326
384
|
* and tablespoon when set to false. Defaults to true.
|
|
327
385
|
*/
|
|
328
386
|
allowHouseholdVolumeUnits?: boolean;
|
|
387
|
+
/**
|
|
388
|
+
* Allows mapping normalized site phrases (e.g., "left arm") to
|
|
389
|
+
* institution-specific codings. Keys are normalized with the same logic as
|
|
390
|
+
* the default site dictionary (trimmed, lower-cased, collapsing whitespace).
|
|
391
|
+
*/
|
|
392
|
+
siteCodeMap?: Record<string, BodySiteDefinition>;
|
|
393
|
+
/**
|
|
394
|
+
* Callback(s) that can translate detected site text into a coded body site.
|
|
395
|
+
* Return a promise when using asynchronous terminology services.
|
|
396
|
+
*/
|
|
397
|
+
siteCodeResolvers?: SiteCodeResolver | SiteCodeResolver[];
|
|
398
|
+
/**
|
|
399
|
+
* Callback(s) that surface possible coded body sites for interactive flows
|
|
400
|
+
* when the parser cannot confidently resolve a site, or the input explicitly
|
|
401
|
+
* requested a lookup via `{site}` placeholders.
|
|
402
|
+
*/
|
|
403
|
+
siteCodeSuggestionResolvers?: SiteCodeSuggestionResolver | SiteCodeSuggestionResolver[];
|
|
329
404
|
}
|
|
330
405
|
export interface ParseResult {
|
|
331
406
|
fhir: FhirDosage;
|
|
@@ -338,7 +413,15 @@ export interface ParseResult {
|
|
|
338
413
|
normalized: {
|
|
339
414
|
route?: RouteCode;
|
|
340
415
|
unit?: string;
|
|
416
|
+
site?: {
|
|
417
|
+
text?: string;
|
|
418
|
+
coding?: BodySiteCode;
|
|
419
|
+
};
|
|
341
420
|
};
|
|
421
|
+
siteLookups?: Array<{
|
|
422
|
+
request: SiteCodeLookupRequest;
|
|
423
|
+
suggestions: SiteCodeSuggestion[];
|
|
424
|
+
}>;
|
|
342
425
|
};
|
|
343
426
|
}
|
|
344
427
|
/**
|