fhirsmith 0.8.5 → 0.9.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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +52 -22
  3. package/extension-tracker/extension-tracker-template.html +3 -1
  4. package/library/html-server.js +7 -0
  5. package/library/logger.js +234 -194
  6. package/library/regex-utilities.js +13 -0
  7. package/package.json +4 -2
  8. package/packages/packages-template.html +3 -1
  9. package/publisher/publisher-template.html +1 -0
  10. package/publisher/publisher.js +28 -7
  11. package/registry/registry-template.html +3 -1
  12. package/root-bare-template.html +9759 -37
  13. package/root-template.html +3 -2
  14. package/server.js +48 -12
  15. package/translations/Messages.properties +2 -1
  16. package/translations/rendering-phrases.properties +3 -1
  17. package/tx/cs/cs-api.js +4 -0
  18. package/tx/cs/cs-country.js +2 -1
  19. package/tx/cs/cs-cs.js +9 -4
  20. package/tx/cs/cs-loinc.js +2 -1
  21. package/tx/cs/cs-snomed.js +5 -1
  22. package/tx/data/OperationDefinition-ValueSet-related.json +133 -0
  23. package/tx/html/tx-template.html +3 -2
  24. package/tx/importers/atc-to-fhir.js +27 -27
  25. package/tx/library/codesystem.js +4 -0
  26. package/tx/library/renderer.js +20 -4
  27. package/tx/library/ucum-parsers.js +2 -1
  28. package/tx/ocl/cs-ocl.cjs +48 -15
  29. package/tx/ocl/vs-ocl.cjs +57 -34
  30. package/tx/operation-context.js +74 -19
  31. package/tx/tx-html.js +5 -5
  32. package/tx/tx.fhir.org.yml +4 -4
  33. package/tx/tx.js +1 -0
  34. package/tx/vs/vs-database.js +150 -100
  35. package/tx/vs/vs-vsac.js +90 -31
  36. package/tx/workers/expand.js +154 -113
  37. package/tx/workers/metadata.js +6 -3
  38. package/tx/workers/read.js +6 -3
  39. package/tx/workers/related.js +228 -87
  40. package/xig/xig-template.html +3 -1
  41. package/library/logger-telnet.js +0 -205
@@ -18,8 +18,9 @@ const {SearchFilterText} = require("../library/designations");
18
18
  const {ArrayMatcher} = require("../../library/utilities");
19
19
  const {debugLog} = require("../operation-context");
20
20
 
21
-
22
21
  class RelatedWorker extends TerminologyWorker {
22
+ showLogic = false;
23
+
23
24
  /**
24
25
  * @param {OperationContext} opContext - Operation context
25
26
  * @param {Logger} log - Logger instance
@@ -115,7 +116,6 @@ class RelatedWorker extends TerminologyWorker {
115
116
  this.setupAdditionalResources(params);
116
117
  let txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n, false);
117
118
  txp.readParams(params);
118
-
119
119
  this.params = txp;
120
120
 
121
121
  let thisVS = await this.readValueSet(res, "this", params, txp);
@@ -199,6 +199,7 @@ class RelatedWorker extends TerminologyWorker {
199
199
  }
200
200
 
201
201
  async doRelated(txp, thisVS, otherVS) {
202
+
202
203
  // ok, we have to compare the composes. we don't care about anything else
203
204
  const thisC = thisVS.jsonObj.compose;
204
205
  const otherC = otherVS.jsonObj.compose;
@@ -213,28 +214,35 @@ class RelatedWorker extends TerminologyWorker {
213
214
  Extensions.checkNoModifiers(otherC, 'RelatedWorker.doRelated', 'compose', otherVS.vurl)
214
215
  this.checkNoLockedDate(otherVS.vurl, otherC);
215
216
 
216
- let systems = new Map(); // tracks whether they are version dependent or not
217
+ let systems = new Map(); // tracks whether the comparison is version dependent or not
217
218
 
218
219
  // ok, first, if we can determine that the value sets match from the definitions, we will
219
220
  // if that fails, then we have to do the expansions, and then decide
220
221
 
222
+ let allCriteria = [...thisC.include || [], ...thisC.exclude || [], ...otherC.include || [], ...otherC.exclude || []];
221
223
  // first, we sort the includes by system, and then compare them as a group
222
224
  // Build a map of system -> { this: [...includes], other: [...includes] }
223
225
  const systemMap = new Map();
224
- await this.addIncludes(systems, systemMap, thisC.include || [], 'this', txp);
225
- await this.addIncludes(systems, systemMap, otherC.include || [], 'other', txp);
226
- await this.addIncludes(systems, systemMap, thisC.exclude || [], 'thisEx', txp);
227
- await this.addIncludes(systems, systemMap, otherC.exclude || [], 'otherEx', txp);
228
-
229
- let status = { left: false, right: false, fail: false, common : false};
230
-
231
- for (const [key, value] of systemMap.entries()) {
232
- if (key) {
233
- let cs = await this.findCodeSystem(key, null, txp, ['complete', 'fragment'], null, true);
234
- await this.compareSystems(systems, status, cs, value);
235
- } else {
236
- this.compareNonSystems(status, value);
226
+ await this.addIncludes(systems, systemMap, thisC.include || [], 'this', txp, allCriteria);
227
+ await this.addIncludes(systems, systemMap, otherC.include || [], 'other', txp, allCriteria);
228
+ await this.addIncludes(systems, systemMap, thisC.exclude || [], 'thisEx', txp, allCriteria);
229
+ await this.addIncludes(systems, systemMap, otherC.exclude || [], 'otherEx', txp, allCriteria);
230
+
231
+ let status = { empty: false, left: false, right: false, fail: false, common : false};
232
+ let diagnostics = {};
233
+
234
+ let canBeQuick = !this.hasMultipleVersionsForAnySystem(systems, systemMap);
235
+ if (canBeQuick) {
236
+ for (const [key, value] of systemMap.entries()) {
237
+ if (key) {
238
+ let cs = await this.findCodeSystem(key, null, txp, ['complete', 'fragment'], null, true);
239
+ await this.compareSystems(systems, status, cs, value, diagnostics);
240
+ } else {
241
+ this.compareNonSystems(status, value, diagnostics);
242
+ }
237
243
  }
244
+ } else {
245
+ status.fail = true;
238
246
  }
239
247
 
240
248
  let exp = false;
@@ -242,13 +250,15 @@ class RelatedWorker extends TerminologyWorker {
242
250
  // expansions might not work (infinite value sets) so
243
251
  // we can't tell.
244
252
  if (status.fail) {
245
- status.fail = false;
253
+ status = { left: false, right: false, fail: false, common : false}; // reset;
246
254
  exp = true;
247
- await this.compareExpansions(systems, status, thisVS, otherVS);
255
+ await this.compareExpansions(systems, status, thisVS, otherVS, diagnostics);
248
256
  }
249
257
  let outcome;
250
258
  if (status.fail) {
251
259
  outcome = this.makeOutcome("indeterminate", `Unable to compare ${thisVS.vurl} and ${otherVS.vurl}: `+status.reason);
260
+ } else if (status.empty) {
261
+ outcome = this.makeOutcome("empty", `Both the value sets ${thisVS.vurl} and ${otherVS.vurl} are empty`);
252
262
  } else if (!status.common) {
253
263
  outcome = this.makeOutcome("disjoint", `No shared codes between the value sets ${thisVS.vurl} and ${otherVS.vurl}`);
254
264
  } else if (!status.left && !status.right) {
@@ -260,36 +270,56 @@ class RelatedWorker extends TerminologyWorker {
260
270
  } else {
261
271
  outcome = this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a seb-set of the valueSet ${otherVS.vurl}`);
262
272
  }
263
- if (exp) {
264
- outcome.parameter.push({name: 'expansion', valueBoolean: exp})
273
+ if (txp.diagnostics) {
274
+ outcome.parameter.push({name: 'performed-expansion', valueBoolean: exp ? true : false})
275
+ if (diagnostics.missing && diagnostics.missing.length > 0) {
276
+ outcome.parameter.push({name: 'missing-codes', valueString: diagnostics.missing.map(c => c.code).join(',') })
277
+ }
278
+ if (diagnostics.extra && diagnostics.extra.length > 0) {
279
+ outcome.parameter.push({name: 'extra-codes', valueString: diagnostics.extra.map(c => c.code).join(',') })
280
+ }
281
+ if (diagnostics.common && diagnostics.common.length > 0) {
282
+ outcome.parameter.push({name: 'common-codes', valueString: diagnostics.common.map(c => c.left.code).join(',') })
283
+ }
284
+ if (!exp) {
285
+ if (diagnostics.missingCodes && diagnostics.missingCodes.length > 0) {
286
+ outcome.parameter.push({name: 'missing-codes', valueString: diagnostics.missingCodes.join(',')})
287
+ }
288
+ if (diagnostics.extraCodes && diagnostics.extraCodes.length > 0) {
289
+ outcome.parameter.push({name: 'extra-codes', valueString: diagnostics.extraCodes.join(',')})
290
+ }
291
+ if (diagnostics.commonCodes && diagnostics.commonCodes.length > 0) {
292
+ outcome.parameter.push({name: 'common-codes', valueString: diagnostics.commonCodes.join(',')})
293
+ }
294
+ }
265
295
  }
266
296
  return outcome;
267
297
  }
268
298
 
269
- async addIncludes(systems, systemMap, includes, side, txp) {
299
+ async addIncludes(systems, systemMap, includes, side, txp, allCriteria) {
270
300
  for (const inc of includes) {
271
301
  let key = inc.system || '';
272
302
  let v = {};
273
- if (await this.versionMatters(systems, key, inc.version, v, txp)) {
303
+ if (await this.versionMatters(systems, key, inc.version, v, txp, allCriteria)) {
274
304
  key = key + "|" + v.version;
275
305
  }
276
306
  if (!systemMap.has(key)) {
277
- systemMap.set(key, {this: [], other: []});
307
+ systemMap.set(key, {this: [], other: [], thisEx: [], otherEx: []});
278
308
  }
279
309
  systemMap.get(key)[side].push(inc);
280
310
  }
281
311
  }
282
312
 
283
- async versionMatters(systems, key, version, v, txp) {
284
- if (systems.has(key)) {
285
- return systems.get(key);
286
- }
313
+ async versionMatters(systems, key, version, v, txp, allCriteria) {
287
314
  let cs = await this.findCodeSystem(key, version, txp, ['complete', 'fragment'], null, true);
288
- let res = cs == null || cs.versionNeeded();
315
+ let alreadyVersionDependent = systems.has(key) && systems.get(key).criteria;
316
+ let res = cs != null && (alreadyVersionDependent || ((version || cs.version()) && (cs.versionNeeded() || this.anyCriteriaHasFilters(allCriteria, key)))); // if there's filters, the version always matters
289
317
  if (res) {
290
318
  v.version = version || cs ? cs.version() : undefined;
291
319
  }
292
- systems.set(key, res);
320
+ if (!systems.has(key)) {
321
+ systems.set(key, {criteria: res, codes: cs ? cs.versionNeeded() : false});
322
+ }
293
323
  return res;
294
324
  }
295
325
 
@@ -298,8 +328,8 @@ class RelatedWorker extends TerminologyWorker {
298
328
  status.fail = true;
299
329
  }
300
330
 
301
- async compareSystems(systems, status, cs, value) {
302
- if (value.thisEx || value.otherEx) {
331
+ async compareSystems(systems, status, cs, value, diagnostics) {
332
+ if ((value.thisEx && value.thisEx.length > 0) || (value.otherEx && value.otherEx.length > 0)) {
303
333
  // we don't try in this case
304
334
  status.fail = true;
305
335
  status.common = true;
@@ -341,25 +371,33 @@ class RelatedWorker extends TerminologyWorker {
341
371
  status.common = true;
342
372
  status.right = true;
343
373
  return;
344
- } else if (this.isConcepts(value.this[0]) && this.isConcepts(value.other[0])) {
345
- this.compareCodeLists(status, value.this[0], value.other[0]);
346
- return;
347
- } else if (this.isFilter(value.this[0]) && this.isFilter(value.other[0])) {
374
+ } else if (value.this.length > 1 || value.other.length > 1) {
375
+ status.common = true;
376
+ // if we have mixed concepts, or multiple filters, we can't reason about them (too many scenarios where they overlap in
377
+ // unpredictable ways. If they're not identical, we fail
348
378
  if (value.this.length != value.other.length) {
349
379
  status.fail = true;
350
- return;
351
380
  } else {
352
381
  for (let i = 0; i < value.this.length; i++) {
353
382
  let t = value.this[i];
354
383
  let o = value.other[i];
355
- if (!await this.filterSetsMatch(status, cs, t, o)) {
384
+ if (!this.includesIdentical(t, o)) {
356
385
  status.fail = true;
357
- return;
386
+ break;
358
387
  }
359
- status.common = true;
360
- return;
361
388
  }
362
389
  }
390
+ return;
391
+ } else if (this.isConcepts(value.this[0]) && this.isConcepts(value.other[0])) {
392
+ this.compareCodeLists(status, value.this[0], value.other[0], diagnostics);
393
+ return;
394
+ } else if (this.isFilter(value.this[0]) && this.isFilter(value.other[0])) {
395
+ let t = value.this[0];
396
+ let o = value.other[0];
397
+ if (!await this.filterSetsMatch(status, cs, t, o)) {
398
+ status.fail = true;
399
+ }
400
+ return;
363
401
  }
364
402
  }
365
403
  status.fail = true; // not sure why we got to here, but it doesn't matter: we can't tell
@@ -383,6 +421,15 @@ class RelatedWorker extends TerminologyWorker {
383
421
  return false;
384
422
  }
385
423
 
424
+ hasFilters(list) {
425
+ for (const inc of list) {
426
+ if (inc.filter?.length > 0) {
427
+ return true;
428
+ }
429
+ }
430
+ return false;
431
+ }
432
+
386
433
  tidyIncludes(list) {
387
434
  let collector = null;
388
435
  for (let i = list.length - 1; i >= 0; i--) {
@@ -431,13 +478,16 @@ class RelatedWorker extends TerminologyWorker {
431
478
  );
432
479
  }
433
480
 
434
- compareCodeLists(status, t, o) {
481
+ compareCodeLists(status, t, o, diagnostics) {
435
482
  const tSet = new Set(t.concept.map(x => x.code));
436
483
  const oSet = new Set(o.concept.map(x => x.code));
437
484
 
438
- status.common = [...tSet].filter(c => oSet.has(c)).length > 0;
439
- status.left = [...tSet].filter(c => !oSet.has(c)).length > 0;
440
- status.right = [...oSet].filter(c => !tSet.has(c)).length > 0;
485
+ diagnostics.commonCodes = [...tSet].filter(c => oSet.has(c));
486
+ diagnostics.missingCodes = [...tSet].filter(c => !oSet.has(c));
487
+ diagnostics.extraCodes = [...oSet].filter(c => !tSet.has(c));
488
+ status.common = diagnostics.commonCodes.length > 0;
489
+ status.left = diagnostics.missingCodes.length > 0;
490
+ status.right =diagnostics.extraCodes.length > 0;
441
491
  }
442
492
 
443
493
  makeOutcome(code, msg) {
@@ -457,15 +507,45 @@ class RelatedWorker extends TerminologyWorker {
457
507
  return !inc.concept && !inc.filter;
458
508
  }
459
509
 
460
- async compareExpansions(systems, status, thisC, otherC) {
461
- const expThis = await this.doExpand(thisC);
462
- const expOther = await this.doExpand(otherC);
510
+ async compareExpansions(systems, status, thisC, otherC, diagnostics) {
511
+
512
+ const expResThis = await this.doExpand(thisC);
513
+ this.opContext.unSeeAll();
514
+ const expResOther = await this.doExpand(otherC);
463
515
 
516
+ if (expResThis.error || expResOther.error) {
517
+ status.fail = true;
518
+ if (expResThis.error && expResOther.error) {
519
+ if (expResThis.error == expResOther.error) {
520
+ status.reason = "Both expansions failed: "+expResThis.error.message;
521
+ } else {
522
+ status.reason = "Both expansions failed with different errors: "+expResThis.error.message+"; "+expResOther.error.message;
523
+ }
524
+ } else if (expResThis.error) {
525
+ status.reason = "This expansion failed: "+expResThis.error.message
526
+ } else {
527
+ status.reason = "Other expansion failed: "+expResOther.error.message
528
+ }
529
+ return;
530
+ }
531
+ let expThis = expResThis.vs;
532
+ let expOther = expResOther.vs;
464
533
  if (this.isUnclosed(expThis) || this.isUnclosed(expOther)) {
465
534
  status.fail = true;
535
+ if (this.isUnclosed(expThis) && this.isUnclosed(expOther)) {
536
+ status.reason = "Both expansions are unclosed."
537
+ } else if (this.isUnclosed(expThis)) {
538
+ status.reason = "This expansion is unclosed."
539
+ } else {
540
+ status.reason = "Other expansion is unclosed."
541
+ }
466
542
  return;
467
543
  }
468
544
 
545
+ if ((!expThis.expansion.contains || expThis.expansion.contains.length == 0) && (!expOther.expansion.contains || expOther.expansion.contains.length == 0)) {
546
+ status.empty = true;
547
+ return;
548
+ }
469
549
  const matcher = new ArrayMatcher((l, r) =>
470
550
  this.matchContains(systems, l, r)
471
551
  );
@@ -482,6 +562,11 @@ class RelatedWorker extends TerminologyWorker {
482
562
  if (matcher.unmatchedRight.length > 0) {
483
563
  status.right = true;
484
564
  }
565
+ if (matcher.unmatchedLeft.length > 0 || matcher.unmatchedRight.length > 0) {
566
+ diagnostics.common = matcher.matched;
567
+ }
568
+ diagnostics.missing = matcher.unmatchedLeft;
569
+ diagnostics.extra = matcher.unmatchedRight;
485
570
  }
486
571
 
487
572
  isUnclosed(vs) {
@@ -495,7 +580,7 @@ class RelatedWorker extends TerminologyWorker {
495
580
  if (thisC.code != otherC.code) {
496
581
  return false;
497
582
  }
498
- let versionMatters = systems.get(thisC.system);
583
+ let versionMatters = systems.has(thisC.system) && systems.get(thisC.system).codes;
499
584
  if (versionMatters && thisC.version != otherC.version) {
500
585
  return false;
501
586
  } else {
@@ -504,16 +589,25 @@ class RelatedWorker extends TerminologyWorker {
504
589
  }
505
590
 
506
591
  async doExpand(vs) {
507
- let txpe = this.params.clone();
508
- txpe.limit = 10000;
509
- txpe.excludeNested = true;
510
- let exp = new ValueSetExpander(this, txpe);
511
- let vse = await exp.expand(vs, new SearchFilterText(''), true);
512
- return vse
592
+ try {
593
+ let txpe = this.params.clone();
594
+ txpe.limit = 10000;
595
+ txpe.excludeNested = true;
596
+ let start = new Date();
597
+ console.log("Expanding value set");
598
+ let exp = new ValueSetExpander(this, txpe);
599
+ exp.noDetails = true;
600
+ let vse = await exp.expand(vs, new SearchFilterText(''), true);
601
+ console.log("Expanded value set - took " + (new Date() - start) + "ms");
602
+ return {vs: vse, error: null};
603
+ } catch (error) {
604
+ debugLog(error, "Error expanding value set");
605
+ return {vs: null, error: error};
606
+ }
513
607
  }
514
608
 
515
609
  isConcepts(inc) {
516
- return inc.concept && inc.concept.length > 0;
610
+ return inc.concept && inc.concept.length > 0 && !this.isFilter(inc);
517
611
  }
518
612
 
519
613
  isFilter(inc) {
@@ -522,52 +616,99 @@ class RelatedWorker extends TerminologyWorker {
522
616
 
523
617
  async filterSetsMatch(status, cs, t, o) {
524
618
  // two includes have matching filters if the set of filters match.
525
-
526
- let localstatus = { left: false, right: false};
527
-
528
- const matcher = new ArrayMatcher((l, r) =>
529
- this.filtersMatch(localstatus, cs, l, r)
530
- );
531
- await matcher.match(t.filter, o.filter);
532
-
533
- if (matcher.unmatchedLeft.length > 0 || matcher.unmatchedRight.length > 0) {
619
+ if (t.filter.length != o.filter.length) {
534
620
  return false;
621
+ }
622
+ if (t.filter.length > 1) {
623
+ t.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value));
624
+ o.filter.sort((a, b) => (a.property || '').localeCompare(b.property) || (a.op || '').localeCompare(b.op) || (a.value || '').localeCompare(b.value))
625
+ // we can't draw any conclusions if there's more than one filter, and they aren't identical,
626
+ // because we don't guess how they might interact with each other
627
+ for (let i = 0; i < (t.filter || []).length; i++) {
628
+ if (t.filter[i].property !== o.filter[i].property || t.filter[i].op !== o.filter[i].op || t.filter[i].value !== o.filter[i].value) {
629
+ return false;
630
+ }
631
+ }
632
+ status.common = true;
633
+ return true;
535
634
  } else {
536
- if (localstatus.left) {
537
- status.left = true;
635
+ let tf = t.filter[0];
636
+ let of = o.filter[0];
637
+ if (tf.property != of.property || tf.op != of.op) {
638
+ return false;
538
639
  }
539
- if (localstatus.right) {
540
- status.right = true;
640
+ if (tf.value == of.value) {
641
+ status.common = true;
642
+ return true;
643
+ } else if (tf.op == 'is-a') {
644
+ let rel = await cs.subsumesTest(tf.value, of.value)
645
+ switch (rel) {
646
+ case 'equivalent':
647
+ return true;
648
+ case 'subsumes':
649
+ status.common = true;
650
+ status.left = true;
651
+ return true;
652
+ case 'subsumed-by':
653
+ status.common = true;
654
+ status.right = true;
655
+ return true;
656
+ default:
657
+ // we know that the codes aren't related, but we don't know whether they have common children
658
+ // well, that depends on whether there's a multi-heirarchy in play
659
+ if (!cs.hasMultiHierarchy()) {
660
+ status.common = false;
661
+ status.left = true;
662
+ status.right = true;
663
+ return true;
664
+
665
+ } else {
666
+ return false;
667
+ }
668
+ }
669
+ } else {
670
+ return false;
541
671
  }
542
- return true;
543
672
  }
544
673
  }
545
674
 
546
- async filtersMatch(status, cs, t, o) {
547
- if (t.property != o.property || t.op != o.op) {
675
+ includesIdentical(t, o) {
676
+ if ((t.concept || []).length !== (o.concept || []).length) {
548
677
  return false;
549
678
  }
550
- if (t.value == o.value) {
551
- return true;
679
+ for (let i = 0; i < (t.concept || []).length; i++) {
680
+ if (t.concept[i].code !== o.concept[i].code) {
681
+ return false;
682
+ }
552
683
  }
553
- if (t.op == 'is-a') {
554
- let rel = await cs.subsumesTest(t.value, o.value)
555
- switch (rel) {
556
- case 'equivalent':
557
- return true;
558
- case 'subsumes':
559
- status.left = true;
560
- return true;
561
- case 'subsumed-by':
562
- status.right = true;
563
- return true;
564
- default:
565
- return false;
684
+ if ((t.filter || []).length !== (o.filter || []).length) {
685
+ return false;
686
+ }
687
+ for (let i = 0; i < (t.filter || []).length; i++) {
688
+ if (t.filter[i].property !== o.filter[i].property || t.filter[i].op !== o.filter[i].op || t.filter[i].value !== o.filter[i].value ) {
689
+ return false;
566
690
  }
567
691
  }
568
- return false;
692
+
693
+ return true;
569
694
  }
570
695
 
696
+ anyCriteriaHasFilters(allCriteria, key) {
697
+ return allCriteria.some(c => c.system === key && c.filter && c.filter.length > 0);
698
+ }
699
+
700
+ hasMultipleVersionsForAnySystem(systems, systemMap) {
701
+ return [...systems.entries()].some(([url, val]) => {
702
+ if (val.criteria !== true) return false;
703
+ let count = 0;
704
+ for (const k of systemMap.keys()) {
705
+ if (k.startsWith(url)) {
706
+ count++;
707
+ }
708
+ }
709
+ return count > 1;
710
+ });
711
+ }
571
712
  }
572
713
 
573
714
  module.exports = {
@@ -90,7 +90,9 @@
90
90
  <p>
91
91
  <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
92
92
  <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
93
- XIG built as of [%download-date%] &nbsp;|&nbsp; [%total-resources%] resources in [%total-packages%] packages &nbsp;|&nbsp; ([%ms%] ms)
93
+ XIG built as of [%download-date%] &nbsp;|&nbsp; [%total-resources%] resources in [%total-packages%] packages &nbsp;|
94
+ &nbsp; ([%ms%] ms)
95
+ [%sponsorMessage%]
94
96
  </p>
95
97
  </div> <!-- /inner-wrapper -->
96
98
  </div> <!-- /container -->