@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.
- package/LICENSE +22 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +5392 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
- package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
- package/src/__tests__/ascii-canvas-write.test.ts +132 -0
- package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +150 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
- package/src/__tests__/ascii-class-padding.test.ts +104 -0
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
- package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
- package/src/__tests__/ascii-edge-styles.test.ts +149 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-er-padding.test.ts +114 -0
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
- package/src/__tests__/ascii-lane-search.test.ts +207 -0
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
- package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
- package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
- package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
- package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
- package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
- package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
- package/src/__tests__/ascii-shapes-special.test.ts +344 -0
- package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
- package/src/__tests__/ascii-shapes-state.test.ts +224 -0
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-territory-unit.test.ts +219 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/class-arrow-directions.test.ts +505 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-cell-styles.test.ts +278 -0
- package/src/__tests__/grid-occupancy.test.ts +240 -0
- package/src/__tests__/helpers/ascii-form.ts +142 -0
- package/src/__tests__/helpers/terminal-display-width.ts +74 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
- package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
- package/src/__tests__/testdata/ascii/comments.txt +23 -0
- package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
- package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
- package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
- package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
- package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
- package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
- package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
- package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
- package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/ascii/single_node.txt +8 -0
- package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
- package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
- package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
- package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
- package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
- package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
- package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
- package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
- package/src/__tests__/testdata/unicode/comments.txt +23 -0
- package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
- package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
- package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
- package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
- package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
- package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/unicode/single_node.txt +8 -0
- package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
- package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
- package/src/__tests__/xychart-ascii.test.ts +376 -0
- package/src/ansi.ts +490 -0
- package/src/canvas.ts +757 -0
- package/src/class-diagram.ts +2001 -0
- package/src/converter.ts +446 -0
- package/src/coords.ts +58 -0
- package/src/display-width.ts +151 -0
- package/src/draw-arrows.ts +593 -0
- package/src/draw-boxes.ts +267 -0
- package/src/draw-bundles.ts +611 -0
- package/src/draw-lines.ts +174 -0
- package/src/draw-subgraphs.ts +108 -0
- package/src/draw.ts +350 -0
- package/src/edge-bundling.ts +435 -0
- package/src/edge-cell-styles.ts +209 -0
- package/src/edge-routing.ts +1070 -0
- package/src/er-diagram.ts +1488 -0
- package/src/flowchart.ts +94 -0
- package/src/grid-occupancy.ts +234 -0
- package/src/grid.ts +1309 -0
- package/src/hyperlinks.ts +248 -0
- package/src/index.ts +163 -0
- package/src/lane-search.ts +68 -0
- package/src/multiline-utils.ts +82 -0
- package/src/pathfinder.ts +448 -0
- package/src/registry.ts +88 -0
- package/src/sequence.ts +1318 -0
- package/src/shapes/circle.ts +31 -0
- package/src/shapes/corners.ts +273 -0
- package/src/shapes/diamond.ts +31 -0
- package/src/shapes/hexagon.ts +35 -0
- package/src/shapes/index.ts +123 -0
- package/src/shapes/rectangle.ts +199 -0
- package/src/shapes/rounded.ts +31 -0
- package/src/shapes/special.ts +360 -0
- package/src/shapes/stadium.ts +122 -0
- package/src/shapes/state.ts +204 -0
- package/src/shapes/types.ts +78 -0
- package/src/territory.ts +136 -0
- package/src/types.ts +454 -0
- package/src/validate.ts +189 -0
- package/src/xychart.ts +1085 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — type definitions
|
|
3
|
+
//
|
|
4
|
+
// Ported from AlexanderGrooff/mermaid-ascii (Go).
|
|
5
|
+
// These types model the grid-based coordinate system, 2D text canvas,
|
|
6
|
+
// and graph structures used by the ASCII/Unicode renderer.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import type { NodeShape } from '@zombie-mermaid/core'
|
|
10
|
+
import type { Grid } from './grid-occupancy.ts'
|
|
11
|
+
|
|
12
|
+
// Re-export NodeShape for convenience
|
|
13
|
+
export type { NodeShape }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shape type for ASCII rendering — maps parser shapes to ASCII renderers.
|
|
17
|
+
* Most shapes from the parser are supported, with fallback to 'rectangle'.
|
|
18
|
+
*/
|
|
19
|
+
export type AsciiNodeShape = NodeShape
|
|
20
|
+
|
|
21
|
+
/** Logical grid coordinate — nodes occupy 3x3 blocks on this grid. */
|
|
22
|
+
export interface GridCoord {
|
|
23
|
+
x: number
|
|
24
|
+
y: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A mutable, render-wide A* iteration budget shared across every getPath
|
|
29
|
+
* call made while routing one graph's edges (see pathfinder.ts's getPath
|
|
30
|
+
* and grid.ts's createMapping). Bounds total pathfinding work for the whole
|
|
31
|
+
* render, independent of any single call's own per-call iteration cap.
|
|
32
|
+
*/
|
|
33
|
+
export interface PathBudget {
|
|
34
|
+
remaining: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Character-level coordinate on the 2D text canvas. */
|
|
38
|
+
export interface DrawingCoord {
|
|
39
|
+
x: number
|
|
40
|
+
y: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Direction constants model positions on a node's 3x3 grid block.
|
|
45
|
+
* Each node occupies grid cells [x..x+2, y..y+2].
|
|
46
|
+
* Directions are offsets into that block, used for edge attachment points.
|
|
47
|
+
*
|
|
48
|
+
* (0,0) UL (1,0) Up (2,0) UR
|
|
49
|
+
* (0,1) Left (1,1) Mid (2,1) Right
|
|
50
|
+
* (0,2) LL (1,2) Down (2,2) LR
|
|
51
|
+
*/
|
|
52
|
+
export interface Direction {
|
|
53
|
+
readonly x: number
|
|
54
|
+
readonly y: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const Up: Direction = { x: 1, y: 0 }
|
|
58
|
+
export const Down: Direction = { x: 1, y: 2 }
|
|
59
|
+
export const Left: Direction = { x: 0, y: 1 }
|
|
60
|
+
export const Right: Direction = { x: 2, y: 1 }
|
|
61
|
+
export const UpperRight: Direction = { x: 2, y: 0 }
|
|
62
|
+
export const UpperLeft: Direction = { x: 0, y: 0 }
|
|
63
|
+
export const LowerRight: Direction = { x: 2, y: 2 }
|
|
64
|
+
export const LowerLeft: Direction = { x: 0, y: 2 }
|
|
65
|
+
export const Middle: Direction = { x: 1, y: 1 }
|
|
66
|
+
|
|
67
|
+
/** All named directions for iteration. */
|
|
68
|
+
export const ALL_DIRECTIONS: readonly Direction[] = [
|
|
69
|
+
Up,
|
|
70
|
+
Down,
|
|
71
|
+
Left,
|
|
72
|
+
Right,
|
|
73
|
+
UpperRight,
|
|
74
|
+
UpperLeft,
|
|
75
|
+
LowerRight,
|
|
76
|
+
LowerLeft,
|
|
77
|
+
Middle,
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
/** Compare directions by value (not reference). */
|
|
81
|
+
export function dirEquals(a: Direction, b: Direction): boolean {
|
|
82
|
+
return a.x === b.x && a.y === b.y
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
declare const cardinalDirectionBrand: unique symbol
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The 4 pure cardinal directions — Up/Down/Left/Right — the only Direction
|
|
89
|
+
* values pathfinder.ts's routeEdge/tryDirectPath know how to turn into a
|
|
90
|
+
* single L-shaped route (horizontal-first for Left/Right, vertical-first
|
|
91
|
+
* for Up/Down). The other 5 Direction values (the four diagonals, plus
|
|
92
|
+
* Middle) don't correspond to one routing axis. Direction can't
|
|
93
|
+
* structurally distinguish these on its own — all 9 constants share the
|
|
94
|
+
* same {x, y} shape — so this is a nominal brand: requireCardinalDirection
|
|
95
|
+
* below is the only way to produce one, and it checks the actual value
|
|
96
|
+
* rather than trusting a caller's claim.
|
|
97
|
+
*/
|
|
98
|
+
export type CardinalDirection = Direction & {
|
|
99
|
+
readonly [cardinalDirectionBrand]: true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isCardinalDirection(d: Direction): d is CardinalDirection {
|
|
103
|
+
return (
|
|
104
|
+
dirEquals(d, Up) ||
|
|
105
|
+
dirEquals(d, Down) ||
|
|
106
|
+
dirEquals(d, Left) ||
|
|
107
|
+
dirEquals(d, Right)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Narrow a Direction into a CardinalDirection, throwing if it isn't one of
|
|
113
|
+
* Up/Down/Left/Right. Mirrors requireGridCoord/requirePathBudget elsewhere
|
|
114
|
+
* in this module family: the invariant that routeEdge only ever receives a
|
|
115
|
+
* cardinal direction lives in caller logic spread across edge-routing.ts
|
|
116
|
+
* and edge-bundling.ts, not in Direction's type, so it's checked explicitly
|
|
117
|
+
* at the boundary rather than trusted silently across modules.
|
|
118
|
+
*/
|
|
119
|
+
export function requireCardinalDirection(d: Direction): CardinalDirection {
|
|
120
|
+
if (!isCardinalDirection(d)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Expected a cardinal direction (Up/Down/Left/Right); got {x:${d.x}, y:${d.y}}`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return d
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 2D text canvas — column-major (canvas[x][y]).
|
|
130
|
+
* Each cell holds a single character (or space).
|
|
131
|
+
*/
|
|
132
|
+
export type Canvas = string[][]
|
|
133
|
+
|
|
134
|
+
/** A node in the ASCII graph, positioned on the grid. */
|
|
135
|
+
export interface AsciiNode {
|
|
136
|
+
/** Unique identity key — the original node ID from the parser (e.g. "A", "B"). */
|
|
137
|
+
name: string
|
|
138
|
+
/** Human-readable label for rendering inside the box (e.g. "Web Server"). */
|
|
139
|
+
displayLabel: string
|
|
140
|
+
/** Node shape from the parser (e.g. "rectangle", "diamond", "circle"). */
|
|
141
|
+
shape: AsciiNodeShape
|
|
142
|
+
index: number
|
|
143
|
+
gridCoord: GridCoord | null
|
|
144
|
+
drawingCoord: DrawingCoord | null
|
|
145
|
+
drawing: Canvas | null
|
|
146
|
+
drawn: boolean
|
|
147
|
+
styleClassName: string
|
|
148
|
+
styleClass: AsciiStyleClass
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Style class for colored node text (ported from Go's classDef). */
|
|
152
|
+
export interface AsciiStyleClass {
|
|
153
|
+
name: string
|
|
154
|
+
styles: Record<string, string>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Edge line style for ASCII rendering. */
|
|
158
|
+
export type AsciiEdgeStyle = 'solid' | 'dotted' | 'thick' | 'invisible'
|
|
159
|
+
|
|
160
|
+
/** An edge in the ASCII graph, with a routed path. */
|
|
161
|
+
export interface AsciiEdge {
|
|
162
|
+
from: AsciiNode
|
|
163
|
+
to: AsciiNode
|
|
164
|
+
text: string
|
|
165
|
+
path: GridCoord[]
|
|
166
|
+
labelLine: GridCoord[]
|
|
167
|
+
startDir: Direction
|
|
168
|
+
endDir: Direction
|
|
169
|
+
/** Line style: solid (default), dotted (-.->) or thick (==>) */
|
|
170
|
+
style: AsciiEdgeStyle
|
|
171
|
+
/** Whether to render an arrowhead at the start (source end) of the edge */
|
|
172
|
+
hasArrowStart: boolean
|
|
173
|
+
/** Whether to render an arrowhead at the end (target end) of the edge */
|
|
174
|
+
hasArrowEnd: boolean
|
|
175
|
+
/**
|
|
176
|
+
* Terminator shape at the source end when it's a circle/cross marker
|
|
177
|
+
* (`o--`/`x--`) rather than a plain arrowhead. Undefined draws the
|
|
178
|
+
* regular arrowhead glyph (per `hasArrowStart`). See issue #330.
|
|
179
|
+
*/
|
|
180
|
+
startMarker?: 'circle' | 'cross'
|
|
181
|
+
/** Terminator shape at the target end (`--o`/`--x`). See `startMarker`. */
|
|
182
|
+
endMarker?: 'circle' | 'cross'
|
|
183
|
+
/** Bundle this edge belongs to (if any). Set during bundling analysis. */
|
|
184
|
+
bundle?: EdgeBundle
|
|
185
|
+
/**
|
|
186
|
+
* For bundled edges: path from source/target to the junction point.
|
|
187
|
+
* The full visual path is: pathToJunction + bundle.sharedPath (for fan-in)
|
|
188
|
+
* or bundle.sharedPath + pathToJunction (for fan-out).
|
|
189
|
+
*/
|
|
190
|
+
pathToJunction?: GridCoord[]
|
|
191
|
+
/**
|
|
192
|
+
* Set when this edge connects the same *pair* of nodes as one or more
|
|
193
|
+
* sibling edges — either in the same direction (`A -->|One| B` and
|
|
194
|
+
* `A -->|Two| B`) or, for a pair laid out side by side, in opposite
|
|
195
|
+
* directions (`A -->|req| B` and `B -->|res| A`). See
|
|
196
|
+
* `parallelGroupKey` in edge-routing.ts for the exact rule. This is as
|
|
197
|
+
* opposed to edges that merely share one endpoint, which `bundle` above
|
|
198
|
+
* already handles via fan-in/fan-out junctions.
|
|
199
|
+
*
|
|
200
|
+
* `index` is this edge's 0-based position within the group (declaration
|
|
201
|
+
* order); `total` is the group's size. `index === 0` keeps the ordinary
|
|
202
|
+
* single-edge route (determinePath's ordinary preferred/alternative
|
|
203
|
+
* search), so a graph with no parallel edges renders identically to
|
|
204
|
+
* before this field existed. `index > 0` routes through an offset lane
|
|
205
|
+
* instead — see determinePath in edge-routing.ts — so sibling edges never
|
|
206
|
+
* compute the identical path (and therefore identically-positioned,
|
|
207
|
+
* mutually-corrupting labels; see #329 and #629).
|
|
208
|
+
*
|
|
209
|
+
* `usedOffsets` is the *same* Set object, by reference, on every edge in
|
|
210
|
+
* the group (assigned once in assignParallelEdgeLanes) — the lane-offset
|
|
211
|
+
* search in buildParallelLanePath can land two different lane indices on
|
|
212
|
+
* the same actual offset when both have to detour around the same
|
|
213
|
+
* obstacle (see that function's doc), so each successful search records
|
|
214
|
+
* its chosen offset here and skips any offset a sibling already claimed,
|
|
215
|
+
* keeping every lane in the group on a genuinely distinct path.
|
|
216
|
+
*/
|
|
217
|
+
parallelLane?: { index: number; total: number; usedOffsets: Set<number> }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A subgraph container with bounding box for rendering. */
|
|
221
|
+
export interface AsciiSubgraph {
|
|
222
|
+
name: string
|
|
223
|
+
nodes: AsciiNode[]
|
|
224
|
+
parent: AsciiSubgraph | null
|
|
225
|
+
children: AsciiSubgraph[]
|
|
226
|
+
minX: number
|
|
227
|
+
minY: number
|
|
228
|
+
maxX: number
|
|
229
|
+
maxY: number
|
|
230
|
+
/** Optional direction override for layout within this subgraph (LR or TD). */
|
|
231
|
+
direction?: 'LR' | 'TD'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Padding defaults
|
|
236
|
+
//
|
|
237
|
+
// Shared by every renderer that reads AsciiConfig's padding fields. Layouts
|
|
238
|
+
// that predate `paddingX`/`paddingY`/`boxBorderPadding` becoming configurable
|
|
239
|
+
// (sequence, class, ER — see issue #343) compute their own spacing as an
|
|
240
|
+
// offset from these defaults rather than substituting the raw config value
|
|
241
|
+
// directly, so that *not* passing a padding option still renders exactly as
|
|
242
|
+
// it always has (no baseline/snapshot churn) while explicitly passing one
|
|
243
|
+
// still visibly changes spacing. The flowchart/state grid layout (grid.ts)
|
|
244
|
+
// predates this convention and uses `paddingX`/`paddingY` directly as its
|
|
245
|
+
// column/row gap — don't "fix" that to go through these constants too, its
|
|
246
|
+
// existing default already equals them, so behavior is identical.
|
|
247
|
+
// ============================================================================
|
|
248
|
+
|
|
249
|
+
/** `AsciiConfig.paddingX`'s default — see the block comment above. */
|
|
250
|
+
export const DEFAULT_PADDING_X = 5
|
|
251
|
+
/** `AsciiConfig.paddingY`'s default — see the block comment above. */
|
|
252
|
+
export const DEFAULT_PADDING_Y = 5
|
|
253
|
+
/** `AsciiConfig.boxBorderPadding`'s default — see the block comment above. */
|
|
254
|
+
export const DEFAULT_BOX_BORDER_PADDING = 1
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Derive a diagram-local spacing constant from a padding option, offset from
|
|
258
|
+
* that option's default so a diagram whose own historical constant differs
|
|
259
|
+
* from the shared default (e.g. class diagrams' 4-column gap vs. the shared
|
|
260
|
+
* default of 5) still renders unchanged when the caller passes no explicit
|
|
261
|
+
* padding override. `-x`/`-y`/`-p` still visibly widen or tighten spacing
|
|
262
|
+
* because they shift `configValue` away from `defaultValue`.
|
|
263
|
+
*
|
|
264
|
+
* Shared by sequence.ts/class-diagram.ts/er-diagram.ts so the three
|
|
265
|
+
* renderers wired up for issue #343 apply padding the same way instead of
|
|
266
|
+
* each re-deriving this arithmetic.
|
|
267
|
+
*
|
|
268
|
+
* @param floor - Minimum result, so a large negative padding can't collapse
|
|
269
|
+
* the spacing to zero or negative (which would overlap adjacent elements).
|
|
270
|
+
*/
|
|
271
|
+
export function paddingOffset(
|
|
272
|
+
configValue: number,
|
|
273
|
+
defaultValue: number,
|
|
274
|
+
base: number,
|
|
275
|
+
floor: number,
|
|
276
|
+
): number {
|
|
277
|
+
return Math.max(floor, base + (configValue - defaultValue))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Configuration for ASCII rendering. */
|
|
281
|
+
export interface AsciiConfig {
|
|
282
|
+
/** true = ASCII chars (+,-,|), false = Unicode box-drawing (┌,─,│). Default: false */
|
|
283
|
+
useAscii: boolean
|
|
284
|
+
/** Horizontal spacing between nodes. Default: 5 */
|
|
285
|
+
paddingX: number
|
|
286
|
+
/** Vertical spacing between nodes. Default: 5 */
|
|
287
|
+
paddingY: number
|
|
288
|
+
/** Padding inside node boxes. Default: 1 */
|
|
289
|
+
boxBorderPadding: number
|
|
290
|
+
/** Graph direction: "LR" or "TD". */
|
|
291
|
+
graphDirection: 'LR' | 'TD'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Full ASCII graph state used during layout and rendering. */
|
|
295
|
+
export interface AsciiGraph {
|
|
296
|
+
nodes: AsciiNode[]
|
|
297
|
+
edges: AsciiEdge[]
|
|
298
|
+
canvas: Canvas
|
|
299
|
+
/** Role canvas — tracks the role of each character for colored output. */
|
|
300
|
+
roleCanvas: RoleCanvas
|
|
301
|
+
/** Grid occupancy map — tracks which "x,y" cells are reserved. */
|
|
302
|
+
grid: Grid
|
|
303
|
+
columnWidth: Map<number, number>
|
|
304
|
+
rowHeight: Map<number, number>
|
|
305
|
+
subgraphs: AsciiSubgraph[]
|
|
306
|
+
config: AsciiConfig
|
|
307
|
+
/** Offset applied to all drawing coords to accommodate subgraph borders. */
|
|
308
|
+
offsetX: number
|
|
309
|
+
offsetY: number
|
|
310
|
+
/** Edge bundles for parallel link visualization. Set during bundling analysis. */
|
|
311
|
+
bundles: EdgeBundle[]
|
|
312
|
+
/**
|
|
313
|
+
* Render-wide A* iteration budget shared across every getPath call made
|
|
314
|
+
* while routing this graph's edges. Bounds total pathfinding work (and
|
|
315
|
+
* therefore memory) for graphs with many edges, independent of any
|
|
316
|
+
* single call's own iteration cap. Set at the start of createMapping's
|
|
317
|
+
* edge-routing phase; undefined before then. See pathfinder.ts's
|
|
318
|
+
* PathBudget for details.
|
|
319
|
+
*/
|
|
320
|
+
pathBudget?: PathBudget
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Coordinate helpers
|
|
325
|
+
// ============================================================================
|
|
326
|
+
|
|
327
|
+
export function gridCoordEquals(a: GridCoord, b: GridCoord): boolean {
|
|
328
|
+
return a.x === b.x && a.y === b.y
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function drawingCoordEquals(a: DrawingCoord, b: DrawingCoord): boolean {
|
|
332
|
+
return a.x === b.x && a.y === b.y
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Apply a direction offset to a grid coordinate (move into the 3x3 block). */
|
|
336
|
+
export function gridCoordDirection(c: GridCoord, dir: Direction): GridCoord {
|
|
337
|
+
return { x: c.x + dir.x, y: c.y + dir.y }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Key for storing GridCoord in a Map. */
|
|
341
|
+
export function gridKey(c: GridCoord): string {
|
|
342
|
+
return `${c.x},${c.y}`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get a node's grid coordinate. Every consumer of a node's `gridCoord` after
|
|
347
|
+
* layout (edge routing, edge bundling, drawing) runs strictly after
|
|
348
|
+
* `createMapping` (grid.ts) has placed every node, so this is always defined
|
|
349
|
+
* for a real graph — but that guarantee lives in layout's control flow, not
|
|
350
|
+
* in `AsciiNode`'s type (`gridCoord: GridCoord | null`), so it's narrowed
|
|
351
|
+
* here explicitly rather than trusted silently across the module boundary.
|
|
352
|
+
*
|
|
353
|
+
* Lives here (not in grid.ts, where it originated) because it's a pure
|
|
354
|
+
* predicate over `AsciiNode` with zero dependency on grid/layout state —
|
|
355
|
+
* keeping it here lets `draw-boxes.ts` and `draw-bundles.ts` reuse it
|
|
356
|
+
* without pulling in all of grid.ts's transitive imports.
|
|
357
|
+
*/
|
|
358
|
+
export function requireGridCoord(node: AsciiNode): GridCoord {
|
|
359
|
+
const gc = node.gridCoord
|
|
360
|
+
if (gc === null) {
|
|
361
|
+
/* v8 ignore next */
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Node "${node.name}" has no gridCoord; grid layout must run before it is read`,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
return gc
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Default empty style class. */
|
|
370
|
+
export const EMPTY_STYLE: AsciiStyleClass = { name: '', styles: {} }
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// Character role types for colored output
|
|
374
|
+
// ============================================================================
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Role of a character in the ASCII diagram, used for theming.
|
|
378
|
+
* Each role maps to a different color when colors are enabled.
|
|
379
|
+
*/
|
|
380
|
+
export type CharRole =
|
|
381
|
+
| 'text' // Node labels, edge labels
|
|
382
|
+
| 'border' // Node box borders, subgraph borders
|
|
383
|
+
| 'line' // Edge lines (paths between nodes)
|
|
384
|
+
| 'arrow' // Arrowheads (▲▼◄► or ^v<>)
|
|
385
|
+
| 'corner' // Corner characters at path bends
|
|
386
|
+
| 'junction' // Junction characters (┬┴├┤ where edges meet boxes)
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Role canvas — parallel to Canvas, tracks the role of each character.
|
|
390
|
+
* Same column-major structure: roleCanvas[x][y] gives the role at (x, y).
|
|
391
|
+
* null means the character has no role (whitespace).
|
|
392
|
+
*/
|
|
393
|
+
export type RoleCanvas = (CharRole | null)[][]
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Theme colors for ASCII output — hex color strings.
|
|
397
|
+
* Derived from the SVG theme system for visual consistency.
|
|
398
|
+
*/
|
|
399
|
+
export interface AsciiTheme {
|
|
400
|
+
/** Text color (node labels, edge labels) */
|
|
401
|
+
fg: string
|
|
402
|
+
/** Box border color (node borders, subgraph borders) */
|
|
403
|
+
border: string
|
|
404
|
+
/** Edge line color (paths between nodes) */
|
|
405
|
+
line: string
|
|
406
|
+
/** Arrowhead color (▲▼◄► or ^v<>) */
|
|
407
|
+
arrow: string
|
|
408
|
+
/** Theme accent color (optional, used by xycharts for series 0) */
|
|
409
|
+
accent?: string
|
|
410
|
+
/** Background color (optional, used by xycharts for dark-mode-aware shading) */
|
|
411
|
+
bg?: string
|
|
412
|
+
/** Corner character color (optional, defaults to line) */
|
|
413
|
+
corner?: string
|
|
414
|
+
/** Junction character color (optional, defaults to border) */
|
|
415
|
+
junction?: string
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Color mode for output. */
|
|
419
|
+
export type ColorMode =
|
|
420
|
+
| 'none' // No colors (plain text)
|
|
421
|
+
| 'ansi16' // 16-color ANSI (basic terminals)
|
|
422
|
+
| 'ansi256' // 256-color ANSI (xterm)
|
|
423
|
+
| 'truecolor' // 24-bit RGB (modern terminals)
|
|
424
|
+
| 'html' // HTML <span> tags with inline color styles (browsers)
|
|
425
|
+
|
|
426
|
+
// ============================================================================
|
|
427
|
+
// Edge bundling types
|
|
428
|
+
// ============================================================================
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Edge bundle — groups edges that share a common source or target.
|
|
432
|
+
* Used to visually merge parallel links before they reach the shared node.
|
|
433
|
+
*
|
|
434
|
+
* For fan-in (A & B --> C): multiple sources converge to one target.
|
|
435
|
+
* For fan-out (A --> B & C): one source diverges to multiple targets.
|
|
436
|
+
*/
|
|
437
|
+
export interface EdgeBundle {
|
|
438
|
+
/** Bundle type: fan-in = many→one, fan-out = one→many */
|
|
439
|
+
type: 'fan-in' | 'fan-out'
|
|
440
|
+
/** Edges in this bundle */
|
|
441
|
+
edges: AsciiEdge[]
|
|
442
|
+
/** The common node (target for fan-in, source for fan-out) */
|
|
443
|
+
sharedNode: AsciiNode
|
|
444
|
+
/** The non-shared nodes (sources for fan-in, targets for fan-out) */
|
|
445
|
+
otherNodes: AsciiNode[]
|
|
446
|
+
/** Junction point where edges merge/split — set during routing */
|
|
447
|
+
junctionPoint: GridCoord | null
|
|
448
|
+
/** Path from junction to shared node (drawn once for all edges) */
|
|
449
|
+
sharedPath: GridCoord[]
|
|
450
|
+
/** Direction when entering/exiting the junction */
|
|
451
|
+
junctionDir: Direction
|
|
452
|
+
/** Direction when entering/exiting the shared node */
|
|
453
|
+
sharedNodeDir: Direction
|
|
454
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Rendering Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides validation functions for ASCII diagram output,
|
|
5
|
+
* including diagonal line detection to ensure orthogonal-only routing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Characters that represent diagonal lines in ASCII and Unicode modes.
|
|
10
|
+
* These should never appear in properly rendered diagrams.
|
|
11
|
+
*/
|
|
12
|
+
export const DIAGONAL_CHARS = {
|
|
13
|
+
ascii: ['/', '\\'],
|
|
14
|
+
unicode: ['\u2571', '\u2572'], // ╱ ╲
|
|
15
|
+
all: ['/', '\\', '\u2571', '\u2572'],
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Position of a diagonal character in ASCII output.
|
|
20
|
+
*/
|
|
21
|
+
export interface DiagonalPosition {
|
|
22
|
+
line: number
|
|
23
|
+
col: number
|
|
24
|
+
char: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type guard narrowing a single character to one of `DIAGONAL_CHARS.all`.
|
|
29
|
+
*/
|
|
30
|
+
function isDiagonalChar(
|
|
31
|
+
char: string,
|
|
32
|
+
): char is (typeof DIAGONAL_CHARS.all)[number] {
|
|
33
|
+
return (DIAGONAL_CHARS.all as readonly string[]).includes(char)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if ASCII output contains any diagonal line characters.
|
|
38
|
+
* Returns true if diagonals are found (which is an error condition).
|
|
39
|
+
*
|
|
40
|
+
* @param asciiOutput - The rendered ASCII diagram string
|
|
41
|
+
* @returns true if diagonal characters are present, false otherwise
|
|
42
|
+
*/
|
|
43
|
+
export function hasDiagonalLines(asciiOutput: string): boolean {
|
|
44
|
+
return DIAGONAL_CHARS.all.some((char) => asciiOutput.includes(char))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find all diagonal line character positions in ASCII output.
|
|
49
|
+
* Useful for debugging when diagonals are detected.
|
|
50
|
+
*
|
|
51
|
+
* Skips diagonal characters that appear inside node labels (between box borders).
|
|
52
|
+
* This prevents false positives from labels like "feature/auth" or "release/1.0".
|
|
53
|
+
*
|
|
54
|
+
* @param asciiOutput - The rendered ASCII diagram string
|
|
55
|
+
* @returns Array of positions where diagonal characters were found
|
|
56
|
+
*/
|
|
57
|
+
export function findDiagonalLines(asciiOutput: string): DiagonalPosition[] {
|
|
58
|
+
const positions: DiagonalPosition[] = []
|
|
59
|
+
const lines = asciiOutput.split('\n')
|
|
60
|
+
|
|
61
|
+
// Box-drawing characters that indicate node boundaries
|
|
62
|
+
const boxBorders = new Set(['│', '┤', '├', '║', '┃', '|'])
|
|
63
|
+
|
|
64
|
+
for (const [lineNum, line] of lines.entries()) {
|
|
65
|
+
// Find all box border positions in this line
|
|
66
|
+
const borderPositions: number[] = []
|
|
67
|
+
for (let col = 0; col < line.length; col++) {
|
|
68
|
+
if (boxBorders.has(line[col]!)) {
|
|
69
|
+
borderPositions.push(col)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let col = 0; col < line.length; col++) {
|
|
74
|
+
const char = line[col]!
|
|
75
|
+
if (isDiagonalChar(char)) {
|
|
76
|
+
// Check if this position is inside a node (between two box borders)
|
|
77
|
+
// Find the nearest borders before and after this position
|
|
78
|
+
let insideNode = false
|
|
79
|
+
for (let i = 0; i < borderPositions.length - 1; i++) {
|
|
80
|
+
const leftBorder = borderPositions[i]!
|
|
81
|
+
const rightBorder = borderPositions[i + 1]!
|
|
82
|
+
if (col > leftBorder && col < rightBorder) {
|
|
83
|
+
// This diagonal char is between two borders - likely inside a node label
|
|
84
|
+
insideNode = true
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!insideNode) {
|
|
90
|
+
positions.push({
|
|
91
|
+
line: lineNum + 1, // 1-indexed for human readability
|
|
92
|
+
col: col + 1,
|
|
93
|
+
char,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return positions
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Assert that ASCII output contains no diagonal lines.
|
|
105
|
+
* Throws an error with detailed position information if diagonals are found.
|
|
106
|
+
*
|
|
107
|
+
* @param asciiOutput - The rendered ASCII diagram string
|
|
108
|
+
* @param context - Optional context string for error message (e.g., diagram name)
|
|
109
|
+
* @throws Error if diagonal characters are present
|
|
110
|
+
*/
|
|
111
|
+
export function assertNoDiagonals(asciiOutput: string, context?: string): void {
|
|
112
|
+
if (!hasDiagonalLines(asciiOutput)) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const positions = findDiagonalLines(asciiOutput)
|
|
117
|
+
const contextStr = context ? ` in "${context}"` : ''
|
|
118
|
+
const positionStr = positions
|
|
119
|
+
.map((p) => ` Line ${p.line}, Col ${p.col}: '${p.char}'`)
|
|
120
|
+
.join('\n')
|
|
121
|
+
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Diagonal lines detected${contextStr}. ` +
|
|
124
|
+
`Edges must use orthogonal Manhattan routing (90° bends only).\n` +
|
|
125
|
+
`Found ${positions.length} diagonal character(s):\n${positionStr}`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Position of an orphaned tee/junction character in ASCII output.
|
|
131
|
+
*/
|
|
132
|
+
export interface OrphanedJunctionPosition {
|
|
133
|
+
line: number
|
|
134
|
+
col: number
|
|
135
|
+
char: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Find "orphaned" tee/junction characters — a ├/┤/┬/┴ whose perpendicular
|
|
140
|
+
* arm (the part of the glyph that's supposed to come from a pre-existing
|
|
141
|
+
* border or line, not from the edge that merged into it) has nothing on
|
|
142
|
+
* either side. A genuine junction always has *something* immediately on
|
|
143
|
+
* both sides of that arm (a border character, another line, an arrowhead,
|
|
144
|
+
* etc.); a blank cell on both sides means the character was written as a
|
|
145
|
+
* junction without a real line to justify it (see issue #86).
|
|
146
|
+
*
|
|
147
|
+
* - ├ / ┤ carry a vertical arm, so both sides means the row above and below.
|
|
148
|
+
* - ┬ / ┴ carry a horizontal arm, so both sides means the column left and right.
|
|
149
|
+
*
|
|
150
|
+
* @param asciiOutput - The rendered ASCII diagram string
|
|
151
|
+
* @returns Array of positions where an orphaned junction was found
|
|
152
|
+
*/
|
|
153
|
+
export function findOrphanedJunctions(
|
|
154
|
+
asciiOutput: string,
|
|
155
|
+
): OrphanedJunctionPosition[] {
|
|
156
|
+
const lines = asciiOutput.split('\n')
|
|
157
|
+
const positions: OrphanedJunctionPosition[] = []
|
|
158
|
+
|
|
159
|
+
for (const [lineNum, line] of lines.entries()) {
|
|
160
|
+
for (let col = 0; col < line.length; col++) {
|
|
161
|
+
const char = line[col]!
|
|
162
|
+
if (char === '├' || char === '┤') {
|
|
163
|
+
const above = lines[lineNum - 1]?.[col] ?? ' '
|
|
164
|
+
const below = lines[lineNum + 1]?.[col] ?? ' '
|
|
165
|
+
if (above === ' ' && below === ' ') {
|
|
166
|
+
positions.push({ line: lineNum + 1, col: col + 1, char })
|
|
167
|
+
}
|
|
168
|
+
} else if (char === '┬' || char === '┴') {
|
|
169
|
+
const left = line[col - 1] ?? ' '
|
|
170
|
+
const right = line[col + 1] ?? ' '
|
|
171
|
+
if (left === ' ' && right === ' ') {
|
|
172
|
+
positions.push({ line: lineNum + 1, col: col + 1, char })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return positions
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if ASCII output contains any orphaned tee/junction characters.
|
|
183
|
+
* Returns true if any are found (which is an error condition).
|
|
184
|
+
*
|
|
185
|
+
* @param asciiOutput - The rendered ASCII diagram string
|
|
186
|
+
*/
|
|
187
|
+
export function hasOrphanedJunctions(asciiOutput: string): boolean {
|
|
188
|
+
return findOrphanedJunctions(asciiOutput).length > 0
|
|
189
|
+
}
|