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.
Files changed (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
package/tx/tx-html.js ADDED
@@ -0,0 +1,1063 @@
1
+ //
2
+ // TX HTML Rendering Module
3
+ //
4
+ // Renders FHIR resources as HTML for browser clients
5
+ //
6
+
7
+ const path = require('path');
8
+ const htmlServer = require('../library/html-server');
9
+ const Logger = require('../library/logger');
10
+ const packageJson = require("../package.json");
11
+
12
+ const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
13
+
14
+ const TEMPLATE_PATH = path.join(__dirname, 'html', 'tx-template.html');
15
+
16
+ // Search parameters for the search form
17
+ const SEARCH_PARAMS = [
18
+ { name: 'url', type: 'text', label: 'URL' },
19
+ { name: 'version', type: 'text', label: 'Version' },
20
+ { name: 'name', type: 'text', label: 'Name' },
21
+ { name: 'title', type: 'text', label: 'Title' },
22
+ { name: 'status', type: 'select', label: 'Status', options: ['', 'draft', 'active', 'retired', 'unknown'] },
23
+ { name: 'publisher', type: 'text', label: 'Publisher' },
24
+ { name: 'description', type: 'text', label: 'Description' },
25
+ { name: 'identifier', type: 'text', label: 'Identifier' },
26
+ { name: 'jurisdiction', type: 'text', label: 'Jurisdiction' },
27
+ { name: 'date', type: 'text', label: 'Date' }
28
+ ];
29
+
30
+ const CODESYSTEM_PARAMS = [
31
+ ...SEARCH_PARAMS,
32
+ { name: 'content-mode', type: 'select', label: 'Content Mode', options: ['', 'not-present', 'example', 'fragment', 'complete', 'supplement'] },
33
+ { name: 'supplements', type: 'text', label: 'Supplements' },
34
+ { name: 'system', type: 'text', label: 'System' }
35
+ ];
36
+
37
+ const SORT_OPTIONS = ['', 'id', 'url', 'version', 'date', 'name', 'vurl'];
38
+
39
+ const ELEMENT_OPTIONS = ['id', 'url', 'version', 'name', 'title', 'status', 'date', 'publisher', 'description'];
40
+
41
+ /**
42
+ * Load the TX HTML template
43
+ */
44
+ function loadTemplate() {
45
+ try {
46
+ const templateLoaded = htmlServer.loadTemplate('tx', TEMPLATE_PATH);
47
+ if (!templateLoaded) {
48
+ txHtmlLog.error('Failed to load TX HTML template');
49
+ }
50
+ } catch (error) {
51
+ txHtmlLog.error(`Failed to load TX HTML template: ${error.message}`);
52
+ }
53
+ }
54
+
55
+
56
+ class TxHtmlRenderer {
57
+ renderer;
58
+ liquid;
59
+
60
+ constructor(renderer, liquid) {
61
+ this.renderer = renderer;
62
+ this.liquid = liquid;
63
+ }
64
+
65
+ /**
66
+ * Escape HTML special characters
67
+ */
68
+ escapeHtml(text) {
69
+ if (text === null || text === undefined) {
70
+ return '';
71
+ }
72
+ if (typeof text !== 'string') {
73
+ return String(text);
74
+ }
75
+
76
+ const map = {
77
+ '&': '&',
78
+ '<': '&lt;',
79
+ '>': '&gt;',
80
+ '"': '&quot;',
81
+ "'": '&#x27;'
82
+ };
83
+
84
+ return text.replace(/[&<>"']/g, m => map[m]);
85
+ }
86
+
87
+ /**
88
+ * Render a page with the TX template
89
+ */
90
+ renderPage(title, content, endpoint, startTime) {
91
+ const options = {
92
+ version: packageJson.version,
93
+ endpointpath: endpoint.path,
94
+ fhirversion: endpoint.fhirVersion,
95
+ ms: Date.now() - startTime
96
+ };
97
+
98
+ return htmlServer.renderPage('tx', title, content, options);
99
+ }
100
+
101
+ /**
102
+ * Check if request accepts HTML
103
+ */
104
+ acceptsHtml(req) {
105
+ const accept = req.headers.accept || '';
106
+ return accept.includes('text/html');
107
+ }
108
+
109
+ /**
110
+ * Build page title from JSON response
111
+ */
112
+ buildTitle(json, req) {
113
+ if (req.path == "/") {
114
+ return "Server Home";
115
+ } else {
116
+ const resourceType = json.resourceType || 'Response';
117
+
118
+ if (resourceType === 'Bundle' && json.type === 'searchset') {
119
+ // Extract the resource type being searched from self link or entries
120
+ const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
121
+ const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\?/);
122
+ if (typeMatch) {
123
+ return `Search: ${typeMatch[1]}`;
124
+ }
125
+ const firstEntry = json.entry?.[0]?.resource;
126
+ const searchedType = firstEntry?.resourceType || 'Resources';
127
+ return `Search: ${searchedType}`;
128
+ }
129
+
130
+ if (resourceType === 'OperationOutcome') {
131
+ const severity = json.issue?.[0]?.severity || 'info';
132
+ return `${severity.charAt(0).toUpperCase() + severity.slice(1)}`;
133
+ }
134
+
135
+ if (json.id) {
136
+ return `${resourceType}/${json.id}`;
137
+ }
138
+
139
+ if (json.name) {
140
+ return `${resourceType}: ${json.name}`;
141
+ }
142
+
143
+ return resourceType;
144
+ }
145
+ }
146
+
147
+ // eslint-disable-next-line no-unused-vars
148
+ async buildSearchForm(req, mode, params) {
149
+ const html = await this.liquid.renderFile('search-form', { baseUrl: this.escapeHtml(req.baseUrl) });
150
+ return html;
151
+ }
152
+
153
+ async buildHomePage(req) {
154
+ const provider = req.txProvider;
155
+
156
+ let html = '';
157
+
158
+ // ===== Summary Section =====
159
+
160
+ // Calculate uptime
161
+ const uptimeMs = Date.now() - provider.startTime;
162
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
163
+ const uptimeDays = Math.floor(uptimeSeconds / 86400);
164
+ const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600);
165
+ const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60);
166
+ const uptimeSecs = uptimeSeconds % 60;
167
+ let uptimeStr = '';
168
+ if (uptimeDays > 0) uptimeStr += `${uptimeDays}d `;
169
+ if (uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeHours}h `;
170
+ if (uptimeMinutes > 0 || uptimeHours > 0 || uptimeDays > 0) uptimeStr += `${uptimeMinutes}m `;
171
+ uptimeStr += `${uptimeSecs}s`;
172
+
173
+ // Memory usage
174
+ const memUsage = process.memoryUsage();
175
+ const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
176
+ const heapTotalMB = (memUsage.heapTotal / 1024 / 1024).toFixed(2);
177
+ const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
178
+
179
+ html += '<table class="grid">';
180
+ html += '<tr>';
181
+ html += `<td><strong>FHIR Version:</strong> ${this.escapeHtml(provider.getFhirVersion())}</td>`;
182
+ html += `<td><strong>Uptime:</strong> ${this.escapeHtml(uptimeStr)}</td>`;
183
+ html += `<td><strong>Request Count:</strong> ${provider.requestCount}</td>`;
184
+ html += '</tr>';
185
+ html += '<tr>';
186
+ html += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
187
+ html += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
188
+ html += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
189
+ html += '</tr>';
190
+
191
+ // Count unique code systems
192
+ const uniqueFactorySystems = new Set();
193
+ for (const factory of provider.codeSystemFactories.values()) {
194
+ uniqueFactorySystems.add(factory.system());
195
+ }
196
+ const uniqueCodeSystems = new Set();
197
+ for (const cs of provider.codeSystems.values()) {
198
+ uniqueCodeSystems.add(cs.url);
199
+ }
200
+ html += '<tr>';
201
+ html += `<td><strong>CodeSystem #:</strong> ${new Set([...uniqueFactorySystems, ...uniqueCodeSystems]).size}</td>`;
202
+
203
+ // Count value sets
204
+ let totalValueSets = 0;
205
+ for (const vsp of provider.valueSetProviders) {
206
+ totalValueSets += vsp.vsCount();
207
+ }
208
+ html += `<td><strong>ValueSet #:</strong> ${totalValueSets || 'Unknown'}</td>`;
209
+
210
+ let totalConceptMaps = 0;
211
+ for (const cmp of provider.conceptMapProviders) {
212
+ totalConceptMaps += cmp.cmCount();
213
+ }
214
+ html += `<td><strong>ConceptMap #:</strong> ${totalConceptMaps || 'Unknown'}</td>`;
215
+ html += '</tr>';
216
+ html += '</table>';
217
+
218
+ html += '<hr/>';
219
+ html += await this.buildSearchForm(req);
220
+
221
+ // ===== Packages and Factories Section =====
222
+ html += '<hr/><h3>Content Sources &amp; Code System Factories</h3>';
223
+
224
+ // List content sources
225
+ html += '<h6>Content Sources</h6>';
226
+ if (provider.contentSources && provider.contentSources.length > 0) {
227
+ const sorted = [...provider.contentSources].sort();
228
+ html += '<ul>';
229
+ for (const source of sorted) {
230
+ html += `<li>${this.escapeHtml(source)}</li>`;
231
+ }
232
+ html += '</ul>';
233
+ } else {
234
+ html += '<p><em>No content sources available</em></p>';
235
+ }
236
+
237
+ // Code System Factories table
238
+ // Code System Factories table
239
+ html += '<h6 class="mt-4">External CodeSystems</h6>';
240
+ html += '<table class="grid">';
241
+ html += '<thead><tr><th>Name</th><th>URI</th><th>Version</th><th>Use Count</th></tr></thead>';
242
+ html += '<tbody>';
243
+
244
+ // Deduplicate factories and sort by system URL
245
+ const seenFactories = new Set();
246
+ const uniqueFactories = [];
247
+ for (const factory of provider.codeSystemFactories.values()) {
248
+ const key = factory.system() + '|' + (factory.version() || '');
249
+ if (!seenFactories.has(key)) {
250
+ seenFactories.add(key);
251
+ uniqueFactories.push(factory);
252
+ }
253
+ }
254
+ uniqueFactories.sort((a, b) => a.name().localeCompare(b.name()));
255
+
256
+ for (const factory of uniqueFactories) {
257
+ html += '<tr>';
258
+ html += `<td>${this.escapeHtml(factory.name())}</td>`;
259
+ html += `<td>${this.escapeHtml(factory.system())}</td>`;
260
+ html += `<td>${this.escapeHtml(factory.version() || '-')}</td>`;
261
+ html += `<td>${factory.useCount ? factory.useCount() : '-'}</td>`;
262
+ html += '</tr>';
263
+ }
264
+
265
+ html += '</tbody></table>';
266
+ html += '</div></div>';
267
+
268
+ return html;
269
+ }
270
+
271
+ /**
272
+ * Main render - determines what to render based on resource type
273
+ */
274
+ async render(json, req, inBundle = false) {
275
+ if (req && req.path == "/") {
276
+ return await this.buildHomePage(req);
277
+ } else {
278
+ try {
279
+ const resourceType = json.resourceType;
280
+
281
+ switch (resourceType) {
282
+ case 'Parameters':
283
+ return await this.renderParameters(json);
284
+ case 'CodeSystem':
285
+ return await this.renderCodeSystem(json, inBundle);
286
+ case 'ValueSet':
287
+ return await this.renderValueSet(json, inBundle);
288
+ case 'ConceptMap':
289
+ return await this.renderConceptMap(json, inBundle);
290
+ case 'CapabilityStatement':
291
+ return await this.renderCapabilityStatement(json, inBundle);
292
+ case 'TerminologyCapabilities':
293
+ return await this.renderTerminologyCapabilities(json, inBundle);
294
+ case 'Bundle':
295
+ return await this.renderBundle(json, req, inBundle);
296
+ case 'OperationOutcome':
297
+ return await this.renderOperationOutcome(json, req);
298
+ case 'Operations':
299
+ return await this.renderOperationsForm(json, req);
300
+ default:
301
+ return await this.renderGeneric(json, inBundle);
302
+ }
303
+ } catch (error) {
304
+ console.error(error);
305
+ throw error;
306
+ }
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Render Parameters resource
312
+ */
313
+ async renderParameters(json) {
314
+ let html = '<table class="table grid">';
315
+ html += '<thead><tr><th>Name</th><th>Value</th></tr></thead>';
316
+ html += '<tbody>';
317
+
318
+ if (json.parameter && Array.isArray(json.parameter)) {
319
+ for (const param of json.parameter) {
320
+ html += await this.renderParameter(param);
321
+ }
322
+ }
323
+
324
+ html += '</tbody></table>';
325
+
326
+ // Collapsible JSON source
327
+ const resourceId = this.generateResourceId();
328
+ html += '<div class="json-source">';
329
+ html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
330
+ html += 'Show JSON Source</button>';
331
+ html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
332
+ html += `<pre>${this.escapeHtml(JSON.stringify(json, null, 2))}</pre>`;
333
+ html += '</div>';
334
+ html += '</div>';
335
+
336
+ return html;
337
+ }
338
+
339
+ /**
340
+ * Render a single parameter row
341
+ */
342
+ async renderParameter(param) {
343
+ let html = '<tr>';
344
+ html += `<td>${this.escapeHtml(param.name || '')}</td>`;
345
+ html += '<td>';
346
+ html += await this.renderParameterValue(param);
347
+ html += '</td>';
348
+ html += '</tr>';
349
+ return html;
350
+ }
351
+
352
+ /**
353
+ * Render the value portion of a parameter
354
+ */
355
+ async renderParameterValue(param) {
356
+ // Check for parts (nested parameters)
357
+ if (param.part && Array.isArray(param.part)) {
358
+ let html = '<ul>';
359
+ for (const part of param.part) {
360
+ html += '<li>';
361
+ html += `<strong>${this.escapeHtml(part.name || '')}:</strong> `;
362
+ html += await this.renderParameterValue(part);
363
+ html += '</li>';
364
+ }
365
+ html += '</ul>';
366
+ return html;
367
+ }
368
+
369
+ // Check for resource
370
+ if (param.resource) {
371
+ return await this.render(param.resource, null, true);
372
+ }
373
+
374
+ // Check for complex datatypes
375
+ if (param.valueCoding) {
376
+ return this.renderCoding(param.valueCoding);
377
+ }
378
+ if (param.valueCodeableConcept) {
379
+ return this.renderCodeableConcept(param.valueCodeableConcept);
380
+ }
381
+ if (param.valueQuantity) {
382
+ return this.renderQuantity(param.valueQuantity);
383
+ }
384
+ if (param.valueAttachment) {
385
+ return this.renderAttachment(param.valueAttachment);
386
+ }
387
+ if (param.valueIdentifier) {
388
+ return this.renderIdentifier(param.valueIdentifier);
389
+ }
390
+ if (param.valuePeriod) {
391
+ return this.renderPeriod(param.valuePeriod);
392
+ }
393
+
394
+ // Primitive types
395
+ if (param.valueString !== undefined) {
396
+ return this.escapeHtml(param.valueString);
397
+ }
398
+ if (param.valueBoolean !== undefined) {
399
+ return param.valueBoolean ? 'true' : 'false';
400
+ }
401
+ if (param.valueInteger !== undefined) {
402
+ return this.escapeHtml(String(param.valueInteger));
403
+ }
404
+ if (param.valueDecimal !== undefined) {
405
+ return this.escapeHtml(String(param.valueDecimal));
406
+ }
407
+ if (param.valueUri !== undefined) {
408
+ return this.escapeHtml(param.valueUri);
409
+ }
410
+ if (param.valueUrl !== undefined) {
411
+ return this.escapeHtml(param.valueUrl);
412
+ }
413
+ if (param.valueCanonical !== undefined) {
414
+ return this.escapeHtml(param.valueCanonical);
415
+ }
416
+ if (param.valueCode !== undefined) {
417
+ return `<code>${this.escapeHtml(param.valueCode)}</code>`;
418
+ }
419
+ if (param.valueDate !== undefined) {
420
+ return this.escapeHtml(param.valueDate);
421
+ }
422
+ if (param.valueDateTime !== undefined) {
423
+ return this.escapeHtml(param.valueDateTime);
424
+ }
425
+ if (param.valueTime !== undefined) {
426
+ return this.escapeHtml(param.valueTime);
427
+ }
428
+ if (param.valueInstant !== undefined) {
429
+ return this.escapeHtml(param.valueInstant);
430
+ }
431
+
432
+ return '<em>(empty)</em>';
433
+ }
434
+
435
+ /**
436
+ * Render Coding datatype
437
+ */
438
+ async renderCoding(coding) {
439
+ if (!coding) return '';
440
+
441
+ let parts = [];
442
+ if (coding.system) {
443
+ parts.push(this.escapeHtml(coding.system));
444
+ }
445
+ if (coding.code) {
446
+ parts.push(`<code>${this.escapeHtml(coding.code)}</code>`);
447
+ }
448
+ if (coding.display) {
449
+ parts.push(`"${this.escapeHtml(coding.display)}"`);
450
+ }
451
+ if (coding.version) {
452
+ parts.push(`(version: ${this.escapeHtml(coding.version)})`);
453
+ }
454
+
455
+ return parts.join(' | ') || '<em>(empty coding)</em>';
456
+ }
457
+
458
+ /**
459
+ * Render CodeableConcept datatype
460
+ */
461
+ async renderCodeableConcept(cc) {
462
+ if (!cc) return '';
463
+
464
+ let html = '';
465
+
466
+ if (cc.text) {
467
+ html += `<strong>${this.escapeHtml(cc.text)}</strong>`;
468
+ }
469
+
470
+ if (cc.coding && Array.isArray(cc.coding) && cc.coding.length > 0) {
471
+ if (cc.text) html += '<br/>';
472
+ html += '<ul style="margin: 0; padding-left: 20px;">';
473
+ for (const coding of cc.coding) {
474
+ html += `<li>${this.renderCoding(coding)}</li>`;
475
+ }
476
+ html += '</ul>';
477
+ }
478
+
479
+ return html || '<em>(empty CodeableConcept)</em>';
480
+ }
481
+
482
+ /**
483
+ * Render Quantity datatype
484
+ */
485
+ async renderQuantity(qty) {
486
+ if (!qty) return '';
487
+
488
+ let html = '';
489
+
490
+ if (qty.comparator) {
491
+ html += this.escapeHtml(qty.comparator) + ' ';
492
+ }
493
+ if (qty.value !== undefined) {
494
+ html += this.escapeHtml(String(qty.value));
495
+ }
496
+ if (qty.unit) {
497
+ html += ' ' + this.escapeHtml(qty.unit);
498
+ } else if (qty.code) {
499
+ html += ' ' + this.escapeHtml(qty.code);
500
+ }
501
+ if (qty.system) {
502
+ html += ` <small>(${this.escapeHtml(qty.system)})</small>`;
503
+ }
504
+
505
+ return html || '<em>(empty Quantity)</em>';
506
+ }
507
+
508
+ /**
509
+ * Render Attachment datatype
510
+ */
511
+ async renderAttachment(att) {
512
+ if (!att) return '';
513
+
514
+ let html = '';
515
+
516
+ if (att.title) {
517
+ html += `<strong>${this.escapeHtml(att.title)}</strong><br/>`;
518
+ }
519
+ if (att.contentType) {
520
+ html += `Content-Type: ${this.escapeHtml(att.contentType)}<br/>`;
521
+ }
522
+ if (att.url) {
523
+ html += `URL: <a href="${this.escapeHtml(att.url)}">${this.escapeHtml(att.url)}</a><br/>`;
524
+ }
525
+ if (att.size !== undefined) {
526
+ html += `Size: ${this.escapeHtml(String(att.size))} bytes<br/>`;
527
+ }
528
+ if (att.language) {
529
+ html += `Language: ${this.escapeHtml(att.language)}<br/>`;
530
+ }
531
+ if (att.data) {
532
+ html += `<small>(base64 data present, ${att.data.length} chars)</small>`;
533
+ }
534
+
535
+ return html || '<em>(empty Attachment)</em>';
536
+ }
537
+
538
+ /**
539
+ * Render Identifier datatype
540
+ */
541
+ async renderIdentifier(id) {
542
+ if (!id) return '';
543
+
544
+ let parts = [];
545
+
546
+ if (id.use) {
547
+ parts.push(`[${this.escapeHtml(id.use)}]`);
548
+ }
549
+ if (id.type && id.type.text) {
550
+ parts.push(this.escapeHtml(id.type.text));
551
+ }
552
+ if (id.system) {
553
+ parts.push(this.escapeHtml(id.system));
554
+ }
555
+ if (id.value) {
556
+ parts.push(`<strong>${this.escapeHtml(id.value)}</strong>`);
557
+ }
558
+ if (id.period) {
559
+ parts.push(this.renderPeriod(id.period));
560
+ }
561
+
562
+ return parts.join(' | ') || '<em>(empty Identifier)</em>';
563
+ }
564
+
565
+ /**
566
+ * Render Period datatype
567
+ */
568
+ async renderPeriod(period) {
569
+ if (!period) return '';
570
+
571
+ let html = '';
572
+
573
+ if (period.start && period.end) {
574
+ html = `${this.escapeHtml(period.start)} to ${this.escapeHtml(period.end)}`;
575
+ } else if (period.start) {
576
+ html = `from ${this.escapeHtml(period.start)}`;
577
+ } else if (period.end) {
578
+ html = `until ${this.escapeHtml(period.end)}`;
579
+ }
580
+
581
+ return html || '<em>(empty Period)</em>';
582
+ }
583
+
584
+ /**
585
+ * Render CodeSystem resource
586
+ */
587
+ async renderCodeSystem(json, inBundle) {
588
+ let html = await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
589
+
590
+ if (!inBundle) {
591
+ html += await this.liquid.renderFile('codesystem-operations', {
592
+ opsId: this.generateResourceId(),
593
+ url: this.escapeHtml(json.url || '')
594
+ });
595
+ }
596
+
597
+ return html;
598
+ }
599
+
600
+ /**
601
+ * Render ValueSet resource
602
+ */
603
+ async renderValueSet(json, inBundle) {
604
+ let html = await this.renderResourceWithNarrative(json, await this.renderer.renderValueSet(json));
605
+
606
+ if (!inBundle) {
607
+ html += await this.liquid.renderFile('valueset-operations', {
608
+ opsId: this.generateResourceId(),
609
+ vcSystemId: this.generateResourceId(),
610
+ inferSystemId: this.generateResourceId(),
611
+ url: this.escapeHtml(json.url || '')
612
+ });
613
+ }
614
+
615
+ return html;
616
+ }
617
+
618
+ /**
619
+ * Render ConceptMap resource
620
+ */
621
+ // eslint-disable-next-line no-unused-vars
622
+ async renderConceptMap(json, inBundle) {
623
+ return this.renderResourceWithNarrative(json);
624
+ }
625
+
626
+ /**
627
+ * Render CapabilityStatement resource
628
+ */
629
+ // eslint-disable-next-line no-unused-vars
630
+ async renderCapabilityStatement(json, inBundle) {
631
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderCapabilityStatement(json));
632
+ }
633
+
634
+ // eslint-disable-next-line no-unused-vars
635
+ async renderTerminologyCapabilities(json, inBundle) {
636
+ return await this.renderResourceWithNarrative(json, await this.renderer.renderTerminologyCapabilities(json));
637
+ }
638
+
639
+ /**
640
+ * Render OperationOutcome resource
641
+ */
642
+ async renderOperationOutcome(json) {
643
+ let html = '<div class="operation-outcome">';
644
+ html += `<h4>OperationOutcome</h4>`;
645
+
646
+ if (json.issue && Array.isArray(json.issue)) {
647
+ for (const issue of json.issue) {
648
+ html += '<div class="alert ';
649
+
650
+ // Determine alert style based on this issue's severity
651
+ const severity = issue.severity || 'information';
652
+ switch (severity) {
653
+ case 'error':
654
+ case 'fatal':
655
+ html += 'alert-danger';
656
+ break;
657
+ case 'warning':
658
+ html += 'alert-warning';
659
+ break;
660
+ case 'information':
661
+ html += 'alert-info';
662
+ break;
663
+ default:
664
+ html += 'alert-secondary';
665
+ }
666
+
667
+ html += '">';
668
+ html += `<strong>${this.escapeHtml(issue.severity || 'unknown')}:</strong> `;
669
+ html += `[${this.escapeHtml(issue.code || 'unknown')}] `;
670
+ html += this.escapeHtml(issue.diagnostics || issue.details?.text || 'No details');
671
+ html += '</div>';
672
+ }
673
+ }
674
+
675
+ html += '</div>';
676
+ return html;
677
+ }
678
+
679
+ /**
680
+ * Render Bundle resource
681
+ */
682
+ async renderBundle(json, req) {
683
+ if (json.type === 'searchset') {
684
+ return await this.renderSearchBundle(json, req);
685
+ }
686
+
687
+ // Generic bundle rendering
688
+ return await this.renderGenericBundle(json, req);
689
+ }
690
+
691
+ /**
692
+ * Render a search result Bundle
693
+ */
694
+ async renderSearchBundle(json, req) {
695
+
696
+ // Check if there are any actual search parameters (not just pagination/control params)
697
+ const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
698
+ const hasSearchParams = this.checkForSearchParams(selfLink);
699
+
700
+ // If no search params provided, show the search form
701
+ if (!hasSearchParams) {
702
+ return this.renderSearchForm(json, req);
703
+ }
704
+
705
+ // Check if _elements was specified (look in self link)
706
+ const elementsMatch = selfLink.match(/[?&]_elements=([^&]*)/);
707
+ const elements = elementsMatch ? decodeURIComponent(elementsMatch[1]).split(',').map(e => e.trim()) : null;
708
+
709
+ if (elements && elements.length > 0) {
710
+ return this.renderSearchTable(json, elements, req);
711
+ }
712
+
713
+ // Default: render as summary with individual resources
714
+ return await this.renderSearchSummary(json, req);
715
+ }
716
+
717
+ /**
718
+ * Check if URL has any actual search parameters (not just _offset, _count, _elements, _sort)
719
+ */
720
+ checkForSearchParams(url) {
721
+ try {
722
+ const urlObj = new URL(url);
723
+ const controlParams = ['_offset', '_count', '_sort'];
724
+
725
+ for (const [key, value] of urlObj.searchParams) {
726
+ if (!controlParams.includes(key) && value) {
727
+ return true;
728
+ }
729
+ }
730
+ return false;
731
+ } catch {
732
+ return false;
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Render search form (when no search params provided)
738
+ */
739
+ async renderSearchForm(json, req) {
740
+ const resourceType = this.getSearchResourceType(json);
741
+ const params = resourceType === 'CodeSystem' ? CODESYSTEM_PARAMS : SEARCH_PARAMS;
742
+
743
+ let html = '<div class="alert alert-info">Enter search criteria:</div>';
744
+ html += `<form method="get" action="${this.escapeHtml(req.baseUrl)}/${this.escapeHtml(resourceType)}">`;
745
+ html += '<div class="row">';
746
+
747
+ // Build form fields
748
+ for (const param of params) {
749
+ html += '<div class="col-md-4 mb-3">';
750
+ html += `<label for="${param.name}" class="form-label">${this.escapeHtml(param.label)}</label>`;
751
+
752
+ if (param.type === 'select') {
753
+ html += `<select name="${param.name}" id="${param.name}" class="form-select">`;
754
+ for (const opt of param.options) {
755
+ html += `<option value="${this.escapeHtml(opt)}">${this.escapeHtml(opt || '(any)')}</option>`;
756
+ }
757
+ html += '</select>';
758
+ } else {
759
+ html += `<input type="text" name="${param.name}" id="${param.name}"/>`;
760
+ }
761
+
762
+ html += '</div>';
763
+ }
764
+
765
+ html += '</div>';
766
+
767
+ // Sort dropdown
768
+ html += '<div class="row">';
769
+ html += '<div class="col-md-4 mb-3">';
770
+ html += '<label for="_sort" class="form-label">Sort By</label>';
771
+ html += '<select name="_sort" id="_sort" class="form-select">';
772
+ for (const opt of SORT_OPTIONS) {
773
+ html += `<option value="${this.escapeHtml(opt)}">${this.escapeHtml(opt || '(default)')}</option>`;
774
+ }
775
+ html += '</select>';
776
+ html += '</div>';
777
+ html += '</div>';
778
+
779
+ // Elements checkboxes
780
+ html += '<div class="mb-3">';
781
+ html += '<label class="form-label">Elements to include:</label><br/>';
782
+ for (const elem of ELEMENT_OPTIONS) {
783
+ html += `<div class="form-check form-check-inline">`;
784
+ html += `<input type="checkbox" name="_elements" value="${this.escapeHtml(elem)}" id="elem_${elem}" class="form-check-input"/>`;
785
+ html += `<label for="elem_${elem}" class="form-check-label">${this.escapeHtml(elem)}</label>`;
786
+ html += '</div>';
787
+ }
788
+ html += '</div>';
789
+
790
+ html += '<button type="submit" class="btn btn-primary">Search</button>';
791
+ html += '</form>';
792
+
793
+ return html;
794
+ }
795
+
796
+ /**
797
+ * Get resource type from search bundle (from self link or first entry)
798
+ */
799
+ getSearchResourceType(json) {
800
+ // Try to get from self link first
801
+ const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
802
+ const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\?/);
803
+ if (typeMatch) {
804
+ return typeMatch[1];
805
+ }
806
+
807
+ // Fall back to first entry
808
+ const firstEntry = json.entry?.[0]?.resource;
809
+ return firstEntry?.resourceType || 'Resource';
810
+ }
811
+
812
+ /**
813
+ * Render search results as a table (when _elements is specified)
814
+ */
815
+ async renderSearchTable(json, elements, req) {
816
+ const entries = json.entry || [];
817
+ const total = json.total || 0;
818
+
819
+ let html = `<p>Found ${total} result(s)</p>`;
820
+
821
+ // Pagination links
822
+ html += this.renderPaginationLinks(json);
823
+
824
+ // Build table
825
+ html += '<table class="table table-striped grid">';
826
+ html += '<thead><tr>';
827
+ html += '<th>ID</th>';
828
+ for (const elem of elements) {
829
+ if (elem !== 'id') {
830
+ html += `<th>${this.escapeHtml(elem)}</th>`;
831
+ }
832
+ }
833
+ html += '</tr></thead>';
834
+ html += '<tbody>';
835
+
836
+ for (const entry of entries) {
837
+ const resource = entry.resource;
838
+ if (!resource) continue;
839
+
840
+ html += '<tr>';
841
+
842
+ // ID column with link
843
+ const id = resource.id || '';
844
+ const resourceType = resource.resourceType || '';
845
+ html += `<td><a href="${this.escapeHtml(req.baseUrl)}/${this.escapeHtml(resourceType)}/${this.escapeHtml(id)}">${this.escapeHtml(id)}</a></td>`;
846
+
847
+ // Other element columns
848
+ for (const elem of elements) {
849
+ if (elem !== 'id') {
850
+ const value = resource[elem];
851
+ html += `<td>${this.escapeHtml(this.formatValue(value))}</td>`;
852
+ }
853
+ }
854
+
855
+ html += '</tr>';
856
+ }
857
+
858
+ html += '</tbody></table>';
859
+
860
+ // Pagination links again at bottom
861
+ html += this.renderPaginationLinks(json);
862
+
863
+ return html;
864
+ }
865
+
866
+ /**
867
+ * Render search results as summary with individual resources
868
+ */
869
+ async renderSearchSummary(json, req) {
870
+ const entries = json.entry || [];
871
+ const total = json.total || 0;
872
+
873
+ let html = `<p>Found ${total} result(s)</p>`;
874
+
875
+ // Pagination links
876
+ html += this.renderPaginationLinks(json);
877
+
878
+ // Bundle summary
879
+ html += '<div class="card mb-3">';
880
+ html += '<div class="card-header">Bundle Summary</div>';
881
+ html += '<div class="card-body">';
882
+ html += `<p><strong>Type:</strong> ${this.escapeHtml(json.type)}</p>`;
883
+ html += `<p><strong>Total:</strong> ${total}</p>`;
884
+ html += '</div>';
885
+ html += '</div>';
886
+
887
+ // Each entry
888
+ for (const entry of entries) {
889
+ html += '<hr/>';
890
+
891
+ if (entry.resource) {
892
+ const resource = entry.resource;
893
+ html += `<h4>${this.escapeHtml(resource.resourceType)}/${this.escapeHtml(resource.id || 'unknown')}</h4>`;
894
+
895
+ if (entry.fullUrl) {
896
+ html += `<p><small><a href="${this.escapeHtml(entry.fullUrl)}">${this.escapeHtml(entry.fullUrl)}</a></small></p>`;
897
+ }
898
+
899
+ // Render the resource
900
+ html += await this.render(resource, req, true);
901
+ }
902
+ }
903
+
904
+ // Pagination links again at bottom
905
+ html += this.renderPaginationLinks(json);
906
+
907
+ return html;
908
+ }
909
+
910
+ /**
911
+ * Render pagination links
912
+ */
913
+ renderPaginationLinks(json) {
914
+ const links = json.link || [];
915
+ if (links.length === 0) return '';
916
+
917
+ let html = '<nav><ul class="pagination">';
918
+
919
+ const linkOrder = ['first', 'previous', 'self', 'next', 'last'];
920
+
921
+ for (const rel of linkOrder) {
922
+ const link = links.find(l => l.relation === rel);
923
+ if (link) {
924
+ const isDisabled = rel === 'self';
925
+ const label = rel.charAt(0).toUpperCase() + rel.slice(1);
926
+
927
+ if (isDisabled) {
928
+ html += `<li class="page-item active"><span class="page-link">${this.escapeHtml(label)}</span></li>`;
929
+ } else {
930
+ html += `<li class="page-item"><a class="page-link" href="${this.escapeHtml(link.url)}">${this.escapeHtml(label)}</a></li>`;
931
+ }
932
+ }
933
+ }
934
+
935
+ html += '</ul></nav>';
936
+ return html;
937
+ }
938
+
939
+ /**
940
+ * Render a generic bundle (non-search)
941
+ */
942
+ async renderGenericBundle(json, req) {
943
+ let html = '<div class="card mb-3">';
944
+ html += '<div class="card-header">Bundle</div>';
945
+ html += '<div class="card-body">';
946
+ html += `<p><strong>Type:</strong> ${this.escapeHtml(json.type)}</p>`;
947
+ html += `<p><strong>Total:</strong> ${json.total || 'N/A'}</p>`;
948
+ html += '</div>';
949
+ html += '</div>';
950
+
951
+ // Links
952
+ if (json.link && json.link.length > 0) {
953
+ html += '<h4>Links</h4>';
954
+ html += '<ul>';
955
+ for (const link of json.link) {
956
+ html += `<li><strong>${this.escapeHtml(link.relation)}:</strong> <a href="${this.escapeHtml(link.url)}">${this.escapeHtml(link.url)}</a></li>`;
957
+ }
958
+ html += '</ul>';
959
+ }
960
+
961
+ // Entries
962
+ if (json.entry && json.entry.length > 0) {
963
+ for (const entry of json.entry) {
964
+ html += '<hr/>';
965
+ if (entry.resource) {
966
+ html += await this.render(entry.resource, req, true);
967
+ }
968
+ }
969
+ }
970
+
971
+ return html;
972
+ }
973
+
974
+ /**
975
+ * Render generic resource (fallback)
976
+ */
977
+ async renderGeneric(json, inBundle) {
978
+ return this.renderResourceWithNarrative(json, inBundle);
979
+ }
980
+
981
+ /**
982
+ * Format a value for display
983
+ */
984
+ formatValue(value) {
985
+ if (value === null || value === undefined) {
986
+ return '';
987
+ }
988
+ if (typeof value === 'object') {
989
+ return JSON.stringify(value);
990
+ }
991
+ return String(value);
992
+ }
993
+
994
+ /**
995
+ * Generate a unique ID for collapsible sections
996
+ */
997
+ let
998
+ resourceIdCounter = 0;
999
+
1000
+ generateResourceId() {
1001
+ return 'resource_' + (++this.resourceIdCounter);
1002
+ }
1003
+
1004
+
1005
+ /**
1006
+ * Render resource with text/div narrative and collapsible JSON source
1007
+ */
1008
+ async renderResourceWithNarrative(json, rendered) {
1009
+ const resourceId = this.generateResourceId();
1010
+
1011
+ let html = "";
1012
+
1013
+ // Show text/div narrative if present
1014
+ if (rendered) {
1015
+ html += '<div class="narrative">';
1016
+ html += rendered; // Already HTML, render as-is
1017
+ html += '</div>';
1018
+ } else {
1019
+ html += '<div class="narrative">(No Narrative)</div>';
1020
+ }
1021
+ if (json.text && json.text.div) {
1022
+ // Collapsible JSON source
1023
+ html += '<div class="xhtml">';
1024
+ html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
1025
+ html += 'Show Original Narrative</button>';
1026
+ html += `<div id="${resourceId}x" class="original-narrative" style="display: none; margin-top: 10px;">`;
1027
+
1028
+ html += '<div class="narrative">';
1029
+ html += json.text.div; // Already HTML, render as-is
1030
+ html += '</div>';
1031
+ }
1032
+ html += '</div>';
1033
+ html += '</div>';
1034
+
1035
+
1036
+ // Collapsible JSON source
1037
+ html += '<div class="json-source">';
1038
+ html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleJsonSource('${resourceId}')">`;
1039
+ html += 'Show JSON Source</button>';
1040
+ html += `<div id="${resourceId}" class="json-content" style="display: none; margin-top: 10px;">`;
1041
+ html += `<pre>${this.escapeHtml(JSON.stringify(json, null, 2))}</pre>`;
1042
+ html += '</div>';
1043
+ html += '</div>';
1044
+
1045
+ return html;
1046
+ }
1047
+
1048
+ // eslint-disable-next-line no-unused-vars
1049
+ async renderOperationsForm(json, req) {
1050
+ const vcSystemId = this.generateResourceId();
1051
+ const inferSystemId = this.generateResourceId();
1052
+
1053
+ return await this.liquid.renderFile('operations-form', {
1054
+ vcSystemId,
1055
+ inferSystemId,
1056
+ valueSetsJson: JSON.stringify(json.valueSets || [])
1057
+ });
1058
+ }
1059
+ }
1060
+
1061
+ module.exports = {
1062
+ TxHtmlRenderer, loadTemplate
1063
+ };