@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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { allocateTerritory } from '../territory.ts'
|
|
3
|
+
import type { TerritoryGeometry } from '../territory.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Test items carry their own geometry so the accessor stays trivial and the
|
|
7
|
+
* cases below read as pure geometry — which is the whole point of
|
|
8
|
+
* `allocateTerritory` being storage-agnostic (issue #618).
|
|
9
|
+
*/
|
|
10
|
+
interface Item extends TerritoryGeometry {
|
|
11
|
+
name: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const item = (
|
|
15
|
+
name: string,
|
|
16
|
+
idealMid: number,
|
|
17
|
+
start: number,
|
|
18
|
+
end: number,
|
|
19
|
+
rowStart = 0,
|
|
20
|
+
rowEnd = 0,
|
|
21
|
+
): Item => ({ name, idealMid, start, end, rowStart, rowEnd })
|
|
22
|
+
|
|
23
|
+
const geom = (i: Item): TerritoryGeometry => i
|
|
24
|
+
|
|
25
|
+
const allocate = (items: readonly Item[]) => allocateTerritory(items, geom)
|
|
26
|
+
|
|
27
|
+
/** `{ left, right }` for `name`, for terser assertions. */
|
|
28
|
+
const bounds = (
|
|
29
|
+
items: readonly Item[],
|
|
30
|
+
name: string,
|
|
31
|
+
): { left: number; right: number } => {
|
|
32
|
+
const target = items.find((i) => i.name === name)
|
|
33
|
+
expect(target).toBeDefined()
|
|
34
|
+
if (!target) throw new Error('unreachable')
|
|
35
|
+
const t = allocate(items).get(target)
|
|
36
|
+
expect(t).toBeDefined()
|
|
37
|
+
if (!t) throw new Error('unreachable')
|
|
38
|
+
return t
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('allocateTerritory', () => {
|
|
42
|
+
it('returns an empty map for no items', () => {
|
|
43
|
+
expect(allocate([]).size).toBe(0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('leaves a lone item completely unconstrained', () => {
|
|
47
|
+
const items = [item('only', 10, 5, 15)]
|
|
48
|
+
expect(bounds(items, 'only')).toEqual({ left: -Infinity, right: Infinity })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('has an entry for every item', () => {
|
|
52
|
+
const items = [
|
|
53
|
+
item('a', 0, 0, 10),
|
|
54
|
+
item('b', 5, 1, 9),
|
|
55
|
+
item('c', 50, 45, 55),
|
|
56
|
+
]
|
|
57
|
+
const result = allocate(items)
|
|
58
|
+
expect(result.size).toBe(3)
|
|
59
|
+
for (const i of items) expect(result.has(i)).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves items whose natural spans do not overlap unconstrained', () => {
|
|
63
|
+
// Same row, but the spans are disjoint — no contest, so no split.
|
|
64
|
+
const items = [item('left', 5, 0, 9), item('right', 25, 20, 29)]
|
|
65
|
+
expect(bounds(items, 'left')).toEqual({ left: -Infinity, right: Infinity })
|
|
66
|
+
expect(bounds(items, 'right')).toEqual({ left: -Infinity, right: Infinity })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('splits contested space at the midpoint of two overlapping items', () => {
|
|
70
|
+
// Spans 0..14 and 5..19 overlap; midpoint of idealMid 7 and 12 is 9.
|
|
71
|
+
const items = [item('a', 7, 0, 14), item('b', 12, 5, 19)]
|
|
72
|
+
expect(bounds(items, 'a')).toEqual({ left: -Infinity, right: 9 })
|
|
73
|
+
expect(bounds(items, 'b')).toEqual({ left: 10, right: Infinity })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('gives each side of the split disjoint, adjacent bounds', () => {
|
|
77
|
+
// The left item's `right` and the right item's `left` must differ by
|
|
78
|
+
// exactly 1 (the `+1`), so the two territories touch without overlapping
|
|
79
|
+
// — an off-by-one either way would double-book or waste a column.
|
|
80
|
+
for (const [midA, midB] of [
|
|
81
|
+
[7, 12],
|
|
82
|
+
[7, 13],
|
|
83
|
+
[0, 1],
|
|
84
|
+
[100, 101],
|
|
85
|
+
]) {
|
|
86
|
+
const items = [
|
|
87
|
+
item('a', midA ?? 0, -50, 50),
|
|
88
|
+
item('b', midB ?? 0, -50, 50),
|
|
89
|
+
]
|
|
90
|
+
const a = bounds(items, 'a')
|
|
91
|
+
const b = bounds(items, 'b')
|
|
92
|
+
expect(b.left).toBe(a.right + 1)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not split items whose rows do not overlap', () => {
|
|
97
|
+
// Identical spans, but drawn on rows that never intersect.
|
|
98
|
+
const items = [
|
|
99
|
+
item('top', 10, 0, 20, 0, 0),
|
|
100
|
+
item('bottom', 12, 0, 20, 5, 5),
|
|
101
|
+
]
|
|
102
|
+
expect(bounds(items, 'top')).toEqual({ left: -Infinity, right: Infinity })
|
|
103
|
+
expect(bounds(items, 'bottom')).toEqual({
|
|
104
|
+
left: -Infinity,
|
|
105
|
+
right: Infinity,
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('treats rows as inclusive, so a single shared row counts as overlap', () => {
|
|
110
|
+
const items = [item('a', 10, 0, 20, 0, 3), item('b', 14, 0, 20, 3, 6)]
|
|
111
|
+
expect(bounds(items, 'a').right).toBe(12)
|
|
112
|
+
expect(bounds(items, 'b').left).toBe(13)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('skips a non-colliding neighbour to reach the real competitor (issue #531)', () => {
|
|
116
|
+
// `mid` sits between `a` and `c` by idealMid but on a different row, so
|
|
117
|
+
// stopping at the immediate neighbour would leave a and c unsplit — the
|
|
118
|
+
// exact regression #531 fixed.
|
|
119
|
+
const items = [
|
|
120
|
+
item('a', 10, 0, 30, 0, 0),
|
|
121
|
+
item('mid', 20, 0, 30, 9, 9),
|
|
122
|
+
item('c', 30, 0, 30, 0, 0),
|
|
123
|
+
]
|
|
124
|
+
expect(bounds(items, 'a')).toEqual({ left: -Infinity, right: 20 })
|
|
125
|
+
expect(bounds(items, 'c')).toEqual({ left: 21, right: Infinity })
|
|
126
|
+
// The interloper collides with neither, so it keeps the full width.
|
|
127
|
+
expect(bounds(items, 'mid')).toEqual({ left: -Infinity, right: Infinity })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('constrains a middle item from both sides', () => {
|
|
131
|
+
const items = [
|
|
132
|
+
item('a', 0, 0, 30, 0, 0),
|
|
133
|
+
item('b', 20, 0, 40, 0, 0),
|
|
134
|
+
item('c', 40, 20, 60, 0, 0),
|
|
135
|
+
]
|
|
136
|
+
expect(bounds(items, 'b')).toEqual({ left: 11, right: 30 })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('is independent of the order items are supplied in', () => {
|
|
140
|
+
const a = item('a', 7, 0, 14)
|
|
141
|
+
const b = item('b', 12, 5, 19)
|
|
142
|
+
const c = item('c', 30, 25, 39)
|
|
143
|
+
const forward = allocateTerritory([a, b, c], geom)
|
|
144
|
+
const shuffled = allocateTerritory([c, a, b], geom)
|
|
145
|
+
for (const i of [a, b, c]) {
|
|
146
|
+
expect(shuffled.get(i)).toEqual(forward.get(i))
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('handles negative midpoints without flipping the split', () => {
|
|
151
|
+
const items = [item('a', -12, -20, -5, 0, 0), item('b', -7, -14, 0, 0, 0)]
|
|
152
|
+
const a = bounds(items, 'a')
|
|
153
|
+
const b = bounds(items, 'b')
|
|
154
|
+
// Math.floor rounds toward -Infinity, so this pins the intended
|
|
155
|
+
// behaviour rather than a truncation-toward-zero variant.
|
|
156
|
+
expect(a.right).toBe(-10)
|
|
157
|
+
expect(b.left).toBe(-9)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('calls the geometry accessor exactly once per item, in input order', () => {
|
|
161
|
+
const items = [
|
|
162
|
+
item('c', 30, 25, 39),
|
|
163
|
+
item('a', 7, 0, 14),
|
|
164
|
+
item('b', 12, 5, 19),
|
|
165
|
+
]
|
|
166
|
+
const seen: string[] = []
|
|
167
|
+
allocateTerritory(items, (i) => {
|
|
168
|
+
seen.push(i.name)
|
|
169
|
+
return i
|
|
170
|
+
})
|
|
171
|
+
expect(seen).toEqual(['c', 'a', 'b'])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('keeps input order for items tying on idealMid (stable sort)', () => {
|
|
175
|
+
// Two items share an idealMid; `first` was supplied first, so it stays
|
|
176
|
+
// the left-hand side of the split. Reversing the input reverses which
|
|
177
|
+
// one wins — proving order is decided by the stable sort, not by chance.
|
|
178
|
+
const first = item('first', 10, 0, 20, 0, 0)
|
|
179
|
+
const second = item('second', 10, 0, 20, 0, 0)
|
|
180
|
+
const forward = allocateTerritory([first, second], geom)
|
|
181
|
+
expect(forward.get(first)).toEqual({ left: -Infinity, right: 10 })
|
|
182
|
+
expect(forward.get(second)).toEqual({ left: 11, right: Infinity })
|
|
183
|
+
|
|
184
|
+
const reversed = allocateTerritory([second, first], geom)
|
|
185
|
+
expect(reversed.get(second)).toEqual({ left: -Infinity, right: 10 })
|
|
186
|
+
expect(reversed.get(first)).toEqual({ left: 11, right: Infinity })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('splits every member of a dense same-row cluster', () => {
|
|
190
|
+
const items = [
|
|
191
|
+
item('a', 0, 0, 20, 0, 0),
|
|
192
|
+
item('b', 10, 0, 20, 0, 0),
|
|
193
|
+
item('c', 20, 0, 20, 0, 0),
|
|
194
|
+
item('d', 30, 10, 30, 0, 0),
|
|
195
|
+
]
|
|
196
|
+
const result = allocate(items)
|
|
197
|
+
for (const i of items) {
|
|
198
|
+
const t = result.get(i)
|
|
199
|
+
expect(t).toBeDefined()
|
|
200
|
+
}
|
|
201
|
+
expect(bounds(items, 'a')).toEqual({ left: -Infinity, right: 5 })
|
|
202
|
+
expect(bounds(items, 'b')).toEqual({ left: 6, right: 15 })
|
|
203
|
+
expect(bounds(items, 'c')).toEqual({ left: 16, right: 25 })
|
|
204
|
+
expect(bounds(items, 'd')).toEqual({ left: 26, right: Infinity })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('treats spans touching at a single column as overlapping', () => {
|
|
208
|
+
// a ends exactly where b starts — one shared column is still a contest.
|
|
209
|
+
const items = [item('a', 5, 0, 10, 0, 0), item('b', 15, 10, 20, 0, 0)]
|
|
210
|
+
expect(bounds(items, 'a').right).toBe(10)
|
|
211
|
+
expect(bounds(items, 'b').left).toBe(11)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('leaves spans that merely abut (no shared column) unconstrained', () => {
|
|
215
|
+
const items = [item('a', 5, 0, 9, 0, 0), item('b', 15, 10, 20, 0, 0)]
|
|
216
|
+
expect(bounds(items, 'a')).toEqual({ left: -Infinity, right: Infinity })
|
|
217
|
+
expect(bounds(items, 'b')).toEqual({ left: -Infinity, right: Infinity })
|
|
218
|
+
})
|
|
219
|
+
})
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/ascii/validate.ts — diagonal-line detection and
|
|
3
|
+
* orphaned-junction detection utilities used to sanity-check ASCII/Unicode
|
|
4
|
+
* diagram output.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
DIAGONAL_CHARS,
|
|
9
|
+
hasDiagonalLines,
|
|
10
|
+
findDiagonalLines,
|
|
11
|
+
assertNoDiagonals,
|
|
12
|
+
findOrphanedJunctions,
|
|
13
|
+
hasOrphanedJunctions,
|
|
14
|
+
} from '../validate.ts'
|
|
15
|
+
|
|
16
|
+
describe('DIAGONAL_CHARS', () => {
|
|
17
|
+
it('exposes ascii, unicode, and combined character sets', () => {
|
|
18
|
+
expect(DIAGONAL_CHARS.ascii).toEqual(['/', '\\'])
|
|
19
|
+
expect(DIAGONAL_CHARS.unicode).toEqual(['╱', '╲'])
|
|
20
|
+
expect(DIAGONAL_CHARS.all).toEqual(['/', '\\', '╱', '╲'])
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('hasDiagonalLines', () => {
|
|
25
|
+
it('returns true when an ascii diagonal is present', () => {
|
|
26
|
+
expect(hasDiagonalLines('A / B')).toBe(true)
|
|
27
|
+
expect(hasDiagonalLines('A \\ B')).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns true when a unicode diagonal is present', () => {
|
|
31
|
+
expect(hasDiagonalLines('A ╱ B')).toBe(true)
|
|
32
|
+
expect(hasDiagonalLines('A ╲ B')).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('returns false when no diagonal characters are present', () => {
|
|
36
|
+
expect(hasDiagonalLines('┌───┐\n│ A │\n└───┘')).toBe(false)
|
|
37
|
+
expect(hasDiagonalLines('')).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('findDiagonalLines', () => {
|
|
42
|
+
it('returns an empty array for output with no diagonals', () => {
|
|
43
|
+
expect(findDiagonalLines('┌───┐\n│ A │\n└───┘')).toEqual([])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('reports 1-indexed line/col positions for diagonals outside any node', () => {
|
|
47
|
+
const output = 'a / b'
|
|
48
|
+
expect(findDiagonalLines(output)).toEqual([{ line: 1, col: 3, char: '/' }])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('reports multiple diagonals across multiple lines', () => {
|
|
52
|
+
const output = 'a / b\nc \\ d'
|
|
53
|
+
expect(findDiagonalLines(output)).toEqual([
|
|
54
|
+
{ line: 1, col: 3, char: '/' },
|
|
55
|
+
{ line: 2, col: 3, char: '\\' },
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('skips diagonal characters inside a node label between box borders', () => {
|
|
60
|
+
const output = '│feature/auth│'
|
|
61
|
+
expect(findDiagonalLines(output)).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('still reports a diagonal that is outside the borders on a line that also has a label', () => {
|
|
65
|
+
const output = '│feature/auth│ / stray'
|
|
66
|
+
const found = findDiagonalLines(output)
|
|
67
|
+
expect(found).toHaveLength(1)
|
|
68
|
+
expect(found[0]).toMatchObject({ char: '/' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('recognizes all supported box-border characters when suppressing label diagonals', () => {
|
|
72
|
+
for (const border of ['│', '┤', '├', '║', '┃', '|']) {
|
|
73
|
+
const output = `${border}a/b${border}`
|
|
74
|
+
expect(findDiagonalLines(output)).toEqual([])
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('does not suppress a diagonal when only one border is present on the line', () => {
|
|
79
|
+
const output = '│a/b'
|
|
80
|
+
expect(findDiagonalLines(output)).toEqual([{ line: 1, col: 3, char: '/' }])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('detects unicode diagonal characters outside of labels', () => {
|
|
84
|
+
const output = 'x ╱ y ╲ z'
|
|
85
|
+
const found = findDiagonalLines(output)
|
|
86
|
+
expect(found).toEqual([
|
|
87
|
+
{ line: 1, col: 3, char: '╱' },
|
|
88
|
+
{ line: 1, col: 7, char: '╲' },
|
|
89
|
+
])
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('assertNoDiagonals', () => {
|
|
94
|
+
it('does not throw when there are no diagonals', () => {
|
|
95
|
+
expect(() => assertNoDiagonals('┌───┐\n│ A │\n└───┘')).not.toThrow()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('throws with position details when diagonals are found', () => {
|
|
99
|
+
expect(() => assertNoDiagonals('a / b')).toThrow(
|
|
100
|
+
/Diagonal lines detected\. .*Found 1 diagonal character\(s\):\n\s*Line 1, Col 3: '\/'/s,
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('includes the context string in the error message when provided', () => {
|
|
105
|
+
expect(() => assertNoDiagonals('a / b', 'my-diagram')).toThrow(
|
|
106
|
+
/Diagonal lines detected in "my-diagram"\./,
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('omits the context clause when no context is provided', () => {
|
|
111
|
+
try {
|
|
112
|
+
assertNoDiagonals('a / b')
|
|
113
|
+
throw new Error('expected assertNoDiagonals to throw')
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
116
|
+
expect(message).toContain('Diagonal lines detected. ')
|
|
117
|
+
expect(message).not.toContain(' in "')
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('findOrphanedJunctions', () => {
|
|
123
|
+
it('returns an empty array when there are no junction characters', () => {
|
|
124
|
+
expect(findOrphanedJunctions('┌───┐\n│ A │\n└───┘')).toEqual([])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('flags a ├ with blank cells directly above and below', () => {
|
|
128
|
+
const output = ' \n├──\n '
|
|
129
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
130
|
+
{ line: 2, col: 1, char: '├' },
|
|
131
|
+
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('flags a ┤ with blank cells directly above and below', () => {
|
|
135
|
+
const output = ' \n──┤\n '
|
|
136
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
137
|
+
{ line: 2, col: 3, char: '┤' },
|
|
138
|
+
])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('does not flag a ├ that has content above it', () => {
|
|
142
|
+
const output = '│ \n├──\n '
|
|
143
|
+
expect(findOrphanedJunctions(output)).toEqual([])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('does not flag a ├ that has content below it', () => {
|
|
147
|
+
const output = ' \n├──\n│ '
|
|
148
|
+
expect(findOrphanedJunctions(output)).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('treats a ├/┤ on the first line as having a blank row above (out of bounds)', () => {
|
|
152
|
+
const output = '├──\n '
|
|
153
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
154
|
+
{ line: 1, col: 1, char: '├' },
|
|
155
|
+
])
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('treats a ├/┤ on the last line as having a blank row below (out of bounds)', () => {
|
|
159
|
+
const output = ' \n├──'
|
|
160
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
161
|
+
{ line: 2, col: 1, char: '├' },
|
|
162
|
+
])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('flags a ┬ with blank cells directly left and right', () => {
|
|
166
|
+
const output = ' ┬ '
|
|
167
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
168
|
+
{ line: 1, col: 2, char: '┬' },
|
|
169
|
+
])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('flags a ┴ with blank cells directly left and right', () => {
|
|
173
|
+
const output = ' ┴ '
|
|
174
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
175
|
+
{ line: 1, col: 2, char: '┴' },
|
|
176
|
+
])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('does not flag a ┬ that has content to its left', () => {
|
|
180
|
+
const output = '─┬ '
|
|
181
|
+
expect(findOrphanedJunctions(output)).toEqual([])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('does not flag a ┴ that has content to its right', () => {
|
|
185
|
+
const output = ' ┴─'
|
|
186
|
+
expect(findOrphanedJunctions(output)).toEqual([])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('treats a ┬ at the start of a line as having a blank column to the left (out of bounds)', () => {
|
|
190
|
+
const output = '┬ '
|
|
191
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
192
|
+
{ line: 1, col: 1, char: '┬' },
|
|
193
|
+
])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('treats a ┴ at the end of a line as having a blank column to the right (out of bounds)', () => {
|
|
197
|
+
const output = ' ┴'
|
|
198
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
199
|
+
{ line: 1, col: 3, char: '┴' },
|
|
200
|
+
])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('finds multiple orphaned junctions across a multi-line diagram', () => {
|
|
204
|
+
const output = ' ┬ \n \n ┴ '
|
|
205
|
+
expect(findOrphanedJunctions(output)).toEqual([
|
|
206
|
+
{ line: 1, col: 2, char: '┬' },
|
|
207
|
+
{ line: 3, col: 2, char: '┴' },
|
|
208
|
+
])
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('hasOrphanedJunctions', () => {
|
|
213
|
+
it('returns false when there are no orphaned junctions', () => {
|
|
214
|
+
expect(hasOrphanedJunctions('┌───┐\n│ A │\n└───┘')).toBe(false)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('returns true when at least one orphaned junction is found', () => {
|
|
218
|
+
expect(hasOrphanedJunctions(' ┬ ')).toBe(true)
|
|
219
|
+
})
|
|
220
|
+
})
|