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,82 @@
1
+ /**
2
+ * Error-message quality tests for the ER diagram parser (issue #541).
3
+ *
4
+ * Before this pass, `parseErDiagram` had zero `throw`/`Error` statements in
5
+ * the entire file (see docs/parser-error-audit-541.md): a relationship line
6
+ * with invalid cardinality tokens or a missing `: label` silently dropped
7
+ * the *entire* line — both entities included — with no indication anything
8
+ * was wrong. These tests assert the parser now throws an actionable error
9
+ * instead, while confirming valid syntax and genuinely unrelated lines
10
+ * (e.g. `click`, which ER's own grammar has no directive for) are
11
+ * unaffected.
12
+ */
13
+ import { describe, it, expect } from 'vitest'
14
+ import { parseErDiagram } from '@zombie-mermaid/mermaid-parser'
15
+ import { splitStatements } from '@zombie-mermaid/core'
16
+
17
+ function parse(text: string) {
18
+ return parseErDiagram(splitStatements(text))
19
+ }
20
+
21
+ describe('parseErDiagram – invalid cardinality tokens', () => {
22
+ it('throws on non-crow-foot cardinality characters', () => {
23
+ expect(() =>
24
+ parse(`erDiagram
25
+ CUSTOMER ??--?? ORDER : places`),
26
+ ).toThrow(/Invalid ER relationship cardinality "\?\?--\?\?"/)
27
+ })
28
+
29
+ it('throws on a left-side cardinality that is not one of the four valid tokens', () => {
30
+ expect(() =>
31
+ parse(`erDiagram
32
+ CUSTOMER |--|| ORDER : places`),
33
+ ).toThrow(/Invalid ER relationship cardinality "\|--\|\|"/)
34
+ })
35
+
36
+ it('mentions the valid cardinality tokens in the message', () => {
37
+ expect(() =>
38
+ parse(`erDiagram
39
+ CUSTOMER ??--?? ORDER : places`),
40
+ ).toThrow(/\|\|, \|o, \}\|, \}o/)
41
+ })
42
+ })
43
+
44
+ describe('parseErDiagram – missing relationship label', () => {
45
+ it('throws when the colon has no label after it', () => {
46
+ expect(() =>
47
+ parse(`erDiagram
48
+ CUSTOMER ||--o{ ORDER :`),
49
+ ).toThrow(/missing a ": label"/)
50
+ })
51
+
52
+ it('throws when there is no colon at all', () => {
53
+ expect(() =>
54
+ parse(`erDiagram
55
+ CUSTOMER ||--o{ ORDER`),
56
+ ).toThrow(/missing a ": label"/)
57
+ })
58
+ })
59
+
60
+ describe('parseErDiagram – valid syntax and unrelated lines are unaffected', () => {
61
+ it('still parses a fully valid relationship without throwing', () => {
62
+ const d = parse(`erDiagram
63
+ CUSTOMER ||--o{ ORDER : places`)
64
+ expect(d.relationships).toHaveLength(1)
65
+ expect(d.entities.map((e) => e.id).sort()).toEqual(['CUSTOMER', 'ORDER'])
66
+ })
67
+
68
+ it('does not throw for a click line (no ER click grammar; see er-click-unsupported.test.ts)', () => {
69
+ expect(() =>
70
+ parse(`erDiagram
71
+ CUSTOMER ||--o{ ORDER : places
72
+ click CUSTOMER "https://example.com" "Tooltip"`),
73
+ ).not.toThrow()
74
+ })
75
+
76
+ it('does not throw for a standalone entity name with no relationship or block', () => {
77
+ expect(() =>
78
+ parse(`erDiagram
79
+ CUSTOMER`),
80
+ ).not.toThrow()
81
+ })
82
+ })
@@ -5,15 +5,12 @@
5
5
  * relationships with all cardinality types, identifying/non-identifying lines.
6
6
  */
7
7
  import { describe, it, expect } from 'vitest'
8
- import { parseErDiagram } from '../er/parser.ts'
8
+ import { parseErDiagram } from '@zombie-mermaid/mermaid-parser'
9
+ import { splitStatements } from '@zombie-mermaid/core'
9
10
 
10
11
  /** Helper to parse — preprocesses text the same way index.ts does */
11
12
  function parse(text: string) {
12
- const lines = text
13
- .split('\n')
14
- .map((l) => l.trim())
15
- .filter((l) => l.length > 0 && !l.startsWith('%%'))
16
- return parseErDiagram(lines)
13
+ return parseErDiagram(splitStatements(text))
17
14
  }
18
15
 
19
16
  // ============================================================================
@@ -14,7 +14,7 @@ import {
14
14
  matchExpandedBlock,
15
15
  resolveShapeName,
16
16
  knownShapeNames,
17
- } from '../expanded-shapes.ts'
17
+ } from '@zombie-mermaid/mermaid-parser'
18
18
 
19
19
  function parse(statement: string) {
20
20
  return parseMermaid(`flowchart TD\n ${statement}`)
@@ -8,7 +8,7 @@ import {
8
8
  pathCells,
9
9
  cloneGrid,
10
10
  NODE_BLOCK_SIZE,
11
- } from '../ascii/grid-occupancy.ts'
11
+ } from '../../packages/ascii-renderer/src/grid-occupancy.ts'
12
12
 
13
13
  describe('grid-occupancy', () => {
14
14
  it('createGrid starts empty', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { checkSequenceActivationsHandler } from '../mcp/tools/check-sequence-activations.ts'
2
+ import { checkSequenceActivationsHandler } from '../../packages/mcp/src/tools/check-sequence-activations.ts'
3
3
 
4
4
  function parseReport(text: string): { ok: boolean; issues: unknown[] } {
5
5
  return JSON.parse(text) as { ok: boolean; issues: unknown[] }
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { fixSequenceActivationsHandler } from '../../packages/mcp/src/tools/fix-sequence-activations.ts'
3
+
4
+ interface FixReport {
5
+ ok: boolean
6
+ fixedDiagram: string
7
+ fixesApplied: string[]
8
+ remainingIssues: unknown[]
9
+ }
10
+
11
+ function parseReport(text: string): FixReport {
12
+ return JSON.parse(text) as FixReport
13
+ }
14
+
15
+ describe('fixSequenceActivationsHandler', () => {
16
+ it('reports ok: true with no fixes for an already-balanced diagram', () => {
17
+ const result = fixSequenceActivationsHandler({
18
+ diagram: 'sequenceDiagram\n A->>+B: hello\n B-->>-A: world',
19
+ })
20
+ expect(result.isError).toBeUndefined()
21
+ const [content] = result.content
22
+ if (content?.type !== 'text') {
23
+ throw new Error('Expected text content')
24
+ }
25
+ const report = parseReport(content.text)
26
+ expect(report.ok).toBe(true)
27
+ expect(report.fixesApplied).toEqual([])
28
+ expect(report.remainingIssues).toEqual([])
29
+ expect(report.fixedDiagram).toBe(
30
+ 'sequenceDiagram\n A->>+B: hello\n B-->>-A: world',
31
+ )
32
+ })
33
+
34
+ it('appends a matching deactivate for a dangling activation', () => {
35
+ const result = fixSequenceActivationsHandler({
36
+ diagram: 'sequenceDiagram\n activate A\n A->>B: hello',
37
+ })
38
+ expect(result.isError).toBeUndefined()
39
+ const [content] = result.content
40
+ if (content?.type !== 'text') {
41
+ throw new Error('Expected text content')
42
+ }
43
+ const report = parseReport(content.text)
44
+ expect(report.ok).toBe(true)
45
+ expect(report.fixesApplied).toHaveLength(1)
46
+ expect(report.remainingIssues).toEqual([])
47
+ expect(report.fixedDiagram.trim().endsWith('deactivate A')).toBe(true)
48
+ })
49
+
50
+ it('fixes multiple dangling activations across different actors', () => {
51
+ const result = fixSequenceActivationsHandler({
52
+ diagram: 'sequenceDiagram\n activate A\n activate B\n A->>B: hello',
53
+ })
54
+ expect(result.isError).toBeUndefined()
55
+ const [content] = result.content
56
+ if (content?.type !== 'text') {
57
+ throw new Error('Expected text content')
58
+ }
59
+ const report = parseReport(content.text)
60
+ expect(report.ok).toBe(true)
61
+ expect(report.fixesApplied).toHaveLength(2)
62
+ expect(report.fixedDiagram).toContain('deactivate A')
63
+ expect(report.fixedDiagram).toContain('deactivate B')
64
+ })
65
+
66
+ it('does not auto-fix an unmatched deactivation, reporting it instead', () => {
67
+ const result = fixSequenceActivationsHandler({
68
+ diagram: 'sequenceDiagram\n A->>B: hello\n deactivate A',
69
+ })
70
+ expect(result.isError).toBeUndefined()
71
+ const [content] = result.content
72
+ if (content?.type !== 'text') {
73
+ throw new Error('Expected text content')
74
+ }
75
+ const report = parseReport(content.text)
76
+ expect(report.ok).toBe(false)
77
+ expect(report.fixesApplied).toEqual([])
78
+ expect(report.fixedDiagram).toBe(
79
+ 'sequenceDiagram\n A->>B: hello\n deactivate A',
80
+ )
81
+ expect(report.remainingIssues).toEqual([
82
+ expect.objectContaining({
83
+ code: 'UNMATCHED_DEACTIVATION',
84
+ actorId: 'A',
85
+ }),
86
+ ])
87
+ })
88
+
89
+ it('fixes the dangling activation while still reporting an unmatched one', () => {
90
+ const result = fixSequenceActivationsHandler({
91
+ diagram: 'sequenceDiagram\n activate A\n A->>B: hello\n deactivate B',
92
+ })
93
+ expect(result.isError).toBeUndefined()
94
+ const [content] = result.content
95
+ if (content?.type !== 'text') {
96
+ throw new Error('Expected text content')
97
+ }
98
+ const report = parseReport(content.text)
99
+ expect(report.ok).toBe(false)
100
+ expect(report.fixesApplied).toHaveLength(1)
101
+ expect(report.fixedDiagram).toContain('deactivate A')
102
+ expect(report.remainingIssues).toEqual([
103
+ expect.objectContaining({
104
+ code: 'UNMATCHED_DEACTIVATION',
105
+ actorId: 'B',
106
+ }),
107
+ ])
108
+ })
109
+
110
+ it('rejects a non-sequence diagram with a clear error instead of a false report', () => {
111
+ const result = fixSequenceActivationsHandler({
112
+ diagram: 'graph LR\n A --> B',
113
+ })
114
+ expect(result.isError).toBe(true)
115
+ const [content] = result.content
116
+ if (content?.type !== 'text') {
117
+ throw new Error('Expected text content')
118
+ }
119
+ expect(content.text).toContain('only fixes sequence diagrams')
120
+ expect(content.text).toContain('"flowchart"')
121
+ })
122
+
123
+ it('returns an MCP tool error instead of throwing on invalid sequence syntax', () => {
124
+ const result = fixSequenceActivationsHandler({
125
+ diagram: 'sequenceDiagram\n box\n box',
126
+ })
127
+ expect(result.isError).toBe(true)
128
+ const [content] = result.content
129
+ if (content?.type !== 'text') {
130
+ throw new Error('Expected text content')
131
+ }
132
+ expect(content.text).toContain('Failed to fix diagram activations')
133
+ })
134
+ })
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
- import { renderAsciiHandler } from '../mcp/tools/render-ascii.ts'
3
- import { renderMermaidASCII } from '../ascii/index.ts'
2
+ import { renderAsciiHandler } from '../../packages/mcp/src/tools/render-ascii.ts'
3
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
4
4
 
5
5
  // ============================================================================
6
6
  // renderMermaidASCII is wrapped (not fully replaced) so every test above
@@ -8,8 +8,9 @@ import { renderMermaidASCII } from '../ascii/index.ts'
8
8
  // non-Error throw (impossible to provoke from real Mermaid source, since
9
9
  // the renderer only ever throws Error instances) overrides it per-call.
10
10
  // ============================================================================
11
- vi.mock('../ascii/index.ts', async (importOriginal) => {
12
- const actual = await importOriginal<typeof import('../ascii/index.ts')>()
11
+ vi.mock('@zombie-mermaid/ascii-renderer', async (importOriginal) => {
12
+ const actual =
13
+ await importOriginal<typeof import('@zombie-mermaid/ascii-renderer')>()
13
14
  return { ...actual, renderMermaidASCII: vi.fn(actual.renderMermaidASCII) }
14
15
  })
15
16
 
@@ -29,8 +29,8 @@ describe('render-svg.ts module load — theme registry invariant', () => {
29
29
  }))
30
30
  vi.resetModules()
31
31
 
32
- await expect(import('../mcp/tools/render-svg.ts')).rejects.toThrow(
33
- 'Expected at least one built-in theme to be registered',
34
- )
32
+ await expect(
33
+ import('../../packages/mcp/src/tools/render-svg.ts'),
34
+ ).rejects.toThrow('Expected at least one built-in theme to be registered')
35
35
  })
36
36
  })
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest'
2
2
  import {
3
3
  renderSvgHandler,
4
4
  renderSvgInputShape,
5
- } from '../mcp/tools/render-svg.ts'
5
+ } from '../../packages/mcp/src/tools/render-svg.ts'
6
6
  import { THEMES } from '@zombie-mermaid/core'
7
7
  import { renderMermaidSVG } from '../index.ts'
8
8
 
@@ -11,7 +11,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
11
11
  import { Client } from '@modelcontextprotocol/sdk/client/index.js'
12
12
  import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
13
13
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
14
- import { createMcpServer } from '../mcp/server.ts'
14
+ import { createMcpServer } from '../../packages/mcp/src/server.ts'
15
15
 
16
16
  let server: McpServer
17
17
  let client: Client
@@ -39,6 +39,7 @@ describe('createMcpServer', () => {
39
39
  const names = tools.map((t) => t.name).sort()
40
40
  expect(names).toEqual([
41
41
  'check_mermaid_sequence_activations',
42
+ 'fix_mermaid_sequence_activations',
42
43
  'render_mermaid_ascii',
43
44
  'render_mermaid_svg',
44
45
  ])
@@ -131,4 +132,32 @@ describe('createMcpServer', () => {
131
132
  expect.objectContaining({ code: 'DANGLING_ACTIVATION', actorId: 'A' }),
132
133
  ])
133
134
  })
135
+
136
+ it('fixes a dangling activation via a real tool call', async () => {
137
+ const result = await client.callTool({
138
+ name: 'fix_mermaid_sequence_activations',
139
+ arguments: {
140
+ diagram: 'sequenceDiagram\n activate A\n A->>B: hello',
141
+ },
142
+ })
143
+ expect(result.isError).toBeFalsy()
144
+ const content = result.content
145
+ if (
146
+ !Array.isArray(content) ||
147
+ content[0]?.type !== 'text' ||
148
+ typeof content[0].text !== 'string'
149
+ ) {
150
+ throw new Error('Expected text content')
151
+ }
152
+ const report = JSON.parse(content[0].text) as {
153
+ ok: boolean
154
+ fixedDiagram: string
155
+ fixesApplied: string[]
156
+ remainingIssues: unknown[]
157
+ }
158
+ expect(report.ok).toBe(true)
159
+ expect(report.fixedDiagram).toContain('deactivate A')
160
+ expect(report.fixesApplied).toHaveLength(1)
161
+ expect(report.remainingIssues).toEqual([])
162
+ })
134
163
  })
@@ -10,18 +10,26 @@
10
10
  */
11
11
  import { describe, it, expect } from 'vitest'
12
12
  import { parseMermaid } from '../parser.ts'
13
- import { parseSequenceDiagram } from '../sequence/parser.ts'
14
- import { parseClassDiagram } from '../class/parser.ts'
15
- import { parseErDiagram } from '../er/parser.ts'
13
+ import {
14
+ parseSequenceDiagram,
15
+ parseClassDiagram,
16
+ parseErDiagram,
17
+ } from '@zombie-mermaid/mermaid-parser'
16
18
  import {
17
19
  measureMultilineText,
18
20
  LINE_HEIGHT_RATIO,
19
21
  measureTextWidth,
20
22
  normalizeBrTags,
21
23
  stripFormattingTags,
24
+ splitStatements,
22
25
  } from '@zombie-mermaid/core'
23
26
  import { renderMermaid } from '../index.ts'
24
27
 
28
+ /** Build a `Statement[]` from plain source lines, one statement per line. */
29
+ function toLines(sourceLines: string[]) {
30
+ return splitStatements(sourceLines.join('\n'))
31
+ }
32
+
25
33
  // ============================================================================
26
34
  // Parser: <br> tag normalization
27
35
  // ============================================================================
@@ -131,45 +139,45 @@ describe('parseMermaid – <br> tag normalization', () => {
131
139
 
132
140
  describe('sequence diagram labels', () => {
133
141
  it('normalizes <br> in participant alias labels', () => {
134
- const lines = [
142
+ const lines = toLines([
135
143
  'sequenceDiagram',
136
144
  'participant A as First<br>Line',
137
145
  'A->>A: test',
138
- ]
146
+ ])
139
147
  const diagram = parseSequenceDiagram(lines)
140
148
  expect(diagram.actors[0]!.label).toBe('First\nLine')
141
149
  })
142
150
 
143
151
  it('normalizes <br> in message labels', () => {
144
- const lines = ['sequenceDiagram', 'A->>B: Hello<br>World']
152
+ const lines = toLines(['sequenceDiagram', 'A->>B: Hello<br>World'])
145
153
  const diagram = parseSequenceDiagram(lines)
146
154
  expect(diagram.messages[0]!.label).toBe('Hello\nWorld')
147
155
  })
148
156
 
149
157
  it('normalizes <br> in note text', () => {
150
- const lines = [
158
+ const lines = toLines([
151
159
  'sequenceDiagram',
152
160
  'A->>B: Hello',
153
161
  'Note over A,B: First<br>Second',
154
- ]
162
+ ])
155
163
  const diagram = parseSequenceDiagram(lines)
156
164
  expect(diagram.notes[0]!.text).toBe('First\nSecond')
157
165
  })
158
166
 
159
167
  it('normalizes <br> in block labels', () => {
160
- const lines = [
168
+ const lines = toLines([
161
169
  'sequenceDiagram',
162
170
  'A->>B: Hello',
163
171
  'loop Every<br>30s',
164
172
  'A->>B: Ping',
165
173
  'end',
166
- ]
174
+ ])
167
175
  const diagram = parseSequenceDiagram(lines)
168
176
  expect(diagram.blocks[0]!.label).toBe('Every\n30s')
169
177
  })
170
178
 
171
179
  it('normalizes <br> in divider labels', () => {
172
- const lines = [
180
+ const lines = toLines([
173
181
  'sequenceDiagram',
174
182
  'A->>B: Hello',
175
183
  'alt First<br>case',
@@ -177,7 +185,7 @@ describe('parseMermaid – <br> tag normalization', () => {
177
185
  'else Second<br>case',
178
186
  'A->>B: b',
179
187
  'end',
180
- ]
188
+ ])
181
189
  const diagram = parseSequenceDiagram(lines)
182
190
  expect(diagram.blocks[0]!.dividers[0]!.label).toBe('Second\ncase')
183
191
  })
@@ -185,19 +193,19 @@ describe('parseMermaid – <br> tag normalization', () => {
185
193
 
186
194
  describe('class diagram labels', () => {
187
195
  it('normalizes <br> in relationship labels', () => {
188
- const lines = ['classDiagram', 'A --> B : uses<br>internally']
196
+ const lines = toLines(['classDiagram', 'A --> B : uses<br>internally'])
189
197
  const diagram = parseClassDiagram(lines)
190
198
  expect(diagram.relationships[0]!.label).toBe('uses\ninternally')
191
199
  })
192
200
 
193
201
  it('normalizes <br> in fromCardinality labels', () => {
194
- const lines = ['classDiagram', 'A "one<br>to" --> B']
202
+ const lines = toLines(['classDiagram', 'A "one<br>to" --> B'])
195
203
  const diagram = parseClassDiagram(lines)
196
204
  expect(diagram.relationships[0]!.fromCardinality).toBe('one\nto')
197
205
  })
198
206
 
199
207
  it('normalizes <br> in toCardinality labels', () => {
200
- const lines = ['classDiagram', 'A --> "many<br>items" B']
208
+ const lines = toLines(['classDiagram', 'A --> "many<br>items" B'])
201
209
  const diagram = parseClassDiagram(lines)
202
210
  expect(diagram.relationships[0]!.toCardinality).toBe('many\nitems')
203
211
  })
@@ -205,18 +213,21 @@ describe('parseMermaid – <br> tag normalization', () => {
205
213
 
206
214
  describe('ER diagram labels', () => {
207
215
  it('normalizes <br> in relationship labels', () => {
208
- const lines = ['erDiagram', 'CUSTOMER ||--o{ ORDER : places<br>orders']
216
+ const lines = toLines([
217
+ 'erDiagram',
218
+ 'CUSTOMER ||--o{ ORDER : places<br>orders',
219
+ ])
209
220
  const diagram = parseErDiagram(lines)
210
221
  expect(diagram.relationships[0]!.label).toBe('places\norders')
211
222
  })
212
223
 
213
224
  it('normalizes <br> in attribute comments', () => {
214
- const lines = [
225
+ const lines = toLines([
215
226
  'erDiagram',
216
227
  'CUSTOMER {',
217
228
  'int id PK "primary<br>key"',
218
229
  '}',
219
- ]
230
+ ])
220
231
  const diagram = parseErDiagram(lines)
221
232
  expect(diagram.entities[0]!.attributes[0]!.comment).toBe('primary\nkey')
222
233
  })
@@ -10,11 +10,14 @@
10
10
  * - Comments and error cases
11
11
  */
12
12
  import { describe, it, expect } from 'vitest'
13
- import { parseMermaid, toDirection } from '../parser.ts'
14
- // `isDirection` moved to @zombie-mermaid/core under #625 — `toDirection`
15
- // (still here) is its only in-parser caller, and core's
16
- // `direction-override.ts` needs the guard without importing the parser.
17
- import { isDirection } from '@zombie-mermaid/core'
13
+ import { parseMermaid } from '../parser.ts'
14
+ // `isDirection` moved to @zombie-mermaid/core under #625; `toDirection`
15
+ // followed it under #624 once `er/parser.ts` (now
16
+ // packages/mermaid-parser/src/er/parser.ts) became a second caller that
17
+ // can't import this file without a cycle. `../parser.ts` still re-exports
18
+ // it for backward compatibility, but the tests below import it straight
19
+ // from `core`, same as `isDirection`.
20
+ import { isDirection, toDirection } from '@zombie-mermaid/core'
18
21
 
19
22
  // ============================================================================
20
23
  // Graph header parsing
@@ -374,6 +377,115 @@ describe('parseMermaid – edges (original)', () => {
374
377
  })
375
378
  })
376
379
 
380
+ // ============================================================================
381
+ // Continuation-line edge chains (mermaid-js/mermaid#6049)
382
+ //
383
+ // Mermaid lets a vertex-chain statement wrap across lines, with the link
384
+ // operator leading the next line rather than trailing the previous one.
385
+ // `splitStatements` only splits on newlines/`;`, so without merging these
386
+ // back together the continuation lines look like they start with a bare
387
+ // arrow and no source node — parseEdgeLine can't find a node group, so it
388
+ // dropped the node and edge entirely instead of continuing the chain.
389
+ // ============================================================================
390
+
391
+ describe('parseMermaid – continuation-line edge chains (mermaid-js/mermaid#6049)', () => {
392
+ it('chains a vertex statement split across lines, same as one line', () => {
393
+ const g = parseMermaid(
394
+ [
395
+ 'flowchart TB',
396
+ ' start([Start])',
397
+ ' ==> green([Change some code])',
398
+ ' ==> finish([Finish])',
399
+ ].join('\n'),
400
+ )
401
+
402
+ expect([...g.nodes.keys()]).toEqual(['start', 'green', 'finish'])
403
+ expect(g.nodes.get('start')!.label).toBe('Start')
404
+ expect(g.nodes.get('green')!.label).toBe('Change some code')
405
+ expect(g.nodes.get('finish')!.label).toBe('Finish')
406
+
407
+ expect(g.edges).toHaveLength(2)
408
+ expect(g.edges[0]).toMatchObject({
409
+ source: 'start',
410
+ target: 'green',
411
+ style: 'thick',
412
+ })
413
+ expect(g.edges[1]).toMatchObject({
414
+ source: 'green',
415
+ target: 'finish',
416
+ style: 'thick',
417
+ })
418
+ })
419
+
420
+ it('keeps a self-loop on a later, unrelated line intact', () => {
421
+ const g = parseMermaid(
422
+ [
423
+ 'flowchart TB',
424
+ ' start([Start])',
425
+ ' ==> green([Change some code])',
426
+ ' ==> finish([Finish])',
427
+ '',
428
+ ' green -.->|Incomplete ?| green',
429
+ ].join('\n'),
430
+ )
431
+
432
+ expect(g.edges).toHaveLength(3)
433
+ expect(g.edges[2]).toMatchObject({
434
+ source: 'green',
435
+ target: 'green',
436
+ label: 'Incomplete ?',
437
+ style: 'dotted',
438
+ })
439
+ })
440
+
441
+ it('matches the equivalent single-line chain', () => {
442
+ const multiline = parseMermaid(
443
+ [
444
+ 'flowchart TB',
445
+ ' start([Start])',
446
+ ' ==> green([Change some code])',
447
+ ' ==> finish([Finish])',
448
+ ].join('\n'),
449
+ )
450
+ const singleLine = parseMermaid(
451
+ 'flowchart TB\n start([Start]) ==> green([Change some code]) ==> finish([Finish])',
452
+ )
453
+
454
+ expect([...multiline.nodes.entries()]).toEqual([
455
+ ...singleLine.nodes.entries(),
456
+ ])
457
+ expect(multiline.edges).toEqual(singleLine.edges)
458
+ })
459
+
460
+ // Regressions caught in review (PR #730): the continuation-merge heuristic
461
+ // above must not (a) mistake a bare `x`/`o` node id for the `x`/`o`
462
+ // start-marker ARROW_REGEX also accepts, or (b) merge across an explicit
463
+ // `;` statement boundary on the same physical line.
464
+
465
+ it('does not misread a bare node id "x" as a marked-continuation opener', () => {
466
+ const g = parseMermaid('flowchart TD\nx-->Y')
467
+ expect(g.direction).toBe('TD')
468
+ expect([...g.nodes.keys()]).toEqual(['x', 'Y'])
469
+ expect(g.edges).toHaveLength(1)
470
+ expect(g.edges[0]).toMatchObject({ source: 'x', target: 'Y' })
471
+ })
472
+
473
+ it('does not misread a bare node id "o" as a marked-continuation opener', () => {
474
+ const g = parseMermaid('flowchart TD\no-->Y')
475
+ expect(g.direction).toBe('TD')
476
+ expect([...g.nodes.keys()]).toEqual(['o', 'Y'])
477
+ expect(g.edges).toHaveLength(1)
478
+ expect(g.edges[0]).toMatchObject({ source: 'o', target: 'Y' })
479
+ })
480
+
481
+ it('keeps an explicit `;` a real statement boundary, not a continuation', () => {
482
+ const g = parseMermaid('flowchart TD\nA --> B; --> C')
483
+ expect([...g.nodes.keys()]).toEqual(['A', 'B'])
484
+ expect(g.edges).toHaveLength(1)
485
+ expect(g.edges[0]).toMatchObject({ source: 'A', target: 'B' })
486
+ })
487
+ })
488
+
377
489
  // ============================================================================
378
490
  // Circle-end / cross-end edges: --o, --x (issue #65)
379
491
  //
@@ -5,10 +5,17 @@ import {
5
5
  mergePath,
6
6
  createPathBudget,
7
7
  DEFAULT_PATH_BUDGET,
8
- } from '../ascii/pathfinder.ts'
9
- import type { GridCoord, PathBudget } from '../ascii/types.ts'
10
- import { createGrid, placeBlock, type Grid } from '../ascii/grid-occupancy.ts'
11
- import { renderMermaidASCII } from '../ascii/index.ts'
8
+ } from '../../packages/ascii-renderer/src/pathfinder.ts'
9
+ import type {
10
+ GridCoord,
11
+ PathBudget,
12
+ } from '../../packages/ascii-renderer/src/types.ts'
13
+ import {
14
+ createGrid,
15
+ placeBlock,
16
+ type Grid,
17
+ } from '../../packages/ascii-renderer/src/grid-occupancy.ts'
18
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
12
19
 
13
20
  /**
14
21
  * Helper to build an occupied grid from a list of coordinates, going through