fhirsmith 0.3.0
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 +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025, Health Intersections Pty Ltd (http://www.healthintersections.com.au)
|
|
3
|
+
//
|
|
4
|
+
// Licensed under BSD-3: https://opensource.org/license/bsd-3-clause
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Value Set Composition Language (VCL) Parser for JavaScript FHIR
|
|
9
|
+
* Based on the Java VCL parser but adapted for JavaScript
|
|
10
|
+
* Compatible with ES6+ and both Node.js and browser environments
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class VCLParseException extends Error {
|
|
14
|
+
constructor(message, position = -1) {
|
|
15
|
+
super(position >= 0 ? `${message} at position ${position}` : message);
|
|
16
|
+
this.name = 'VCLParseException';
|
|
17
|
+
this.position = position;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TokenType = {
|
|
22
|
+
DASH: 'DASH',
|
|
23
|
+
OPEN: 'OPEN',
|
|
24
|
+
CLOSE: 'CLOSE',
|
|
25
|
+
LCRLY: 'LCRLY',
|
|
26
|
+
RCRLY: 'RCRLY',
|
|
27
|
+
SEMI: 'SEMI',
|
|
28
|
+
COMMA: 'COMMA',
|
|
29
|
+
DOT: 'DOT',
|
|
30
|
+
STAR: 'STAR',
|
|
31
|
+
EQ: 'EQ',
|
|
32
|
+
IS_A: 'IS_A',
|
|
33
|
+
IS_NOT_A: 'IS_NOT_A',
|
|
34
|
+
DESC_OF: 'DESC_OF',
|
|
35
|
+
REGEX: 'REGEX',
|
|
36
|
+
IN: 'IN',
|
|
37
|
+
NOT_IN: 'NOT_IN',
|
|
38
|
+
GENERALIZES: 'GENERALIZES',
|
|
39
|
+
CHILD_OF: 'CHILD_OF',
|
|
40
|
+
DESC_LEAF: 'DESC_LEAF',
|
|
41
|
+
EXISTS: 'EXISTS',
|
|
42
|
+
URI: 'URI',
|
|
43
|
+
SCODE: 'SCODE',
|
|
44
|
+
QUOTED_VALUE: 'QUOTED_VALUE',
|
|
45
|
+
EOF: 'EOF'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const FilterOperator = {
|
|
49
|
+
EQUAL: '=',
|
|
50
|
+
IS_A: 'is-a',
|
|
51
|
+
IS_NOT_A: 'is-not-a',
|
|
52
|
+
DESCENDENT_OF: 'descendent-of',
|
|
53
|
+
REGEX: 'regex',
|
|
54
|
+
IN: 'in',
|
|
55
|
+
NOT_IN: 'not-in',
|
|
56
|
+
GENERALIZES: 'generalizes',
|
|
57
|
+
CHILD_OF: 'child-of',
|
|
58
|
+
DESCENDENT_LEAF: 'descendent-leaf',
|
|
59
|
+
EXISTS: 'exists'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
class Token {
|
|
63
|
+
constructor(type, value, position) {
|
|
64
|
+
this.type = type;
|
|
65
|
+
this.value = value;
|
|
66
|
+
this.position = position;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toString() {
|
|
70
|
+
return `${this.type}(${this.value})`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class VCLLexer {
|
|
75
|
+
constructor(input) {
|
|
76
|
+
this.input = input.trim();
|
|
77
|
+
this.pos = 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
current() {
|
|
81
|
+
return this.pos < this.input.length ? this.input[this.pos] : '\0';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
peek(offset = 0) {
|
|
85
|
+
const peekPos = this.pos + 1 + offset;
|
|
86
|
+
return peekPos < this.input.length ? this.input[peekPos] : '\0';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
skipWhitespace() {
|
|
90
|
+
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
|
|
91
|
+
this.pos++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isIdentifierChar(c) {
|
|
96
|
+
return /[a-zA-Z0-9:?&%+\-.@#$!{}_]/.test(c);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isUriChar(c) {
|
|
100
|
+
return /[a-zA-Z0-9?&%+\-.@#$!{}_/]/.test(c);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isCodeChar(c) {
|
|
104
|
+
return /[a-zA-Z0-9\-_]/.test(c);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
isVersionChar(c) {
|
|
108
|
+
return /[a-zA-Z0-9\-_.+]/.test(c);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
readIdentifierChars() {
|
|
112
|
+
let result = '';
|
|
113
|
+
while (this.pos < this.input.length && this.isIdentifierChar(this.input[this.pos])) {
|
|
114
|
+
result += this.input[this.pos];
|
|
115
|
+
this.pos++;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
readUriChars() {
|
|
121
|
+
let result = '';
|
|
122
|
+
while (this.pos < this.input.length && this.isUriChar(this.input[this.pos])) {
|
|
123
|
+
result += this.input[this.pos];
|
|
124
|
+
this.pos++;
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
readCodeChars() {
|
|
130
|
+
let result = '';
|
|
131
|
+
while (this.pos < this.input.length && this.isCodeChar(this.input[this.pos])) {
|
|
132
|
+
result += this.input[this.pos];
|
|
133
|
+
this.pos++;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
readVersionChars() {
|
|
139
|
+
let result = '';
|
|
140
|
+
while (this.pos < this.input.length && this.isVersionChar(this.input[this.pos])) {
|
|
141
|
+
result += this.input[this.pos];
|
|
142
|
+
this.pos++;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
readQuotedValue(startPos) {
|
|
148
|
+
let value = '';
|
|
149
|
+
this.pos++; // Skip opening quote
|
|
150
|
+
|
|
151
|
+
while (this.pos < this.input.length) {
|
|
152
|
+
const ch = this.input[this.pos];
|
|
153
|
+
if (ch === '"') {
|
|
154
|
+
this.pos++;
|
|
155
|
+
return new Token(TokenType.QUOTED_VALUE, value, startPos);
|
|
156
|
+
} else if (ch === '\\' && this.pos + 1 < this.input.length) {
|
|
157
|
+
this.pos++;
|
|
158
|
+
const escaped = this.input[this.pos];
|
|
159
|
+
if (escaped === '"' || escaped === '\\') {
|
|
160
|
+
value += escaped;
|
|
161
|
+
} else {
|
|
162
|
+
value += '\\' + escaped;
|
|
163
|
+
}
|
|
164
|
+
this.pos++;
|
|
165
|
+
} else {
|
|
166
|
+
value += ch;
|
|
167
|
+
this.pos++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new VCLParseException('Unterminated quoted string', startPos);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
tokenize() {
|
|
175
|
+
const tokens = [];
|
|
176
|
+
|
|
177
|
+
while (this.pos < this.input.length) {
|
|
178
|
+
this.skipWhitespace();
|
|
179
|
+
if (this.pos >= this.input.length) break;
|
|
180
|
+
|
|
181
|
+
const startPos = this.pos;
|
|
182
|
+
const ch = this.input[this.pos];
|
|
183
|
+
|
|
184
|
+
switch (ch) {
|
|
185
|
+
case '-':
|
|
186
|
+
tokens.push(new Token(TokenType.DASH, '-', startPos));
|
|
187
|
+
this.pos++;
|
|
188
|
+
break;
|
|
189
|
+
case '(':
|
|
190
|
+
tokens.push(new Token(TokenType.OPEN, '(', startPos));
|
|
191
|
+
this.pos++;
|
|
192
|
+
break;
|
|
193
|
+
case ')':
|
|
194
|
+
tokens.push(new Token(TokenType.CLOSE, ')', startPos));
|
|
195
|
+
this.pos++;
|
|
196
|
+
break;
|
|
197
|
+
case '{':
|
|
198
|
+
tokens.push(new Token(TokenType.LCRLY, '{', startPos));
|
|
199
|
+
this.pos++;
|
|
200
|
+
break;
|
|
201
|
+
case '}':
|
|
202
|
+
tokens.push(new Token(TokenType.RCRLY, '}', startPos));
|
|
203
|
+
this.pos++;
|
|
204
|
+
break;
|
|
205
|
+
case ';':
|
|
206
|
+
tokens.push(new Token(TokenType.SEMI, ';', startPos));
|
|
207
|
+
this.pos++;
|
|
208
|
+
break;
|
|
209
|
+
case ',':
|
|
210
|
+
tokens.push(new Token(TokenType.COMMA, ',', startPos));
|
|
211
|
+
this.pos++;
|
|
212
|
+
break;
|
|
213
|
+
case '.':
|
|
214
|
+
tokens.push(new Token(TokenType.DOT, '.', startPos));
|
|
215
|
+
this.pos++;
|
|
216
|
+
break;
|
|
217
|
+
case '*':
|
|
218
|
+
tokens.push(new Token(TokenType.STAR, '*', startPos));
|
|
219
|
+
this.pos++;
|
|
220
|
+
break;
|
|
221
|
+
case '=':
|
|
222
|
+
tokens.push(new Token(TokenType.EQ, '=', startPos));
|
|
223
|
+
this.pos++;
|
|
224
|
+
break;
|
|
225
|
+
case '/':
|
|
226
|
+
tokens.push(new Token(TokenType.REGEX, '/', startPos));
|
|
227
|
+
this.pos++;
|
|
228
|
+
break;
|
|
229
|
+
case '^':
|
|
230
|
+
tokens.push(new Token(TokenType.IN, '^', startPos));
|
|
231
|
+
this.pos++;
|
|
232
|
+
break;
|
|
233
|
+
case '>':
|
|
234
|
+
if (this.peek() === '>') {
|
|
235
|
+
tokens.push(new Token(TokenType.GENERALIZES, '>>', startPos));
|
|
236
|
+
this.pos += 2;
|
|
237
|
+
} else {
|
|
238
|
+
throw new VCLParseException(`Unexpected character: ${ch}`, this.pos);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case '<':
|
|
242
|
+
if (this.peek() === '<') {
|
|
243
|
+
tokens.push(new Token(TokenType.IS_A, '<<', startPos));
|
|
244
|
+
this.pos += 2;
|
|
245
|
+
} else if (this.peek() === '!') {
|
|
246
|
+
tokens.push(new Token(TokenType.CHILD_OF, '<!', startPos));
|
|
247
|
+
this.pos += 2;
|
|
248
|
+
} else {
|
|
249
|
+
tokens.push(new Token(TokenType.DESC_OF, '<', startPos));
|
|
250
|
+
this.pos++;
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case '~':
|
|
254
|
+
if (this.peek() === '<' && this.peek(1) === '<') {
|
|
255
|
+
tokens.push(new Token(TokenType.IS_NOT_A, '~<<', startPos));
|
|
256
|
+
this.pos += 3;
|
|
257
|
+
} else if (this.peek() === '^') {
|
|
258
|
+
tokens.push(new Token(TokenType.NOT_IN, '~^', startPos));
|
|
259
|
+
this.pos += 2;
|
|
260
|
+
} else {
|
|
261
|
+
throw new VCLParseException(`Unexpected character: ${ch}`, this.pos);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case '!':
|
|
265
|
+
if (this.peek() === '!' && this.peek(1) === '<') {
|
|
266
|
+
tokens.push(new Token(TokenType.DESC_LEAF, '!!<', startPos));
|
|
267
|
+
this.pos += 3;
|
|
268
|
+
} else {
|
|
269
|
+
throw new VCLParseException(`Unexpected character: ${ch}`, this.pos);
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
case '?':
|
|
273
|
+
tokens.push(new Token(TokenType.EXISTS, '?', startPos));
|
|
274
|
+
this.pos++;
|
|
275
|
+
break;
|
|
276
|
+
case '"':
|
|
277
|
+
tokens.push(this.readQuotedValue(startPos));
|
|
278
|
+
break;
|
|
279
|
+
default:
|
|
280
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
281
|
+
const value = this.readCodeChars();
|
|
282
|
+
|
|
283
|
+
if (this.current() === ':') {
|
|
284
|
+
this.pos++;
|
|
285
|
+
// Read rest of URI
|
|
286
|
+
const uriRest = this.readUriChars();
|
|
287
|
+
let fullValue = value + ':' + uriRest;
|
|
288
|
+
|
|
289
|
+
// Check for version
|
|
290
|
+
if (this.pos < this.input.length && this.input[this.pos] === '|') {
|
|
291
|
+
this.pos++;
|
|
292
|
+
fullValue += '|' + this.readVersionChars();
|
|
293
|
+
}
|
|
294
|
+
tokens.push(new Token(TokenType.URI, fullValue, startPos));
|
|
295
|
+
} else {
|
|
296
|
+
tokens.push(new Token(TokenType.SCODE, value, startPos));
|
|
297
|
+
}
|
|
298
|
+
} else if (/[0-9]/.test(ch)) {
|
|
299
|
+
const value = this.readCodeChars();
|
|
300
|
+
tokens.push(new Token(TokenType.SCODE, value, startPos));
|
|
301
|
+
} else {
|
|
302
|
+
throw new VCLParseException(`Unexpected character: ${ch}`, this.pos);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
tokens.push(new Token(TokenType.EOF, '', this.pos));
|
|
308
|
+
return tokens;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
class VCLParserClass {
|
|
313
|
+
constructor(tokens, fhirFactory = null) {
|
|
314
|
+
this.tokens = tokens;
|
|
315
|
+
this.pos = 0;
|
|
316
|
+
this.fhirFactory = fhirFactory;
|
|
317
|
+
this.valueSet = this.createValueSet();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
createValueSet() {
|
|
321
|
+
if (this.fhirFactory && typeof this.fhirFactory.createValueSet === 'function') {
|
|
322
|
+
return this.fhirFactory.createValueSet();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Default FHIR ValueSet structure
|
|
326
|
+
return {
|
|
327
|
+
resourceType: 'ValueSet',
|
|
328
|
+
status: 'draft',
|
|
329
|
+
compose: {
|
|
330
|
+
include: [],
|
|
331
|
+
exclude: []
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
current() {
|
|
337
|
+
return this.pos < this.tokens.length ? this.tokens[this.pos] : new Token(TokenType.EOF, '', -1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
peek() {
|
|
341
|
+
return this.pos + 1 < this.tokens.length ? this.tokens[this.pos + 1] : new Token(TokenType.EOF, '', -1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
consume(expected) {
|
|
345
|
+
const current = this.current();
|
|
346
|
+
if (current.type !== expected) {
|
|
347
|
+
throw new VCLParseException(`Expected ${expected} but got ${current.type}`, current.position);
|
|
348
|
+
}
|
|
349
|
+
this.pos++;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
expect(expected) {
|
|
353
|
+
const current = this.current();
|
|
354
|
+
if (current.type !== expected) {
|
|
355
|
+
throw new VCLParseException(`Expected ${expected} but got ${current.type}`, current.position);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
isFilterOperator(tokenType) {
|
|
360
|
+
return [
|
|
361
|
+
TokenType.EQ, TokenType.IS_A, TokenType.IS_NOT_A, TokenType.DESC_OF,
|
|
362
|
+
TokenType.REGEX, TokenType.IN, TokenType.NOT_IN, TokenType.GENERALIZES,
|
|
363
|
+
TokenType.CHILD_OF, TokenType.DESC_LEAF, TokenType.EXISTS
|
|
364
|
+
].includes(tokenType);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
tokenTypeToFilterOperator(tokenType) {
|
|
368
|
+
const mapping = {
|
|
369
|
+
[TokenType.EQ]: FilterOperator.EQUAL,
|
|
370
|
+
[TokenType.IS_A]: FilterOperator.IS_A,
|
|
371
|
+
[TokenType.IS_NOT_A]: FilterOperator.IS_NOT_A,
|
|
372
|
+
[TokenType.DESC_OF]: FilterOperator.DESCENDENT_OF,
|
|
373
|
+
[TokenType.REGEX]: FilterOperator.REGEX,
|
|
374
|
+
[TokenType.IN]: FilterOperator.IN,
|
|
375
|
+
[TokenType.NOT_IN]: FilterOperator.NOT_IN,
|
|
376
|
+
[TokenType.GENERALIZES]: FilterOperator.GENERALIZES,
|
|
377
|
+
[TokenType.CHILD_OF]: FilterOperator.CHILD_OF,
|
|
378
|
+
[TokenType.DESC_LEAF]: FilterOperator.DESCENDENT_LEAF,
|
|
379
|
+
[TokenType.EXISTS]: FilterOperator.EXISTS
|
|
380
|
+
};
|
|
381
|
+
return mapping[tokenType];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
isSimpleCodeList() {
|
|
385
|
+
let lookahead = this.pos;
|
|
386
|
+
|
|
387
|
+
while (lookahead < this.tokens.length) {
|
|
388
|
+
const token = this.tokens[lookahead];
|
|
389
|
+
|
|
390
|
+
if (token.type === TokenType.CLOSE) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (token.type === TokenType.OPEN && lookahead + 2 < this.tokens.length) {
|
|
395
|
+
if (this.tokens[lookahead + 1].type === TokenType.URI &&
|
|
396
|
+
this.tokens[lookahead + 2].type === TokenType.CLOSE) {
|
|
397
|
+
lookahead += 3;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (token.type === TokenType.OPEN || token.type === TokenType.DASH || this.isFilterOperator(token.type)) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
lookahead++;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
createConceptSet(systemUri, isExclusion) {
|
|
413
|
+
const conceptSet = {
|
|
414
|
+
system: '',
|
|
415
|
+
concept: [],
|
|
416
|
+
filter: [],
|
|
417
|
+
valueSet: []
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
if (systemUri) {
|
|
421
|
+
const pipePos = systemUri.indexOf('|');
|
|
422
|
+
if (pipePos >= 0) {
|
|
423
|
+
conceptSet.system = systemUri.substring(0, pipePos);
|
|
424
|
+
conceptSet.version = systemUri.substring(pipePos + 1);
|
|
425
|
+
} else {
|
|
426
|
+
conceptSet.system = systemUri;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (isExclusion) {
|
|
431
|
+
this.valueSet.compose.exclude.push(conceptSet);
|
|
432
|
+
} else {
|
|
433
|
+
this.valueSet.compose.include.push(conceptSet);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return conceptSet;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
getCurrentConceptSet(isExclusion) {
|
|
440
|
+
const list = isExclusion ? this.valueSet.compose.exclude : this.valueSet.compose.include;
|
|
441
|
+
return list.length > 0 ? list[list.length - 1] : this.createConceptSet('', isExclusion);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
toImplicitVcl(system, expression) {
|
|
445
|
+
return 'http://fhir.org/VCL?v1='
|
|
446
|
+
+ encodeURIComponent(`(${system})(${expression})`)
|
|
447
|
+
.replace(/\(/g, '%28')
|
|
448
|
+
.replace(/\)/g, '%29'); // need to encode parentheses for VCL as well
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
parseExpr() {
|
|
452
|
+
this.parseSubExpr(false);
|
|
453
|
+
|
|
454
|
+
switch (this.current().type) {
|
|
455
|
+
case TokenType.COMMA:
|
|
456
|
+
this.parseConjunction();
|
|
457
|
+
break;
|
|
458
|
+
case TokenType.SEMI:
|
|
459
|
+
this.parseDisjunction();
|
|
460
|
+
break;
|
|
461
|
+
case TokenType.DASH:
|
|
462
|
+
this.parseExclusion();
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
parseSubExpr(isExclusion) {
|
|
468
|
+
let systemUri = '';
|
|
469
|
+
|
|
470
|
+
// Check for system URI in parentheses
|
|
471
|
+
if (this.current().type === TokenType.OPEN && this.peek().type === TokenType.URI) {
|
|
472
|
+
this.consume(TokenType.OPEN);
|
|
473
|
+
systemUri = this.current().value;
|
|
474
|
+
this.consume(TokenType.URI);
|
|
475
|
+
this.consume(TokenType.CLOSE);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (this.current().type === TokenType.OPEN) {
|
|
479
|
+
this.consume(TokenType.OPEN);
|
|
480
|
+
|
|
481
|
+
// Check for nested system URI
|
|
482
|
+
if (this.current().type === TokenType.OPEN && this.peek().type === TokenType.URI) {
|
|
483
|
+
this.consume(TokenType.OPEN);
|
|
484
|
+
systemUri = this.current().value;
|
|
485
|
+
this.consume(TokenType.URI);
|
|
486
|
+
this.consume(TokenType.CLOSE);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (this.isSimpleCodeList()) {
|
|
490
|
+
this.parseSimpleCodeList(systemUri, isExclusion);
|
|
491
|
+
} else {
|
|
492
|
+
this.parseExprWithinParentheses(isExclusion);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// This should fail for unmatched parentheses
|
|
496
|
+
this.consume(TokenType.CLOSE);
|
|
497
|
+
} else {
|
|
498
|
+
this.parseSimpleExpr(systemUri, isExclusion);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
parseSimpleCodeList(systemUri, isExclusion) {
|
|
503
|
+
const conceptSet = this.createConceptSet(systemUri, isExclusion);
|
|
504
|
+
|
|
505
|
+
if (this.current().type === TokenType.STAR) {
|
|
506
|
+
this.consume(TokenType.STAR);
|
|
507
|
+
conceptSet.filter.push({
|
|
508
|
+
property: 'concept',
|
|
509
|
+
op: FilterOperator.EXISTS,
|
|
510
|
+
value: 'true'
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
} else if (this.current().type === TokenType.IN) {
|
|
514
|
+
this.parseIncludeVs(conceptSet);
|
|
515
|
+
return;
|
|
516
|
+
} else {
|
|
517
|
+
const code = this.parseCode();
|
|
518
|
+
conceptSet.concept.push({code});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
while ([TokenType.SEMI, TokenType.COMMA].includes(this.current().type)) {
|
|
522
|
+
this.consume(this.current().type);
|
|
523
|
+
|
|
524
|
+
if (this.current().type === TokenType.STAR) {
|
|
525
|
+
this.consume(TokenType.STAR);
|
|
526
|
+
conceptSet.filter.push({
|
|
527
|
+
property: 'concept',
|
|
528
|
+
op: FilterOperator.EXISTS,
|
|
529
|
+
value: 'true'
|
|
530
|
+
});
|
|
531
|
+
} else if (this.current().type === TokenType.IN) {
|
|
532
|
+
this.parseIncludeVs(conceptSet);
|
|
533
|
+
} else {
|
|
534
|
+
const code = this.parseCode();
|
|
535
|
+
conceptSet.concept.push({code});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
parseSimpleExpr(systemUri, isExclusion) {
|
|
541
|
+
const conceptSet = this.createConceptSet(systemUri, isExclusion);
|
|
542
|
+
|
|
543
|
+
if (this.peek().type === TokenType.DOT) {
|
|
544
|
+
this.parseOf(systemUri, conceptSet);
|
|
545
|
+
} else if (this.current().type === TokenType.STAR) {
|
|
546
|
+
this.consume(TokenType.STAR);
|
|
547
|
+
conceptSet.filter.push({
|
|
548
|
+
property: 'concept',
|
|
549
|
+
op: FilterOperator.EXISTS,
|
|
550
|
+
value: 'true'
|
|
551
|
+
});
|
|
552
|
+
} else if ([TokenType.SCODE, TokenType.QUOTED_VALUE].includes(this.current().type)) {
|
|
553
|
+
if (this.current().type === TokenType.SCODE && this.current().value.includes('.')) {
|
|
554
|
+
this.parseOf(systemUri, conceptSet);
|
|
555
|
+
} else {
|
|
556
|
+
const code = this.parseCode();
|
|
557
|
+
|
|
558
|
+
if (this.isFilterOperator(this.current().type)) {
|
|
559
|
+
this.parseFilter(conceptSet, code);
|
|
560
|
+
} else {
|
|
561
|
+
conceptSet.concept.push({ code });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else if (this.current().type === TokenType.IN) {
|
|
565
|
+
this.parseIncludeVs(conceptSet);
|
|
566
|
+
} else {
|
|
567
|
+
this.parseOf(systemUri, conceptSet);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
parseOf(systemUri, conceptSet) {
|
|
572
|
+
let isVcl = false;
|
|
573
|
+
const sb = [];
|
|
574
|
+
|
|
575
|
+
switch (this.current().type) {
|
|
576
|
+
case TokenType.LCRLY: {
|
|
577
|
+
// codeList or filterList
|
|
578
|
+
this.consume(TokenType.LCRLY);
|
|
579
|
+
if (this.peek().type === TokenType.COMMA) {
|
|
580
|
+
sb.push(this.parseCode());
|
|
581
|
+
while (this.current().type === TokenType.COMMA) {
|
|
582
|
+
this.consume(TokenType.COMMA);
|
|
583
|
+
sb.push(this.parseCode());
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
isVcl = true;
|
|
587
|
+
this.parseFilterList(sb);
|
|
588
|
+
}
|
|
589
|
+
this.consume(TokenType.RCRLY);
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case TokenType.STAR: {
|
|
593
|
+
sb.push(this.current().value);
|
|
594
|
+
this.consume(TokenType.STAR);
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case TokenType.URI: {
|
|
598
|
+
sb.push(this.current().value);
|
|
599
|
+
this.consume(TokenType.URI);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case TokenType.SCODE:
|
|
603
|
+
case TokenType.QUOTED_VALUE: {
|
|
604
|
+
sb.push(this.current().value);
|
|
605
|
+
this.parseCode();
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
default:
|
|
609
|
+
throw new VCLParseException("Expected code, codeList, STAR, URI or filterList", this.current().position);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.consume(TokenType.DOT);
|
|
613
|
+
|
|
614
|
+
const property = this.parseCode();
|
|
615
|
+
const implicitVcl = isVcl ? this.toImplicitVcl(systemUri, sb.join(',')) : sb.join(',');
|
|
616
|
+
|
|
617
|
+
conceptSet.filter.push({
|
|
618
|
+
property: property,
|
|
619
|
+
op: FilterOperator.EQUAL, // FIXME: Is this really the value to use?
|
|
620
|
+
_op: {
|
|
621
|
+
extension: [{
|
|
622
|
+
// WARNING: pre-adopting an extension that may change in before R6
|
|
623
|
+
url: 'http://hl7.org/fhir/6.0/StructureDefinition/extension-ValueSet.compose.include.filter.op',
|
|
624
|
+
valueString: 'of'
|
|
625
|
+
}]
|
|
626
|
+
},
|
|
627
|
+
value: implicitVcl,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
parseExprWithinParentheses(isExclusion) {
|
|
632
|
+
this.parseSubExpr(isExclusion);
|
|
633
|
+
|
|
634
|
+
while ([TokenType.COMMA, TokenType.SEMI, TokenType.DASH].includes(this.current().type)) {
|
|
635
|
+
switch (this.current().type) {
|
|
636
|
+
case TokenType.COMMA:
|
|
637
|
+
this.parseConjunctionWithFlag(isExclusion);
|
|
638
|
+
break;
|
|
639
|
+
case TokenType.SEMI:
|
|
640
|
+
this.parseDisjunctionWithFlag(isExclusion);
|
|
641
|
+
break;
|
|
642
|
+
case TokenType.DASH:
|
|
643
|
+
this.parseExclusion();
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
parseFilter(conceptSet, propertyCode) {
|
|
650
|
+
const op = this.current().type;
|
|
651
|
+
this.consume(op);
|
|
652
|
+
|
|
653
|
+
const filter = {
|
|
654
|
+
property: propertyCode,
|
|
655
|
+
op: this.tokenTypeToFilterOperator(op)
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
switch (op) {
|
|
659
|
+
case TokenType.EQ:
|
|
660
|
+
case TokenType.IS_A:
|
|
661
|
+
case TokenType.IS_NOT_A:
|
|
662
|
+
case TokenType.DESC_OF:
|
|
663
|
+
case TokenType.GENERALIZES:
|
|
664
|
+
case TokenType.CHILD_OF:
|
|
665
|
+
case TokenType.DESC_LEAF:
|
|
666
|
+
case TokenType.EXISTS:
|
|
667
|
+
filter.value = this.parseCode();
|
|
668
|
+
break;
|
|
669
|
+
case TokenType.REGEX:
|
|
670
|
+
filter.value = this.parseQuotedString();
|
|
671
|
+
break;
|
|
672
|
+
case TokenType.IN:
|
|
673
|
+
case TokenType.NOT_IN:
|
|
674
|
+
filter.value = this.parseFilterValue(conceptSet.system);
|
|
675
|
+
break;
|
|
676
|
+
default:
|
|
677
|
+
throw new VCLParseException(`Unexpected filter operator: ${op}`, this.current().position);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
conceptSet.filter.push(filter);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
parseIncludeVs(conceptSet) {
|
|
684
|
+
this.consume(TokenType.IN);
|
|
685
|
+
|
|
686
|
+
let uri;
|
|
687
|
+
if (this.current().type === TokenType.URI) {
|
|
688
|
+
uri = this.current().value;
|
|
689
|
+
this.consume(TokenType.URI);
|
|
690
|
+
} else if (this.current().type === TokenType.OPEN) {
|
|
691
|
+
this.consume(TokenType.OPEN);
|
|
692
|
+
uri = this.current().value;
|
|
693
|
+
this.consume(TokenType.URI);
|
|
694
|
+
this.consume(TokenType.CLOSE);
|
|
695
|
+
} else {
|
|
696
|
+
throw new VCLParseException('Expected URI after ^', this.current().position);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
conceptSet.valueSet.push(uri);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
parseConjunction() {
|
|
703
|
+
const currentConceptSet = this.getCurrentConceptSet(false);
|
|
704
|
+
|
|
705
|
+
while (this.current().type === TokenType.COMMA) {
|
|
706
|
+
this.consume(TokenType.COMMA);
|
|
707
|
+
|
|
708
|
+
if ([TokenType.SCODE, TokenType.QUOTED_VALUE].includes(this.current().type)) {
|
|
709
|
+
const code = this.parseCode();
|
|
710
|
+
if (this.isFilterOperator(this.current().type)) {
|
|
711
|
+
this.parseFilter(currentConceptSet, code);
|
|
712
|
+
} else {
|
|
713
|
+
currentConceptSet.concept.push({code});
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
this.parseSubExpr(false);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
parseConjunctionWithFlag(isExclusion) {
|
|
722
|
+
const currentConceptSet = this.getCurrentConceptSet(isExclusion);
|
|
723
|
+
|
|
724
|
+
while (this.current().type === TokenType.COMMA) {
|
|
725
|
+
this.consume(TokenType.COMMA);
|
|
726
|
+
|
|
727
|
+
if ([TokenType.SCODE, TokenType.QUOTED_VALUE].includes(this.current().type)) {
|
|
728
|
+
const code = this.parseCode();
|
|
729
|
+
if (this.isFilterOperator(this.current().type)) {
|
|
730
|
+
this.parseFilter(currentConceptSet, code);
|
|
731
|
+
} else {
|
|
732
|
+
currentConceptSet.concept.push({code});
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
this.parseSubExpr(isExclusion);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
parseDisjunction() {
|
|
741
|
+
while (this.current().type === TokenType.SEMI) {
|
|
742
|
+
this.consume(TokenType.SEMI);
|
|
743
|
+
this.parseSubExpr(false);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
parseDisjunctionWithFlag(isExclusion) {
|
|
748
|
+
while (this.current().type === TokenType.SEMI) {
|
|
749
|
+
this.consume(TokenType.SEMI);
|
|
750
|
+
this.parseSubExpr(isExclusion);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
parseExclusion() {
|
|
755
|
+
this.consume(TokenType.DASH);
|
|
756
|
+
this.parseSubExpr(true);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
parseCode() {
|
|
760
|
+
if (this.current().type === TokenType.SCODE) {
|
|
761
|
+
const code = this.current().value;
|
|
762
|
+
this.consume(TokenType.SCODE);
|
|
763
|
+
return code;
|
|
764
|
+
} else if (this.current().type === TokenType.QUOTED_VALUE) {
|
|
765
|
+
const code = this.current().value;
|
|
766
|
+
this.consume(TokenType.QUOTED_VALUE);
|
|
767
|
+
return code;
|
|
768
|
+
} else {
|
|
769
|
+
throw new VCLParseException('Expected code', this.current().position);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
parseQuotedString() {
|
|
774
|
+
if (this.current().type === TokenType.QUOTED_VALUE) {
|
|
775
|
+
const value = this.current().value;
|
|
776
|
+
this.consume(TokenType.QUOTED_VALUE);
|
|
777
|
+
return value;
|
|
778
|
+
} else {
|
|
779
|
+
throw new VCLParseException('Expected quoted string', this.current().position);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
cleanupEmptyArrays(obj) {
|
|
784
|
+
if (Array.isArray(obj)) {
|
|
785
|
+
return obj.map(item => this.cleanupEmptyArrays(item));
|
|
786
|
+
} else if (obj && typeof obj === 'object') {
|
|
787
|
+
const cleaned = {};
|
|
788
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
789
|
+
if (Array.isArray(value)) {
|
|
790
|
+
if (value.length > 0) {
|
|
791
|
+
cleaned[key] = this.cleanupEmptyArrays(value);
|
|
792
|
+
}
|
|
793
|
+
} else if (value && typeof value === 'object') {
|
|
794
|
+
cleaned[key] = this.cleanupEmptyArrays(value);
|
|
795
|
+
} else if (value !== undefined && value !== null && value !== '') {
|
|
796
|
+
cleaned[key] = value;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return cleaned;
|
|
800
|
+
}
|
|
801
|
+
return obj;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
parseFilterValue(systemUri) {
|
|
805
|
+
if (this.current().type === TokenType.LCRLY) {
|
|
806
|
+
this.consume(TokenType.LCRLY);
|
|
807
|
+
const codes = [this.parseCode()];
|
|
808
|
+
|
|
809
|
+
if (this.isFilterOperator(this.current().type)) {
|
|
810
|
+
this.parseFilterList(codes);
|
|
811
|
+
this.consume(TokenType.RCRLY);
|
|
812
|
+
return this.toImplicitVcl(systemUri, codes.join(''));
|
|
813
|
+
} else {
|
|
814
|
+
while (this.current().type === TokenType.COMMA) {
|
|
815
|
+
this.consume(TokenType.COMMA);
|
|
816
|
+
codes.push(this.parseCode());
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
this.consume(TokenType.RCRLY);
|
|
820
|
+
return codes.join(',');
|
|
821
|
+
}
|
|
822
|
+
} else if (this.current().type === TokenType.URI) {
|
|
823
|
+
const uri = this.current().value;
|
|
824
|
+
this.consume(TokenType.URI);
|
|
825
|
+
return uri;
|
|
826
|
+
} else {
|
|
827
|
+
return this.parseCode();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
parseFilterList(terms) {
|
|
832
|
+
let depth = 1;
|
|
833
|
+
while (depth > 0) {
|
|
834
|
+
const tokenType = this.current().type;
|
|
835
|
+
|
|
836
|
+
switch (tokenType) {
|
|
837
|
+
case TokenType.LCRLY:
|
|
838
|
+
depth++;
|
|
839
|
+
break;
|
|
840
|
+
case TokenType.RCRLY:
|
|
841
|
+
depth--;
|
|
842
|
+
break;
|
|
843
|
+
default:
|
|
844
|
+
// do nothing
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (depth > 0) {
|
|
848
|
+
terms.push(this.current().value);
|
|
849
|
+
this.consume(tokenType);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
parse() {
|
|
855
|
+
try {
|
|
856
|
+
this.parseExpr();
|
|
857
|
+
this.expect(TokenType.EOF);
|
|
858
|
+
return this.cleanupEmptyArrays(this.valueSet);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
// Make sure we're throwing VCLParseException for any parsing error
|
|
861
|
+
if (error instanceof VCLParseException) {
|
|
862
|
+
throw error;
|
|
863
|
+
} else {
|
|
864
|
+
throw new VCLParseException(`Parse error: ${error.message}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Main parsing functions
|
|
871
|
+
function parseVCL(vclExpression, fhirFactory = null) {
|
|
872
|
+
if (!vclExpression || vclExpression.trim() === '') {
|
|
873
|
+
throw new VCLParseException('VCL expression cannot be empty');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const lexer = new VCLLexer(vclExpression);
|
|
877
|
+
const tokens = lexer.tokenize();
|
|
878
|
+
|
|
879
|
+
const parser = new VCLParserClass(tokens, fhirFactory);
|
|
880
|
+
const result = parser.parse();
|
|
881
|
+
|
|
882
|
+
if (!result) {
|
|
883
|
+
throw new VCLParseException('Parser returned null result');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function parseVCLAndSetId(vclExpression, fhirFactory = null) {
|
|
890
|
+
// Use the same parsing logic as parseVCL to avoid scope issues
|
|
891
|
+
if (!vclExpression || vclExpression.trim() === '') {
|
|
892
|
+
throw new VCLParseException('VCL expression cannot be empty');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const lexer = new VCLLexer(vclExpression);
|
|
896
|
+
const tokens = lexer.tokenize();
|
|
897
|
+
const parser = new VCLParserClass(tokens, fhirFactory);
|
|
898
|
+
const valueSet = parser.parse();
|
|
899
|
+
|
|
900
|
+
if (!valueSet) {
|
|
901
|
+
throw new VCLParseException('Failed to create ValueSet');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Generate hash-based ID (similar to Java version)
|
|
905
|
+
const jsonString = JSON.stringify(valueSet);
|
|
906
|
+
if (!jsonString) {
|
|
907
|
+
throw new VCLParseException('Failed to serialize ValueSet to JSON');
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Create hash directly inline to avoid scoping issues
|
|
911
|
+
let hash = 0;
|
|
912
|
+
for (let i = 0; i < jsonString.length; i++) {
|
|
913
|
+
const char = jsonString.charCodeAt(i);
|
|
914
|
+
hash = ((hash << 5) - hash) + char;
|
|
915
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
916
|
+
}
|
|
917
|
+
const hashCode = Math.abs(hash);
|
|
918
|
+
|
|
919
|
+
valueSet.url = `cid:${hashCode}`;
|
|
920
|
+
|
|
921
|
+
return valueSet;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Utility functions
|
|
925
|
+
function validateVCLExpression(vclExpression) {
|
|
926
|
+
if (!vclExpression || vclExpression.trim() === '') {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
// Make sure we call the same parseVCL function that's being exported
|
|
932
|
+
const lexer = new VCLLexer(vclExpression);
|
|
933
|
+
const tokens = lexer.tokenize();
|
|
934
|
+
const parser = new VCLParserClass(tokens, null);
|
|
935
|
+
parser.parse();
|
|
936
|
+
return true;
|
|
937
|
+
} catch (e) {
|
|
938
|
+
// Return false for any parsing error, regardless of type
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function createVCLValueSet(id, name, description) {
|
|
944
|
+
const valueSet = {
|
|
945
|
+
resourceType: 'ValueSet',
|
|
946
|
+
status: 'draft',
|
|
947
|
+
experimental: true,
|
|
948
|
+
compose: {
|
|
949
|
+
include: [],
|
|
950
|
+
exclude: []
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
if (id) valueSet.id = id;
|
|
955
|
+
if (name) valueSet.name = name;
|
|
956
|
+
if (description) valueSet.description = description;
|
|
957
|
+
|
|
958
|
+
return valueSet;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function splitSystemUri(systemUri) {
|
|
962
|
+
const pipePos = systemUri.indexOf('|');
|
|
963
|
+
if (pipePos >= 0) {
|
|
964
|
+
return {
|
|
965
|
+
system: systemUri.substring(0, pipePos),
|
|
966
|
+
version: systemUri.substring(pipePos + 1)
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
system: systemUri,
|
|
971
|
+
version: ''
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function isVCLCompatible(valueSet) {
|
|
976
|
+
if (!valueSet.compose) {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const supportedOps = [
|
|
981
|
+
FilterOperator.EQUAL, FilterOperator.IS_A, FilterOperator.IS_NOT_A,
|
|
982
|
+
FilterOperator.DESCENDENT_OF, FilterOperator.REGEX, FilterOperator.IN,
|
|
983
|
+
FilterOperator.NOT_IN, FilterOperator.GENERALIZES, FilterOperator.CHILD_OF,
|
|
984
|
+
FilterOperator.DESCENDENT_LEAF, FilterOperator.EXISTS
|
|
985
|
+
];
|
|
986
|
+
|
|
987
|
+
// Check includes
|
|
988
|
+
if (valueSet.compose.include) {
|
|
989
|
+
for (const include of valueSet.compose.include) {
|
|
990
|
+
if (include.filter) {
|
|
991
|
+
for (const filter of include.filter) {
|
|
992
|
+
if (!supportedOps.includes(filter.op)) {
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Check excludes
|
|
1001
|
+
if (valueSet.compose.exclude) {
|
|
1002
|
+
for (const exclude of valueSet.compose.exclude) {
|
|
1003
|
+
if (exclude.filter) {
|
|
1004
|
+
for (const filter of exclude.filter) {
|
|
1005
|
+
if (!supportedOps.includes(filter.op)) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Simple hash function for generating IDs
|
|
1017
|
+
function simpleHash(str) {
|
|
1018
|
+
let hash = 0;
|
|
1019
|
+
for (let i = 0; i < str.length; i++) {
|
|
1020
|
+
const char = str.charCodeAt(i);
|
|
1021
|
+
hash = ((hash << 5) - hash) + char;
|
|
1022
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
1023
|
+
}
|
|
1024
|
+
return Math.abs(hash);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Export for different module systems
|
|
1028
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1029
|
+
// Node.js
|
|
1030
|
+
module.exports = {
|
|
1031
|
+
parseVCL,
|
|
1032
|
+
parseVCLAndSetId,
|
|
1033
|
+
validateVCLExpression,
|
|
1034
|
+
createVCLValueSet,
|
|
1035
|
+
splitSystemUri,
|
|
1036
|
+
isVCLCompatible,
|
|
1037
|
+
VCLParseException,
|
|
1038
|
+
TokenType,
|
|
1039
|
+
FilterOperator,
|
|
1040
|
+
// Export classes for debugging
|
|
1041
|
+
VCLLexer,
|
|
1042
|
+
VCLParserClass,
|
|
1043
|
+
// Export utility functions
|
|
1044
|
+
simpleHash
|
|
1045
|
+
};
|
|
1046
|
+
} else if (typeof window !== 'undefined') {
|
|
1047
|
+
// Browser - attach everything to window.VCLParser
|
|
1048
|
+
window.VCLParser = {
|
|
1049
|
+
parseVCL,
|
|
1050
|
+
parseVCLAndSetId,
|
|
1051
|
+
validateVCLExpression,
|
|
1052
|
+
createVCLValueSet,
|
|
1053
|
+
splitSystemUri,
|
|
1054
|
+
isVCLCompatible,
|
|
1055
|
+
VCLParseException,
|
|
1056
|
+
TokenType,
|
|
1057
|
+
FilterOperator,
|
|
1058
|
+
// Export classes for debugging
|
|
1059
|
+
VCLLexer,
|
|
1060
|
+
VCLParserClass,
|
|
1061
|
+
// Export utility functions
|
|
1062
|
+
simpleHash
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
// Also make classes available globally for debugging
|
|
1066
|
+
window.VCLLexer = VCLLexer;
|
|
1067
|
+
window.VCLParseException = VCLParseException;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Examples of usage:
|
|
1071
|
+
/*
|
|
1072
|
+
// Basic parsing
|
|
1073
|
+
const valueSet = parseVCL('(http://snomed.info/sct) 123456789; 987654321');
|
|
1074
|
+
|
|
1075
|
+
// With filters
|
|
1076
|
+
const valueSet2 = parseVCL('(http://snomed.info/sct) 123456789 << 64572001');
|
|
1077
|
+
|
|
1078
|
+
// With version and exclusions
|
|
1079
|
+
const valueSet3 = parseVCL('(http://snomed-alike.info/sct|20210131) * - 123456789');
|
|
1080
|
+
|
|
1081
|
+
// With auto-generated ID
|
|
1082
|
+
const valueSet4 = parseVCLAndSetId('(http://snomed.info/sct) 123456789');
|
|
1083
|
+
|
|
1084
|
+
// Validation
|
|
1085
|
+
if (validateVCLExpression(myExpression)) {
|
|
1086
|
+
const valueSet = parseVCL(myExpression);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// With custom FHIR factory
|
|
1090
|
+
const customFactory = {
|
|
1091
|
+
createValueSet: () => ({
|
|
1092
|
+
resourceType: 'ValueSet',
|
|
1093
|
+
status: 'active', // Different default
|
|
1094
|
+
compose: { include: [], exclude: [] }
|
|
1095
|
+
})
|
|
1096
|
+
};
|
|
1097
|
+
const valueSet5 = parseVCL('(http://snomed.info/sct) 123456789', customFactory);
|
|
1098
|
+
*/
|