zombie-mermaid 2.2.1 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -2
  3. package/dist/ascii.cjs +9 -9
  4. package/dist/ascii.cjs.map +1 -1
  5. package/dist/ascii.js +4069 -3969
  6. package/dist/ascii.js.map +1 -1
  7. package/dist/cli.js +7605 -7381
  8. package/dist/index.cjs +52 -52
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +7613 -7459
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.cjs +14142 -13717
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +14142 -13717
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +27 -5
  17. package/packages/ascii-renderer/LICENSE +22 -0
  18. package/{src/ascii → packages/ascii-renderer/src}/class-diagram.ts +277 -57
  19. package/{src/ascii → packages/ascii-renderer/src}/er-diagram.ts +2 -2
  20. package/packages/ascii-renderer/src/flowchart.ts +94 -0
  21. package/{src/ascii → packages/ascii-renderer/src}/grid.ts +23 -0
  22. package/{src/ascii → packages/ascii-renderer/src}/index.ts +13 -84
  23. package/{src/ascii → packages/ascii-renderer/src}/registry.ts +23 -12
  24. package/{src/ascii → packages/ascii-renderer/src}/sequence.ts +55 -8
  25. package/{src/ascii → packages/ascii-renderer/src}/xychart.ts +6 -3
  26. package/packages/core/LICENSE +22 -0
  27. package/packages/core/src/click-directive.ts +106 -0
  28. package/packages/core/src/color-utils.ts +205 -0
  29. package/packages/core/src/diagram-type.ts +35 -0
  30. package/packages/core/src/direction-override.ts +46 -0
  31. package/packages/core/src/direction.ts +56 -0
  32. package/packages/core/src/index.ts +34 -0
  33. package/packages/core/src/init-directive.ts +248 -0
  34. package/packages/core/src/multiline-utils.ts +275 -0
  35. package/packages/core/src/statements.ts +197 -0
  36. package/packages/core/src/style-directives.ts +214 -0
  37. package/packages/core/src/text-metrics.ts +498 -0
  38. package/packages/core/src/theme.ts +696 -0
  39. package/packages/core/src/types.ts +670 -0
  40. package/packages/mcp/LICENSE +22 -0
  41. package/{src/mcp → packages/mcp/src}/server.ts +34 -2
  42. package/{src/mcp → packages/mcp/src}/tools/check-sequence-activations.ts +8 -5
  43. package/packages/mcp/src/tools/fix-sequence-activations.ts +77 -0
  44. package/{src/mcp → packages/mcp/src}/tools/render-ascii.ts +2 -2
  45. package/{src/mcp → packages/mcp/src}/tools/render-svg.ts +1 -1
  46. package/packages/mermaid-parser/LICENSE +22 -0
  47. package/{src → packages/mermaid-parser/src}/class/parser.ts +86 -8
  48. package/{src → packages/mermaid-parser/src}/er/parser.ts +59 -26
  49. package/packages/mermaid-parser/src/index.ts +48 -0
  50. package/{src → packages/mermaid-parser/src}/sequence/activation-check.ts +4 -3
  51. package/packages/mermaid-parser/src/sequence/activation-fix.ts +112 -0
  52. package/{src → packages/mermaid-parser/src}/sequence/parser.ts +74 -16
  53. package/{src → packages/mermaid-parser/src}/xychart/parser.ts +84 -6
  54. package/packages/svg-renderer/LICENSE +22 -0
  55. package/{src → packages/svg-renderer/src}/class/layout.ts +8 -4
  56. package/{src → packages/svg-renderer/src}/class/renderer.ts +7 -6
  57. package/packages/svg-renderer/src/edge-curves.ts +204 -0
  58. package/packages/svg-renderer/src/elk-instance.ts +292 -0
  59. package/{src → packages/svg-renderer/src}/er/layout.ts +6 -4
  60. package/{src → packages/svg-renderer/src}/er/renderer.ts +4 -4
  61. package/packages/svg-renderer/src/index.ts +58 -0
  62. package/packages/svg-renderer/src/layout-engine/constants.ts +19 -0
  63. package/packages/svg-renderer/src/layout-engine/edge-bundling.ts +379 -0
  64. package/packages/svg-renderer/src/layout-engine/elk-adapter-utils.ts +81 -0
  65. package/packages/svg-renderer/src/layout-engine/elk-graph-builder.ts +240 -0
  66. package/packages/svg-renderer/src/layout-engine/from-elk.ts +685 -0
  67. package/packages/svg-renderer/src/layout-engine/layer-alignment.ts +174 -0
  68. package/packages/svg-renderer/src/layout-engine/to-elk.ts +695 -0
  69. package/packages/svg-renderer/src/layout-engine.ts +74 -0
  70. package/packages/svg-renderer/src/layout.ts +8 -0
  71. package/packages/svg-renderer/src/renderer.ts +1485 -0
  72. package/packages/svg-renderer/src/resolve-colors.ts +339 -0
  73. package/{src → packages/svg-renderer/src}/sequence/layout.ts +2 -6
  74. package/{src → packages/svg-renderer/src}/sequence/renderer.ts +4 -4
  75. package/packages/svg-renderer/src/shape-clipping.ts +197 -0
  76. package/packages/svg-renderer/src/styles.ts +118 -0
  77. package/{src → packages/svg-renderer/src}/xychart/layout.ts +2 -2
  78. package/{src → packages/svg-renderer/src}/xychart/renderer.ts +6 -6
  79. package/src/__tests__/ascii-ansi.test.ts +6 -2
  80. package/src/__tests__/ascii-canvas-write.test.ts +2 -2
  81. package/src/__tests__/ascii-charset-border-junctions.test.ts +1 -1
  82. package/src/__tests__/ascii-cjk-width.test.ts +1 -1
  83. package/src/__tests__/ascii-class-box-occupancy.test.ts +147 -45
  84. package/src/__tests__/ascii-class-column-width-488-489.test.ts +1 -1
  85. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  86. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +1 -1
  87. package/src/__tests__/ascii-class-diagram-compartments.test.ts +1 -1
  88. package/src/__tests__/ascii-class-label-box-collision.test.ts +1 -1
  89. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +1 -1
  90. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +1 -1
  91. package/src/__tests__/ascii-class-padding.test.ts +1 -1
  92. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +86 -1
  93. package/src/__tests__/ascii-combining-mark-width.test.ts +2 -2
  94. package/src/__tests__/ascii-coords-overlay.test.ts +2 -2
  95. package/src/__tests__/ascii-decision-lr-box-start.test.ts +2 -2
  96. package/src/__tests__/ascii-display-width-unit.test.ts +1 -1
  97. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +1 -1
  98. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +12 -5
  99. package/src/__tests__/ascii-draw-bundles.test.ts +5 -5
  100. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +1 -1
  101. package/src/__tests__/ascii-edge-bundling-routing.test.ts +9 -5
  102. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +10 -6
  103. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +1 -1
  104. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +2 -2
  105. package/src/__tests__/ascii-edge-routing-fixes.test.ts +1 -1
  106. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +8 -4
  107. package/src/__tests__/ascii-edge-styles.test.ts +1 -1
  108. package/src/__tests__/ascii-emoji-cluster-width.test.ts +2 -2
  109. package/src/__tests__/ascii-er-box-occupancy.test.ts +1 -1
  110. package/src/__tests__/ascii-er-cardinality.test.ts +1 -1
  111. package/src/__tests__/ascii-er-corner-glyphs.test.ts +1 -1
  112. package/src/__tests__/ascii-er-jog-stray-line.test.ts +1 -1
  113. package/src/__tests__/ascii-er-label-padding.test.ts +1 -1
  114. package/src/__tests__/ascii-er-padding.test.ts +1 -1
  115. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +1 -1
  116. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +1 -1
  117. package/src/__tests__/ascii-er-stray-connectors.test.ts +7 -4
  118. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +1 -1
  119. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +1 -1
  120. package/src/__tests__/ascii-grid-deep-chain.test.ts +4 -4
  121. package/src/__tests__/ascii-hyperlinks.test.ts +9 -6
  122. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +8 -4
  123. package/src/__tests__/ascii-lane-search.test.ts +1 -1
  124. package/src/__tests__/ascii-multibox-cjk-width.test.ts +1 -1
  125. package/src/__tests__/ascii-multiline.test.ts +6 -3
  126. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +8 -5
  127. package/src/__tests__/ascii-package-boundary.test.ts +47 -37
  128. package/src/__tests__/ascii-padding-edge-cases.test.ts +1 -1
  129. package/src/__tests__/ascii-parallel-edges-329.test.ts +8 -5
  130. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +14 -4
  131. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +9 -5
  132. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +1 -1
  133. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +1 -1
  134. package/src/__tests__/ascii-sequence-box-group.test.ts +1 -1
  135. package/src/__tests__/ascii-sequence-cjk-width.test.ts +1 -1
  136. package/src/__tests__/ascii-sequence-create-destroy.test.ts +1 -1
  137. package/src/__tests__/ascii-sequence-form-invariants.test.ts +48 -50
  138. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +1 -1
  139. package/src/__tests__/ascii-sequence-notes.test.ts +1 -1
  140. package/src/__tests__/ascii-sequence-padding.test.ts +1 -1
  141. package/src/__tests__/ascii-sequence-self-arrow.test.ts +1 -1
  142. package/src/__tests__/ascii-shape-diamond.test.ts +3 -3
  143. package/src/__tests__/ascii-shape-rectangle.test.ts +5 -5
  144. package/src/__tests__/ascii-shape-rounded.test.ts +3 -3
  145. package/src/__tests__/ascii-shapes-circle.test.ts +3 -3
  146. package/src/__tests__/ascii-shapes-hexagon.test.ts +3 -3
  147. package/src/__tests__/ascii-shapes-special.test.ts +10 -5
  148. package/src/__tests__/ascii-shapes-stadium.test.ts +3 -3
  149. package/src/__tests__/ascii-shapes-state.test.ts +15 -5
  150. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +1 -1
  151. package/src/__tests__/ascii-subgraph-bbox.test.ts +8 -4
  152. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +1 -1
  153. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +88 -25
  154. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +8 -4
  155. package/src/__tests__/ascii-subgraph-title-padding.test.ts +1 -1
  156. package/src/__tests__/ascii-territory-unit.test.ts +2 -2
  157. package/src/__tests__/ascii-validate.test.ts +1 -1
  158. package/src/__tests__/ascii.test.ts +5 -2
  159. package/src/__tests__/box-color.test.ts +1 -1
  160. package/src/__tests__/class-arrow-directions.test.ts +1 -1
  161. package/src/__tests__/class-click-interactions.test.ts +3 -6
  162. package/src/__tests__/class-integration.test.ts +1 -1
  163. package/src/__tests__/class-notes.test.ts +3 -3
  164. package/src/__tests__/class-parser-error-messages.test.ts +162 -0
  165. package/src/__tests__/class-parser.test.ts +6 -6
  166. package/src/__tests__/class-styling.test.ts +2 -2
  167. package/src/__tests__/cli-render.test.ts +16 -1
  168. package/src/__tests__/direction-override.test.ts +1 -1
  169. package/src/__tests__/draw-lines.test.ts +2 -2
  170. package/src/__tests__/edge-cell-styles.test.ts +5 -2
  171. package/src/__tests__/elk-layout-cache.test.ts +6 -4
  172. package/src/__tests__/er-click-unsupported.test.ts +5 -8
  173. package/src/__tests__/er-parser-error-messages.test.ts +82 -0
  174. package/src/__tests__/er-parser.test.ts +3 -6
  175. package/src/__tests__/expanded-shapes.test.ts +1 -1
  176. package/src/__tests__/grid-occupancy.test.ts +1 -1
  177. package/src/__tests__/mcp-check-sequence-activations-tool.test.ts +1 -1
  178. package/src/__tests__/mcp-fix-sequence-activations-tool.test.ts +134 -0
  179. package/src/__tests__/mcp-render-ascii-tool.test.ts +5 -4
  180. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +3 -3
  181. package/src/__tests__/mcp-render-svg-tool.test.ts +1 -1
  182. package/src/__tests__/mcp-server.test.ts +30 -1
  183. package/src/__tests__/multiline-labels.test.ts +29 -18
  184. package/src/__tests__/parser.test.ts +117 -5
  185. package/src/__tests__/pathfinder.test.ts +11 -4
  186. package/src/__tests__/render-options-overrides.test.ts +28 -21
  187. package/src/__tests__/render-options-per-type.test.ts +13 -9
  188. package/src/__tests__/resolve-colors.test.ts +1 -1
  189. package/src/__tests__/self-loop-edges.test.ts +112 -0
  190. package/src/__tests__/sequence-activation-check.test.ts +7 -8
  191. package/src/__tests__/sequence-activation-fix.test.ts +91 -0
  192. package/src/__tests__/sequence-layout.test.ts +4 -7
  193. package/src/__tests__/sequence-parser-syntax-errors.test.ts +179 -0
  194. package/src/__tests__/sequence-parser.test.ts +3 -6
  195. package/src/__tests__/statements.test.ts +53 -35
  196. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +21 -21
  197. package/src/__tests__/theme-palette-contrast.test.ts +84 -0
  198. package/src/__tests__/workspace-package-boundaries.test.ts +199 -25
  199. package/src/__tests__/xychart-ascii.test.ts +1 -1
  200. package/src/__tests__/xychart-colors.test.ts +3 -2
  201. package/src/__tests__/xychart-layout.test.ts +1 -1
  202. package/src/__tests__/xychart-parser-errors.test.ts +95 -0
  203. package/src/browser.ts +8 -2
  204. package/src/cli/mcp.ts +1 -1
  205. package/src/cli/render.ts +59 -25
  206. package/src/cli/web.ts +1 -1
  207. package/src/diagram-registry.ts +190 -25
  208. package/src/index.ts +34 -131
  209. package/src/parser.ts +102 -40
  210. /package/{src/ascii → packages/ascii-renderer/src}/ansi.ts +0 -0
  211. /package/{src/ascii → packages/ascii-renderer/src}/canvas.ts +0 -0
  212. /package/{src/ascii → packages/ascii-renderer/src}/converter.ts +0 -0
  213. /package/{src/ascii → packages/ascii-renderer/src}/coords.ts +0 -0
  214. /package/{src/ascii → packages/ascii-renderer/src}/display-width.ts +0 -0
  215. /package/{src/ascii → packages/ascii-renderer/src}/draw-arrows.ts +0 -0
  216. /package/{src/ascii → packages/ascii-renderer/src}/draw-boxes.ts +0 -0
  217. /package/{src/ascii → packages/ascii-renderer/src}/draw-bundles.ts +0 -0
  218. /package/{src/ascii → packages/ascii-renderer/src}/draw-lines.ts +0 -0
  219. /package/{src/ascii → packages/ascii-renderer/src}/draw-subgraphs.ts +0 -0
  220. /package/{src/ascii → packages/ascii-renderer/src}/draw.ts +0 -0
  221. /package/{src/ascii → packages/ascii-renderer/src}/edge-bundling.ts +0 -0
  222. /package/{src/ascii → packages/ascii-renderer/src}/edge-cell-styles.ts +0 -0
  223. /package/{src/ascii → packages/ascii-renderer/src}/edge-routing.ts +0 -0
  224. /package/{src/ascii → packages/ascii-renderer/src}/grid-occupancy.ts +0 -0
  225. /package/{src/ascii → packages/ascii-renderer/src}/hyperlinks.ts +0 -0
  226. /package/{src/ascii → packages/ascii-renderer/src}/lane-search.ts +0 -0
  227. /package/{src/ascii → packages/ascii-renderer/src}/multiline-utils.ts +0 -0
  228. /package/{src/ascii → packages/ascii-renderer/src}/pathfinder.ts +0 -0
  229. /package/{src/ascii → packages/ascii-renderer/src}/shapes/circle.ts +0 -0
  230. /package/{src/ascii → packages/ascii-renderer/src}/shapes/corners.ts +0 -0
  231. /package/{src/ascii → packages/ascii-renderer/src}/shapes/diamond.ts +0 -0
  232. /package/{src/ascii → packages/ascii-renderer/src}/shapes/hexagon.ts +0 -0
  233. /package/{src/ascii → packages/ascii-renderer/src}/shapes/index.ts +0 -0
  234. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rectangle.ts +0 -0
  235. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rounded.ts +0 -0
  236. /package/{src/ascii → packages/ascii-renderer/src}/shapes/special.ts +0 -0
  237. /package/{src/ascii → packages/ascii-renderer/src}/shapes/stadium.ts +0 -0
  238. /package/{src/ascii → packages/ascii-renderer/src}/shapes/state.ts +0 -0
  239. /package/{src/ascii → packages/ascii-renderer/src}/shapes/types.ts +0 -0
  240. /package/{src/ascii → packages/ascii-renderer/src}/territory.ts +0 -0
  241. /package/{src/ascii → packages/ascii-renderer/src}/types.ts +0 -0
  242. /package/{src/ascii → packages/ascii-renderer/src}/validate.ts +0 -0
  243. /package/{src/mcp → packages/mcp/src}/index.ts +0 -0
  244. /package/{src → packages/mermaid-parser/src}/class/format.ts +0 -0
  245. /package/{src → packages/mermaid-parser/src}/class/types.ts +0 -0
  246. /package/{src → packages/mermaid-parser/src}/er/types.ts +0 -0
  247. /package/{src → packages/mermaid-parser/src}/expanded-shapes.ts +0 -0
  248. /package/{src → packages/mermaid-parser/src}/sequence/box-color.ts +0 -0
  249. /package/{src → packages/mermaid-parser/src}/sequence/types.ts +0 -0
  250. /package/{src → packages/mermaid-parser/src}/xychart/colors.ts +0 -0
  251. /package/{src → packages/mermaid-parser/src}/xychart/types.ts +0 -0
@@ -0,0 +1,498 @@
1
+ // ============================================================================
2
+ // Text Metrics — Variable-width character measurement for SVG layout
3
+ // ============================================================================
4
+ //
5
+ // Provides font-agnostic text width estimation using character class buckets.
6
+ // More accurate than uniform character width for proportional fonts.
7
+ //
8
+ // Width ratios are normalized where 1.0 = average lowercase letter.
9
+ // Final pixel width = sum(charWidths) * fontSize * baseRatio
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Exact advance widths for every printable ASCII character (U+0020–U+007E),
14
+ * measured from the real Inter font (canvas measureText, headless Chrome,
15
+ * weight 400) and normalized so that ratio × fontSize × baseRatio reproduces
16
+ * the measured pixel advance. Completeness over this range is asserted by
17
+ * the `getCharWidth` "table completeness" test in text-metrics.test.ts.
18
+ *
19
+ * The coarse character-class buckets below remain as fallback for
20
+ * non-ASCII characters not in this table. The buckets systematically
21
+ * underestimated Inter widths (spaces, m/w, digits, punctuation), and the
22
+ * error grew with text length — wide edge labels overflowed their
23
+ * 8px-padded background rects.
24
+ */
25
+ const INTER_ADVANCES: Record<string, number> = {
26
+ ' ': 0.521,
27
+ '!': 0.533,
28
+ '"': 0.863,
29
+ '#': 1.173,
30
+ $: 1.188,
31
+ '%': 1.818,
32
+ '&': 1.193,
33
+ "'": 0.555,
34
+ '(': 0.675,
35
+ ')': 0.675,
36
+ '*': 0.928,
37
+ '+': 1.225,
38
+ ',': 0.534,
39
+ '-': 0.852,
40
+ '.': 0.534,
41
+ '/': 0.667,
42
+ '0': 1.168,
43
+ '1': 0.753,
44
+ '2': 1.129,
45
+ '3': 1.144,
46
+ '4': 1.196,
47
+ '5': 1.099,
48
+ '6': 1.148,
49
+ '7': 1.048,
50
+ '8': 1.146,
51
+ '9': 1.148,
52
+ ':': 0.534,
53
+ ';': 0.559,
54
+ '<': 1.225,
55
+ '=': 1.225,
56
+ '>': 1.225,
57
+ '?': 0.947,
58
+ '@': 1.789,
59
+ A: 1.278,
60
+ B: 1.212,
61
+ C: 1.353,
62
+ D: 1.336,
63
+ E: 1.113,
64
+ F: 1.093,
65
+ G: 1.382,
66
+ H: 1.376,
67
+ I: 0.497,
68
+ J: 1.057,
69
+ K: 1.244,
70
+ L: 1.047,
71
+ M: 1.673,
72
+ N: 1.395,
73
+ O: 1.416,
74
+ P: 1.183,
75
+ Q: 1.416,
76
+ R: 1.192,
77
+ S: 1.188,
78
+ T: 1.195,
79
+ U: 1.378,
80
+ V: 1.278,
81
+ W: 1.825,
82
+ X: 1.263,
83
+ Y: 1.257,
84
+ Z: 1.165,
85
+ '[': 0.675,
86
+ '\\': 0.667,
87
+ ']': 0.675,
88
+ '^': 0.873,
89
+ _: 0.845,
90
+ '`': 0.598,
91
+ a: 1.04,
92
+ b: 1.134,
93
+ c: 1.058,
94
+ d: 1.134,
95
+ e: 1.08,
96
+ f: 0.685,
97
+ g: 1.136,
98
+ h: 1.095,
99
+ i: 0.448,
100
+ j: 0.448,
101
+ k: 1.016,
102
+ l: 0.448,
103
+ m: 1.622,
104
+ n: 1.094,
105
+ o: 1.11,
106
+ p: 1.134,
107
+ q: 1.134,
108
+ r: 0.697,
109
+ s: 0.977,
110
+ t: 0.606,
111
+ u: 1.095,
112
+ v: 1.041,
113
+ w: 1.515,
114
+ x: 1.011,
115
+ y: 1.041,
116
+ z: 1.023,
117
+ '{': 0.789,
118
+ '|': 0.616,
119
+ '}': 0.789,
120
+ '~': 1.225,
121
+ }
122
+
123
+ /**
124
+ * Whether `char` has an exact measured Inter advance in `INTER_ADVANCES`,
125
+ * as opposed to falling through to a coarse character-class bucket or the
126
+ * default width in `getCharWidth`. Exported only so tests can assert the
127
+ * table covers every printable ASCII character (U+0020–U+007E) — a gap
128
+ * here means a real character silently degrades to an approximate width.
129
+ */
130
+ export function hasMeasuredAdvance(char: string): boolean {
131
+ return Object.hasOwn(INTER_ADVANCES, char)
132
+ }
133
+
134
+ /**
135
+ * Narrow characters - visually thin glyphs.
136
+ * Note: '1' is included because in proportional fonts (like Inter), it's
137
+ * significantly narrower than other digits which use tabular/uniform width.
138
+ */
139
+ const NARROW_CHARS = new Set([
140
+ 'i',
141
+ 'l',
142
+ 't',
143
+ 'f',
144
+ 'j',
145
+ 'I',
146
+ '1',
147
+ '!',
148
+ '|',
149
+ '.',
150
+ ',',
151
+ ':',
152
+ ';',
153
+ "'",
154
+ ])
155
+
156
+ /**
157
+ * Wide characters - visually wide glyphs
158
+ */
159
+ const WIDE_CHARS = new Set(['W', 'M', 'w', 'm', '@', '%'])
160
+
161
+ /**
162
+ * Very wide characters - widest Latin glyphs
163
+ */
164
+ const VERY_WIDE_CHARS = new Set(['W', 'M'])
165
+
166
+ /**
167
+ * Semi-narrow punctuation - brackets and slashes are narrower than letters
168
+ * but wider than narrow chars like dots/commas
169
+ */
170
+ const SEMI_NARROW_PUNCT = new Set([
171
+ '(',
172
+ ')',
173
+ '[',
174
+ ']',
175
+ '{',
176
+ '}',
177
+ '/',
178
+ '\\',
179
+ '-',
180
+ '"',
181
+ '`',
182
+ ])
183
+
184
+ /**
185
+ * Check if a code point is a combining diacritical mark (zero-width overlay)
186
+ */
187
+ function isCombiningMark(code: number): boolean {
188
+ // Combining Diacritical Marks: U+0300–U+036F
189
+ // Combining Diacritical Marks Extended: U+1AB0–U+1AFF
190
+ // Combining Diacritical Marks Supplement: U+1DC0–U+1DFF
191
+ // Combining Diacritical Marks for Symbols: U+20D0–U+20FF
192
+ // Combining Half Marks: U+FE20–U+FE2F
193
+ return (
194
+ (code >= 0x0300 && code <= 0x036f) ||
195
+ (code >= 0x1ab0 && code <= 0x1aff) ||
196
+ (code >= 0x1dc0 && code <= 0x1dff) ||
197
+ (code >= 0x20d0 && code <= 0x20ff) ||
198
+ (code >= 0xfe20 && code <= 0xfe2f)
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Check if a code point is fullwidth (CJK, emoji, etc.)
204
+ * These characters occupy approximately 2x the width of Latin letters.
205
+ */
206
+ function isFullwidth(code: number): boolean {
207
+ // CJK Radicals Supplement: U+2E80–U+2EFF
208
+ // Kangxi Radicals: U+2F00–U+2FDF
209
+ // CJK Symbols and Punctuation: U+3000–U+303F
210
+ // Hiragana: U+3040–U+309F
211
+ // Katakana: U+30A0–U+30FF
212
+ // Bopomofo: U+3100–U+312F
213
+ // Hangul Compatibility Jamo: U+3130–U+318F
214
+ // Kanbun: U+3190–U+319F
215
+ // Bopomofo Extended: U+31A0–U+31BF
216
+ // CJK Strokes: U+31C0–U+31EF
217
+ // Katakana Phonetic Extensions: U+31F0–U+31FF
218
+ // Enclosed CJK Letters and Months: U+3200–U+32FF
219
+ // CJK Compatibility: U+3300–U+33FF
220
+ // CJK Unified Ideographs Extension A: U+3400–U+4DBF
221
+ // CJK Unified Ideographs: U+4E00–U+9FFF
222
+ // Hangul Syllables: U+AC00–U+D7AF
223
+ // CJK Compatibility Ideographs: U+F900–U+FAFF
224
+ // Halfwidth and Fullwidth Forms (fullwidth part): U+FF00–U+FF60, U+FFE0–U+FFE6
225
+ // CJK Unified Ideographs Extension B+: U+20000–U+2A6DF (and beyond)
226
+
227
+ return (
228
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
229
+ (code >= 0x2e80 && code <= 0x2eff) || // CJK Radicals Supplement
230
+ (code >= 0x2f00 && code <= 0x2fdf) || // Kangxi Radicals
231
+ (code >= 0x3000 && code <= 0x303f) || // CJK Symbols and Punctuation
232
+ (code >= 0x3040 && code <= 0x309f) || // Hiragana
233
+ (code >= 0x30a0 && code <= 0x30ff) || // Katakana
234
+ (code >= 0x3100 && code <= 0x312f) || // Bopomofo
235
+ (code >= 0x3130 && code <= 0x318f) || // Hangul Compatibility Jamo
236
+ (code >= 0x3190 && code <= 0x31ff) || // Kanbun + extensions
237
+ (code >= 0x3200 && code <= 0x33ff) || // Enclosed CJK + Compatibility
238
+ (code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
239
+ (code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
240
+ (code >= 0xac00 && code <= 0xd7af) || // Hangul Syllables
241
+ (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
242
+ (code >= 0xff00 && code <= 0xff60) || // Fullwidth ASCII
243
+ (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth symbols
244
+ code >= 0x20000 // CJK Extension B and beyond
245
+ )
246
+ }
247
+
248
+ /**
249
+ * Regex for emoji detection using Unicode property escapes.
250
+ * Uses Emoji_Presentation and Extended_Pictographic (not just Emoji)
251
+ * because \p{Emoji} includes digits and # which we don't want as fullwidth.
252
+ *
253
+ * \p{Extended_Pictographic} on its own is broader than "renders wide" — it
254
+ * flags characters that are merely emoji-*capable* (could take a VS16
255
+ * selector to become emoji), not characters that actually render wide by
256
+ * default. Most such characters (e.g. ❤ U+2764) are still conventionally
257
+ * treated as wide, so the OR is kept — except for the Geometric Shapes
258
+ * block (U+25A0–U+25FF, see `isGeometricShapesTextDefault` below), which
259
+ * this renderer itself uses for narrow box-drawing/arrowhead glyphs like
260
+ * ▶ U+25B6 and ◀ U+25C0 (see src/ascii/sequence.ts); those render as a
261
+ * single narrow column in every real terminal despite being flagged
262
+ * Extended_Pictographic.
263
+ */
264
+ const EMOJI_PRESENTATION_REGEX = /\p{Emoji_Presentation}/u
265
+ const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u
266
+
267
+ /**
268
+ * Geometric Shapes block (U+25A0–U+25FF). Characters here that also carry
269
+ * Emoji_Presentation (e.g. ◽ ◾) are unaffected by this — they're already
270
+ * caught by the Emoji_Presentation check in `isEmoji` before this applies.
271
+ */
272
+ function isGeometricShapesTextDefault(code: number): boolean {
273
+ return code >= 0x25a0 && code <= 0x25ff
274
+ }
275
+
276
+ /**
277
+ * Check if a character is an emoji (fullwidth)
278
+ */
279
+ function isEmoji(char: string): boolean {
280
+ if (EMOJI_PRESENTATION_REGEX.test(char)) return true
281
+ const code = char.codePointAt(0)
282
+ if (code !== undefined && isGeometricShapesTextDefault(code)) return false
283
+ return EXTENDED_PICTOGRAPHIC_REGEX.test(char)
284
+ }
285
+
286
+ /**
287
+ * Check if a single character (code point) is "wide" — i.e. renders as two
288
+ * columns in a monospace terminal / two grid cells wide instead of one.
289
+ * Covers CJK/kana/hangul/fullwidth-form characters (via `isFullwidth`) as
290
+ * well as emoji. Shared by the SVG text-measurement path (`getCharWidth`
291
+ * above) and the ASCII grid's display-width helpers
292
+ * (`src/ascii/display-width.ts`), so both agree on what counts as wide.
293
+ */
294
+ export function isWideChar(char: string): boolean {
295
+ const code = char.codePointAt(0)
296
+ if (code === undefined) return false
297
+ return isFullwidth(code) || isEmoji(char)
298
+ }
299
+
300
+ /**
301
+ * Get the relative width of a single character.
302
+ *
303
+ * Returns a normalized width ratio where:
304
+ * - 0.0 = zero-width (combining marks)
305
+ * - 0.3 = space
306
+ * - 0.4 = narrow (i, l, t, f, j, I, 1)
307
+ * - 0.8 = semi-narrow (r)
308
+ * - 1.0 = average lowercase
309
+ * - 1.2 = wide lowercase / uppercase
310
+ * - 1.5 = very wide (W, M)
311
+ * - 2.0 = fullwidth (CJK, emoji)
312
+ */
313
+ export function getCharWidth(char: string): number {
314
+ const code = char.codePointAt(0)
315
+ if (code === undefined) return 0
316
+
317
+ // Exact measured advance for printable ASCII (Inter). The own-property
318
+ // check must come first: without it, bracket access on a plain object
319
+ // literal also resolves inherited Object.prototype properties (e.g.
320
+ // `INTER_ADVANCES['toString']` is a function, not undefined).
321
+ if (Object.hasOwn(INTER_ADVANCES, char)) {
322
+ const measured = INTER_ADVANCES[char]
323
+ // The `undefined` case is unreachable: INTER_ADVANCES is a `Record<string,
324
+ // number>` object literal whose every value is a numeric literal, and
325
+ // `noUncheckedIndexedAccess` is what forces this check's type, not any
326
+ // real possibility that an own property here holds `undefined`. Given
327
+ // `Object.hasOwn` is already true, `measured` is always a defined
328
+ // number — kept (not simplified to a bare `return measured`) so the
329
+ // check still holds if a future entry's value type ever widens.
330
+ /* v8 ignore else */
331
+ if (measured !== undefined) return measured
332
+ }
333
+
334
+ // Zero-width: combining diacritical marks
335
+ if (isCombiningMark(code)) return 0
336
+
337
+ // Fullwidth: CJK, emoji
338
+ if (isFullwidth(code) || isEmoji(char)) return 2.0
339
+
340
+ // Accented Latin: use the base letter's measured advance (é → e, Ü → U)
341
+ const base = char.normalize('NFD')[0]
342
+ if (base !== undefined && base !== char) {
343
+ const baseMeasured = INTER_ADVANCES[base]
344
+ if (baseMeasured !== undefined) return baseMeasured
345
+ }
346
+
347
+ // Space
348
+ if (char === ' ') return 0.3
349
+
350
+ // Very wide Latin
351
+ if (VERY_WIDE_CHARS.has(char)) return 1.5
352
+
353
+ // Wide Latin
354
+ if (WIDE_CHARS.has(char)) return 1.2
355
+
356
+ // Narrow Latin
357
+ if (NARROW_CHARS.has(char)) return 0.4
358
+
359
+ // Semi-narrow punctuation (brackets, slashes, hyphens)
360
+ if (SEMI_NARROW_PUNCT.has(char)) return 0.5
361
+
362
+ // Semi-narrow letter
363
+ if (char === 'r') return 0.8
364
+
365
+ // Uppercase (slightly wider than lowercase on average)
366
+ if (code >= 65 && code <= 90) return 1.2
367
+
368
+ // Digits (uniform width in most fonts)
369
+ if (code >= 48 && code <= 57) return 1.0
370
+
371
+ // Default: average lowercase width
372
+ return 1.0
373
+ }
374
+
375
+ /**
376
+ * Measure the pixel width of a text string.
377
+ *
378
+ * Uses character class buckets for more accurate width estimation
379
+ * than uniform character width assumptions.
380
+ *
381
+ * @param text - The text to measure
382
+ * @param fontSize - Font size in pixels
383
+ * @param fontWeight - Font weight (affects width slightly)
384
+ * @returns Estimated width in pixels
385
+ */
386
+ /** Advance width of a monospace glyph, as a fraction of font size. */
387
+ const MONO_ADVANCE = 0.6
388
+
389
+ /**
390
+ * Whether text is measured with monospace metrics.
391
+ *
392
+ * The character buckets above model a proportional face (they are calibrated for Inter), but
393
+ * `RenderOptions.font` accepts any family. Measuring a monospace font with them sizes every box
394
+ * for the wrong glyph widths: narrow strings under-measure by ~60% and overflow, wide ones
395
+ * over-measure by ~37%.
396
+ *
397
+ * The switch lives here rather than in `styles.ts` because this is the single choke point every
398
+ * diagram type measures through — `estimateTextWidth` and `measureMultilineText` both land here,
399
+ * and flowchart layout only reaches text metrics via the latter.
400
+ */
401
+ let monospaceMetrics = false
402
+
403
+ /** Fonts whose glyphs all share one advance width. */
404
+ export function isMonospaceFont(font: string): boolean {
405
+ // `mono` unanchored so it also catches `ui-monospace`; `code` bounded so it does not fire on
406
+ // unrelated names. "Mona Sans" is deliberately not a match.
407
+ return /mono|consol|menlo|courier|\bcode\b/i.test(font)
408
+ }
409
+
410
+ /** Select the metrics model for subsequent measurements. Called once per render. */
411
+ export function setMonospaceMetrics(monospace: boolean): void {
412
+ monospaceMetrics = monospace
413
+ }
414
+
415
+ export function measureTextWidth(
416
+ text: string,
417
+ fontSize: number,
418
+ fontWeight: number,
419
+ ): number {
420
+ // Add minimum padding to prevent truncation at text boundaries
421
+ // Increased from 0.1 to 0.15 for better label separation and collision prevention
422
+ const minPadding = fontSize * 0.15
423
+
424
+ if (monospaceMetrics) {
425
+ let count = 0
426
+ // Count code points, so surrogate pairs stay one cell wide.
427
+ for (const _ of text) count += 1
428
+ return count * fontSize * MONO_ADVANCE + minPadding
429
+ }
430
+
431
+ // Base ratio calibrated for Inter font family
432
+ // Heavier weights are slightly wider
433
+ // Added +0.02 buffer to prevent edge truncation of characters like 's' at line ends
434
+ const baseRatio = fontWeight >= 600 ? 0.6 : fontWeight >= 500 ? 0.57 : 0.54
435
+
436
+ let totalWidth = 0
437
+
438
+ // Iterate over code points (handles surrogate pairs for emoji/CJK)
439
+ for (const char of text) {
440
+ totalWidth += getCharWidth(char)
441
+ }
442
+
443
+ return totalWidth * fontSize * baseRatio + minPadding
444
+ }
445
+
446
+ // ============================================================================
447
+ // Multi-line Text Measurement
448
+ // ============================================================================
449
+
450
+ /** Standard line height ratio for multi-line text (1.3 = 130% of font size) */
451
+ export const LINE_HEIGHT_RATIO = 1.3
452
+
453
+ /** Metrics for multi-line text measurement */
454
+ export interface MultilineMetrics {
455
+ /** Maximum line width in pixels */
456
+ width: number
457
+ /** Total height in pixels (lines × lineHeight) */
458
+ height: number
459
+ /** Individual lines after splitting */
460
+ lines: string[]
461
+ /** Computed line height in pixels */
462
+ lineHeight: number
463
+ }
464
+
465
+ /**
466
+ * Measure multi-line text dimensions.
467
+ *
468
+ * Splits text on newlines and returns the maximum width across all lines,
469
+ * total height based on line count, and the split lines for rendering.
470
+ *
471
+ * @param text - The text to measure (may contain \n)
472
+ * @param fontSize - Font size in pixels
473
+ * @param fontWeight - Font weight (affects width slightly)
474
+ * @returns Metrics including width, height, lines array, and lineHeight
475
+ */
476
+ export function measureMultilineText(
477
+ text: string,
478
+ fontSize: number,
479
+ fontWeight: number,
480
+ ): MultilineMetrics {
481
+ const lines = text.split('\n')
482
+ const lineHeight = fontSize * LINE_HEIGHT_RATIO
483
+
484
+ // Width = max of all line widths
485
+ let maxWidth = 0
486
+ for (const line of lines) {
487
+ const plain = line.replace(/<\/?(?:b|strong|i|em|u|s|del)\s*>/gi, '')
488
+ const w = measureTextWidth(plain, fontSize, fontWeight)
489
+ if (w > maxWidth) maxWidth = w
490
+ }
491
+
492
+ return {
493
+ width: maxWidth,
494
+ height: lines.length * lineHeight,
495
+ lines,
496
+ lineHeight,
497
+ }
498
+ }