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.
@@ -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 filename = sanitizeFilename(canonicalUrl)
19
- + (version ? `_${sanitizeFilename(version)}` : '')
20
- + (paramsKey && paramsKey !== 'default' ? `_p_${sanitizeFilename(paramsKey)}` : '')
21
- + '.json';
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 direct = this.conceptMapMap.get(`${url}|${version}`) || this.conceptMapMap.get(url);
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
- const trimmed = system.trim();
24
+ let trimmed = system.trim();
25
25
  if (!trimmed) {
26
26
  return trimmed;
27
27
  }
28
28
 
29
- // Treat canonical URLs with and without trailing slash as equivalent.
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
- if (pathValue.startsWith('http://') || pathValue.startsWith('https://')) {
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
- // Verificação extra: payload deve ser objeto ou array
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
- return `${normalizedSystem}|${this.version() || ''}`;
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() {