@zombie-mermaid/ascii-renderer 2.2.1

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 (232) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +8 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +146 -0
  5. package/dist/index.d.ts +146 -0
  6. package/dist/index.js +5392 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +36 -0
  9. package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
  10. package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
  11. package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
  12. package/src/__tests__/ascii-canvas-write.test.ts +132 -0
  13. package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
  14. package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
  15. package/src/__tests__/ascii-cjk-width.test.ts +150 -0
  16. package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
  17. package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
  18. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  19. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
  20. package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
  21. package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
  22. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
  23. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
  24. package/src/__tests__/ascii-class-padding.test.ts +104 -0
  25. package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
  26. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
  27. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
  28. package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
  29. package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
  30. package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
  31. package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
  32. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
  33. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
  34. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
  35. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
  36. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
  37. package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
  38. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
  39. package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
  40. package/src/__tests__/ascii-edge-styles.test.ts +149 -0
  41. package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
  42. package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
  43. package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
  44. package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
  45. package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
  46. package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
  47. package/src/__tests__/ascii-er-padding.test.ts +114 -0
  48. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
  49. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
  50. package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
  51. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
  52. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
  53. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
  54. package/src/__tests__/ascii-lane-search.test.ts +207 -0
  55. package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
  56. package/src/__tests__/ascii-multiline.test.ts +288 -0
  57. package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
  58. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
  59. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
  60. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
  61. package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
  62. package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
  63. package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
  64. package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
  65. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
  66. package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
  67. package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
  68. package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
  69. package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
  70. package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
  71. package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
  72. package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
  73. package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
  74. package/src/__tests__/ascii-shapes-special.test.ts +344 -0
  75. package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
  76. package/src/__tests__/ascii-shapes-state.test.ts +224 -0
  77. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
  78. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
  79. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
  80. package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
  81. package/src/__tests__/ascii-territory-unit.test.ts +219 -0
  82. package/src/__tests__/ascii-validate.test.ts +220 -0
  83. package/src/__tests__/ascii.test.ts +325 -0
  84. package/src/__tests__/class-arrow-directions.test.ts +505 -0
  85. package/src/__tests__/draw-lines.test.ts +93 -0
  86. package/src/__tests__/edge-cell-styles.test.ts +278 -0
  87. package/src/__tests__/grid-occupancy.test.ts +240 -0
  88. package/src/__tests__/helpers/ascii-form.ts +142 -0
  89. package/src/__tests__/helpers/terminal-display-width.ts +74 -0
  90. package/src/__tests__/pathfinder.test.ts +239 -0
  91. package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
  92. package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
  93. package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
  94. package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
  95. package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
  96. package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
  97. package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
  98. package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
  99. package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
  100. package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
  101. package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
  102. package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
  103. package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
  104. package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
  105. package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
  106. package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
  107. package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
  108. package/src/__tests__/testdata/ascii/comments.txt +23 -0
  109. package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
  110. package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
  111. package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
  112. package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
  113. package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
  114. package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
  115. package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
  116. package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
  117. package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
  118. package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
  119. package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
  120. package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
  121. package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
  122. package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
  123. package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
  124. package/src/__tests__/testdata/ascii/single_node.txt +8 -0
  125. package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
  126. package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
  127. package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
  128. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
  129. package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
  130. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
  131. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
  132. package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
  133. package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
  134. package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
  135. package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
  136. package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
  137. package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
  138. package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
  139. package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
  140. package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
  141. package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
  142. package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
  143. package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
  144. package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
  145. package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
  146. package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
  147. package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
  148. package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
  149. package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
  150. package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
  151. package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
  152. package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
  153. package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
  154. package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
  155. package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
  156. package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
  157. package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
  158. package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
  159. package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
  160. package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
  161. package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
  162. package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
  163. package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
  164. package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
  165. package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
  166. package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
  167. package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
  168. package/src/__tests__/testdata/unicode/comments.txt +23 -0
  169. package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
  170. package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
  171. package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
  172. package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
  173. package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
  174. package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
  175. package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
  176. package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
  177. package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
  178. package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
  179. package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
  180. package/src/__tests__/testdata/unicode/single_node.txt +8 -0
  181. package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
  182. package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
  183. package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
  184. package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
  185. package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
  186. package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
  187. package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
  188. package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
  189. package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
  190. package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
  191. package/src/__tests__/xychart-ascii.test.ts +376 -0
  192. package/src/ansi.ts +490 -0
  193. package/src/canvas.ts +757 -0
  194. package/src/class-diagram.ts +2001 -0
  195. package/src/converter.ts +446 -0
  196. package/src/coords.ts +58 -0
  197. package/src/display-width.ts +151 -0
  198. package/src/draw-arrows.ts +593 -0
  199. package/src/draw-boxes.ts +267 -0
  200. package/src/draw-bundles.ts +611 -0
  201. package/src/draw-lines.ts +174 -0
  202. package/src/draw-subgraphs.ts +108 -0
  203. package/src/draw.ts +350 -0
  204. package/src/edge-bundling.ts +435 -0
  205. package/src/edge-cell-styles.ts +209 -0
  206. package/src/edge-routing.ts +1070 -0
  207. package/src/er-diagram.ts +1488 -0
  208. package/src/flowchart.ts +94 -0
  209. package/src/grid-occupancy.ts +234 -0
  210. package/src/grid.ts +1309 -0
  211. package/src/hyperlinks.ts +248 -0
  212. package/src/index.ts +163 -0
  213. package/src/lane-search.ts +68 -0
  214. package/src/multiline-utils.ts +82 -0
  215. package/src/pathfinder.ts +448 -0
  216. package/src/registry.ts +88 -0
  217. package/src/sequence.ts +1318 -0
  218. package/src/shapes/circle.ts +31 -0
  219. package/src/shapes/corners.ts +273 -0
  220. package/src/shapes/diamond.ts +31 -0
  221. package/src/shapes/hexagon.ts +35 -0
  222. package/src/shapes/index.ts +123 -0
  223. package/src/shapes/rectangle.ts +199 -0
  224. package/src/shapes/rounded.ts +31 -0
  225. package/src/shapes/special.ts +360 -0
  226. package/src/shapes/stadium.ts +122 -0
  227. package/src/shapes/state.ts +204 -0
  228. package/src/shapes/types.ts +78 -0
  229. package/src/territory.ts +136 -0
  230. package/src/types.ts +454 -0
  231. package/src/validate.ts +189 -0
  232. package/src/xychart.ts +1085 -0
@@ -0,0 +1,248 @@
1
+ // ============================================================================
2
+ // ASCII renderer — OSC 8 terminal hyperlinks (opt-in)
3
+ //
4
+ // Modern terminals (iTerm2, WezTerm, kitty, Windows Terminal, VTE-based
5
+ // terminals such as GNOME Terminal) turn a run of text wrapped in an OSC 8
6
+ // escape pair into a clickable link:
7
+ //
8
+ // ESC ] 8 ; ; https://example.com ESC \ Docs ESC ] 8 ; ; ESC \
9
+ //
10
+ // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
11
+ // for the de-facto spec. The escape sequences are zero-width, so they must
12
+ // never take part in layout, column-width math, or box drawing — this module
13
+ // records *which canvas cells* carry a link in a `LinkCanvas` (parallel to
14
+ // `RoleCanvas`, the same column-major shape) at draw time, and the
15
+ // canvas-to-string conversion (`canvasToString` / `colorizeLine`) inserts
16
+ // the escape pairs around each run of linked cells as it emits a row.
17
+ // Marking cells rather than searching rendered text for the label means a
18
+ // label that repeats, or appears inside another label, still links only the
19
+ // node that actually declared the `click`.
20
+ //
21
+ // This is what lets a `click A "https://..."` directive survive into
22
+ // terminal output — see docs/decisions/no-script-interactivity.md for where
23
+ // that sits in the interactivity tiers. Off by default
24
+ // (`AsciiRenderOptions.hyperlinks`): not every terminal or pager handles
25
+ // OSC 8 gracefully, and HTML color mode (the demo site's terminal mockup)
26
+ // never emits it at all.
27
+ // ============================================================================
28
+
29
+ import type { Canvas, DrawingCoord, AsciiGraph } from './types.ts'
30
+ import type { NodeInteraction } from '@zombie-mermaid/core'
31
+ import { safeHref } from '@zombie-mermaid/core'
32
+
33
+ /**
34
+ * [maxX, maxY] of a canvas — the same arithmetic as canvas.ts's
35
+ * `getCanvasSize`, inlined so this module (imported by canvas.ts for
36
+ * `joinWithLinks`) doesn't import canvas.ts back.
37
+ */
38
+ function canvasBounds(canvas: Canvas): [number, number] {
39
+ return [canvas.length - 1, (canvas[0]?.length ?? 1) - 1]
40
+ }
41
+
42
+ /**
43
+ * Link canvas — parallel to Canvas, records the href each cell is part of.
44
+ * Same column-major structure: linkCanvas[x][y] gives the href at (x, y).
45
+ * null means the cell is not part of any link.
46
+ */
47
+ export type LinkCanvas = (string | null)[][]
48
+
49
+ /** OSC 8 close sequence (an empty URI ends the current link). */
50
+ export const OSC8_CLOSE = '\x1b]8;;\x1b\\'
51
+
52
+ /**
53
+ * OSC 8 open sequence for `href`.
54
+ *
55
+ * The URI portion is limited to printable ASCII (0x21–0x7E) by the spec
56
+ * linked above — anything else is percent-encoded here so a URL with a
57
+ * non-ASCII path or a literal space can't break the sequence or leak a
58
+ * control character into the terminal stream. `encodeURIComponent` on one
59
+ * code point yields its UTF-8 percent-encoding; the `u` flag keeps a
60
+ * surrogate pair together so it encodes as one code point rather than two
61
+ * lone surrogates (which `encodeURIComponent` would throw on).
62
+ */
63
+ export function osc8Open(href: string): string {
64
+ const uri = href.replace(/[^\x21-\x7e]/gu, (ch) => encodeURIComponent(ch))
65
+ return `\x1b]8;;${uri}\x1b\\`
66
+ }
67
+
68
+ /**
69
+ * Matches one OSC 8 sequence — open or close, ST- or BEL-terminated. The
70
+ * URI can never contain ESC or BEL (`osc8Open` percent-encodes anything
71
+ * outside printable ASCII), so scanning to the first terminator is exact.
72
+ */
73
+ export const OSC8_SEQUENCE = /\x1b\]8;[^\x07\x1b]*(?:\x1b\\|\x07)/g
74
+
75
+ /** Remove every OSC 8 sequence, leaving the visible text (and any SGR color codes) intact. */
76
+ export function stripOsc8(text: string): string {
77
+ return text.replace(OSC8_SEQUENCE, '')
78
+ }
79
+
80
+ // ============================================================================
81
+ // Link canvas creation and marking
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Create a blank link canvas filled with nulls.
86
+ * Dimensions are inclusive, matching `mkCanvas`: mkLinkCanvas(3, 2) covers
87
+ * indices 0..3 by 0..2.
88
+ */
89
+ export function mkLinkCanvas(x: number, y: number): LinkCanvas {
90
+ const linkCanvas: LinkCanvas = []
91
+ for (let i = 0; i <= x; i++) {
92
+ const col: (string | null)[] = []
93
+ for (let j = 0; j <= y; j++) {
94
+ col.push(null)
95
+ }
96
+ linkCanvas.push(col)
97
+ }
98
+ return linkCanvas
99
+ }
100
+
101
+ /**
102
+ * Bounds-checked write of an href to one cell. Out-of-range coordinates
103
+ * are a silent no-op, the same contract as `write()` in canvas.ts — the
104
+ * link canvas is created at the finished canvas's size, so a cell outside
105
+ * it can't be printed anyway.
106
+ */
107
+ export function setLink(
108
+ linkCanvas: LinkCanvas,
109
+ x: number,
110
+ y: number,
111
+ href: string,
112
+ ): void {
113
+ const col = linkCanvas[x]
114
+ if (col === undefined || y < 0 || y >= col.length) return
115
+ col[y] = href
116
+ }
117
+
118
+ /**
119
+ * Flip the link canvas vertically to match `flipCanvasVertically` (used for
120
+ * BT direction). Mutates in place and returns it.
121
+ */
122
+ export function flipLinkCanvasVertically(linkCanvas: LinkCanvas): LinkCanvas {
123
+ for (const col of linkCanvas) {
124
+ col.reverse()
125
+ }
126
+ return linkCanvas
127
+ }
128
+
129
+ /**
130
+ * Mark the label text inside a drawn box as linked to `href`.
131
+ *
132
+ * `box` is a standalone box canvas (from `drawNode`/`drawMultiBox`) whose
133
+ * border occupies its outermost row and column; `offset` is where its
134
+ * top-left corner lands on the main canvas. For each interior row (or only
135
+ * `rows.from..rows.to`, inclusive, when given — class diagrams pass just
136
+ * the header's class-name rows), the linked span runs from the first to the
137
+ * last non-space cell, so a multi-word label ("Web Server") is one link
138
+ * rather than two, and each line of a multi-line label becomes its own
139
+ * span. A row with no text is left unmarked.
140
+ *
141
+ * Working from the box's own geometry (rather than scanning the merged
142
+ * canvas for the label's characters) is what keeps this exact for labels
143
+ * that repeat across nodes, contain box-drawing-like punctuation, or hold
144
+ * wide characters whose placeholder cell (`WIDE_CHAR_PLACEHOLDER`) is an
145
+ * empty string rather than a space.
146
+ */
147
+ export function markBoxLabelLinks(
148
+ linkCanvas: LinkCanvas,
149
+ box: Canvas,
150
+ offset: DrawingCoord,
151
+ href: string,
152
+ rows?: { from: number; to: number },
153
+ ): void {
154
+ const [maxX, maxY] = canvasBounds(box)
155
+ const firstRow = Math.max(1, rows?.from ?? 1)
156
+ const lastRow = Math.min(maxY - 1, rows?.to ?? maxY - 1)
157
+
158
+ for (let y = firstRow; y <= lastRow; y++) {
159
+ let first = -1
160
+ let last = -1
161
+ for (let x = 1; x <= maxX - 1; x++) {
162
+ if (box[x]?.[y] !== ' ') {
163
+ if (first === -1) first = x
164
+ last = x
165
+ }
166
+ }
167
+ if (first === -1) continue
168
+ for (let x = first; x <= last; x++) {
169
+ setLink(linkCanvas, x + offset.x, y + offset.y, href)
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Build the link canvas for a drawn flowchart/state graph: every node whose
176
+ * `click` declared a safe href (see `safeHref` — `javascript:`/`data:` and
177
+ * the like are dropped there, never here) gets its label cells marked. A
178
+ * `call`/`callback`-only interaction has no href and marks nothing.
179
+ *
180
+ * Must run after `drawGraph` (so every node has its `drawing` and
181
+ * `drawingCoord`) and before any BT flip of the canvas — flip this canvas
182
+ * alongside it with `flipLinkCanvasVertically`.
183
+ */
184
+ export function buildNodeLinkCanvas(
185
+ graph: AsciiGraph,
186
+ interactions: ReadonlyMap<string, NodeInteraction>,
187
+ ): LinkCanvas {
188
+ const [maxX, maxY] = canvasBounds(graph.canvas)
189
+ const linkCanvas = mkLinkCanvas(maxX, maxY)
190
+
191
+ for (const node of graph.nodes) {
192
+ if (!node.drawing || !node.drawingCoord) continue
193
+ const href = safeHref(interactions.get(node.name)?.href)
194
+ if (href === undefined) continue
195
+ markBoxLabelLinks(linkCanvas, node.drawing, node.drawingCoord, href)
196
+ }
197
+
198
+ return linkCanvas
199
+ }
200
+
201
+ // ============================================================================
202
+ // Emitting link runs while serializing a row
203
+ // ============================================================================
204
+
205
+ /**
206
+ * Tracks the link state across one row's cells and hands back the escape
207
+ * text to insert before each cell: a close when leaving a linked run, an
208
+ * open when entering one (both when two different links touch), and
209
+ * nothing while the state is unchanged. `finish()` closes a run still open
210
+ * at the end of the row.
211
+ *
212
+ * The markers are pure insertions into the character stream — the SGR
213
+ * color grouping in `colorizeLine` is left exactly as it would be without
214
+ * links — so stripping every OSC 8 sequence from hyperlinked output yields
215
+ * the non-hyperlinked output byte for byte.
216
+ */
217
+ export class LinkRunTracker {
218
+ private current: string | null = null
219
+
220
+ /** Escape text to emit before a cell carrying `link` (null = no link). */
221
+ advance(link: string | null): string {
222
+ if (link === this.current) return ''
223
+ let out = this.current !== null ? OSC8_CLOSE : ''
224
+ if (link !== null) out += osc8Open(link)
225
+ this.current = link
226
+ return out
227
+ }
228
+
229
+ /** Escape text to emit after the row's last cell. */
230
+ finish(): string {
231
+ const out = this.current !== null ? OSC8_CLOSE : ''
232
+ this.current = null
233
+ return out
234
+ }
235
+ }
236
+
237
+ /** Join a row's cells into plain (uncolored) text with OSC 8 pairs around each linked run. */
238
+ export function joinWithLinks(
239
+ chars: readonly string[],
240
+ links: readonly (string | null)[],
241
+ ): string {
242
+ const tracker = new LinkRunTracker()
243
+ let line = ''
244
+ for (const [i, char] of chars.entries()) {
245
+ line += tracker.advance(links[i] ?? null) + char
246
+ }
247
+ return line + tracker.finish()
248
+ }
package/src/index.ts ADDED
@@ -0,0 +1,163 @@
1
+ // ============================================================================
2
+ // zombie-mermaid — ASCII renderer public API
3
+ //
4
+ // Renders Mermaid diagrams to ASCII or Unicode box-drawing art.
5
+ // No external dependencies — pure TypeScript.
6
+ //
7
+ // Supported diagram types:
8
+ // - Flowcharts (graph TD / flowchart LR) — grid-based layout with A* pathfinding
9
+ // - State diagrams (stateDiagram-v2) — same pipeline as flowcharts
10
+ // - Sequence diagrams (sequenceDiagram) — column-based timeline layout
11
+ // - Class diagrams (classDiagram) — level-based UML layout
12
+ // - ER diagrams (erDiagram) — grid layout with crow's foot notation
13
+ //
14
+ // Usage:
15
+ // import { renderMermaidASCII } from 'zombie-mermaid'
16
+ // const ascii = renderMermaidASCII('graph LR\n A --> B')
17
+ // ============================================================================
18
+
19
+ import { detectDiagramType } from '@zombie-mermaid/core'
20
+ import type { Direction, DiagramType } from '@zombie-mermaid/core'
21
+ import { asciiRegistry } from './registry.ts'
22
+ import { addCoordsOverlay } from './coords.ts'
23
+ import {
24
+ detectColorMode,
25
+ DEFAULT_ASCII_THEME,
26
+ diagramColorsToAsciiTheme,
27
+ } from './ansi.ts'
28
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
29
+ import {
30
+ DEFAULT_PADDING_X,
31
+ DEFAULT_PADDING_Y,
32
+ DEFAULT_BOX_BORDER_PADDING,
33
+ } from './types.ts'
34
+
35
+ // Re-export types for external use
36
+ export type { AsciiTheme, ColorMode }
37
+ export { DEFAULT_ASCII_THEME, detectColorMode, diagramColorsToAsciiTheme }
38
+
39
+ export interface AsciiRenderOptions {
40
+ /** true = ASCII chars (+,-,|,>), false = Unicode box-drawing (┌,─,│,►). Default: false */
41
+ useAscii?: boolean
42
+ /** Horizontal spacing between nodes. Default: 5 */
43
+ paddingX?: number
44
+ /** Vertical spacing between nodes. Default: 5 */
45
+ paddingY?: number
46
+ /** Padding inside node boxes. Default: 1 */
47
+ boxBorderPadding?: number
48
+ /**
49
+ * Color mode for output.
50
+ * - 'none': No colors (plain text)
51
+ * - 'auto': Auto-detect (terminal ANSI capabilities, or HTML in browsers)
52
+ * - 'ansi16': 16-color ANSI
53
+ * - 'ansi256': 256-color xterm
54
+ * - 'truecolor': 24-bit RGB
55
+ * - 'html': HTML <span> tags with inline color styles (for browser rendering)
56
+ * Default: 'auto'
57
+ */
58
+ colorMode?: ColorMode | 'auto'
59
+ /** Theme colors for ASCII output. Uses default theme if not provided. */
60
+ theme?: Partial<AsciiTheme>
61
+ /**
62
+ * Overlay spreadsheet-style row/column indices on the rendered output, to
63
+ * help debug layout spacing. Off by default.
64
+ */
65
+ showCoords?: boolean
66
+ /**
67
+ * Force the diagram's layout direction, overriding the one its source
68
+ * declares (a flowchart's `graph LR` header, or a state diagram's
69
+ * top-level `direction LR` line). Applied after parsing and before
70
+ * layout — the source text is never rewritten. Replaces only the
71
+ * top-level direction: a nested subgraph's or composite state's own
72
+ * `direction` line still applies on top of it, exactly as it does on top
73
+ * of the diagram's own header.
74
+ *
75
+ * Flowchart and state diagrams only in ASCII output. The ASCII ER layout
76
+ * has no direction concept (it already ignores a source `direction`
77
+ * line), and sequence/class/XY-chart diagrams have none to override, so
78
+ * all of those ignore this option (no error; output is identical with or
79
+ * without it). Same semantics as `RenderOptions.direction` for SVG. See
80
+ * issue #276.
81
+ */
82
+ direction?: Direction
83
+ /**
84
+ * Emit OSC 8 terminal hyperlinks: each node whose `click` directive
85
+ * declared an http/https/mailto/relative href gets its label wrapped in
86
+ * an `ESC ] 8 ; ; <url> ESC \` … `ESC ] 8 ; ; ESC \` pair, which
87
+ * terminals that support OSC 8 (iTerm2, WezTerm, kitty, Windows
88
+ * Terminal, VTE-based terminals) render as a clickable link. The
89
+ * sequences are zero-width and never affect layout; `click ... call fn()`
90
+ * bindings emit nothing. Off by default — not every terminal or pager
91
+ * handles OSC 8 gracefully (`less` needs `-R`), and no capability
92
+ * detection is attempted here; the caller decides. Ignored when
93
+ * `colorMode` is 'html'.
94
+ */
95
+ hyperlinks?: boolean
96
+ }
97
+
98
+ /**
99
+ * Render Mermaid diagram text to an ASCII/Unicode string.
100
+ *
101
+ * Synchronous — no async layout engine needed (unlike the SVG renderer).
102
+ * Auto-detects diagram type from the header line and dispatches to
103
+ * the appropriate renderer.
104
+ *
105
+ * @param text - Mermaid source text (any supported diagram type)
106
+ * @param options - Rendering options
107
+ * @returns Multi-line ASCII/Unicode string
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const result = renderMermaidASCII(`
112
+ * graph LR
113
+ * A --> B --> C
114
+ * `, { useAscii: true })
115
+ *
116
+ * // Output:
117
+ * // +---+ +---+ +---+
118
+ * // | | | | | |
119
+ * // | A |---->| B |---->| C |
120
+ * // | | | | | |
121
+ * // +---+ +---+ +---+
122
+ * ```
123
+ */
124
+ export function renderMermaidASCII(
125
+ text: string,
126
+ options: AsciiRenderOptions = {},
127
+ ): string {
128
+ const config: AsciiConfig = {
129
+ useAscii: options.useAscii ?? false,
130
+ paddingX: options.paddingX ?? DEFAULT_PADDING_X,
131
+ paddingY: options.paddingY ?? DEFAULT_PADDING_Y,
132
+ boxBorderPadding: options.boxBorderPadding ?? DEFAULT_BOX_BORDER_PADDING,
133
+ graphDirection: 'TD', // default; renderFlowchartAscii overrides this for flowcharts/state diagrams
134
+ }
135
+
136
+ // Resolve color mode ('auto' or unset → detect environment, otherwise use specified mode)
137
+ const colorMode: ColorMode =
138
+ options.colorMode === 'auto' || options.colorMode === undefined
139
+ ? detectColorMode()
140
+ : options.colorMode
141
+
142
+ // Merge user theme with defaults
143
+ const theme: AsciiTheme = { ...DEFAULT_ASCII_THEME, ...options.theme }
144
+
145
+ const diagramType: DiagramType = detectDiagramType(text)
146
+
147
+ // Registry dispatch (see src/ascii/registry.ts — issue #533 / #745):
148
+ // every diagram type, including 'flowchart', is registered there and
149
+ // dispatches to the exact same renderer calls each type's original
150
+ // switch case (or, for 'flowchart', inline sequence) used to make.
151
+ // `extras` carries every ASCII-only per-type option: `hyperlinks` (read
152
+ // by 'class' and 'flowchart') and `direction` (read by 'flowchart'
153
+ // only); both are ignored by every other registered type.
154
+ const result = asciiRegistry[diagramType](text, config, colorMode, theme, {
155
+ hyperlinks: options.hyperlinks ?? false,
156
+ direction: options.direction,
157
+ })
158
+
159
+ return options.showCoords ? addCoordsOverlay(result) : result
160
+ }
161
+
162
+ /** @deprecated Use `renderMermaidASCII` */
163
+ export const renderMermaidAscii = renderMermaidASCII
@@ -0,0 +1,68 @@
1
+ // ============================================================================
2
+ // ASCII renderers — shared "nearest free lane" search
3
+ //
4
+ // `er-diagram.ts` (`chooseFreeRow`) and `class-diagram.ts`
5
+ // (`findClearColumn`) independently grew the same search: start at a
6
+ // preferred row/column, and if that one is occupied, walk outward one step
7
+ // at a time — forward first, then backward — until a candidate passes the
8
+ // caller's occupancy check.
9
+ //
10
+ // Only the *search order* is shared here. The occupancy check itself stays
11
+ // with each renderer and arrives as a callback, because the two renderers
12
+ // answer "is this lane usable?" from genuinely different sources: ER scans
13
+ // the rendered canvas itself, while class-diagram tests against the placed
14
+ // box rectangles. Unifying that *storage* was scoped out as unsafe — see
15
+ // `docs/decisions/ascii-occupancy-unification-532.md` (issue #532) — so this
16
+ // module deliberately extracts the algorithm and nothing else.
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Caller-supplied occupancy check: whether the whole region a candidate
21
+ * lane would occupy is usable. Called at most once per candidate, and only
22
+ * for candidates already inside `[min, max]` (plus `preferred`, see below),
23
+ * so an expensive check isn't run on lanes that would be rejected anyway.
24
+ */
25
+ export type LaneFree = (candidate: number) => boolean
26
+
27
+ /**
28
+ * Find the nearest usable lane (a row or a column) to `preferred`, scanning
29
+ * outward in alternating directions and stopping at the first candidate
30
+ * `isFree` accepts.
31
+ *
32
+ * Search order: `preferred`, then `preferred + 1`, `preferred - 1`,
33
+ * `preferred + 2`, `preferred - 2`, … — the forward direction is always
34
+ * tried first at each distance, which is what both original call sites did
35
+ * (ER scanned below-then-above; class-diagram right-then-left).
36
+ *
37
+ * `min`/`max` are *inclusive* bounds on the outward candidates only.
38
+ * `preferred` itself is tested unconditionally, even when it falls outside
39
+ * them: both call sites relied on that. ER's bounds are the open interval
40
+ * between two entity borders, so a gap too narrow to hold any candidate row
41
+ * still lets the plain midpoint through; class-diagram's `min` of 0 keeps
42
+ * the search on-canvas while `preferred` is a box's own centre column.
43
+ *
44
+ * Returns `undefined` when no candidate in range is free — including when
45
+ * `min > max` leaves no candidates at all. Each caller supplies its own
46
+ * fallback for that case (they differ), rather than this function inventing
47
+ * one.
48
+ */
49
+ export function findFreeLane(
50
+ preferred: number,
51
+ min: number,
52
+ max: number,
53
+ isFree: LaneFree,
54
+ ): number | undefined {
55
+ if (isFree(preferred)) return preferred
56
+
57
+ // Far enough to reach whichever bound is further from `preferred`; beyond
58
+ // that every remaining candidate is out of range in both directions.
59
+ const maxOffset = Math.max(preferred - min, max - preferred)
60
+ for (let offset = 1; offset <= maxOffset; offset++) {
61
+ const forward = preferred + offset
62
+ if (forward <= max && isFree(forward)) return forward
63
+ const backward = preferred - offset
64
+ if (backward >= min && isFree(backward)) return backward
65
+ }
66
+
67
+ return undefined
68
+ }
@@ -0,0 +1,82 @@
1
+ // ============================================================================
2
+ // ASCII renderer — multi-line text utilities
3
+ //
4
+ // Shared utilities for handling multi-line labels (containing \n from <br> tags)
5
+ // in ASCII/Unicode rendering. Provides consistent text splitting, sizing, and
6
+ // centered rendering across all diagram types.
7
+ // ============================================================================
8
+
9
+ import type { Canvas } from './types.ts'
10
+ import { drawText } from './canvas.ts'
11
+ import { displayWidth } from './display-width.ts'
12
+
13
+ /**
14
+ * Split a label into lines.
15
+ * Labels are already normalized by parsers (br tags → \n).
16
+ */
17
+ export function splitLines(label: string): string[] {
18
+ return label.split('\n')
19
+ }
20
+
21
+ /**
22
+ * Get the maximum line width (in terminal display columns) for sizing
23
+ * calculations. Used to determine column widths for multi-line labels.
24
+ *
25
+ * Uses `displayWidth` rather than `.length` so CJK/kana/hangul/fullwidth-form
26
+ * and emoji characters — which render as two terminal columns each — are
27
+ * accounted for correctly. Using code-unit `.length` here would produce
28
+ * boxes too narrow for wide-character labels.
29
+ */
30
+ export function maxLineWidth(label: string): number {
31
+ const lines = splitLines(label)
32
+ return Math.max(...lines.map((l) => displayWidth(l)), 0)
33
+ }
34
+
35
+ /**
36
+ * Get the number of lines for height calculations.
37
+ * Used to determine row heights for multi-line labels.
38
+ */
39
+ export function lineCount(label: string): number {
40
+ return splitLines(label).length
41
+ }
42
+
43
+ /**
44
+ * Draw multi-line text centered at (cx, cy).
45
+ * Expands vertically from the center point.
46
+ * Each line is horizontally centered independently.
47
+ */
48
+ export function drawMultilineTextCentered(
49
+ canvas: Canvas,
50
+ label: string,
51
+ cx: number,
52
+ cy: number,
53
+ ): void {
54
+ const lines = splitLines(label)
55
+ const totalHeight = lines.length
56
+ // Center vertically: start y positions lines evenly around cy
57
+ const startY = cy - Math.floor((totalHeight - 1) / 2)
58
+
59
+ for (const [i, line] of lines.entries()) {
60
+ // Center each line horizontally (in display columns, not code units)
61
+ const startX = cx - Math.floor(displayWidth(line) / 2)
62
+ // Force overwrite for node labels (they take priority)
63
+ drawText(canvas, { x: startX, y: startY + i }, line, true)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Draw multi-line text left-aligned starting at (x, y).
69
+ * Each subsequent line is placed one row below.
70
+ */
71
+ export function drawMultilineTextLeft(
72
+ canvas: Canvas,
73
+ label: string,
74
+ x: number,
75
+ y: number,
76
+ ): void {
77
+ const lines = splitLines(label)
78
+ for (const [i, line] of lines.entries()) {
79
+ // Force overwrite for node labels (they take priority)
80
+ drawText(canvas, { x, y: y + i }, line, true)
81
+ }
82
+ }