zombie-mermaid 2.2.3 → 2.2.6

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 (321) hide show
  1. package/README.md +4 -4
  2. package/dist/ascii.cjs +1 -11
  3. package/dist/ascii.d.cts +1 -167
  4. package/dist/ascii.d.ts +1 -167
  5. package/dist/ascii.js +1 -6591
  6. package/dist/cli.js +836 -10946
  7. package/dist/index.cjs +2 -81
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +42 -680
  10. package/dist/index.d.ts +42 -680
  11. package/dist/index.js +408 -10002
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp.cjs +1 -17909
  14. package/dist/mcp.d.cts +1 -11
  15. package/dist/mcp.d.ts +1 -11
  16. package/dist/mcp.js +1 -17884
  17. package/package.json +29 -29
  18. package/src/__tests__/workspace-package-boundaries.test.ts +15 -16
  19. package/src/__tests__/xychart-parser-categories.test.ts +78 -0
  20. package/src/ascii-entry.ts +14 -0
  21. package/src/cli/html-viewer.ts +5 -0
  22. package/src/mcp-entry.ts +21 -0
  23. package/dist/ascii.cjs.map +0 -1
  24. package/dist/ascii.js.map +0 -1
  25. package/dist/mcp.cjs.map +0 -1
  26. package/dist/mcp.js.map +0 -1
  27. package/packages/ascii-renderer/LICENSE +0 -22
  28. package/packages/ascii-renderer/src/ansi.ts +0 -490
  29. package/packages/ascii-renderer/src/canvas.ts +0 -658
  30. package/packages/ascii-renderer/src/class-diagram.ts +0 -2001
  31. package/packages/ascii-renderer/src/converter.ts +0 -446
  32. package/packages/ascii-renderer/src/coords.ts +0 -58
  33. package/packages/ascii-renderer/src/display-width.ts +0 -151
  34. package/packages/ascii-renderer/src/draw-arrows.ts +0 -549
  35. package/packages/ascii-renderer/src/draw-boxes.ts +0 -267
  36. package/packages/ascii-renderer/src/draw-bundles.ts +0 -611
  37. package/packages/ascii-renderer/src/draw-lines.ts +0 -174
  38. package/packages/ascii-renderer/src/draw-subgraphs.ts +0 -108
  39. package/packages/ascii-renderer/src/draw.ts +0 -337
  40. package/packages/ascii-renderer/src/edge-bundling.ts +0 -435
  41. package/packages/ascii-renderer/src/edge-cell-styles.ts +0 -81
  42. package/packages/ascii-renderer/src/edge-routing.ts +0 -1049
  43. package/packages/ascii-renderer/src/er-diagram.ts +0 -1488
  44. package/packages/ascii-renderer/src/flowchart.ts +0 -94
  45. package/packages/ascii-renderer/src/grid-occupancy.ts +0 -234
  46. package/packages/ascii-renderer/src/grid.ts +0 -1267
  47. package/packages/ascii-renderer/src/hyperlinks.ts +0 -248
  48. package/packages/ascii-renderer/src/index.ts +0 -163
  49. package/packages/ascii-renderer/src/lane-search.ts +0 -68
  50. package/packages/ascii-renderer/src/multiline-utils.ts +0 -82
  51. package/packages/ascii-renderer/src/pathfinder.ts +0 -448
  52. package/packages/ascii-renderer/src/registry.ts +0 -88
  53. package/packages/ascii-renderer/src/sequence.ts +0 -1318
  54. package/packages/ascii-renderer/src/shapes/circle.ts +0 -31
  55. package/packages/ascii-renderer/src/shapes/corners.ts +0 -273
  56. package/packages/ascii-renderer/src/shapes/diamond.ts +0 -31
  57. package/packages/ascii-renderer/src/shapes/hexagon.ts +0 -31
  58. package/packages/ascii-renderer/src/shapes/index.ts +0 -123
  59. package/packages/ascii-renderer/src/shapes/rectangle.ts +0 -199
  60. package/packages/ascii-renderer/src/shapes/rounded.ts +0 -31
  61. package/packages/ascii-renderer/src/shapes/special.ts +0 -352
  62. package/packages/ascii-renderer/src/shapes/stadium.ts +0 -122
  63. package/packages/ascii-renderer/src/shapes/state.ts +0 -204
  64. package/packages/ascii-renderer/src/shapes/types.ts +0 -78
  65. package/packages/ascii-renderer/src/territory.ts +0 -136
  66. package/packages/ascii-renderer/src/types.ts +0 -454
  67. package/packages/ascii-renderer/src/validate.ts +0 -189
  68. package/packages/ascii-renderer/src/xychart.ts +0 -1085
  69. package/packages/core/LICENSE +0 -22
  70. package/packages/core/src/click-directive.ts +0 -106
  71. package/packages/core/src/color-utils.ts +0 -205
  72. package/packages/core/src/diagram-type.ts +0 -35
  73. package/packages/core/src/direction-override.ts +0 -46
  74. package/packages/core/src/direction.ts +0 -56
  75. package/packages/core/src/index.ts +0 -34
  76. package/packages/core/src/init-directive.ts +0 -248
  77. package/packages/core/src/multiline-utils.ts +0 -275
  78. package/packages/core/src/statements.ts +0 -197
  79. package/packages/core/src/style-directives.ts +0 -214
  80. package/packages/core/src/text-metrics.ts +0 -498
  81. package/packages/core/src/theme.ts +0 -696
  82. package/packages/core/src/types.ts +0 -670
  83. package/packages/mcp/LICENSE +0 -22
  84. package/packages/mcp/src/index.ts +0 -17
  85. package/packages/mcp/src/server.ts +0 -135
  86. package/packages/mcp/src/tools/check-sequence-activations.ts +0 -91
  87. package/packages/mcp/src/tools/fix-sequence-activations.ts +0 -77
  88. package/packages/mcp/src/tools/render-ascii.ts +0 -99
  89. package/packages/mcp/src/tools/render-svg.ts +0 -95
  90. package/packages/mermaid-parser/LICENSE +0 -22
  91. package/packages/mermaid-parser/src/class/format.ts +0 -15
  92. package/packages/mermaid-parser/src/class/parser.ts +0 -593
  93. package/packages/mermaid-parser/src/class/types.ts +0 -166
  94. package/packages/mermaid-parser/src/er/parser.ts +0 -286
  95. package/packages/mermaid-parser/src/er/types.ts +0 -99
  96. package/packages/mermaid-parser/src/expanded-shapes.ts +0 -376
  97. package/packages/mermaid-parser/src/index.ts +0 -48
  98. package/packages/mermaid-parser/src/sequence/activation-check.ts +0 -149
  99. package/packages/mermaid-parser/src/sequence/activation-fix.ts +0 -112
  100. package/packages/mermaid-parser/src/sequence/box-color.ts +0 -85
  101. package/packages/mermaid-parser/src/sequence/parser.ts +0 -613
  102. package/packages/mermaid-parser/src/sequence/types.ts +0 -242
  103. package/packages/mermaid-parser/src/xychart/colors.ts +0 -177
  104. package/packages/mermaid-parser/src/xychart/parser.ts +0 -203
  105. package/packages/mermaid-parser/src/xychart/types.ts +0 -150
  106. package/packages/svg-renderer/LICENSE +0 -22
  107. package/packages/svg-renderer/src/class/layout.ts +0 -360
  108. package/packages/svg-renderer/src/class/renderer.ts +0 -636
  109. package/packages/svg-renderer/src/edge-curves.ts +0 -204
  110. package/packages/svg-renderer/src/elk-instance.ts +0 -292
  111. package/packages/svg-renderer/src/er/layout.ts +0 -200
  112. package/packages/svg-renderer/src/er/renderer.ts +0 -493
  113. package/packages/svg-renderer/src/index.ts +0 -58
  114. package/packages/svg-renderer/src/layout-engine/constants.ts +0 -19
  115. package/packages/svg-renderer/src/layout-engine/edge-bundling.ts +0 -379
  116. package/packages/svg-renderer/src/layout-engine/elk-adapter-utils.ts +0 -81
  117. package/packages/svg-renderer/src/layout-engine/elk-graph-builder.ts +0 -240
  118. package/packages/svg-renderer/src/layout-engine/from-elk.ts +0 -685
  119. package/packages/svg-renderer/src/layout-engine/layer-alignment.ts +0 -174
  120. package/packages/svg-renderer/src/layout-engine/to-elk.ts +0 -695
  121. package/packages/svg-renderer/src/layout-engine.ts +0 -74
  122. package/packages/svg-renderer/src/layout.ts +0 -8
  123. package/packages/svg-renderer/src/renderer.ts +0 -1485
  124. package/packages/svg-renderer/src/resolve-colors.ts +0 -339
  125. package/packages/svg-renderer/src/sequence/layout.ts +0 -698
  126. package/packages/svg-renderer/src/sequence/renderer.ts +0 -546
  127. package/packages/svg-renderer/src/shape-clipping.ts +0 -197
  128. package/packages/svg-renderer/src/styles.ts +0 -118
  129. package/packages/svg-renderer/src/xychart/layout.ts +0 -682
  130. package/packages/svg-renderer/src/xychart/renderer.ts +0 -684
  131. package/src/__tests__/ascii-canvas-write.test.ts +0 -137
  132. package/src/__tests__/ascii-charset-border-junctions.test.ts +0 -63
  133. package/src/__tests__/ascii-cjk-width.test.ts +0 -150
  134. package/src/__tests__/ascii-class-box-occupancy.test.ts +0 -462
  135. package/src/__tests__/ascii-class-column-width-488-489.test.ts +0 -639
  136. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +0 -187
  137. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +0 -114
  138. package/src/__tests__/ascii-class-diagram-compartments.test.ts +0 -70
  139. package/src/__tests__/ascii-class-label-box-collision.test.ts +0 -60
  140. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +0 -135
  141. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +0 -57
  142. package/src/__tests__/ascii-class-padding.test.ts +0 -104
  143. package/src/__tests__/ascii-class-parent-alignment-971.test.ts +0 -139
  144. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +0 -208
  145. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +0 -169
  146. package/src/__tests__/ascii-combining-mark-width.test.ts +0 -70
  147. package/src/__tests__/ascii-coords-overlay.test.ts +0 -64
  148. package/src/__tests__/ascii-decision-lr-box-start.test.ts +0 -106
  149. package/src/__tests__/ascii-display-width-unit.test.ts +0 -154
  150. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +0 -171
  151. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +0 -112
  152. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +0 -72
  153. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +0 -206
  154. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +0 -95
  155. package/src/__tests__/ascii-edge-routing-fixes.test.ts +0 -178
  156. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +0 -107
  157. package/src/__tests__/ascii-edge-styles.test.ts +0 -149
  158. package/src/__tests__/ascii-emoji-cluster-width.test.ts +0 -101
  159. package/src/__tests__/ascii-er-box-occupancy.test.ts +0 -251
  160. package/src/__tests__/ascii-er-cardinality.test.ts +0 -40
  161. package/src/__tests__/ascii-er-corner-glyphs.test.ts +0 -249
  162. package/src/__tests__/ascii-er-jog-stray-line.test.ts +0 -111
  163. package/src/__tests__/ascii-er-label-padding.test.ts +0 -118
  164. package/src/__tests__/ascii-er-padding.test.ts +0 -114
  165. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +0 -458
  166. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +0 -325
  167. package/src/__tests__/ascii-er-stray-connectors.test.ts +0 -347
  168. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +0 -98
  169. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +0 -110
  170. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +0 -172
  171. package/src/__tests__/ascii-lane-search.test.ts +0 -207
  172. package/src/__tests__/ascii-multibox-cjk-width.test.ts +0 -184
  173. package/src/__tests__/ascii-multiline.test.ts +0 -291
  174. package/src/__tests__/ascii-padding-edge-cases.test.ts +0 -154
  175. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +0 -194
  176. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +0 -236
  177. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +0 -217
  178. package/src/__tests__/ascii-sequence-box-group.test.ts +0 -159
  179. package/src/__tests__/ascii-sequence-cjk-width.test.ts +0 -235
  180. package/src/__tests__/ascii-sequence-create-destroy.test.ts +0 -114
  181. package/src/__tests__/ascii-sequence-form-invariants.test.ts +0 -432
  182. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +0 -219
  183. package/src/__tests__/ascii-sequence-notes.test.ts +0 -61
  184. package/src/__tests__/ascii-sequence-padding.test.ts +0 -119
  185. package/src/__tests__/ascii-sequence-self-arrow.test.ts +0 -206
  186. package/src/__tests__/ascii-shape-diamond.test.ts +0 -38
  187. package/src/__tests__/ascii-shape-rectangle.test.ts +0 -258
  188. package/src/__tests__/ascii-shape-rounded.test.ts +0 -36
  189. package/src/__tests__/ascii-shapes-circle.test.ts +0 -41
  190. package/src/__tests__/ascii-shapes-hexagon.test.ts +0 -42
  191. package/src/__tests__/ascii-shapes-special.test.ts +0 -349
  192. package/src/__tests__/ascii-shapes-stadium.test.ts +0 -217
  193. package/src/__tests__/ascii-shapes-state.test.ts +0 -234
  194. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +0 -130
  195. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +0 -90
  196. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +0 -152
  197. package/src/__tests__/ascii-subgraph-title-padding.test.ts +0 -77
  198. package/src/__tests__/ascii-territory-unit.test.ts +0 -219
  199. package/src/__tests__/ascii-validate.test.ts +0 -220
  200. package/src/__tests__/ascii.test.ts +0 -328
  201. package/src/__tests__/box-color.test.ts +0 -80
  202. package/src/__tests__/class-arrow-directions.test.ts +0 -505
  203. package/src/__tests__/click-directive.test.ts +0 -63
  204. package/src/__tests__/color-utils.test.ts +0 -186
  205. package/src/__tests__/diagram-type.test.ts +0 -28
  206. package/src/__tests__/draw-lines.test.ts +0 -93
  207. package/src/__tests__/edge-cell-styles.test.ts +0 -180
  208. package/src/__tests__/elk-adapter-utils.test.ts +0 -166
  209. package/src/__tests__/grid-occupancy.test.ts +0 -240
  210. package/src/__tests__/helpers/ascii-form.ts +0 -142
  211. package/src/__tests__/helpers/terminal-display-width.ts +0 -74
  212. package/src/__tests__/mcp-check-sequence-activations-tool.test.ts +0 -85
  213. package/src/__tests__/mcp-fix-sequence-activations-tool.test.ts +0 -134
  214. package/src/__tests__/mcp-server.test.ts +0 -163
  215. package/src/__tests__/pathfinder.test.ts +0 -246
  216. package/src/__tests__/sequence-activation-fix.test.ts +0 -91
  217. package/src/__tests__/testdata/ascii/ampersand_lhs.txt +0 -18
  218. package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +0 -18
  219. package/src/__tests__/testdata/ascii/ampersand_rhs.txt +0 -18
  220. package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +0 -18
  221. package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +0 -18
  222. package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +0 -18
  223. package/src/__tests__/testdata/ascii/back_reference_from_child.txt +0 -10
  224. package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +0 -22
  225. package/src/__tests__/testdata/ascii/backlink_from_top.txt +0 -22
  226. package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +0 -20
  227. package/src/__tests__/testdata/ascii/cls_all_relationships.txt +0 -19
  228. package/src/__tests__/testdata/ascii/cls_annotation.txt +0 -29
  229. package/src/__tests__/testdata/ascii/cls_association.txt +0 -14
  230. package/src/__tests__/testdata/ascii/cls_basic.txt +0 -15
  231. package/src/__tests__/testdata/ascii/cls_dependency.txt +0 -14
  232. package/src/__tests__/testdata/ascii/cls_inheritance.txt +0 -20
  233. package/src/__tests__/testdata/ascii/cls_methods.txt +0 -21
  234. package/src/__tests__/testdata/ascii/comments.txt +0 -23
  235. package/src/__tests__/testdata/ascii/custom_padding.txt +0 -10
  236. package/src/__tests__/testdata/ascii/duplicate_labels.txt +0 -19
  237. package/src/__tests__/testdata/ascii/er_attributes.txt +0 -21
  238. package/src/__tests__/testdata/ascii/er_basic.txt +0 -8
  239. package/src/__tests__/testdata/ascii/er_identifying.txt +0 -18
  240. package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +0 -29
  241. package/src/__tests__/testdata/ascii/graph_bt_direction.txt +0 -28
  242. package/src/__tests__/testdata/ascii/graph_tb_direction.txt +0 -26
  243. package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +0 -36
  244. package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +0 -23
  245. package/src/__tests__/testdata/ascii/self_reference.txt +0 -10
  246. package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +0 -10
  247. package/src/__tests__/testdata/ascii/seq_basic.txt +0 -17
  248. package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +0 -25
  249. package/src/__tests__/testdata/ascii/seq_self_message.txt +0 -18
  250. package/src/__tests__/testdata/ascii/single_node.txt +0 -8
  251. package/src/__tests__/testdata/ascii/single_node_longer_name.txt +0 -8
  252. package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +0 -38
  253. package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +0 -49
  254. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +0 -47
  255. package/src/__tests__/testdata/ascii/subgraph_empty.txt +0 -10
  256. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +0 -20
  257. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +0 -48
  258. package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +0 -32
  259. package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +0 -16
  260. package/src/__tests__/testdata/ascii/subgraph_nested.txt +0 -24
  261. package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +0 -30
  262. package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +0 -17
  263. package/src/__tests__/testdata/ascii/subgraph_single_node.txt +0 -16
  264. package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +0 -26
  265. package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +0 -44
  266. package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +0 -42
  267. package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +0 -32
  268. package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +0 -24
  269. package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +0 -20
  270. package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +0 -20
  271. package/src/__tests__/testdata/ascii/three_nodes.txt +0 -9
  272. package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +0 -8
  273. package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +0 -19
  274. package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +0 -19
  275. package/src/__tests__/testdata/ascii/two_nodes_linked.txt +0 -8
  276. package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +0 -8
  277. package/src/__tests__/testdata/ascii/two_root_nodes.txt +0 -19
  278. package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +0 -19
  279. package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +0 -19
  280. package/src/__tests__/testdata/unicode/ampersand_lhs.txt +0 -18
  281. package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +0 -18
  282. package/src/__tests__/testdata/unicode/ampersand_rhs.txt +0 -18
  283. package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +0 -18
  284. package/src/__tests__/testdata/unicode/back_reference_from_child.txt +0 -10
  285. package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +0 -22
  286. package/src/__tests__/testdata/unicode/backlink_from_top.txt +0 -22
  287. package/src/__tests__/testdata/unicode/cls_all_relationships.txt +0 -19
  288. package/src/__tests__/testdata/unicode/cls_annotation.txt +0 -29
  289. package/src/__tests__/testdata/unicode/cls_association.txt +0 -14
  290. package/src/__tests__/testdata/unicode/cls_basic.txt +0 -15
  291. package/src/__tests__/testdata/unicode/cls_dependency.txt +0 -14
  292. package/src/__tests__/testdata/unicode/cls_inheritance.txt +0 -20
  293. package/src/__tests__/testdata/unicode/cls_methods.txt +0 -21
  294. package/src/__tests__/testdata/unicode/comments.txt +0 -23
  295. package/src/__tests__/testdata/unicode/duplicate_labels.txt +0 -19
  296. package/src/__tests__/testdata/unicode/er_attributes.txt +0 -21
  297. package/src/__tests__/testdata/unicode/er_basic.txt +0 -8
  298. package/src/__tests__/testdata/unicode/er_identifying.txt +0 -18
  299. package/src/__tests__/testdata/unicode/graph_bt_direction.txt +0 -28
  300. package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +0 -23
  301. package/src/__tests__/testdata/unicode/self_reference.txt +0 -10
  302. package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +0 -10
  303. package/src/__tests__/testdata/unicode/seq_basic.txt +0 -17
  304. package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +0 -25
  305. package/src/__tests__/testdata/unicode/seq_self_message.txt +0 -18
  306. package/src/__tests__/testdata/unicode/single_node.txt +0 -8
  307. package/src/__tests__/testdata/unicode/single_node_longer_name.txt +0 -8
  308. package/src/__tests__/testdata/unicode/three_nodes.txt +0 -9
  309. package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +0 -8
  310. package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +0 -19
  311. package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +0 -19
  312. package/src/__tests__/testdata/unicode/two_nodes_linked.txt +0 -8
  313. package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +0 -8
  314. package/src/__tests__/testdata/unicode/two_root_nodes.txt +0 -19
  315. package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +0 -19
  316. package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +0 -19
  317. package/src/__tests__/text-metrics.test.ts +0 -473
  318. package/src/__tests__/theme-palette-contrast.test.ts +0 -84
  319. package/src/__tests__/theme.test.ts +0 -132
  320. package/src/__tests__/xychart-ascii.test.ts +0 -376
  321. package/src/__tests__/xychart-colors.test.ts +0 -149
@@ -1,473 +0,0 @@
1
- /**
2
- * Tests for text-metrics module — variable-width character measurement.
3
- */
4
- import { describe, it, expect } from 'vitest'
5
- import {
6
- getCharWidth,
7
- measureTextWidth,
8
- isWideChar,
9
- hasMeasuredAdvance,
10
- } from '@zombie-mermaid/core'
11
-
12
- // ============================================================================
13
- // Character width classification
14
- // ============================================================================
15
-
16
- describe('getCharWidth', () => {
17
- describe('narrow characters', () => {
18
- it('measures thin letters as narrow (i, l, t, f, j, I)', () => {
19
- for (const ch of ['i', 'l', 't', 'f', 'j', 'I']) {
20
- expect(getCharWidth(ch), ch).toBeLessThan(0.7)
21
- }
22
- })
23
-
24
- it('measures thin punctuation as narrow', () => {
25
- for (const ch of ['!', '|', '.', ',', ':', ';', "'", '1']) {
26
- expect(getCharWidth(ch), ch).toBeLessThan(0.8)
27
- }
28
- })
29
-
30
- it('measures r as semi-narrow', () => {
31
- expect(getCharWidth('r')).toBeGreaterThan(getCharWidth('i'))
32
- expect(getCharWidth('r')).toBeLessThan(getCharWidth('a'))
33
- })
34
- })
35
-
36
- describe('normal characters', () => {
37
- it('measures average lowercase letters near 1.0', () => {
38
- for (const ch of ['a', 'e', 'o', 'n', 's']) {
39
- expect(getCharWidth(ch), ch).toBeGreaterThan(0.9)
40
- expect(getCharWidth(ch), ch).toBeLessThan(1.15)
41
- }
42
- })
43
-
44
- it('measures digits near 1.0-1.2 (tabular width)', () => {
45
- for (const ch of ['0', '2', '9']) {
46
- expect(getCharWidth(ch), ch).toBeGreaterThan(1.0)
47
- expect(getCharWidth(ch), ch).toBeLessThan(1.2)
48
- }
49
- })
50
- })
51
-
52
- describe('wide characters', () => {
53
- it('measures uppercase letters wider than lowercase (except I)', () => {
54
- for (const ch of ['A', 'B', 'N', 'Z']) {
55
- expect(getCharWidth(ch), ch).toBeGreaterThan(1.1)
56
- expect(getCharWidth(ch), ch).toBeLessThan(1.45)
57
- }
58
- })
59
-
60
- it('measures wide lowercase (w, m) above 1.4', () => {
61
- expect(getCharWidth('w')).toBeGreaterThan(1.4)
62
- expect(getCharWidth('m')).toBeGreaterThan(1.4)
63
- })
64
-
65
- it('measures very wide characters (W, M) above 1.6', () => {
66
- expect(getCharWidth('W')).toBeGreaterThan(1.6)
67
- expect(getCharWidth('M')).toBeGreaterThan(1.6)
68
- })
69
- })
70
-
71
- describe('space', () => {
72
- it('measures space at its real Inter advance (~0.52)', () => {
73
- // The old bucket value of 0.3 underestimated every word gap by ~1.3px
74
- expect(getCharWidth(' ')).toBeCloseTo(0.521, 3)
75
- })
76
- })
77
-
78
- describe('previously-omitted punctuation (measured Inter advances)', () => {
79
- // Real Inter advances (canvas measureText, headless Chrome, weight 400,
80
- // same method as the rest of INTER_ADVANCES), measured at 100px and
81
- // normalized by the weight-400 baseRatio (0.54): ratio = px / (100 * 0.54).
82
- // Before this table entry existed, these characters fell through to the
83
- // old coarse bucket (`) or the 1.0 default (all others), which could
84
- // undersize labels with repeated occurrences of these characters.
85
- it.each([
86
- ['#', 1.173],
87
- ['$', 1.188],
88
- ['&', 1.193],
89
- ['*', 0.928],
90
- ['<', 1.225],
91
- ['=', 1.225],
92
- ['>', 1.225],
93
- ['?', 0.947],
94
- ['^', 0.873],
95
- ['`', 0.598],
96
- ['~', 1.225],
97
- ])('measures %s at its real Inter advance', (ch, expected) => {
98
- expect(getCharWidth(ch)).toBeCloseTo(expected, 3)
99
- })
100
- })
101
-
102
- describe('table completeness', () => {
103
- it('has an exact measured Inter advance for every printable ASCII character (U+0020–U+007E)', () => {
104
- const missing: string[] = []
105
- for (let code = 0x20; code <= 0x7e; code++) {
106
- const ch = String.fromCharCode(code)
107
- if (!hasMeasuredAdvance(ch))
108
- missing.push(`U+${code.toString(16).padStart(4, '0')} (${ch})`)
109
- }
110
- expect(missing).toEqual([])
111
- })
112
- })
113
-
114
- describe('inherited Object.prototype keys', () => {
115
- // INTER_ADVANCES is a plain object literal, so bracket access with
116
- // `!== undefined` also matches inherited Object.prototype properties
117
- // like 'toString' and 'constructor' — they are not real table entries,
118
- // but a naive `obj[key] !== undefined` check can't tell the difference.
119
- it.each(['toString', 'constructor', 'hasOwnProperty', '__proto__'])(
120
- 'hasMeasuredAdvance(%s) is false — not an own property of the table',
121
- (key) => {
122
- expect(hasMeasuredAdvance(key)).toBe(false)
123
- },
124
- )
125
-
126
- it('getCharWidth never returns an inherited property instead of a measured width', () => {
127
- // Without an own-property check, `INTER_ADVANCES['toString'] !== undefined`
128
- // is true (it resolves Object.prototype.toString), so getCharWidth('toString')
129
- // would return that function instead of a number. With the own-property
130
- // check in place, the direct-table lookup correctly misses, and the
131
- // multi-character string instead falls through to the (unrelated,
132
- // NFD-based) accented-Latin branch, which matches on its first
133
- // normalized character ('t') — still a real measured number, never a
134
- // function.
135
- const result = getCharWidth('toString')
136
- expect(typeof result).toBe('number')
137
- expect(result).toBe(getCharWidth('t'))
138
- })
139
- })
140
-
141
- describe('combining marks (zero-width)', () => {
142
- it('returns 0 for combining diacritical marks', () => {
143
- // U+0301 = combining acute accent
144
- expect(getCharWidth('\u0301')).toBe(0)
145
- // U+0308 = combining diaeresis
146
- expect(getCharWidth('\u0308')).toBe(0)
147
- // U+0327 = combining cedilla
148
- expect(getCharWidth('\u0327')).toBe(0)
149
- })
150
- })
151
-
152
- describe('accented characters (precomposed)', () => {
153
- it('returns the base letter width for precomposed accented letters', () => {
154
- // Accents don't change the advance — é measures the same as e
155
- expect(getCharWidth('é')).toBe(getCharWidth('e')) // U+00E9
156
- expect(getCharWidth('ñ')).toBe(getCharWidth('n')) // U+00F1
157
- expect(getCharWidth('ü')).toBe(getCharWidth('u')) // U+00FC
158
- expect(getCharWidth('ç')).toBe(getCharWidth('c')) // U+00E7
159
- expect(getCharWidth('ö')).toBe(getCharWidth('o')) // U+00F6
160
- })
161
-
162
- it('falls through to the default character-class logic when the NFD base is not itself a measured ASCII letter', () => {
163
- // U+0439 (й, Cyrillic "short i") NFD-decomposes to U+0438 (и) + a
164
- // combining breve. The base "и" differs from the char, but it is a
165
- // Cyrillic letter, not one of the printable-ASCII keys in
166
- // INTER_ADVANCES — so the lookup misses and getCharWidth must fall
167
- // through to the coarse character-class buckets below instead of
168
- // returning `undefined`-derived garbage.
169
- const char = 'й'
170
- const base = char.normalize('NFD')[0]
171
- if (base === undefined)
172
- throw new Error(
173
- 'unreachable: normalize() of a non-empty string always has index 0',
174
- )
175
- expect(base).not.toBe(char) // sanity: this char does decompose
176
- expect(hasMeasuredAdvance(base)).toBe(false) // sanity: base has no exact advance
177
-
178
- // None of the ASCII character-class buckets match a Cyrillic letter,
179
- // and its code point (0x0439) is outside both the A-Z and 0-9 ranges,
180
- // so it lands on the final default width.
181
- expect(getCharWidth(char)).toBe(1.0)
182
- })
183
- })
184
-
185
- describe('CJK characters (fullwidth)', () => {
186
- it('returns 2.0 for CJK ideographs', () => {
187
- expect(getCharWidth('中')).toBe(2.0) // U+4E2D
188
- expect(getCharWidth('国')).toBe(2.0) // U+56FD
189
- expect(getCharWidth('字')).toBe(2.0) // U+5B57
190
- })
191
-
192
- it('returns 2.0 for Japanese hiragana/katakana', () => {
193
- expect(getCharWidth('あ')).toBe(2.0) // Hiragana
194
- expect(getCharWidth('ア')).toBe(2.0) // Katakana
195
- })
196
-
197
- it('returns 2.0 for Korean hangul', () => {
198
- expect(getCharWidth('한')).toBe(2.0) // U+D55C
199
- expect(getCharWidth('글')).toBe(2.0) // U+AE00
200
- })
201
- })
202
-
203
- describe('emoji (fullwidth)', () => {
204
- it('returns 2.0 for common emoji', () => {
205
- expect(getCharWidth('😀')).toBe(2.0)
206
- expect(getCharWidth('🚀')).toBe(2.0)
207
- expect(getCharWidth('❤')).toBe(2.0)
208
- })
209
- })
210
-
211
- describe('edge cases', () => {
212
- it('returns 0 for empty string', () => {
213
- expect(getCharWidth('')).toBe(0)
214
- })
215
- })
216
- })
217
-
218
- // ============================================================================
219
- // Text width measurement
220
- // ============================================================================
221
-
222
- describe('measureTextWidth', () => {
223
- const fontSize = 13
224
- const fontWeight = 500
225
- const baseRatio = 0.57 // weight 500 (was 0.55, increased for edge truncation safety)
226
- const minPadding = fontSize * 0.15 // minimum padding added to prevent truncation (increased for label separation)
227
-
228
- it('returns minPadding for empty text', () => {
229
- // Empty text still gets minimum padding to prevent edge truncation
230
- expect(measureTextWidth('', fontSize, fontWeight)).toBeCloseTo(
231
- minPadding,
232
- 1,
233
- )
234
- })
235
-
236
- // Sum of per-char ratios — verifies the formula without pinning calibration
237
- const ratioSum = (text: string) =>
238
- [...text].reduce((s, ch) => s + getCharWidth(ch), 0)
239
-
240
- it('handles lowercase text with narrow letters', () => {
241
- const width = measureTextWidth('hello', fontSize, fontWeight)
242
- expect(width).toBeCloseTo(
243
- ratioSum('hello') * fontSize * baseRatio + minPadding,
244
- 1,
245
- )
246
- })
247
-
248
- it('narrow text is narrower than uniform estimate', () => {
249
- // "illiterate" has many narrow chars (i, l, t)
250
- const narrow = measureTextWidth('illicit', fontSize, fontWeight)
251
- const uniform = 'illicit'.length * fontSize * baseRatio
252
- expect(narrow).toBeLessThan(uniform)
253
- })
254
-
255
- it('wide text is wider than uniform estimate', () => {
256
- // "MAMMOTH" has wide chars (M, A, O)
257
- const wide = measureTextWidth('MAMMOTH', fontSize, fontWeight)
258
- const uniform = 'MAMMOTH'.length * fontSize * baseRatio
259
- expect(wide).toBeGreaterThan(uniform)
260
- })
261
-
262
- it('handles mixed Latin text', () => {
263
- const width = measureTextWidth('Will', fontSize, fontWeight)
264
- expect(width).toBeCloseTo(
265
- ratioSum('Will') * fontSize * baseRatio + minPadding,
266
- 1,
267
- )
268
- })
269
-
270
- it('handles spaces correctly', () => {
271
- const width = measureTextWidth('a b', fontSize, fontWeight)
272
- expect(width).toBeCloseTo(
273
- ratioSum('a b') * fontSize * baseRatio + minPadding,
274
- 1,
275
- )
276
- })
277
-
278
- it('handles decomposed accents (base + combining mark)', () => {
279
- // "café" with decomposed é = c + a + f + e + combining accent
280
- // Should be same width as "cafe" since combining mark is zero-width
281
- const decomposed = 'cafe\u0301' // e + combining acute
282
- const precomposed = 'café'
283
- const widthDecomposed = measureTextWidth(decomposed, fontSize, fontWeight)
284
- const widthPrecomposed = measureTextWidth(precomposed, fontSize, fontWeight)
285
- expect(widthDecomposed).toBeCloseTo(widthPrecomposed, 1)
286
- })
287
-
288
- it('handles CJK text', () => {
289
- // "中国" = 2 chars × 2.0 width = 4.0
290
- const width = measureTextWidth('中国', fontSize, fontWeight)
291
- expect(width).toBeCloseTo(4.0 * fontSize * baseRatio + minPadding, 1)
292
- })
293
-
294
- it('handles mixed Latin and CJK', () => {
295
- const width = measureTextWidth('Hello中国', fontSize, fontWeight)
296
- expect(width).toBeCloseTo(
297
- ratioSum('Hello中国') * fontSize * baseRatio + minPadding,
298
- 1,
299
- )
300
- })
301
-
302
- it('heavier weights produce wider estimates', () => {
303
- const regular = measureTextWidth('Test', fontSize, 400)
304
- const medium = measureTextWidth('Test', fontSize, 500)
305
- const bold = measureTextWidth('Test', fontSize, 600)
306
-
307
- expect(medium).toBeGreaterThan(regular)
308
- expect(bold).toBeGreaterThan(medium)
309
- })
310
-
311
- it('scales with font size', () => {
312
- const small = measureTextWidth('Test', 11, fontWeight)
313
- const large = measureTextWidth('Test', 16, fontWeight)
314
-
315
- expect(large).toBeGreaterThan(small)
316
- expect(large / small).toBeCloseTo(16 / 11, 1)
317
- })
318
- })
319
-
320
- // ============================================================================
321
- // Calibration against real Inter metrics
322
- // ============================================================================
323
- //
324
- // Fixtures measured with canvas measureText in headless Chrome 138 with the
325
- // real Inter font loaded (400 weight, 11px — the edge-label spec). If the
326
- // estimator underestimates these, edge-label text overflows its 8px-padded
327
- // background rect; if it grossly overestimates, layout gets bloated.
328
-
329
- describe('calibration against real Inter widths (11px, weight 400)', () => {
330
- const REAL_INTER_WIDTHS: Record<string, number> = {
331
- yes: 18.19,
332
- no: 13.09,
333
- 'on failure': 48.47,
334
- 'validates credentials': 106.56,
335
- 'sends confirmation email': 130.6,
336
- 'asynchronous message processing': 184.1,
337
- 'returns HTTP 401 Unauthorized response': 215.65,
338
- 'WRITES TO DATABASE': 119.44,
339
- 'user_id + session_token validation': 177.51,
340
- 'retry with exponential backoff (max 5)': 197.49,
341
- }
342
-
343
- it('never underestimates real width by more than 3%', () => {
344
- for (const [text, real] of Object.entries(REAL_INTER_WIDTHS)) {
345
- const est = measureTextWidth(text, 11, 400)
346
- expect(
347
- est,
348
- `"${text}" est ${est.toFixed(1)} vs real ${real}`,
349
- ).toBeGreaterThanOrEqual(real * 0.97)
350
- }
351
- })
352
-
353
- it('never overestimates real width by more than 12% + 3px', () => {
354
- for (const [text, real] of Object.entries(REAL_INTER_WIDTHS)) {
355
- const est = measureTextWidth(text, 11, 400)
356
- expect(
357
- est,
358
- `"${text}" est ${est.toFixed(1)} vs real ${real}`,
359
- ).toBeLessThanOrEqual(real * 1.12 + 3)
360
- }
361
- })
362
-
363
- it('preserves at least 6px of the nominal 8px edge-label padding per side', () => {
364
- // Edge-label background rect = estimated width + 8px padding each side.
365
- // The visible gap between glyphs and rect wall must stay close to 8px.
366
- const EDGE_LABEL_PADDING = 8
367
- for (const [text, real] of Object.entries(REAL_INTER_WIDTHS)) {
368
- const est = measureTextWidth(text, 11, 400)
369
- const gapPerSide = (est + EDGE_LABEL_PADDING * 2 - real) / 2
370
- expect(
371
- gapPerSide,
372
- `"${text}" gap ${gapPerSide.toFixed(1)}px`,
373
- ).toBeGreaterThanOrEqual(6)
374
- }
375
- })
376
- })
377
-
378
- // ============================================================================
379
- // Real-world examples
380
- // ============================================================================
381
-
382
- describe('real-world text examples', () => {
383
- const fontSize = 13
384
- const fontWeight = 500
385
-
386
- it('handles typical node labels', () => {
387
- const labels = ['User', 'Database', 'API Gateway', 'Load Balancer']
388
- for (const label of labels) {
389
- const width = measureTextWidth(label, fontSize, fontWeight)
390
- expect(width).toBeGreaterThan(0)
391
- // Width should be reasonable (not too small or too large)
392
- expect(width).toBeGreaterThan(label.length * 3)
393
- expect(width).toBeLessThan(label.length * 15)
394
- }
395
- })
396
-
397
- it('handles Japanese labels', () => {
398
- const width = measureTextWidth('データベース', fontSize, fontWeight)
399
- // 6 CJK chars × 2.0 × 13 × 0.57 + minPadding
400
- const baseRatio = 0.57
401
- const minPadding = fontSize * 0.15
402
- expect(width).toBeCloseTo(6 * 2.0 * fontSize * baseRatio + minPadding, 1)
403
- })
404
-
405
- it('handles Hungarian text with accents', () => {
406
- const width = measureTextWidth('Üdvözöljük', fontSize, fontWeight)
407
- expect(width).toBeGreaterThan(0)
408
- // Should be similar to unaccented version (within 5% difference)
409
- const unaccented = measureTextWidth('Udvozoljuk', fontSize, fontWeight)
410
- const percentDiff = Math.abs(width - unaccented) / unaccented
411
- expect(percentDiff).toBeLessThan(0.05)
412
- })
413
- })
414
-
415
- // ============================================================================
416
- // isWideChar — shared wide-character detection (used by the ASCII grid's
417
- // display-width helpers in src/ascii/display-width.ts)
418
- // ============================================================================
419
-
420
- describe('isWideChar', () => {
421
- it('returns true for CJK ideographs, kana, and hangul', () => {
422
- expect(isWideChar('日')).toBe(true)
423
- expect(isWideChar('本')).toBe(true)
424
- expect(isWideChar('語')).toBe(true)
425
- expect(isWideChar('テ')).toBe(true) // katakana
426
- expect(isWideChar('あ')).toBe(true) // hiragana
427
- expect(isWideChar('한')).toBe(true) // hangul syllable
428
- })
429
-
430
- it('returns true for fullwidth-form characters', () => {
431
- expect(isWideChar('A')).toBe(true) // fullwidth Latin A (U+FF21)
432
- expect(isWideChar('1')).toBe(true) // fullwidth digit 1 (U+FF11)
433
- })
434
-
435
- it('returns true for emoji', () => {
436
- expect(isWideChar('😀')).toBe(true)
437
- expect(isWideChar('🎉')).toBe(true)
438
- })
439
-
440
- it('returns false for ASCII letters, digits, and punctuation', () => {
441
- expect(isWideChar('A')).toBe(false)
442
- expect(isWideChar('a')).toBe(false)
443
- expect(isWideChar('1')).toBe(false)
444
- expect(isWideChar(' ')).toBe(false)
445
- expect(isWideChar('.')).toBe(false)
446
- })
447
-
448
- it('returns false for accented Latin characters', () => {
449
- expect(isWideChar('Ü')).toBe(false)
450
- expect(isWideChar('é')).toBe(false)
451
- })
452
-
453
- it('returns false for arrowhead and box-drawing glyphs used by the ASCII renderer', () => {
454
- // These are Extended_Pictographic (emoji-capable) but have no default
455
- // Emoji_Presentation and render as a single narrow column in every real
456
- // terminal. Regression test for the ▶ (U+25B6) misclassification —
457
- // used as the sequence-diagram arrowhead in src/ascii/sequence.ts.
458
- expect(isWideChar('▶')).toBe(false) // solid ->> / -->> arrowhead
459
- expect(isWideChar('◀')).toBe(false) // solid self-message loop arrowhead
460
- expect(isWideChar('▼')).toBe(false)
461
- expect(isWideChar('▲')).toBe(false)
462
- expect(isWideChar('◄')).toBe(false)
463
- expect(isWideChar('►')).toBe(false)
464
- expect(isWideChar('◁')).toBe(false)
465
- expect(isWideChar('▷')).toBe(false)
466
- expect(isWideChar('◢')).toBe(false)
467
- expect(isWideChar('◣')).toBe(false)
468
- expect(isWideChar('◤')).toBe(false)
469
- expect(isWideChar('◥')).toBe(false)
470
- expect(isWideChar('○')).toBe(false)
471
- expect(isWideChar('✕')).toBe(false)
472
- })
473
- })
@@ -1,84 +0,0 @@
1
- /**
2
- * Guards the "legible, readable contrast" acceptance criterion the #684
3
- * theme-selector restoration's 16 per-theme verification sub-issues
4
- * (#691-706) each individually ask for, systematically across every
5
- * built-in theme at once rather than eyeballing 16 screenshots one at a
6
- * time — see docs/decisions/theme-selector-shared-state.md and this
7
- * repo's CLAUDE.md's "favor tooling over manual scanning" convention.
8
- *
9
- * Computes the actual WCAG 2.x contrast ratio (relative luminance, per
10
- * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance) between each
11
- * theme's `bg`/`fg` pair — not a hand-picked pass/fail per theme, so a
12
- * future THEMES addition with a genuinely illegible pair fails this test
13
- * automatically instead of only being caught by a human noticing a
14
- * screenshot looks wrong.
15
- */
16
- import { describe, it, expect } from 'vitest'
17
- import { THEMES } from '@zombie-mermaid/core'
18
-
19
- /**
20
- * The threshold every theme must clear. WCAG AA's own thresholds are 4.5:1
21
- * for normal text and 3:1 for large/bold text; diagram node labels here
22
- * are typically medium, semi-bold text, closer to the "large text"
23
- * category than dense body copy. 4.0 sits between the two -- strict enough
24
- * to catch a genuinely illegible pair, loose enough not to fail
25
- * `solarized-light` (4.13:1), whose muted bg/fg pair matches Ethan
26
- * Schoonover's actual published Solarized values rather than being a
27
- * mistake in this repo's THEMES data -- #684's own non-goals rule out
28
- * changing THEMES' colors to "fix" a curated editor-theme's real values.
29
- */
30
- const MIN_CONTRAST_RATIO = 4.0
31
-
32
- function hexToRgb(hex: string): [number, number, number] {
33
- const clean = hex.replace('#', '')
34
- return [
35
- parseInt(clean.slice(0, 2), 16),
36
- parseInt(clean.slice(2, 4), 16),
37
- parseInt(clean.slice(4, 6), 16),
38
- ]
39
- }
40
-
41
- /** sRGB channel -> linear-light value, the WCAG relative-luminance step. */
42
- function linearize(channel: number): number {
43
- const c = channel / 255
44
- return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
45
- }
46
-
47
- function relativeLuminance(hex: string): number {
48
- const [r, g, b] = hexToRgb(hex)
49
- return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
50
- }
51
-
52
- /** WCAG contrast ratio between two colors, always >= 1 (order-independent). */
53
- function contrastRatio(hexA: string, hexB: string): number {
54
- const lumA = relativeLuminance(hexA)
55
- const lumB = relativeLuminance(hexB)
56
- const lighter = Math.max(lumA, lumB)
57
- const darker = Math.min(lumA, lumB)
58
- return (lighter + 0.05) / (darker + 0.05)
59
- }
60
-
61
- describe('every built-in theme has a legible bg/fg contrast ratio', () => {
62
- for (const [themeKey, colors] of Object.entries(THEMES)) {
63
- it(`${themeKey}: bg/fg contrast is at least ${MIN_CONTRAST_RATIO}:1`, () => {
64
- const ratio = contrastRatio(colors.bg, colors.fg)
65
- expect(ratio).toBeGreaterThanOrEqual(MIN_CONTRAST_RATIO)
66
- })
67
- }
68
- })
69
-
70
- describe('contrastRatio() itself', () => {
71
- it('returns 1 for identical colors', () => {
72
- expect(contrastRatio('#808080', '#808080')).toBeCloseTo(1, 5)
73
- })
74
-
75
- it('returns 21 for pure black vs pure white (the WCAG maximum)', () => {
76
- expect(contrastRatio('#000000', '#FFFFFF')).toBeCloseTo(21, 1)
77
- })
78
-
79
- it('is symmetric regardless of argument order', () => {
80
- const a = contrastRatio('#1a1b26', '#a9b1d6')
81
- const b = contrastRatio('#a9b1d6', '#1a1b26')
82
- expect(a).toBeCloseTo(b, 10)
83
- })
84
- })
@@ -1,132 +0,0 @@
1
- /**
2
- * Tests for the theme/color-math helpers in packages/core/src/theme.ts.
3
- */
4
- import { describe, it, expect } from 'vitest'
5
- import { getReadableTextColor, buildStyleBlock } from '@zombie-mermaid/core'
6
-
7
- describe('getReadableTextColor', () => {
8
- it('returns dark text for a light pastel fill (6-digit hex)', () => {
9
- expect(getReadableTextColor('#FF6B6B', 'var(--_text)')).toBe('#000000')
10
- expect(getReadableTextColor('#90EE90', 'var(--_text)')).toBe('#000000')
11
- })
12
-
13
- it('returns light text for a dark fill (6-digit hex)', () => {
14
- expect(getReadableTextColor('#111111', 'var(--_text)')).toBe('#FFFFFF')
15
- expect(getReadableTextColor('#000000', 'var(--_text)')).toBe('#FFFFFF')
16
- })
17
-
18
- it('handles 3-digit shorthand hex', () => {
19
- expect(getReadableTextColor('#fff', 'var(--_text)')).toBe('#000000')
20
- expect(getReadableTextColor('#000', 'var(--_text)')).toBe('#FFFFFF')
21
- })
22
-
23
- it('ignores alpha channel in 4/8-digit hex and uses RGB luminance', () => {
24
- expect(getReadableTextColor('#ffffffff', 'var(--_text)')).toBe('#000000')
25
- expect(getReadableTextColor('#000000ff', 'var(--_text)')).toBe('#FFFFFF')
26
- })
27
-
28
- it('falls back when fill is undefined', () => {
29
- expect(getReadableTextColor(undefined, 'var(--_text)')).toBe('var(--_text)')
30
- })
31
-
32
- it('falls back when fill is a CSS variable reference', () => {
33
- expect(getReadableTextColor('var(--surface)', 'var(--_text)')).toBe(
34
- 'var(--_text)',
35
- )
36
- })
37
-
38
- it('falls back when fill is a named CSS color', () => {
39
- expect(getReadableTextColor('lightpink', 'var(--_text)')).toBe(
40
- 'var(--_text)',
41
- )
42
- })
43
-
44
- it('falls back when fill is malformed or an injection attempt', () => {
45
- expect(
46
- getReadableTextColor('red" onmouseover="alert(1)', 'var(--_text)'),
47
- ).toBe('var(--_text)')
48
- expect(getReadableTextColor('#gggggg', 'var(--_text)')).toBe('var(--_text)')
49
- })
50
- })
51
-
52
- describe('buildStyleBlock – font handling', () => {
53
- it('quotes a normal font name and emits a Google Fonts @import (no regression)', () => {
54
- const block = buildStyleBlock('Inter', false)
55
- expect(block).toContain(
56
- "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');",
57
- )
58
- expect(block).toContain(
59
- "text { font-family: 'Inter', system-ui, sans-serif; }",
60
- )
61
- })
62
-
63
- it('skips the Google Fonts @import and emits an unquoted value for a CSS var() reference', () => {
64
- const block = buildStyleBlock('var(--font-family-body)', false)
65
- expect(block).not.toContain('fonts.googleapis.com')
66
- expect(block).toContain(
67
- 'text { font-family: var(--font-family-body), system-ui, sans-serif; }',
68
- )
69
- })
70
-
71
- it('handles a var() reference with a quoted fallback argument', () => {
72
- const block = buildStyleBlock("var(--font, 'Fallback Font')", false)
73
- expect(block).not.toContain('fonts.googleapis.com')
74
- expect(block).toContain(
75
- "text { font-family: var(--font, 'Fallback Font'), system-ui, sans-serif; }",
76
- )
77
- })
78
-
79
- it('treats a var()-like value with unbalanced parens/semicolons as untrusted and falls back to a quoted literal', () => {
80
- const block = buildStyleBlock('var(--x); } .evil{color:red', false)
81
- // Not treated as a var() reference: still gets a (sanitized) Google Fonts import + quoting.
82
- expect(block).toContain('fonts.googleapis.com')
83
- expect(block).not.toMatch(/text \{ font-family: var\(/)
84
- // The sanitizer stripped the `;` and `{`/`}`, so the payload can't
85
- // terminate the declaration/rule early — exactly one <style> block.
86
- expect(block.match(/<style>/g)?.length).toBe(1)
87
- expect(block.match(/<\/style>/g)?.length).toBe(1)
88
- })
89
-
90
- it('sanitizes a malicious font value so it cannot break out of the generated <style> block', () => {
91
- const malicious = '</style><script>alert(1)</script><style>'
92
- const block = buildStyleBlock(malicious, false)
93
- expect(block).not.toContain('</style><script>')
94
- expect(block).not.toContain('<script>')
95
- // The <style> block must still be well-formed: exactly one open/close pair.
96
- expect(block.match(/<style>/g)?.length).toBe(1)
97
- expect(block.match(/<\/style>/g)?.length).toBe(1)
98
- })
99
-
100
- it('sanitizes an attempted CSS-injection payload disguised as a var() reference', () => {
101
- const malicious = "var(--x, 'y'); } .evil { fill: red } /*"
102
- const block = buildStyleBlock(malicious, false)
103
- // Contains a semicolon, so it fails the strict var() check and is
104
- // treated (and sanitized) as a literal, quoted font name instead.
105
- expect(block).not.toMatch(/text \{ font-family: var\(/)
106
- expect(block).not.toContain('.evil { fill: red }')
107
- })
108
-
109
- it('skips the Google Fonts @import for a comma-separated font stack (#223)', () => {
110
- const block = buildStyleBlock('ui-sans-serif, system-ui, sans-serif', false)
111
- expect(block).not.toContain('fonts.googleapis.com')
112
- })
113
-
114
- it('skips the Google Fonts @import for a bare CSS generic family keyword', () => {
115
- const block = buildStyleBlock('system-ui', false)
116
- expect(block).not.toContain('fonts.googleapis.com')
117
- })
118
-
119
- it('still emits the Google Fonts @import for a single real font name (regression guard)', () => {
120
- const block = buildStyleBlock('Roboto', false)
121
- expect(block).toContain(
122
- "@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700&amp;display=swap');",
123
- )
124
- })
125
-
126
- it('still renders a font stack literally in the text { font-family: ... } rule', () => {
127
- const block = buildStyleBlock('ui-sans-serif, system-ui, sans-serif', false)
128
- expect(block).toContain(
129
- "text { font-family: 'ui-sans-serif, system-ui, sans-serif', system-ui, sans-serif; }",
130
- )
131
- })
132
- })