ezmedicationinput 0.1.12 → 0.1.14
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 +25 -6
- package/dist/internal-types.d.ts +1 -0
- package/dist/maps.js +6 -1
- package/dist/parser.js +104 -11
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,9 +138,25 @@ const result = await parseSigAsync("apply to {left temple} nightly", {
|
|
|
138
138
|
system: "http://example.org/custom",
|
|
139
139
|
code: "LTEMP",
|
|
140
140
|
display: "Left temple"
|
|
141
|
-
}
|
|
141
|
+
},
|
|
142
|
+
aliases: ["temporal region, left"],
|
|
143
|
+
text: "Left temple"
|
|
142
144
|
}
|
|
143
145
|
},
|
|
146
|
+
// any overrides that the user explicitly selected
|
|
147
|
+
siteCodeSelections: [
|
|
148
|
+
{
|
|
149
|
+
canonical: "scalp",
|
|
150
|
+
resolution: {
|
|
151
|
+
coding: {
|
|
152
|
+
system: "http://snomed.info/sct",
|
|
153
|
+
code: "39937001",
|
|
154
|
+
display: "Scalp structure"
|
|
155
|
+
},
|
|
156
|
+
text: "Scalp"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
],
|
|
144
160
|
siteCodeResolvers: async (request) => {
|
|
145
161
|
if (request.canonical === "mole on scalp") {
|
|
146
162
|
return {
|
|
@@ -171,11 +187,13 @@ result.meta.siteLookups;
|
|
|
171
187
|
// → [{ request: { text: "left temple", isProbe: true, ... }, suggestions: [...] }]
|
|
172
188
|
```
|
|
173
189
|
|
|
174
|
-
- `siteCodeMap` lets you supply deterministic overrides for normalized site phrases.
|
|
175
|
-
- `
|
|
176
|
-
- `
|
|
177
|
-
-
|
|
178
|
-
- `
|
|
190
|
+
- `siteCodeMap` lets you supply deterministic overrides for normalized site phrases.
|
|
191
|
+
- Entries accept an `aliases` array so punctuation-heavy variants (e.g., "first bicuspid, left") can resolve to the same coding.
|
|
192
|
+
- `siteCodeResolvers` (sync or async) can call external services to resolve sites on demand.
|
|
193
|
+
- `siteCodeSuggestionResolvers` return candidate codes; their results populate `meta.siteLookups[0].suggestions`.
|
|
194
|
+
- `siteCodeSelections` let callers override the automatic match for a detected phrase or range—helpful when a clinician chooses a bundled SNOMED option over a custom override.
|
|
195
|
+
- Each resolver receives the full `SiteCodeLookupRequest`, including the original input, the cleaned site text, and a `{ start, end }` range you can use to highlight the substring in UI workflows.
|
|
196
|
+
- `parseSigAsync` behaves like `parseSig` but awaits asynchronous resolvers and suggestion providers.
|
|
179
197
|
|
|
180
198
|
#### Site resolver signatures
|
|
181
199
|
|
|
@@ -225,6 +243,7 @@ You can specify the number of times (total count) the medication is supposed to
|
|
|
225
243
|
- `allowHouseholdVolumeUnits`: defaults to `true`; set to `false` to ignore
|
|
226
244
|
teaspoon/tablespoon units during parsing and suggestions.
|
|
227
245
|
- Custom `routeMap`, `unitMap`, `freqMap`, and `whenMap` let you augment the built-in dictionaries without mutating them.
|
|
246
|
+
- `siteCodeSelections` override automatic site resolution for matching phrases or ranges so user-picked suggestions stick when re-parsing a sig.
|
|
228
247
|
|
|
229
248
|
### Next due dose generation
|
|
230
249
|
|
package/dist/internal-types.d.ts
CHANGED
package/dist/maps.js
CHANGED
|
@@ -170,7 +170,12 @@ exports.DEFAULT_ROUTE_SYNONYMS = (() => {
|
|
|
170
170
|
* same logic to ensure consistent lookups.
|
|
171
171
|
*/
|
|
172
172
|
function normalizeBodySiteKey(value) {
|
|
173
|
-
return value
|
|
173
|
+
return value
|
|
174
|
+
.trim()
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
177
|
+
.replace(/\s+/g, " ")
|
|
178
|
+
.trim();
|
|
174
179
|
}
|
|
175
180
|
const DEFAULT_BODY_SITE_SNOMED_SOURCE = [
|
|
176
181
|
{
|
package/dist/parser.js
CHANGED
|
@@ -20,6 +20,39 @@ const types_1 = require("./types");
|
|
|
20
20
|
const object_1 = require("./utils/object");
|
|
21
21
|
const array_1 = require("./utils/array");
|
|
22
22
|
const SNOMED_SYSTEM = "http://snomed.info/sct";
|
|
23
|
+
function buildCustomSiteHints(map) {
|
|
24
|
+
if (!map) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const hints = new Set();
|
|
28
|
+
const addPhraseHints = (phrase) => {
|
|
29
|
+
if (!phrase) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const normalized = (0, maps_1.normalizeBodySiteKey)(phrase);
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const part of normalized.split(" ")) {
|
|
37
|
+
if (part) {
|
|
38
|
+
hints.add(part);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
for (const [key, definition] of (0, object_1.objectEntries)(map)) {
|
|
43
|
+
addPhraseHints(key);
|
|
44
|
+
if (definition.aliases) {
|
|
45
|
+
for (const alias of definition.aliases) {
|
|
46
|
+
addPhraseHints(alias);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return hints;
|
|
51
|
+
}
|
|
52
|
+
function isBodySiteHint(word, customSiteHints) {
|
|
53
|
+
var _a;
|
|
54
|
+
return BODY_SITE_HINTS.has(word) || ((_a = customSiteHints === null || customSiteHints === void 0 ? void 0 : customSiteHints.has(word)) !== null && _a !== void 0 ? _a : false);
|
|
55
|
+
}
|
|
23
56
|
const BODY_SITE_HINTS = new Set([
|
|
24
57
|
"left",
|
|
25
58
|
"right",
|
|
@@ -362,7 +395,7 @@ function hasBodySiteContextBefore(internal, tokens, index) {
|
|
|
362
395
|
continue;
|
|
363
396
|
}
|
|
364
397
|
const normalized = normalizeTokenLower(token);
|
|
365
|
-
if (
|
|
398
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
366
399
|
return true;
|
|
367
400
|
}
|
|
368
401
|
if (EYE_SITE_TOKENS[normalized]) {
|
|
@@ -399,7 +432,7 @@ function hasBodySiteContextAfter(internal, tokens, index) {
|
|
|
399
432
|
if (SITE_FILLER_WORDS.has(normalized)) {
|
|
400
433
|
continue;
|
|
401
434
|
}
|
|
402
|
-
if (
|
|
435
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
403
436
|
return true;
|
|
404
437
|
}
|
|
405
438
|
if (seenConnector) {
|
|
@@ -481,7 +514,7 @@ function shouldTreatEyeTokenAsSite(internal, tokens, index, context) {
|
|
|
481
514
|
if (SITE_CONNECTORS.has(normalized)) {
|
|
482
515
|
continue;
|
|
483
516
|
}
|
|
484
|
-
if (
|
|
517
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
485
518
|
return false;
|
|
486
519
|
}
|
|
487
520
|
if (EYE_SITE_TOKENS[normalized]) {
|
|
@@ -1248,7 +1281,8 @@ function parseInternal(input, options) {
|
|
|
1248
1281
|
when: [],
|
|
1249
1282
|
warnings: [],
|
|
1250
1283
|
siteTokenIndices: new Set(),
|
|
1251
|
-
siteLookups: []
|
|
1284
|
+
siteLookups: [],
|
|
1285
|
+
customSiteHints: buildCustomSiteHints(options === null || options === void 0 ? void 0 : options.siteCodeMap)
|
|
1252
1286
|
};
|
|
1253
1287
|
const context = (_a = options === null || options === void 0 ? void 0 : options.context) !== null && _a !== void 0 ? _a : undefined;
|
|
1254
1288
|
const customRouteMap = (options === null || options === void 0 ? void 0 : options.routeMap)
|
|
@@ -1366,7 +1400,7 @@ function parseInternal(input, options) {
|
|
|
1366
1400
|
setRoute(internal, synonym.code, synonym.text);
|
|
1367
1401
|
for (const part of slice) {
|
|
1368
1402
|
mark(internal.consumed, part);
|
|
1369
|
-
if (
|
|
1403
|
+
if (isBodySiteHint(part.lower, internal.customSiteHints)) {
|
|
1370
1404
|
internal.siteTokenIndices.add(part.index);
|
|
1371
1405
|
}
|
|
1372
1406
|
}
|
|
@@ -1710,7 +1744,7 @@ function parseInternal(input, options) {
|
|
|
1710
1744
|
const siteCandidateIndices = new Set();
|
|
1711
1745
|
for (const token of leftoverTokens) {
|
|
1712
1746
|
const normalized = normalizeTokenLower(token);
|
|
1713
|
-
if (
|
|
1747
|
+
if (isBodySiteHint(normalized, internal.customSiteHints)) {
|
|
1714
1748
|
siteCandidateIndices.add(token.index);
|
|
1715
1749
|
}
|
|
1716
1750
|
}
|
|
@@ -1728,7 +1762,7 @@ function parseInternal(input, options) {
|
|
|
1728
1762
|
}
|
|
1729
1763
|
const lower = normalizeTokenLower(token);
|
|
1730
1764
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1731
|
-
|
|
1765
|
+
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
1732
1766
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1733
1767
|
indicesToInclude.add(token.index);
|
|
1734
1768
|
prev -= 1;
|
|
@@ -1744,7 +1778,7 @@ function parseInternal(input, options) {
|
|
|
1744
1778
|
}
|
|
1745
1779
|
const lower = normalizeTokenLower(token);
|
|
1746
1780
|
if (SITE_CONNECTORS.has(lower) ||
|
|
1747
|
-
|
|
1781
|
+
isBodySiteHint(lower, internal.customSiteHints) ||
|
|
1748
1782
|
ROUTE_DESCRIPTOR_FILLER_WORDS.has(lower)) {
|
|
1749
1783
|
indicesToInclude.add(token.index);
|
|
1750
1784
|
next += 1;
|
|
@@ -1802,7 +1836,7 @@ function parseInternal(input, options) {
|
|
|
1802
1836
|
const normalizedLower = sanitized.toLowerCase();
|
|
1803
1837
|
const strippedDescriptor = normalizeRouteDescriptorPhrase(normalizedLower);
|
|
1804
1838
|
const siteWords = normalizedLower.split(/\s+/).filter((word) => word.length > 0);
|
|
1805
|
-
const hasNonSiteWords = siteWords.some((word) => !
|
|
1839
|
+
const hasNonSiteWords = siteWords.some((word) => !isBodySiteHint(word, internal.customSiteHints));
|
|
1806
1840
|
const shouldAttemptRouteDescriptor = strippedDescriptor !== normalizedLower || hasNonSiteWords || strippedDescriptor === "mouth";
|
|
1807
1841
|
const appliedRouteDescriptor = shouldAttemptRouteDescriptor && maybeApplyRouteDescriptor(sanitized);
|
|
1808
1842
|
if (!appliedRouteDescriptor) {
|
|
@@ -1873,8 +1907,9 @@ function runSiteCodingResolutionSync(internal, options) {
|
|
|
1873
1907
|
return;
|
|
1874
1908
|
}
|
|
1875
1909
|
const canonical = request.canonical;
|
|
1910
|
+
const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
|
|
1876
1911
|
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
1877
|
-
let resolution = customDefinition;
|
|
1912
|
+
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
1878
1913
|
if (!resolution) {
|
|
1879
1914
|
// Allow synchronous resolver callbacks to claim the site.
|
|
1880
1915
|
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
@@ -1904,6 +1939,9 @@ function runSiteCodingResolutionSync(internal, options) {
|
|
|
1904
1939
|
return;
|
|
1905
1940
|
}
|
|
1906
1941
|
const suggestionMap = new Map();
|
|
1942
|
+
if (selection) {
|
|
1943
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
|
|
1944
|
+
}
|
|
1907
1945
|
if (customDefinition) {
|
|
1908
1946
|
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
1909
1947
|
}
|
|
@@ -1936,8 +1974,9 @@ function runSiteCodingResolutionAsync(internal, options) {
|
|
|
1936
1974
|
return;
|
|
1937
1975
|
}
|
|
1938
1976
|
const canonical = request.canonical;
|
|
1977
|
+
const selection = pickSiteSelection(options === null || options === void 0 ? void 0 : options.siteCodeSelections, request);
|
|
1939
1978
|
const customDefinition = lookupBodySiteDefinition(options === null || options === void 0 ? void 0 : options.siteCodeMap, canonical);
|
|
1940
|
-
let resolution = customDefinition;
|
|
1979
|
+
let resolution = selection !== null && selection !== void 0 ? selection : customDefinition;
|
|
1941
1980
|
if (!resolution) {
|
|
1942
1981
|
// Await asynchronous resolver callbacks (e.g., HTTP terminology services).
|
|
1943
1982
|
for (const resolver of toArray(options === null || options === void 0 ? void 0 : options.siteCodeResolvers)) {
|
|
@@ -1963,6 +2002,9 @@ function runSiteCodingResolutionAsync(internal, options) {
|
|
|
1963
2002
|
return;
|
|
1964
2003
|
}
|
|
1965
2004
|
const suggestionMap = new Map();
|
|
2005
|
+
if (selection) {
|
|
2006
|
+
addSuggestionToMap(suggestionMap, definitionToSuggestion(selection));
|
|
2007
|
+
}
|
|
1966
2008
|
if (customDefinition) {
|
|
1967
2009
|
addSuggestionToMap(suggestionMap, definitionToSuggestion(customDefinition));
|
|
1968
2010
|
}
|
|
@@ -1997,6 +2039,57 @@ function lookupBodySiteDefinition(map, canonical) {
|
|
|
1997
2039
|
if ((0, maps_1.normalizeBodySiteKey)(key) === canonical) {
|
|
1998
2040
|
return definition;
|
|
1999
2041
|
}
|
|
2042
|
+
if (definition.aliases) {
|
|
2043
|
+
for (const alias of definition.aliases) {
|
|
2044
|
+
if ((0, maps_1.normalizeBodySiteKey)(alias) === canonical) {
|
|
2045
|
+
return definition;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return undefined;
|
|
2051
|
+
}
|
|
2052
|
+
function pickSiteSelection(selections, request) {
|
|
2053
|
+
if (!selections) {
|
|
2054
|
+
return undefined;
|
|
2055
|
+
}
|
|
2056
|
+
const canonical = request.canonical;
|
|
2057
|
+
const normalizedText = (0, maps_1.normalizeBodySiteKey)(request.text);
|
|
2058
|
+
const requestRange = request.range;
|
|
2059
|
+
for (const selection of toArray(selections)) {
|
|
2060
|
+
if (!selection) {
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
let matched = false;
|
|
2064
|
+
if (selection.range) {
|
|
2065
|
+
if (!requestRange) {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
if (selection.range.start !== requestRange.start ||
|
|
2069
|
+
selection.range.end !== requestRange.end) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
matched = true;
|
|
2073
|
+
}
|
|
2074
|
+
if (selection.canonical) {
|
|
2075
|
+
if ((0, maps_1.normalizeBodySiteKey)(selection.canonical) !== canonical) {
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
matched = true;
|
|
2079
|
+
}
|
|
2080
|
+
else if (selection.text) {
|
|
2081
|
+
const normalizedSelection = (0, maps_1.normalizeBodySiteKey)(selection.text);
|
|
2082
|
+
if (normalizedSelection !== canonical && normalizedSelection !== normalizedText) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
matched = true;
|
|
2086
|
+
}
|
|
2087
|
+
if (!selection.range && !selection.canonical && !selection.text) {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (matched) {
|
|
2091
|
+
return selection.resolution;
|
|
2092
|
+
}
|
|
2000
2093
|
}
|
|
2001
2094
|
return undefined;
|
|
2002
2095
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -297,6 +297,12 @@ export interface BodySiteCode {
|
|
|
297
297
|
export interface BodySiteDefinition {
|
|
298
298
|
coding: BodySiteCode;
|
|
299
299
|
text?: string;
|
|
300
|
+
/**
|
|
301
|
+
* Optional phrases that should resolve to the same coding as this entry.
|
|
302
|
+
* Aliases are normalized with the same logic as map keys so callers can
|
|
303
|
+
* provide punctuation-heavy variants such as "first bicuspid, left".
|
|
304
|
+
*/
|
|
305
|
+
aliases?: string[];
|
|
300
306
|
}
|
|
301
307
|
export interface TextRange {
|
|
302
308
|
/** Inclusive start index of the matched substring within the original input. */
|
|
@@ -337,6 +343,24 @@ export interface SiteCodeSuggestion {
|
|
|
337
343
|
export interface SiteCodeSuggestionsResult {
|
|
338
344
|
suggestions: SiteCodeSuggestion[];
|
|
339
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Allows callers to override the parser's automatic site resolution for a
|
|
348
|
+
* specific match. Matches can be scoped by the normalized phrase, the original
|
|
349
|
+
* sanitized text, or the exact character range that was detected.
|
|
350
|
+
*/
|
|
351
|
+
export interface SiteCodeSelection {
|
|
352
|
+
/** Canonical key (punctuation stripped, lower-case) that should trigger this selection. */
|
|
353
|
+
canonical?: string;
|
|
354
|
+
/**
|
|
355
|
+
* Human-friendly site text used to match the extracted phrase. It is
|
|
356
|
+
* normalized with the same logic as canonical keys.
|
|
357
|
+
*/
|
|
358
|
+
text?: string;
|
|
359
|
+
/** Exact range of the detected phrase within the input string. */
|
|
360
|
+
range?: TextRange;
|
|
361
|
+
/** Desired coded definition to apply when the selection matches. */
|
|
362
|
+
resolution: SiteCodeResolution;
|
|
363
|
+
}
|
|
340
364
|
/**
|
|
341
365
|
* Site code resolvers can perform deterministic lookups or remote queries with
|
|
342
366
|
* access to the original sig text and extracted site range.
|
|
@@ -390,6 +414,12 @@ export interface ParseOptions extends FormatOptions {
|
|
|
390
414
|
* the default site dictionary (trimmed, lower-cased, collapsing whitespace).
|
|
391
415
|
*/
|
|
392
416
|
siteCodeMap?: Record<string, BodySiteDefinition>;
|
|
417
|
+
/**
|
|
418
|
+
* Explicit selections that override automatic site resolution for matching
|
|
419
|
+
* phrases. Useful when custom dictionaries provide multiple options but a UI
|
|
420
|
+
* workflow needs to pin a particular coding for a given match or range.
|
|
421
|
+
*/
|
|
422
|
+
siteCodeSelections?: SiteCodeSelection | SiteCodeSelection[];
|
|
393
423
|
/**
|
|
394
424
|
* Callback(s) that can translate detected site text into a coded body site.
|
|
395
425
|
* Return a promise when using asynchronous terminology services.
|