fhirsmith 0.7.0 → 0.7.2
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 +37 -0
- package/README.md +13 -4
- package/config-template.json +16 -0
- package/configurations/readme.md +1 -0
- package/extension-tracker/extension-tracker-template.html +124 -0
- package/extension-tracker/extension-tracker.js +697 -0
- package/extension-tracker/readme.md +63 -0
- package/folder/folder.js +305 -0
- package/folder/readme.md +57 -0
- package/library/html-server.js +8 -2
- package/package.json +4 -2
- package/packages/packages.js +8 -8
- package/server.js +55 -3
- package/tx/cs/cs-snomed.js +5 -3
- package/tx/html/conceptmap-operations.liquid +19 -0
- package/tx/library/extensions.js +6 -2
- package/tx/library/renderer.js +572 -3
- package/tx/ocl/cache/cache-paths.cjs +4 -5
- package/tx/ocl/cm-ocl.cjs +4 -1
- package/tx/ocl/cs-ocl.cjs +9 -9
- package/tx/ocl/vs-ocl.cjs +14 -5
- package/tx/tx-html.js +48 -3
- package/tx/workers/expand.js +23 -12
- package/tx/workers/read.js +23 -8
- package/tx/workers/search.js +62 -16
- package/tx/workers/validate.js +11 -6
package/tx/library/renderer.js
CHANGED
|
@@ -317,8 +317,8 @@ class Renderer {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
translate(msgId) {
|
|
321
|
-
return this.opContext.i18n.formatPhrase(msgId, this.opContext.langs,
|
|
320
|
+
translate(msgId, params = []) {
|
|
321
|
+
return this.opContext.i18n.formatPhrase(msgId, this.opContext.langs, params);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
translatePlural(num, msgId) {
|
|
@@ -439,7 +439,7 @@ class Renderer {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
} else {
|
|
442
|
-
li.tx(this.translate('VALUE_SET_CODES_FROM'));
|
|
442
|
+
li.tx(this.translate('VALUE_SET_CODES_FROM')+" ");
|
|
443
443
|
await this.renderLink(li,inc.system+(inc.version ? "|"+inc.version : ""));
|
|
444
444
|
li.tx(" "+ this.translate('VALUE_SET_WHERE')+" ");
|
|
445
445
|
li.startCommaList("and");
|
|
@@ -1640,6 +1640,575 @@ class Renderer {
|
|
|
1640
1640
|
}
|
|
1641
1641
|
}
|
|
1642
1642
|
}
|
|
1643
|
+
|
|
1644
|
+
// Methods to add to the Renderer class in renderer.js for ConceptMap rendering.
|
|
1645
|
+
// These follow the Java ConceptMapRenderer logic and use the same translated strings.
|
|
1646
|
+
|
|
1647
|
+
// ---- Add these methods to the Renderer class ----
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Render a ConceptMap resource to HTML.
|
|
1651
|
+
* Follows the same pattern as renderValueSet/renderCodeSystem:
|
|
1652
|
+
* metadata table (reusing renderMetadataTable), then group-by-group rendering.
|
|
1653
|
+
*/
|
|
1654
|
+
async renderConceptMap(cm) {
|
|
1655
|
+
if (cm.json) {
|
|
1656
|
+
cm = cm.json;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
let div_ = div();
|
|
1660
|
+
|
|
1661
|
+
// Metadata table
|
|
1662
|
+
div_.h3().tx("Properties");
|
|
1663
|
+
await this.renderMetadataTable(cm, div_.table("grid"));
|
|
1664
|
+
|
|
1665
|
+
div_.h3("Mapping Details");
|
|
1666
|
+
// Source/Target scope line (mirrors Java: CONC_MAP_FROM / CONC_MAP_TO)
|
|
1667
|
+
const p = div_.para();
|
|
1668
|
+
p.tx(this.translate('CONC_MAP_FROM') + " ");
|
|
1669
|
+
const sourceScope = cm.sourceScope || cm.sourceCanonical || cm.sourceUri;
|
|
1670
|
+
if (sourceScope) {
|
|
1671
|
+
await this.renderLink(p, sourceScope);
|
|
1672
|
+
} else {
|
|
1673
|
+
p.tx(this.translate('CONC_MAP_NOT_SPEC'));
|
|
1674
|
+
}
|
|
1675
|
+
p.tx(" " + this.translate('CONC_MAP_TO') + " ");
|
|
1676
|
+
const targetScope = cm.targetScope || cm.targetCanonical || cm.targetUri;
|
|
1677
|
+
if (targetScope) {
|
|
1678
|
+
await this.renderLink(p, targetScope);
|
|
1679
|
+
} else {
|
|
1680
|
+
p.tx(this.translate('CONC_MAP_NOT_SPEC'));
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
div_.br();
|
|
1684
|
+
|
|
1685
|
+
// Render each group
|
|
1686
|
+
let gc = 0;
|
|
1687
|
+
for (const grp of cm.group || []) {
|
|
1688
|
+
gc++;
|
|
1689
|
+
if (gc > 1) {
|
|
1690
|
+
div_.hr();
|
|
1691
|
+
}
|
|
1692
|
+
await this.renderConceptMapGroup(div_, cm, grp, gc);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
return div_.toString();
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Render a single ConceptMap group.
|
|
1700
|
+
* Determines whether this is a "simple" group (1:1 mappings, no dependsOn/product)
|
|
1701
|
+
* or a "complex" group, and delegates accordingly.
|
|
1702
|
+
*/
|
|
1703
|
+
async renderConceptMapGroup(x, cm, grp, gc) {
|
|
1704
|
+
// Analyze the group to determine rendering mode
|
|
1705
|
+
let hasComment = false;
|
|
1706
|
+
let hasProperties = false;
|
|
1707
|
+
let ok = true; // true = simple rendering
|
|
1708
|
+
|
|
1709
|
+
const props = {}; // property code -> Set of systems
|
|
1710
|
+
const sources = { code: new Set() };
|
|
1711
|
+
const targets = { code: new Set() };
|
|
1712
|
+
|
|
1713
|
+
if (grp.source) sources.code.add(grp.source);
|
|
1714
|
+
if (grp.target) targets.code.add(grp.target);
|
|
1715
|
+
|
|
1716
|
+
for (const elem of grp.element || []) {
|
|
1717
|
+
const isSimple = elem.noMap ||
|
|
1718
|
+
(elem.target && elem.target.length === 1 &&
|
|
1719
|
+
(!elem.target[0].dependsOn || elem.target[0].dependsOn.length === 0) &&
|
|
1720
|
+
(!elem.target[0].product || elem.target[0].product.length === 0));
|
|
1721
|
+
ok = ok && isSimple;
|
|
1722
|
+
|
|
1723
|
+
if (Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment')) {
|
|
1724
|
+
hasComment = true;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
for (const tgt of elem.target || []) {
|
|
1728
|
+
if (tgt.comment) {
|
|
1729
|
+
hasComment = true;
|
|
1730
|
+
}
|
|
1731
|
+
for (const pp of tgt.property || []) {
|
|
1732
|
+
if (!props[pp.code]) {
|
|
1733
|
+
props[pp.code] = new Set();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
for (const d of tgt.dependsOn || []) {
|
|
1737
|
+
if (!sources[d.attribute]) {
|
|
1738
|
+
sources[d.attribute] = new Set();
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
for (const d of tgt.product || []) {
|
|
1742
|
+
if (!targets[d.attribute]) {
|
|
1743
|
+
targets[d.attribute] = new Set();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (Object.keys(props).length > 0) {
|
|
1750
|
+
hasProperties = true;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Group header
|
|
1754
|
+
const pp = x.para();
|
|
1755
|
+
pp.b().tx(this.translate('CONC_MAP_GRP', [gc])+ " ");
|
|
1756
|
+
pp.tx(this.translate('CONC_MAP_FROM') + " ");
|
|
1757
|
+
if (grp.source) {
|
|
1758
|
+
await this.renderLink(pp, grp.source);
|
|
1759
|
+
} else {
|
|
1760
|
+
pp.code().tx(this.translate('CONC_MAP_CODE_SYS_UNSPEC'));
|
|
1761
|
+
}
|
|
1762
|
+
pp.tx(" to ");
|
|
1763
|
+
if (grp.target) {
|
|
1764
|
+
await this.renderLink(pp, grp.target);
|
|
1765
|
+
} else {
|
|
1766
|
+
pp.code().tx(this.translate('CONC_MAP_CODE_SYS_UNSPEC'));
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
if (ok) {
|
|
1770
|
+
await this.renderSimpleConceptMapGroup(x, grp, hasComment);
|
|
1771
|
+
} else {
|
|
1772
|
+
await this.renderComplexConceptMapGroup(x, grp, hasComment, hasProperties, props, sources, targets);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Render a simple ConceptMap group: Source | Relationship | Target | Comment
|
|
1778
|
+
* This is the "ok" path from the Java code where all elements have at most
|
|
1779
|
+
* one target and no dependsOn/product.
|
|
1780
|
+
*/
|
|
1781
|
+
async renderSimpleConceptMapGroup(x, grp, hasComment) {
|
|
1782
|
+
const tbl = x.table("grid");
|
|
1783
|
+
let tr = tbl.tr();
|
|
1784
|
+
tr.td().b().tx(this.translate('CONC_MAP_SOURCE'));
|
|
1785
|
+
tr.td().b().tx(this.translate('CONC_MAP_REL'));
|
|
1786
|
+
tr.td().b().tx(this.translate('CONC_MAP_TRGT'));
|
|
1787
|
+
if (hasComment) {
|
|
1788
|
+
tr.td().b().tx(this.translate('GENERAL_COMMENT'));
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
for (const elem of grp.element || []) {
|
|
1792
|
+
tr = tbl.tr();
|
|
1793
|
+
const td = tr.td();
|
|
1794
|
+
td.tx(elem.code);
|
|
1795
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1796
|
+
if (display && !this.isSameCodeAndDisplay(elem.code, display)) {
|
|
1797
|
+
td.tx(" (" + display + ")");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (elem.noMap) {
|
|
1801
|
+
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1802
|
+
if (!hasComment) {
|
|
1803
|
+
tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
|
|
1804
|
+
} else if (nomapComment) {
|
|
1805
|
+
tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
|
|
1806
|
+
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1807
|
+
} else {
|
|
1808
|
+
tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
|
|
1809
|
+
}
|
|
1810
|
+
} else {
|
|
1811
|
+
let first = true;
|
|
1812
|
+
for (const tgt of elem.target || []) {
|
|
1813
|
+
if (first) {
|
|
1814
|
+
first = false;
|
|
1815
|
+
} else {
|
|
1816
|
+
tr = tbl.tr();
|
|
1817
|
+
tr.td().style("opacity: 0.5").tx('"');
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Relationship cell
|
|
1821
|
+
this.renderConceptMapRelationship(tr, tgt);
|
|
1822
|
+
|
|
1823
|
+
// Target code cell
|
|
1824
|
+
const tgtTd = tr.td();
|
|
1825
|
+
tgtTd.tx(tgt.code || '');
|
|
1826
|
+
const tgtDisplay = tgt.display || await this.getDisplayForConcept(grp.target, tgt.code);
|
|
1827
|
+
if (tgtDisplay && !this.isSameCodeAndDisplay(tgt.code, tgtDisplay)) {
|
|
1828
|
+
tgtTd.tx(" (" + tgtDisplay + ")");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (hasComment) {
|
|
1832
|
+
tr.td().tx(tgt.comment || '');
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
this.addUnmapped(tbl, grp);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Render a complex ConceptMap group with dependsOn, product, and/or property columns.
|
|
1842
|
+
* This is the "!ok" path from the Java code.
|
|
1843
|
+
*/
|
|
1844
|
+
async renderComplexConceptMapGroup(x, grp, hasComment, hasProperties, props, sources, targets) {
|
|
1845
|
+
// Check if any targets have relationships
|
|
1846
|
+
let hasRelationships = false;
|
|
1847
|
+
for (const elem of grp.element || []) {
|
|
1848
|
+
for (const tgt of elem.target || []) {
|
|
1849
|
+
if (tgt.relationship) {
|
|
1850
|
+
hasRelationships = true;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const tbl = x.table("grid");
|
|
1856
|
+
|
|
1857
|
+
// First header row: Source Details | Relationship | Target Details | Comment | Properties
|
|
1858
|
+
let tr = tbl.tr();
|
|
1859
|
+
const sourceColCount = 1 + Object.keys(sources).length - 1; // code + dependsOn attributes
|
|
1860
|
+
const targetColCount = 1 + Object.keys(targets).length - 1; // code + product attributes
|
|
1861
|
+
tr.td().colspan(String(sourceColCount + 1)).b().tx(this.translate('CONC_MAP_SRC_DET'));
|
|
1862
|
+
if (hasRelationships) {
|
|
1863
|
+
tr.td().b().tx(this.translate('CONC_MAP_REL'));
|
|
1864
|
+
}
|
|
1865
|
+
tr.td().colspan(String(targetColCount + 1)).b().tx(this.translate('CONC_MAP_TRGT_DET'));
|
|
1866
|
+
if (hasComment) {
|
|
1867
|
+
tr.td().b().tx(this.translate('GENERAL_COMMENT'));
|
|
1868
|
+
}
|
|
1869
|
+
if (hasProperties) {
|
|
1870
|
+
tr.td().colspan(String(Object.keys(props).length)).b().tx(this.translate('GENERAL_PROPS'));
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// Second header row: actual column headers
|
|
1874
|
+
tr = tbl.tr();
|
|
1875
|
+
|
|
1876
|
+
// Source code column
|
|
1877
|
+
if (sources.code.size === 1) {
|
|
1878
|
+
const url = [...sources.code][0];
|
|
1879
|
+
await this.renderCSDetailsLink(tr, url, true);
|
|
1880
|
+
} else {
|
|
1881
|
+
tr.td().b().tx(this.translate('GENERAL_CODE'));
|
|
1882
|
+
}
|
|
1883
|
+
// Source dependsOn attribute columns
|
|
1884
|
+
for (const s of Object.keys(sources)) {
|
|
1885
|
+
if (s !== 'code') {
|
|
1886
|
+
if (sources[s].size === 1) {
|
|
1887
|
+
const url = [...sources[s]][0];
|
|
1888
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1889
|
+
} else {
|
|
1890
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
// Relationship column
|
|
1895
|
+
if (hasRelationships) {
|
|
1896
|
+
tr.td();
|
|
1897
|
+
}
|
|
1898
|
+
// Target code column
|
|
1899
|
+
if (targets.code.size === 1) {
|
|
1900
|
+
const url = [...targets.code][0];
|
|
1901
|
+
await this.renderCSDetailsLink(tr, url, true);
|
|
1902
|
+
} else {
|
|
1903
|
+
tr.td().b().tx(this.translate('GENERAL_CODE'));
|
|
1904
|
+
}
|
|
1905
|
+
// Target product attribute columns
|
|
1906
|
+
for (const s of Object.keys(targets)) {
|
|
1907
|
+
if (s !== 'code') {
|
|
1908
|
+
if (targets[s].size === 1) {
|
|
1909
|
+
const url = [...targets[s]][0];
|
|
1910
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1911
|
+
} else {
|
|
1912
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
// Comment column header
|
|
1917
|
+
if (hasComment) {
|
|
1918
|
+
tr.td();
|
|
1919
|
+
}
|
|
1920
|
+
// Property column headers
|
|
1921
|
+
if (hasProperties) {
|
|
1922
|
+
for (const s of Object.keys(props)) {
|
|
1923
|
+
if (props[s].size === 1) {
|
|
1924
|
+
const url = [...props[s]][0];
|
|
1925
|
+
await this.renderCSDetailsLink(tr, url, false);
|
|
1926
|
+
} else {
|
|
1927
|
+
tr.td().b().tx(this.getDescForConcept(s));
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// Data rows
|
|
1933
|
+
for (const elem of grp.element || []) {
|
|
1934
|
+
if (elem.noMap) {
|
|
1935
|
+
tr = tbl.tr();
|
|
1936
|
+
const td = tr.td().style("border-right-width: 0px");
|
|
1937
|
+
if (sources.code.size === 1) {
|
|
1938
|
+
td.tx(elem.code);
|
|
1939
|
+
} else {
|
|
1940
|
+
td.tx(grp.source + " / " + elem.code);
|
|
1941
|
+
}
|
|
1942
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1943
|
+
tr.td().style("border-left-width: 0px").tx(display || '');
|
|
1944
|
+
|
|
1945
|
+
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1946
|
+
if (nomapComment) {
|
|
1947
|
+
tr.td().colspan("3").style("background-color: #efefef").tx("(not mapped)");
|
|
1948
|
+
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1949
|
+
} else {
|
|
1950
|
+
tr.td().colspan("4").style("background-color: #efefef").tx("(not mapped)");
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
let first = true;
|
|
1954
|
+
for (let ti = 0; ti < (elem.target || []).length; ti++) {
|
|
1955
|
+
const tgt = elem.target[ti];
|
|
1956
|
+
const last = ti === elem.target.length - 1;
|
|
1957
|
+
tr = tbl.tr();
|
|
1958
|
+
|
|
1959
|
+
// Source code cell
|
|
1960
|
+
const td = tr.td().style("border-right-width: 0px");
|
|
1961
|
+
if (!first && !last) {
|
|
1962
|
+
td.style("border-top-style: none; border-bottom-style: none");
|
|
1963
|
+
} else if (!first) {
|
|
1964
|
+
td.style("border-top-style: none");
|
|
1965
|
+
} else if (!last) {
|
|
1966
|
+
td.style("border-bottom-style: none");
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
if (first) {
|
|
1970
|
+
if (sources.code.size === 1) {
|
|
1971
|
+
td.tx(elem.code);
|
|
1972
|
+
} else {
|
|
1973
|
+
td.tx(grp.source + " / " + elem.code);
|
|
1974
|
+
}
|
|
1975
|
+
const display = elem.display || await this.getDisplayForConcept(grp.source, elem.code);
|
|
1976
|
+
const dispTd = tr.td();
|
|
1977
|
+
if (!last) {
|
|
1978
|
+
dispTd.style("border-left-width: 0px; border-bottom-style: none");
|
|
1979
|
+
} else {
|
|
1980
|
+
dispTd.style("border-left-width: 0px");
|
|
1981
|
+
}
|
|
1982
|
+
dispTd.tx(display || '');
|
|
1983
|
+
} else {
|
|
1984
|
+
// Empty display cell for subsequent targets
|
|
1985
|
+
const dispTd = tr.td();
|
|
1986
|
+
if (!last) {
|
|
1987
|
+
dispTd.style("border-left-width: 0px; border-top-style: none; border-bottom-style: none");
|
|
1988
|
+
} else {
|
|
1989
|
+
dispTd.style("border-top-style: none; border-left-width: 0px");
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Source dependsOn columns
|
|
1994
|
+
for (const s of Object.keys(sources)) {
|
|
1995
|
+
if (s !== 'code') {
|
|
1996
|
+
const depTd = tr.td();
|
|
1997
|
+
const val = this.getDependsOnValue(tgt.dependsOn, s, sources[s].size !== 1);
|
|
1998
|
+
depTd.tx(val || '');
|
|
1999
|
+
const depDisplay = this.getDependsOnDisplay(tgt.dependsOn, s);
|
|
2000
|
+
if (depDisplay) {
|
|
2001
|
+
depTd.tx(" (" + depDisplay + ")");
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
first = false;
|
|
2007
|
+
|
|
2008
|
+
// Relationship cell
|
|
2009
|
+
if (hasRelationships) {
|
|
2010
|
+
this.renderConceptMapRelationship(tr, tgt);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Target code cell
|
|
2014
|
+
const tgtTd = tr.td().style("border-right-width: 0px");
|
|
2015
|
+
if (targets.code.size === 1) {
|
|
2016
|
+
tgtTd.tx(tgt.code || '');
|
|
2017
|
+
} else {
|
|
2018
|
+
tgtTd.tx((grp.target || '') + " / " + (tgt.code || ''));
|
|
2019
|
+
}
|
|
2020
|
+
const tgtDisplay = tgt.display || await this.getDisplayForConcept(grp.target, tgt.code);
|
|
2021
|
+
tr.td().style("border-left-width: 0px").tx(tgtDisplay || '');
|
|
2022
|
+
|
|
2023
|
+
// Target product columns
|
|
2024
|
+
for (const s of Object.keys(targets)) {
|
|
2025
|
+
if (s !== 'code') {
|
|
2026
|
+
const prodTd = tr.td();
|
|
2027
|
+
const val = this.getDependsOnValue(tgt.product, s, targets[s].size !== 1);
|
|
2028
|
+
prodTd.tx(val || '');
|
|
2029
|
+
const prodDisplay = this.getDependsOnDisplay(tgt.product, s);
|
|
2030
|
+
if (prodDisplay) {
|
|
2031
|
+
prodTd.tx(" (" + prodDisplay + ")");
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Comment cell
|
|
2037
|
+
if (hasComment) {
|
|
2038
|
+
tr.td().tx(tgt.comment || '');
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Property cells
|
|
2042
|
+
if (hasProperties) {
|
|
2043
|
+
for (const s of Object.keys(props)) {
|
|
2044
|
+
const propTd = tr.td();
|
|
2045
|
+
propTd.tx(this.getPropertyValueFromList(tgt.property, s));
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
this.addUnmapped(tbl, grp);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Render the relationship cell for a target element.
|
|
2056
|
+
* Handles both R5 relationship codes and legacy R4 equivalence codes via extension.
|
|
2057
|
+
*/
|
|
2058
|
+
renderConceptMapRelationship(tr, tgt) {
|
|
2059
|
+
if (tgt.relationship) {
|
|
2060
|
+
tr.td().tx(this.presentRelationshipCode(tgt.relationship));
|
|
2061
|
+
} else if (tgt.equivalence) {
|
|
2062
|
+
tr.td().tx(this.presentEquivalenceCode(tgt.equivalence));
|
|
2063
|
+
} else {
|
|
2064
|
+
tr.td().tx("(" + "equivalent" + ")");
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Render a code system details link in a header cell.
|
|
2070
|
+
* Mirrors Java renderCSDetailsLink.
|
|
2071
|
+
*/
|
|
2072
|
+
async renderCSDetailsLink(tr, url, span2) {
|
|
2073
|
+
const td = tr.td();
|
|
2074
|
+
if (span2) {
|
|
2075
|
+
td.colspan("2");
|
|
2076
|
+
}
|
|
2077
|
+
td.b().tx(this.translate('CONC_MAP_CODES'));
|
|
2078
|
+
td.tx(" " + this.translate('CONC_MAP_FRM') + " ");
|
|
2079
|
+
const linkinfo = this.linkResolver ? await this.linkResolver.resolveURL(this.opContext, url) : null;
|
|
2080
|
+
if (linkinfo) {
|
|
2081
|
+
td.ah(linkinfo.link).tx(linkinfo.description);
|
|
2082
|
+
} else {
|
|
2083
|
+
td.tx(url);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Translate a FHIR R5 ConceptMap relationship code to a human-readable string.
|
|
2089
|
+
* Uses the same strings as the Java renderer.
|
|
2090
|
+
*/
|
|
2091
|
+
presentRelationshipCode(code) {
|
|
2092
|
+
switch (code) {
|
|
2093
|
+
case 'related-to': return 'is related to';
|
|
2094
|
+
case 'equivalent': return 'is equivalent to';
|
|
2095
|
+
case 'source-is-narrower-than-target': return 'is narrower than';
|
|
2096
|
+
case 'source-is-broader-than-target': return 'is broader than';
|
|
2097
|
+
case 'not-related-to': return 'is not related to';
|
|
2098
|
+
default: return code;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Translate a legacy (R2/R3/R4) ConceptMap equivalence code to a human-readable string.
|
|
2104
|
+
* Uses the same strings as the Java renderer.
|
|
2105
|
+
*/
|
|
2106
|
+
presentEquivalenceCode(code) {
|
|
2107
|
+
switch (code) {
|
|
2108
|
+
case 'relatedto': return 'is related to';
|
|
2109
|
+
case 'equivalent': return 'is equivalent to';
|
|
2110
|
+
case 'equal': return 'is equal to';
|
|
2111
|
+
case 'wider': return 'maps to wider concept';
|
|
2112
|
+
case 'subsumes': return 'is subsumed by';
|
|
2113
|
+
case 'narrower': return 'maps to narrower concept';
|
|
2114
|
+
case 'specializes': return 'has specialization';
|
|
2115
|
+
case 'inexact': return 'maps loosely to';
|
|
2116
|
+
case 'unmatched': return 'has no match';
|
|
2117
|
+
case 'disjoint': return 'is not related to';
|
|
2118
|
+
default: return code;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/**
|
|
2123
|
+
* Check if a code and its display text are essentially the same
|
|
2124
|
+
* (ignoring spaces, hyphens, and case).
|
|
2125
|
+
*/
|
|
2126
|
+
isSameCodeAndDisplay(code, display) {
|
|
2127
|
+
if (!code || !display) return false;
|
|
2128
|
+
const c = code.replace(/[ -]/g, '').toLowerCase();
|
|
2129
|
+
const d = display.replace(/[ -]/g, '').toLowerCase();
|
|
2130
|
+
return c === d;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Look up a display string for a concept. Delegates to the linkResolver if available.
|
|
2135
|
+
*/
|
|
2136
|
+
async getDisplayForConcept(system, code) {
|
|
2137
|
+
if (!system || !code) return null;
|
|
2138
|
+
if (!this.linkResolver) return null;
|
|
2139
|
+
const result = await this.linkResolver.resolveCode(this.opContext, system, null, code);
|
|
2140
|
+
return result ? result.description : null;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Get a description for a concept attribute code (used in complex table headers).
|
|
2145
|
+
* Mirrors Java getDescForConcept.
|
|
2146
|
+
*/
|
|
2147
|
+
getDescForConcept(s) {
|
|
2148
|
+
if (s.startsWith('http://hl7.org/fhir/v2/element/')) {
|
|
2149
|
+
return 'v2 ' + s.substring('http://hl7.org/fhir/v2/element/'.length);
|
|
2150
|
+
}
|
|
2151
|
+
return s;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Extract a value from a dependsOn or product list by attribute name.
|
|
2156
|
+
*/
|
|
2157
|
+
getDependsOnValue(list, attribute) {
|
|
2158
|
+
if (!list) return null;
|
|
2159
|
+
for (const item of list) {
|
|
2160
|
+
if (item.attribute === attribute) {
|
|
2161
|
+
// R5 uses value[x], try common types
|
|
2162
|
+
if (item.valueCode) return item.valueCode;
|
|
2163
|
+
if (item.valueString) return item.valueString;
|
|
2164
|
+
if (item.valueCoding) return item.valueCoding.code || '';
|
|
2165
|
+
if (item.value) return String(item.value);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Extract a display from a dependsOn or product list by attribute name.
|
|
2173
|
+
*/
|
|
2174
|
+
// eslint-disable-next-line no-unused-vars
|
|
2175
|
+
getDependsOnDisplay(list, attribute) {
|
|
2176
|
+
// In current FHIR, dependsOn display is not directly available;
|
|
2177
|
+
// would require a lookup. Return null for now (matches Java which also returns null).
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Extract a property value from a target's property list by code.
|
|
2183
|
+
*/
|
|
2184
|
+
getPropertyValueFromList(list, code) {
|
|
2185
|
+
if (!list) return '';
|
|
2186
|
+
const results = [];
|
|
2187
|
+
for (const item of list) {
|
|
2188
|
+
if (item.code === code) {
|
|
2189
|
+
// R5 MappingPropertyComponent uses value[x]
|
|
2190
|
+
if (item.valueCode !== undefined) results.push(item.valueCode);
|
|
2191
|
+
else if (item.valueString !== undefined) results.push(item.valueString);
|
|
2192
|
+
else if (item.valueCoding !== undefined) results.push(item.valueCoding.code || '');
|
|
2193
|
+
else if (item.valueBoolean !== undefined) results.push(String(item.valueBoolean));
|
|
2194
|
+
else if (item.valueInteger !== undefined) results.push(String(item.valueInteger));
|
|
2195
|
+
else if (item.valueDecimal !== undefined) results.push(String(item.valueDecimal));
|
|
2196
|
+
else if (item.valueDateTime !== undefined) results.push(item.valueDateTime);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return results.join(', ');
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Render the unmapped section for a group, if present.
|
|
2204
|
+
* Currently a stub matching the Java implementation.
|
|
2205
|
+
*/
|
|
2206
|
+
addUnmapped(tbl, grp) {
|
|
2207
|
+
if (grp.unmapped) {
|
|
2208
|
+
// TODO: render unmapped mode/code/url when needed
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
1643
2212
|
}
|
|
1644
2213
|
|
|
1645
2214
|
module.exports = { Renderer };
|
|
@@ -15,11 +15,10 @@ function sanitizeFilename(text) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function getCacheFilePath(baseDir, canonicalUrl, version = null, paramsKey = null) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const base = `${canonicalUrl}|${version || ''}|${paramsKey || 'default'}`;
|
|
20
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
21
|
+
const filename = `${hash}.json`;
|
|
23
22
|
return path.join(baseDir, filename);
|
|
24
23
|
}
|
|
25
24
|
|
package/tx/ocl/cm-ocl.cjs
CHANGED
|
@@ -46,7 +46,10 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
46
46
|
async fetchConceptMap(url, version) {
|
|
47
47
|
this._validateFetchParams(url, version);
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const crypto = require('crypto');
|
|
50
|
+
const base = `${url}|${version || ''}`;
|
|
51
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
52
|
+
const direct = this.conceptMapMap.get(hash);
|
|
50
53
|
if (direct) {
|
|
51
54
|
return direct;
|
|
52
55
|
}
|
package/tx/ocl/cs-ocl.cjs
CHANGED
|
@@ -21,13 +21,12 @@ function normalizeCanonicalSystem(system) {
|
|
|
21
21
|
return system;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
let trimmed = system.trim();
|
|
25
25
|
if (!trimmed) {
|
|
26
26
|
return trimmed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
return trimmed.replace(/\/+$/, '');
|
|
29
|
+
return trimmed;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
@@ -517,16 +516,14 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
517
516
|
}
|
|
518
517
|
|
|
519
518
|
#normalizePath(pathValue) {
|
|
519
|
+
// Não normaliza nem remove barras, retorna exatamente o valor fornecido pelo autor
|
|
520
520
|
if (!pathValue) {
|
|
521
521
|
return null;
|
|
522
522
|
}
|
|
523
523
|
if (typeof pathValue !== 'string') {
|
|
524
524
|
return null;
|
|
525
525
|
}
|
|
526
|
-
|
|
527
|
-
return pathValue;
|
|
528
|
-
}
|
|
529
|
-
return `${this.baseUrl}${pathValue.startsWith('/') ? '' : '/'}${pathValue}`;
|
|
526
|
+
return pathValue;
|
|
530
527
|
}
|
|
531
528
|
|
|
532
529
|
async #fetchAllPages(path) {
|
|
@@ -537,7 +534,7 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
537
534
|
logger: console,
|
|
538
535
|
loggerPrefix: '[OCL]'
|
|
539
536
|
});
|
|
540
|
-
//
|
|
537
|
+
// Extra check: payload must be object or array
|
|
541
538
|
if (!result || (typeof result !== 'object' && !Array.isArray(result))) {
|
|
542
539
|
throw new Error('[OCL] Invalid response format: expected object or array');
|
|
543
540
|
}
|
|
@@ -1733,8 +1730,11 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1733
1730
|
}
|
|
1734
1731
|
|
|
1735
1732
|
#resourceKey() {
|
|
1733
|
+
const crypto = require('crypto');
|
|
1736
1734
|
const normalizedSystem = OCLSourceCodeSystemFactory.#normalizeSystem(this.system());
|
|
1737
|
-
|
|
1735
|
+
const base = `${normalizedSystem}|${this.version() || ''}`;
|
|
1736
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
1737
|
+
return hash;
|
|
1738
1738
|
}
|
|
1739
1739
|
|
|
1740
1740
|
currentChecksum() {
|