@cdc/core 4.26.1 → 4.26.3

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 (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -21,7 +21,7 @@ describe('processMarkupVariables', () => {
21
21
  ]
22
22
 
23
23
  const content = 'The state is {{state}}'
24
- const result = processMarkupVariables(content, testData, variables)
24
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
25
25
 
26
26
  expect(result.processedContent).toBe('The state is California, Texas, and Florida')
27
27
  expect(result.shouldHideSection).toBe(false)
@@ -35,7 +35,7 @@ describe('processMarkupVariables', () => {
35
35
  ]
36
36
 
37
37
  const content = 'Data for {{state}} in {{year}}'
38
- const result = processMarkupVariables(content, testData, variables)
38
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
39
39
 
40
40
  expect(result.processedContent).toContain('California, Texas, and Florida')
41
41
  expect(result.processedContent).toContain('2023')
@@ -43,18 +43,16 @@ describe('processMarkupVariables', () => {
43
43
 
44
44
  it('should return original content if no variables defined', () => {
45
45
  const content = 'Text with {{undefined}}'
46
- const result = processMarkupVariables(content, testData, [])
46
+ const result = processMarkupVariables(content, testData, [], { locale: 'en-US' })
47
47
 
48
48
  expect(result.processedContent).toBe('Text with {{undefined}}')
49
49
  })
50
50
 
51
51
  it('should leave unknown variable tags unchanged', () => {
52
- const variables: MarkupVariable[] = [
53
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
54
- ]
52
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
55
53
 
56
54
  const content = 'Known: {{state}}, Unknown: {{unknown}}'
57
- const result = processMarkupVariables(content, testData, variables)
55
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
58
56
 
59
57
  expect(result.processedContent).toContain('California, Texas, and Florida')
60
58
  expect(result.processedContent).toContain('{{unknown}}')
@@ -74,7 +72,7 @@ describe('processMarkupVariables', () => {
74
72
  ]
75
73
 
76
74
  const content = 'Population: {{population}}'
77
- const result = processMarkupVariables(content, testData, variables)
75
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
78
76
 
79
77
  expect(result.processedContent).toContain('39,538,223')
80
78
  expect(result.processedContent).toContain('29,145,505')
@@ -93,7 +91,7 @@ describe('processMarkupVariables', () => {
93
91
  ]
94
92
 
95
93
  const content = 'Population: {{population}}'
96
- const result = processMarkupVariables(content, testData, variables)
94
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
97
95
 
98
96
  expect(result.processedContent).toContain('39538223')
99
97
  expect(result.processedContent).not.toContain('39,538,223')
@@ -107,14 +105,12 @@ describe('processMarkupVariables', () => {
107
105
  name: 'State',
108
106
  tag: '{{state}}',
109
107
  columnName: 'state',
110
- conditions: [
111
- { columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' }
112
- ]
108
+ conditions: [{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' }]
113
109
  }
114
110
  ]
115
111
 
116
112
  const content = 'The state is {{state}}'
117
- const result = processMarkupVariables(content, testData, variables)
113
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
118
114
 
119
115
  expect(result.processedContent).toBe('The state is California')
120
116
  })
@@ -125,14 +121,12 @@ describe('processMarkupVariables', () => {
125
121
  name: 'State',
126
122
  tag: '{{state}}',
127
123
  columnName: 'state',
128
- conditions: [
129
- { columnName: 'state', isOrIsNotEqualTo: 'is not', value: 'California' }
130
- ]
124
+ conditions: [{ columnName: 'state', isOrIsNotEqualTo: 'is not', value: 'California' }]
131
125
  }
132
126
  ]
133
127
 
134
128
  const content = 'States: {{state}}'
135
- const result = processMarkupVariables(content, testData, variables)
129
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
136
130
 
137
131
  expect(result.processedContent).toBe('States: Texas and Florida')
138
132
  expect(result.processedContent).not.toContain('California')
@@ -158,7 +152,7 @@ describe('processMarkupVariables', () => {
158
152
  ]
159
153
 
160
154
  const content = 'State: {{state}}'
161
- const result = processMarkupVariables(content, dataWithYears, variables)
155
+ const result = processMarkupVariables(content, dataWithYears, variables, { locale: 'en-US' })
162
156
 
163
157
  expect(result.processedContent).toBe('State: California')
164
158
  })
@@ -169,14 +163,12 @@ describe('processMarkupVariables', () => {
169
163
  name: 'State',
170
164
  tag: '{{state}}',
171
165
  columnName: 'state',
172
- conditions: [
173
- { columnName: 'state', isOrIsNotEqualTo: 'is', value: 'NonExistent' }
174
- ]
166
+ conditions: [{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'NonExistent' }]
175
167
  }
176
168
  ]
177
169
 
178
170
  const content = 'State: {{state}}'
179
- const result = processMarkupVariables(content, testData, variables)
171
+ const result = processMarkupVariables(content, testData, variables, { locale: 'en-US' })
180
172
 
181
173
  expect(result.processedContent).toBe('State: ')
182
174
  })
@@ -184,70 +176,45 @@ describe('processMarkupVariables', () => {
184
176
 
185
177
  describe('Empty Values and Null Handling', () => {
186
178
  it('should filter out empty string values', () => {
187
- const dataWithEmpty = [
188
- { name: 'Alice' },
189
- { name: '' },
190
- { name: 'Bob' },
191
- { name: '' }
192
- ]
179
+ const dataWithEmpty = [{ name: 'Alice' }, { name: '' }, { name: 'Bob' }, { name: '' }]
193
180
 
194
- const variables: MarkupVariable[] = [
195
- { name: 'Name', tag: '{{name}}', columnName: 'name', conditions: [] }
196
- ]
181
+ const variables: MarkupVariable[] = [{ name: 'Name', tag: '{{name}}', columnName: 'name', conditions: [] }]
197
182
 
198
183
  const content = 'Names: {{name}}'
199
- const result = processMarkupVariables(content, dataWithEmpty, variables)
184
+ const result = processMarkupVariables(content, dataWithEmpty, variables, { locale: 'en-US' })
200
185
 
201
186
  expect(result.processedContent).toBe('Names: Alice and Bob')
202
187
  })
203
188
 
204
189
  it('should handle null values gracefully', () => {
205
- const dataWithNull = [
206
- { value: 'A' },
207
- { value: null },
208
- { value: 'B' }
209
- ]
190
+ const dataWithNull = [{ value: 'A' }, { value: null }, { value: 'B' }]
210
191
 
211
- const variables: MarkupVariable[] = [
212
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
213
- ]
192
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
214
193
 
215
194
  const content = 'Values: {{value}}'
216
- const result = processMarkupVariables(content, dataWithNull, variables)
195
+ const result = processMarkupVariables(content, dataWithNull, variables, { locale: 'en-US' })
217
196
 
218
197
  expect(result.processedContent).toBe('Values: A and B')
219
198
  })
220
199
 
221
200
  it('should handle undefined values', () => {
222
- const dataWithUndefined = [
223
- { value: 'A' },
224
- { value: undefined },
225
- { value: 'B' }
226
- ]
201
+ const dataWithUndefined = [{ value: 'A' }, { value: undefined }, { value: 'B' }]
227
202
 
228
- const variables: MarkupVariable[] = [
229
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
230
- ]
203
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
231
204
 
232
205
  const content = 'Values: {{value}}'
233
- const result = processMarkupVariables(content, dataWithUndefined, variables)
206
+ const result = processMarkupVariables(content, dataWithUndefined, variables, { locale: 'en-US' })
234
207
 
235
208
  expect(result.processedContent).toBe('Values: A and B')
236
209
  })
237
210
 
238
211
  it('should return empty when all values are empty/null', () => {
239
- const dataEmpty = [
240
- { value: '' },
241
- { value: null },
242
- { value: undefined }
243
- ]
212
+ const dataEmpty = [{ value: '' }, { value: null }, { value: undefined }]
244
213
 
245
- const variables: MarkupVariable[] = [
246
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
247
- ]
214
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
248
215
 
249
216
  const content = 'Values: {{value}}'
250
- const result = processMarkupVariables(content, dataEmpty, variables)
217
+ const result = processMarkupVariables(content, dataEmpty, variables, { locale: 'en-US' })
251
218
 
252
219
  expect(result.processedContent).toBe('Values: ')
253
220
  })
@@ -256,36 +223,30 @@ describe('processMarkupVariables', () => {
256
223
  describe('List Formatting', () => {
257
224
  it('should format two items with "and" in production mode', () => {
258
225
  const data = [{ state: 'CA' }, { state: 'TX' }]
259
- const variables: MarkupVariable[] = [
260
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
261
- ]
226
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
262
227
 
263
228
  const content = '{{state}}'
264
- const result = processMarkupVariables(content, data, variables, { isEditor: false })
229
+ const result = processMarkupVariables(content, data, variables, { isEditor: false, locale: 'en-US' })
265
230
 
266
231
  expect(result.processedContent).toBe('CA and TX')
267
232
  })
268
233
 
269
234
  it('should format three items with Oxford comma and "and"', () => {
270
235
  const data = [{ state: 'CA' }, { state: 'TX' }, { state: 'FL' }]
271
- const variables: MarkupVariable[] = [
272
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
273
- ]
236
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
274
237
 
275
238
  const content = '{{state}}'
276
- const result = processMarkupVariables(content, data, variables, { isEditor: false })
239
+ const result = processMarkupVariables(content, data, variables, { isEditor: false, locale: 'en-US' })
277
240
 
278
241
  expect(result.processedContent).toBe('CA, TX, and FL')
279
242
  })
280
243
 
281
244
  it('should use "or" conjunction in editor mode', () => {
282
245
  const data = [{ state: 'CA' }, { state: 'TX' }]
283
- const variables: MarkupVariable[] = [
284
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
285
- ]
246
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
286
247
 
287
248
  const content = '{{state}}'
288
- const result = processMarkupVariables(content, data, variables, { isEditor: true })
249
+ const result = processMarkupVariables(content, data, variables, { isEditor: true, locale: 'en-US' })
289
250
 
290
251
  expect(result.processedContent).toBe('CA or TX')
291
252
  })
@@ -293,17 +254,12 @@ describe('processMarkupVariables', () => {
293
254
 
294
255
  describe('XSS Prevention and Security', () => {
295
256
  it('should handle data with HTML tags safely', () => {
296
- const maliciousData = [
297
- { value: '<script>alert("xss")</script>' },
298
- { value: '<img src=x onerror=alert(1)>' }
299
- ]
257
+ const maliciousData = [{ value: '<script>alert("xss")</script>' }, { value: '<img src=x onerror=alert(1)>' }]
300
258
 
301
- const variables: MarkupVariable[] = [
302
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
303
- ]
259
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
304
260
 
305
261
  const content = 'Data: {{value}}'
306
- const result = processMarkupVariables(content, maliciousData, variables)
262
+ const result = processMarkupVariables(content, maliciousData, variables, { locale: 'en-US' })
307
263
 
308
264
  // Should return the raw strings, parsing responsibility is on the component using html-react-parser
309
265
  expect(result.processedContent).toContain('<script>')
@@ -311,17 +267,12 @@ describe('processMarkupVariables', () => {
311
267
  })
312
268
 
313
269
  it('should handle special characters in data', () => {
314
- const specialData = [
315
- { value: 'Test & Value' },
316
- { value: 'Price: $100 < $200' }
317
- ]
270
+ const specialData = [{ value: 'Test & Value' }, { value: 'Price: $100 < $200' }]
318
271
 
319
- const variables: MarkupVariable[] = [
320
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
321
- ]
272
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
322
273
 
323
274
  const content = '{{value}}'
324
- const result = processMarkupVariables(content, specialData, variables)
275
+ const result = processMarkupVariables(content, specialData, variables, { locale: 'en-US' })
325
276
 
326
277
  expect(result.processedContent).toContain('Test & Value')
327
278
  expect(result.processedContent).toContain('Price: $100 < $200')
@@ -331,14 +282,13 @@ describe('processMarkupVariables', () => {
331
282
  describe('Hide Section Logic', () => {
332
283
  it('should set shouldHideSection when allowHideSection is true and values are empty', () => {
333
284
  const emptyData = [{ value: '' }]
334
- const variables: MarkupVariable[] = [
335
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
336
- ]
285
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
337
286
 
338
287
  const content = '{{value}}'
339
288
  const result = processMarkupVariables(content, emptyData, variables, {
340
289
  allowHideSection: true,
341
- isEditor: false
290
+ isEditor: false,
291
+ locale: 'en-US'
342
292
  })
343
293
 
344
294
  expect(result.shouldHideSection).toBe(true)
@@ -346,14 +296,13 @@ describe('processMarkupVariables', () => {
346
296
 
347
297
  it('should not hide section in editor mode even if values are empty', () => {
348
298
  const emptyData = [{ value: '' }]
349
- const variables: MarkupVariable[] = [
350
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
351
- ]
299
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
352
300
 
353
301
  const content = '{{value}}'
354
302
  const result = processMarkupVariables(content, emptyData, variables, {
355
303
  allowHideSection: true,
356
- isEditor: true
304
+ isEditor: true,
305
+ locale: 'en-US'
357
306
  })
358
307
 
359
308
  expect(result.shouldHideSection).toBe(false)
@@ -363,67 +312,268 @@ describe('processMarkupVariables', () => {
363
312
  describe('No Data Message Logic', () => {
364
313
  it('should set shouldShowNoDataMessage when enabled and values are empty', () => {
365
314
  const emptyData = [{ value: '' }]
366
- const variables: MarkupVariable[] = [
367
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
368
- ]
315
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
369
316
 
370
317
  const content = '{{value}}'
371
318
  const result = processMarkupVariables(content, emptyData, variables, {
372
319
  showNoDataMessage: true,
373
- isEditor: false
320
+ isEditor: false,
321
+ locale: 'en-US'
374
322
  })
375
323
 
376
324
  expect(result.shouldShowNoDataMessage).toBe(true)
377
325
  })
378
326
  })
379
327
 
380
- describe('Edge Cases', () => {
381
- it('should handle empty data array', () => {
328
+ describe('Metadata-Sourced Variables', () => {
329
+ it('should resolve metadata variable from dataMetadata', () => {
330
+ const variables: MarkupVariable[] = [
331
+ {
332
+ name: 'Last Updated',
333
+ tag: '{{lastUpdated}}',
334
+ metadataKey: 'lastUpdated',
335
+ conditions: []
336
+ }
337
+ ]
338
+
339
+ const content = 'Data last updated {{lastUpdated}}'
340
+ const result = processMarkupVariables(content, testData, variables, {
341
+ dataMetadata: { lastUpdated: 'January 15, 2026' }
342
+ })
343
+
344
+ expect(result.processedContent).toBe('Data last updated January 15, 2026')
345
+ expect(result.shouldHideSection).toBe(false)
346
+ expect(result.shouldShowNoDataMessage).toBe(false)
347
+ })
348
+
349
+ it('should return empty string when dataMetadata does not contain the key', () => {
350
+ const variables: MarkupVariable[] = [
351
+ {
352
+ name: 'Last Updated',
353
+ tag: '{{lastUpdated}}',
354
+ metadataKey: 'lastUpdated',
355
+ conditions: []
356
+ }
357
+ ]
358
+
359
+ const content = 'Data last updated {{lastUpdated}}'
360
+ const result = processMarkupVariables(content, testData, variables, {
361
+ dataMetadata: { source: 'CDC' }
362
+ })
363
+
364
+ expect(result.processedContent).toBe('Data last updated ')
365
+ })
366
+
367
+ it('should resolve metadata variable even when conditions are defined', () => {
368
+ const variables: MarkupVariable[] = [
369
+ {
370
+ name: 'Last Updated',
371
+ tag: '{{lastUpdated}}',
372
+ metadataKey: 'lastUpdated',
373
+ conditions: [{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' }]
374
+ }
375
+ ]
376
+
377
+ const content = '{{lastUpdated}}'
378
+ const result = processMarkupVariables(content, testData, variables, {
379
+ dataMetadata: { lastUpdated: 'January 15, 2026' }
380
+ })
381
+
382
+ expect(result.processedContent).toBe('January 15, 2026')
383
+ })
384
+
385
+ it('should handle mixed metadata and column variables in the same content', () => {
386
+ const variables: MarkupVariable[] = [
387
+ {
388
+ name: 'Last Updated',
389
+ tag: '{{lastUpdated}}',
390
+ metadataKey: 'lastUpdated',
391
+ conditions: []
392
+ },
393
+ {
394
+ name: 'State',
395
+ tag: '{{state}}',
396
+ columnName: 'state',
397
+ conditions: [{ columnName: 'state', isOrIsNotEqualTo: 'is', value: 'California' }]
398
+ }
399
+ ]
400
+
401
+ const content = '{{state}} data last updated {{lastUpdated}}'
402
+ const result = processMarkupVariables(content, testData, variables, {
403
+ dataMetadata: { lastUpdated: 'January 15, 2026' }
404
+ })
405
+
406
+ expect(result.processedContent).toBe('California data last updated January 15, 2026')
407
+ })
408
+
409
+ it('should not trigger columnName warning for metadata variable without columnName', () => {
410
+ const variables: MarkupVariable[] = [
411
+ {
412
+ name: 'Source',
413
+ tag: '{{source}}',
414
+ metadataKey: 'source',
415
+ conditions: []
416
+ }
417
+ ]
418
+
419
+ const content = 'Source: {{source}}'
420
+ const result = processMarkupVariables(content, testData, variables, {
421
+ dataMetadata: { source: 'CDC NREVSS' }
422
+ })
423
+
424
+ expect(result.processedContent).toBe('Source: CDC NREVSS')
425
+ })
426
+
427
+ it('should return empty string for metadata variable when dataMetadata is empty', () => {
428
+ const variables: MarkupVariable[] = [
429
+ {
430
+ name: 'Last Updated',
431
+ tag: '{{lastUpdated}}',
432
+ metadataKey: 'lastUpdated',
433
+ conditions: []
434
+ }
435
+ ]
436
+
437
+ const content: string = 'Updated: {{lastUpdated}}'
438
+ const result = processMarkupVariables(content, testData, variables, {
439
+ dataMetadata: {}
440
+ })
441
+
442
+ expect(result.processedContent).toBe('Updated: ')
443
+ })
444
+
445
+ it('should return empty string for metadata variable when dataMetadata is undefined', () => {
446
+ const variables: MarkupVariable[] = [
447
+ {
448
+ name: 'Last Updated',
449
+ tag: '{{lastUpdated}}',
450
+ metadataKey: 'lastUpdated',
451
+ conditions: []
452
+ }
453
+ ]
454
+
455
+ const content = 'Updated: {{lastUpdated}}'
456
+ const result = processMarkupVariables(content, testData, variables)
457
+
458
+ expect(result.processedContent).toBe('Updated: ')
459
+ })
460
+
461
+ it('should format numeric metadata value with commas when addCommas is true', () => {
462
+ const variables: MarkupVariable[] = [
463
+ {
464
+ name: 'Count',
465
+ tag: '{{count}}',
466
+ metadataKey: 'count',
467
+ conditions: [],
468
+ addCommas: true
469
+ }
470
+ ]
471
+
472
+ const content = 'Total: {{count}}'
473
+ const result = processMarkupVariables(content, testData, variables, {
474
+ dataMetadata: { count: '1234567' }
475
+ })
476
+
477
+ expect(result.processedContent).toBe('Total: 1,234,567')
478
+ })
479
+
480
+ it('should not format non-numeric metadata value even when addCommas is true', () => {
382
481
  const variables: MarkupVariable[] = [
383
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
482
+ {
483
+ name: 'Source',
484
+ tag: '{{source}}',
485
+ metadataKey: 'source',
486
+ conditions: [],
487
+ addCommas: true
488
+ }
384
489
  ]
385
490
 
491
+ const content = 'Source: {{source}}'
492
+ const result = processMarkupVariables(content, testData, variables, {
493
+ dataMetadata: { source: 'CDC' }
494
+ })
495
+
496
+ expect(result.processedContent).toBe('Source: CDC')
497
+ })
498
+
499
+ it('should set shouldHideSection when metadata value is empty and allowHideSection is true', () => {
500
+ const variables: MarkupVariable[] = [
501
+ {
502
+ name: 'Last Updated',
503
+ tag: '{{lastUpdated}}',
504
+ metadataKey: 'lastUpdated',
505
+ conditions: [],
506
+ hideOnNull: true
507
+ }
508
+ ]
509
+
510
+ const content = '{{lastUpdated}}'
511
+ const result = processMarkupVariables(content, testData, variables, {
512
+ dataMetadata: {},
513
+ allowHideSection: true,
514
+ isEditor: false
515
+ })
516
+
517
+ expect(result.shouldHideSection).toBe(true)
518
+ })
519
+
520
+ it('should not set shouldHideSection when metadata value exists', () => {
521
+ const variables: MarkupVariable[] = [
522
+ {
523
+ name: 'Last Updated',
524
+ tag: '{{lastUpdated}}',
525
+ metadataKey: 'lastUpdated',
526
+ conditions: [],
527
+ hideOnNull: true
528
+ }
529
+ ]
530
+
531
+ const content = '{{lastUpdated}}'
532
+ const result = processMarkupVariables(content, testData, variables, {
533
+ dataMetadata: { lastUpdated: 'Jan 2026' },
534
+ allowHideSection: true,
535
+ isEditor: false
536
+ })
537
+
538
+ expect(result.shouldHideSection).toBe(false)
539
+ })
540
+ })
541
+
542
+ describe('Edge Cases', () => {
543
+ it('should handle empty data array', () => {
544
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
545
+
386
546
  const content = '{{value}}'
387
- const result = processMarkupVariables(content, [], variables)
547
+ const result = processMarkupVariables(content, [], variables, { locale: 'en-US' })
388
548
 
389
549
  expect(result.processedContent).toBe('')
390
550
  })
391
551
 
392
552
  it('should handle empty content string', () => {
393
- const variables: MarkupVariable[] = [
394
- { name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }
395
- ]
553
+ const variables: MarkupVariable[] = [{ name: 'Value', tag: '{{value}}', columnName: 'value', conditions: [] }]
396
554
 
397
- const result = processMarkupVariables('', testData, variables)
555
+ const result = processMarkupVariables('', testData, variables, { locale: 'en-US' })
398
556
 
399
557
  expect(result.processedContent).toBe('')
400
558
  })
401
559
 
402
560
  it('should handle single item (no conjunction)', () => {
403
561
  const data = [{ state: 'California' }]
404
- const variables: MarkupVariable[] = [
405
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
406
- ]
562
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
407
563
 
408
564
  const content = '{{state}}'
409
- const result = processMarkupVariables(content, data, variables)
565
+ const result = processMarkupVariables(content, data, variables, { locale: 'en-US' })
410
566
 
411
567
  expect(result.processedContent).toBe('California')
412
568
  })
413
569
 
414
570
  it('should remove duplicate values from list', () => {
415
- const duplicateData = [
416
- { state: 'California' },
417
- { state: 'Texas' },
418
- { state: 'California' }
419
- ]
571
+ const duplicateData = [{ state: 'California' }, { state: 'Texas' }, { state: 'California' }]
420
572
 
421
- const variables: MarkupVariable[] = [
422
- { name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }
423
- ]
573
+ const variables: MarkupVariable[] = [{ name: 'State', tag: '{{state}}', columnName: 'state', conditions: [] }]
424
574
 
425
575
  const content = '{{state}}'
426
- const result = processMarkupVariables(content, duplicateData, variables)
576
+ const result = processMarkupVariables(content, duplicateData, variables, { locale: 'en-US' })
427
577
 
428
578
  expect(result.processedContent).toBe('California and Texas')
429
579
  })
@@ -431,9 +581,7 @@ describe('processMarkupVariables', () => {
431
581
  })
432
582
 
433
583
  describe('validateMarkupVariables', () => {
434
- const testData = [
435
- { state: 'CA', population: '1000' }
436
- ]
584
+ const testData = [{ state: 'CA', population: '1000' }]
437
585
 
438
586
  it('should return no errors for valid configuration', () => {
439
587
  const variables: MarkupVariable[] = [
@@ -535,4 +683,47 @@ describe('validateMarkupVariables', () => {
535
683
  const errors3 = validateMarkupVariables('not an array' as any, testData)
536
684
  expect(errors3).toHaveLength(0)
537
685
  })
538
- })
686
+
687
+ it('should skip columnName validation when variable has metadataKey', () => {
688
+ const variables: MarkupVariable[] = [
689
+ {
690
+ name: 'Last Updated',
691
+ tag: '{{lastUpdated}}',
692
+ metadataKey: 'lastUpdated',
693
+ conditions: []
694
+ }
695
+ ]
696
+
697
+ const errors = validateMarkupVariables(variables, testData)
698
+ expect(errors).toHaveLength(0)
699
+ expect(errors).not.toContain('Variable 1: Column name is required')
700
+ })
701
+
702
+ it('should still require columnName when neither metadataKey nor columnName is set', () => {
703
+ const variables: MarkupVariable[] = [
704
+ {
705
+ name: 'Bad Variable',
706
+ tag: '{{bad}}',
707
+ conditions: []
708
+ }
709
+ ]
710
+
711
+ const errors = validateMarkupVariables(variables, testData)
712
+ expect(errors).toContain('Variable 1: Column name is required')
713
+ })
714
+
715
+ it('should skip column-not-found validation when variable has metadataKey', () => {
716
+ const variables: MarkupVariable[] = [
717
+ {
718
+ name: 'Source',
719
+ tag: '{{source}}',
720
+ metadataKey: 'source',
721
+ conditions: []
722
+ }
723
+ ]
724
+
725
+ const errors = validateMarkupVariables(variables, testData)
726
+ expect(errors).toHaveLength(0)
727
+ expect(errors.find(e => e.includes('not found in data'))).toBeUndefined()
728
+ })
729
+ })