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,603 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FHIR XML Base Class
|
|
3
|
+
// Common functionality for all FHIR resource XML serialization/deserialization
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for FHIR XML serialization/deserialization
|
|
8
|
+
* Contains shared methods for converting between FHIR JSON and XML formats
|
|
9
|
+
*/
|
|
10
|
+
class FhirXmlBase {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FHIR elements that should always be arrays in JSON
|
|
14
|
+
* These are elements that are arrays at the TOP LEVEL of resources or in unambiguous contexts
|
|
15
|
+
* @type {Set<string>}
|
|
16
|
+
*/
|
|
17
|
+
static _arrayElements = new Set([
|
|
18
|
+
// Common resource elements
|
|
19
|
+
'identifier',
|
|
20
|
+
'contact',
|
|
21
|
+
'useContext',
|
|
22
|
+
'jurisdiction',
|
|
23
|
+
'extension',
|
|
24
|
+
'modifierExtension',
|
|
25
|
+
|
|
26
|
+
// CodeSystem elements (at resource level)
|
|
27
|
+
'concept', // CodeSystem.concept is array
|
|
28
|
+
'filter', // CodeSystem.filter is array (but ValueSet.compose.include.filter is also array)
|
|
29
|
+
'operator', // CodeSystem.filter.operator is array
|
|
30
|
+
'designation', // concept.designation is array
|
|
31
|
+
|
|
32
|
+
// ValueSet elements
|
|
33
|
+
'include',
|
|
34
|
+
'exclude',
|
|
35
|
+
'contains',
|
|
36
|
+
'parameter',
|
|
37
|
+
'valueSet',
|
|
38
|
+
|
|
39
|
+
// ConceptMap elements
|
|
40
|
+
'group',
|
|
41
|
+
'element',
|
|
42
|
+
'target',
|
|
43
|
+
'dependsOn',
|
|
44
|
+
'product',
|
|
45
|
+
'additionalAttribute',
|
|
46
|
+
|
|
47
|
+
// OperationOutcome elements
|
|
48
|
+
'issue',
|
|
49
|
+
'location',
|
|
50
|
+
'expression',
|
|
51
|
+
|
|
52
|
+
// Parameters elements
|
|
53
|
+
'part',
|
|
54
|
+
|
|
55
|
+
// Common data type elements
|
|
56
|
+
'coding',
|
|
57
|
+
'telecom',
|
|
58
|
+
'address',
|
|
59
|
+
'given',
|
|
60
|
+
'prefix',
|
|
61
|
+
'suffix',
|
|
62
|
+
'line',
|
|
63
|
+
'link',
|
|
64
|
+
'entry',
|
|
65
|
+
|
|
66
|
+
// NamingSystem elements
|
|
67
|
+
'uniqueId',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Elements that are arrays ONLY at resource/backbone level, not inside filters/other contexts
|
|
72
|
+
* 'property' is an array in CodeSystem.property and CodeSystem.concept.property
|
|
73
|
+
* but NOT in ValueSet.compose.include.filter.property (which is a single code)
|
|
74
|
+
* @type {Set<string>}
|
|
75
|
+
*/
|
|
76
|
+
static _resourceLevelArrayElements = new Set([
|
|
77
|
+
'property', // Array in CodeSystem.property and concept.property, but single in filter
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Element names that represent boolean types in FHIR
|
|
82
|
+
* @type {Set<string>}
|
|
83
|
+
*/
|
|
84
|
+
static _booleanElements = new Set([
|
|
85
|
+
'valueBoolean',
|
|
86
|
+
'experimental',
|
|
87
|
+
'caseSensitive',
|
|
88
|
+
'compositional',
|
|
89
|
+
'versionNeeded',
|
|
90
|
+
'inactive',
|
|
91
|
+
'notSelectable',
|
|
92
|
+
'abstract',
|
|
93
|
+
'immutable',
|
|
94
|
+
'lockedDate',
|
|
95
|
+
'preferred',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Element names that represent integer types in FHIR
|
|
100
|
+
* @type {Set<string>}
|
|
101
|
+
*/
|
|
102
|
+
static _integerElements = new Set([
|
|
103
|
+
'valueInteger',
|
|
104
|
+
'valueUnsignedInt',
|
|
105
|
+
'valuePositiveInt',
|
|
106
|
+
'count',
|
|
107
|
+
'offset',
|
|
108
|
+
'total',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Element names that represent decimal types in FHIR
|
|
113
|
+
* @type {Set<string>}
|
|
114
|
+
*/
|
|
115
|
+
static _decimalElements = new Set([
|
|
116
|
+
'valueDecimal',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// ==================== XML PARSING (XML -> JSON) ====================
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert a manually parsed XML element to FHIR JSON format
|
|
123
|
+
* @param {Object} element - Element with {name, attributes, children}
|
|
124
|
+
* @param {string} resourceType - The FHIR resource type
|
|
125
|
+
* @returns {Object} FHIR JSON object
|
|
126
|
+
*/
|
|
127
|
+
static convertElementToFhirJson(element, resourceType) {
|
|
128
|
+
const result = { resourceType };
|
|
129
|
+
|
|
130
|
+
for (const child of element.children) {
|
|
131
|
+
const key = child.name;
|
|
132
|
+
const { value, primitiveExt } = this._convertChildElementWithExt(child, resourceType);
|
|
133
|
+
|
|
134
|
+
// Handle the value
|
|
135
|
+
if (value !== null) {
|
|
136
|
+
if (this._isArrayElement(key, resourceType)) {
|
|
137
|
+
if (!result[key]) {
|
|
138
|
+
result[key] = [];
|
|
139
|
+
}
|
|
140
|
+
result[key].push(value);
|
|
141
|
+
} else {
|
|
142
|
+
result[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle primitive extension (e.g., _value with extension)
|
|
147
|
+
if (primitiveExt !== null) {
|
|
148
|
+
const extKey = '_' + key;
|
|
149
|
+
result[extKey] = primitiveExt;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if an element should be an array based on its name and parent context
|
|
158
|
+
* @param {string} elementName - The element name
|
|
159
|
+
* @param {string} parentContext - The parent element name or context
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
static _isArrayElement(elementName, parentContext) {
|
|
163
|
+
// These are always arrays regardless of context
|
|
164
|
+
if (this._arrayElements.has(elementName)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 'property' is an array in CodeSystem.property, CodeSystem.concept.property
|
|
169
|
+
// but NOT in filter.property (which is a single code)
|
|
170
|
+
if (elementName === 'property') {
|
|
171
|
+
// property is array at resource level or inside concept, but not inside filter
|
|
172
|
+
return parentContext !== 'filter';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Converts a child element to appropriate JSON value, also handling primitive extensions
|
|
180
|
+
* @param {Object} child - Child element with {name, attributes, children}
|
|
181
|
+
* @param {string} parentContext - Parent element name for context-dependent array handling
|
|
182
|
+
* @returns {{value: *, primitiveExt: Object|null}} Converted value and primitive extension if any
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
// eslint-disable-next-line no-unused-vars
|
|
186
|
+
static _convertChildElementWithExt(child, parentContext = '') {
|
|
187
|
+
const hasValue = child.attributes.value !== undefined;
|
|
188
|
+
const hasChildren = child.children.length > 0;
|
|
189
|
+
const isExtensionElement = child.name === 'extension' || child.name === 'modifierExtension';
|
|
190
|
+
|
|
191
|
+
// Extension elements are NEVER primitive extensions - they are always complex elements
|
|
192
|
+
// Only primitive FHIR elements (like string, code, uri, etc.) can have primitive extensions
|
|
193
|
+
if (!isExtensionElement) {
|
|
194
|
+
// Check if children are only extensions (for primitive extension detection)
|
|
195
|
+
const extensionChildren = child.children.filter(
|
|
196
|
+
c => c.name === 'extension' || c.name === 'modifierExtension'
|
|
197
|
+
);
|
|
198
|
+
const nonExtensionChildren = child.children.filter(
|
|
199
|
+
c => c.name !== 'extension' && c.name !== 'modifierExtension'
|
|
200
|
+
);
|
|
201
|
+
const hasOnlyExtensions = hasChildren && nonExtensionChildren.length === 0;
|
|
202
|
+
|
|
203
|
+
// Case 1: Simple primitive with value, no children
|
|
204
|
+
if (hasValue && !hasChildren) {
|
|
205
|
+
return { value: this._convertPrimitiveValue(child.name, child.attributes.value), primitiveExt: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Case 2: Primitive with extension but no value - this is a primitive extension only
|
|
209
|
+
// This ONLY applies when there are NO non-extension children
|
|
210
|
+
if (!hasValue && hasOnlyExtensions) {
|
|
211
|
+
const ext = this._buildExtensionObject(extensionChildren);
|
|
212
|
+
return { value: null, primitiveExt: ext };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Case 3: Primitive with both value and extensions (no other children)
|
|
216
|
+
// This ONLY applies when there are NO non-extension children
|
|
217
|
+
if (hasValue && hasOnlyExtensions) {
|
|
218
|
+
const ext = this._buildExtensionObject(extensionChildren);
|
|
219
|
+
return { value: this._convertPrimitiveValue(child.name, child.attributes.value), primitiveExt: ext };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Case 4: Complex element - process normally (includes extensions as regular children)
|
|
224
|
+
const obj = {};
|
|
225
|
+
const currentContext = child.name; // Use current element name as context for children
|
|
226
|
+
|
|
227
|
+
// Copy non-value attributes (like url for extensions)
|
|
228
|
+
for (const [attrName, attrValue] of Object.entries(child.attributes)) {
|
|
229
|
+
if (attrName !== 'value' && attrName !== 'xmlns') {
|
|
230
|
+
obj[attrName] = attrValue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Process ALL children (including extensions as normal array elements)
|
|
235
|
+
for (const grandchild of child.children) {
|
|
236
|
+
const key = grandchild.name;
|
|
237
|
+
const { value, primitiveExt } = this._convertChildElementWithExt(grandchild, currentContext);
|
|
238
|
+
|
|
239
|
+
// Handle the value
|
|
240
|
+
if (value !== null) {
|
|
241
|
+
if (this._isArrayElement(key, currentContext)) {
|
|
242
|
+
if (!obj[key]) {
|
|
243
|
+
obj[key] = [];
|
|
244
|
+
}
|
|
245
|
+
obj[key].push(value);
|
|
246
|
+
} else if (obj[key] !== undefined) {
|
|
247
|
+
// Convert to array if we see the same key twice
|
|
248
|
+
if (!Array.isArray(obj[key])) {
|
|
249
|
+
obj[key] = [obj[key]];
|
|
250
|
+
}
|
|
251
|
+
obj[key].push(value);
|
|
252
|
+
} else {
|
|
253
|
+
obj[key] = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Handle primitive extension
|
|
258
|
+
if (primitiveExt !== null) {
|
|
259
|
+
const extKey = '_' + key;
|
|
260
|
+
obj[extKey] = primitiveExt;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { value: Object.keys(obj).length > 0 ? obj : null, primitiveExt: null };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Build an extension object from extension children
|
|
269
|
+
* @param {Array} extensionChildren - Array of extension child elements
|
|
270
|
+
* @returns {Object} Extension object with extension/modifierExtension arrays
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
static _buildExtensionObject(extensionChildren) {
|
|
274
|
+
const ext = {};
|
|
275
|
+
for (const extChild of extensionChildren) {
|
|
276
|
+
const key = extChild.name;
|
|
277
|
+
const { value } = this._convertChildElementWithExt(extChild);
|
|
278
|
+
if (!ext[key]) {
|
|
279
|
+
ext[key] = [];
|
|
280
|
+
}
|
|
281
|
+
ext[key].push(value);
|
|
282
|
+
}
|
|
283
|
+
return ext;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Convert a primitive value to the appropriate JavaScript type
|
|
288
|
+
* @param {string} elementName - The element name
|
|
289
|
+
* @param {string} value - The string value from XML
|
|
290
|
+
* @returns {*} Converted value
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
static _convertPrimitiveValue(elementName, value) {
|
|
294
|
+
if (this._booleanElements.has(elementName)) {
|
|
295
|
+
return value === 'true';
|
|
296
|
+
}
|
|
297
|
+
if (this._integerElements.has(elementName)) {
|
|
298
|
+
return parseInt(value, 10);
|
|
299
|
+
}
|
|
300
|
+
if (this._decimalElements.has(elementName)) {
|
|
301
|
+
return parseFloat(value);
|
|
302
|
+
}
|
|
303
|
+
// Everything else stays as string
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Simple child element conversion (without tracking primitive extensions)
|
|
309
|
+
* @param {Object} child - Child element with {name, attributes, children}
|
|
310
|
+
* @param {string} parentContext - Parent context for array handling
|
|
311
|
+
* @returns {*} Converted value
|
|
312
|
+
*/
|
|
313
|
+
static convertChildElement(child, parentContext = '') {
|
|
314
|
+
return this._convertChildElementWithExt(child, parentContext).value;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ==================== XML GENERATION (JSON -> XML) ====================
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get the FHIR namespace
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
323
|
+
static getNamespace() {
|
|
324
|
+
return 'http://hl7.org/fhir';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get indentation string
|
|
329
|
+
* @param {number} level - Indentation level
|
|
330
|
+
* @returns {string}
|
|
331
|
+
*/
|
|
332
|
+
static indent(level) {
|
|
333
|
+
return ' '.repeat(level);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Escape special characters for XML
|
|
338
|
+
* @param {*} value - Value to escape
|
|
339
|
+
* @returns {string}
|
|
340
|
+
*/
|
|
341
|
+
static escapeXml(value) {
|
|
342
|
+
if (value === null || value === undefined) return '';
|
|
343
|
+
return String(value)
|
|
344
|
+
.replace(/&/g, '&')
|
|
345
|
+
.replace(/</g, '<')
|
|
346
|
+
.replace(/>/g, '>')
|
|
347
|
+
.replace(/"/g, '"')
|
|
348
|
+
.replace(/'/g, ''');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Unescape XML entities
|
|
353
|
+
* @param {string} str - String to unescape
|
|
354
|
+
* @returns {string}
|
|
355
|
+
*/
|
|
356
|
+
static unescapeXml(str) {
|
|
357
|
+
return str
|
|
358
|
+
.replace(/&/g, '&')
|
|
359
|
+
.replace(/</g, '<')
|
|
360
|
+
.replace(/>/g, '>')
|
|
361
|
+
.replace(/"/g, '"')
|
|
362
|
+
.replace(/'/g, "'");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Render an element to XML
|
|
367
|
+
* @param {string} name - Element name
|
|
368
|
+
* @param {*} value - Element value
|
|
369
|
+
* @param {number} level - Indentation level
|
|
370
|
+
* @returns {string} XML string
|
|
371
|
+
*/
|
|
372
|
+
static renderElement(name, value, level) {
|
|
373
|
+
if (value === null || value === undefined) {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let xml = '';
|
|
378
|
+
|
|
379
|
+
if (Array.isArray(value)) {
|
|
380
|
+
for (const item of value) {
|
|
381
|
+
xml += this.renderElement(name, item, level);
|
|
382
|
+
}
|
|
383
|
+
} else if (typeof value === 'object') {
|
|
384
|
+
// Special handling for extension - url is an attribute
|
|
385
|
+
if (name === 'extension' || name === 'modifierExtension') {
|
|
386
|
+
const url = value.url ? ` url="${this.escapeXml(value.url)}"` : '';
|
|
387
|
+
xml += `${this.indent(level)}<${name}${url}>\n`;
|
|
388
|
+
xml += this.renderObject(value, level + 1, ['url']);
|
|
389
|
+
xml += `${this.indent(level)}</${name}>\n`;
|
|
390
|
+
} else {
|
|
391
|
+
xml += `${this.indent(level)}<${name}>\n`;
|
|
392
|
+
xml += this.renderObject(value, level + 1);
|
|
393
|
+
xml += `${this.indent(level)}</${name}>\n`;
|
|
394
|
+
}
|
|
395
|
+
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
|
396
|
+
xml += `${this.indent(level)}<${name} value="${value}"/>\n`;
|
|
397
|
+
} else {
|
|
398
|
+
xml += `${this.indent(level)}<${name} value="${this.escapeXml(value)}"/>\n`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return xml;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Render an object's properties to XML
|
|
406
|
+
* @param {Object} obj - Object to render
|
|
407
|
+
* @param {number} level - Indentation level
|
|
408
|
+
* @param {Array<string>} skipKeys - Keys to skip
|
|
409
|
+
* @returns {string} XML string
|
|
410
|
+
*/
|
|
411
|
+
static renderObject(obj, level, skipKeys = []) {
|
|
412
|
+
let xml = '';
|
|
413
|
+
|
|
414
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
415
|
+
// Skip primitive extension keys (handled separately)
|
|
416
|
+
if (key.startsWith('_')) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (skipKeys.includes(key)) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
xml += this.renderElement(key, value, level);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return xml;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Render elements in a specific order
|
|
430
|
+
* @param {Object} obj - Object to render
|
|
431
|
+
* @param {number} level - Indentation level
|
|
432
|
+
* @param {Array<string>} elementOrder - Ordered list of element names
|
|
433
|
+
* @returns {string} XML string
|
|
434
|
+
*/
|
|
435
|
+
static renderElementsInOrder(obj, level, elementOrder) {
|
|
436
|
+
let xml = '';
|
|
437
|
+
|
|
438
|
+
// Process elements in order
|
|
439
|
+
for (const key of elementOrder) {
|
|
440
|
+
if (Object.hasOwn(obj, key) && key !== 'resourceType') {
|
|
441
|
+
xml += this.renderElement(key, obj[key], level);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Process any remaining elements not in the order list
|
|
446
|
+
for (const key of Object.keys(obj)) {
|
|
447
|
+
if (!elementOrder.includes(key) && key !== 'resourceType' && !key.startsWith('_')) {
|
|
448
|
+
xml += this.renderElement(key, obj[key], level);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return xml;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Generate XML declaration and root element wrapper
|
|
457
|
+
* @param {string} resourceType - FHIR resource type
|
|
458
|
+
* @param {string} content - Inner XML content
|
|
459
|
+
* @returns {string} Complete XML document
|
|
460
|
+
*/
|
|
461
|
+
static wrapInRootElement(resourceType, content) {
|
|
462
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
463
|
+
xml += `<${resourceType} xmlns="${this.getNamespace()}">\n`;
|
|
464
|
+
xml += content;
|
|
465
|
+
xml += `</${resourceType}>`;
|
|
466
|
+
return xml;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ==================== XML STRING PARSING ====================
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Manual XML parser - parses XML string to element tree
|
|
473
|
+
*/
|
|
474
|
+
static parseXmlString(xml) {
|
|
475
|
+
const parser = new FhirXmlParser(xml);
|
|
476
|
+
return parser.parse();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Simple XML parser for FHIR resources
|
|
482
|
+
* Parses XML string into {name, attributes, children} structure
|
|
483
|
+
*/
|
|
484
|
+
class FhirXmlParser {
|
|
485
|
+
constructor(xml) {
|
|
486
|
+
this.xml = xml;
|
|
487
|
+
this.pos = 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
parse() {
|
|
491
|
+
this._skipDeclaration();
|
|
492
|
+
this._skipWhitespace();
|
|
493
|
+
return this._parseElement();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_skipDeclaration() {
|
|
497
|
+
this._skipWhitespace();
|
|
498
|
+
if (this.xml.substring(this.pos, this.pos + 5) === '<?xml') {
|
|
499
|
+
const end = this.xml.indexOf('?>', this.pos);
|
|
500
|
+
if (end !== -1) {
|
|
501
|
+
this.pos = end + 2;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_skipWhitespace() {
|
|
507
|
+
while (this.pos < this.xml.length && /\s/.test(this.xml[this.pos])) {
|
|
508
|
+
this.pos++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_parseElement() {
|
|
513
|
+
this._skipWhitespace();
|
|
514
|
+
|
|
515
|
+
if (this.xml[this.pos] !== '<') {
|
|
516
|
+
throw new Error(`Expected '<' at position ${this.pos}`);
|
|
517
|
+
}
|
|
518
|
+
this.pos++; // Skip '<'
|
|
519
|
+
|
|
520
|
+
// Parse element name
|
|
521
|
+
const nameEnd = this.xml.substring(this.pos).search(/[\s/>]/);
|
|
522
|
+
const name = this.xml.substring(this.pos, this.pos + nameEnd);
|
|
523
|
+
this.pos += nameEnd;
|
|
524
|
+
|
|
525
|
+
// Parse attributes
|
|
526
|
+
const attributes = {};
|
|
527
|
+
this._skipWhitespace();
|
|
528
|
+
|
|
529
|
+
while (this.pos < this.xml.length && this.xml[this.pos] !== '>' && this.xml[this.pos] !== '/') {
|
|
530
|
+
const attr = this._parseAttribute();
|
|
531
|
+
if (attr) {
|
|
532
|
+
attributes[attr.name] = attr.value;
|
|
533
|
+
}
|
|
534
|
+
this._skipWhitespace();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const children = [];
|
|
538
|
+
|
|
539
|
+
// Self-closing tag
|
|
540
|
+
if (this.xml[this.pos] === '/') {
|
|
541
|
+
this.pos += 2; // Skip '/>'
|
|
542
|
+
return { name, attributes, children };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.pos++; // Skip '>'
|
|
546
|
+
|
|
547
|
+
// Parse children
|
|
548
|
+
while (this.pos < this.xml.length) {
|
|
549
|
+
this._skipWhitespace();
|
|
550
|
+
|
|
551
|
+
if (this.xml.substring(this.pos, this.pos + 2) === '</') {
|
|
552
|
+
// Closing tag
|
|
553
|
+
const closeEnd = this.xml.indexOf('>', this.pos);
|
|
554
|
+
this.pos = closeEnd + 1;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (this.xml[this.pos] === '<') {
|
|
559
|
+
// Check for comment
|
|
560
|
+
if (this.xml.substring(this.pos, this.pos + 4) === '<!--') {
|
|
561
|
+
const commentEnd = this.xml.indexOf('-->', this.pos);
|
|
562
|
+
this.pos = commentEnd + 3;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
children.push(this._parseElement());
|
|
567
|
+
} else {
|
|
568
|
+
// Text content - skip for FHIR as values are in attributes
|
|
569
|
+
const textEnd = this.xml.indexOf('<', this.pos);
|
|
570
|
+
this.pos = textEnd;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return { name, attributes, children };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_parseAttribute() {
|
|
578
|
+
this._skipWhitespace();
|
|
579
|
+
|
|
580
|
+
if (this.xml[this.pos] === '>' || this.xml[this.pos] === '/') {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Parse attribute name
|
|
585
|
+
const eqPos = this.xml.indexOf('=', this.pos);
|
|
586
|
+
const name = this.xml.substring(this.pos, eqPos).trim();
|
|
587
|
+
this.pos = eqPos + 1;
|
|
588
|
+
|
|
589
|
+
// Skip whitespace and opening quote
|
|
590
|
+
this._skipWhitespace();
|
|
591
|
+
const quote = this.xml[this.pos];
|
|
592
|
+
this.pos++;
|
|
593
|
+
|
|
594
|
+
// Parse attribute value
|
|
595
|
+
const valueEnd = this.xml.indexOf(quote, this.pos);
|
|
596
|
+
const value = FhirXmlBase.unescapeXml(this.xml.substring(this.pos, valueEnd));
|
|
597
|
+
this.pos = valueEnd + 1;
|
|
598
|
+
|
|
599
|
+
return { name, value };
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
module.exports = { FhirXmlBase, FhirXmlParser };
|