@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,40 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram crow's-foot cardinality marker tests
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
7
|
+
|
|
8
|
+
describe('ASCII ER crow-foot cardinality markers', () => {
|
|
9
|
+
it('mirrors the zero-one marker orientation on the left side (regression)', () => {
|
|
10
|
+
// Left-side markers sit at the left entity's right edge, pointing away
|
|
11
|
+
// from it (line nearest the entity, circle furthest): "|o". Before the
|
|
12
|
+
// fix, getCrowsFootChars() always returned "o|" regardless of side,
|
|
13
|
+
// which is only correct on the right side.
|
|
14
|
+
const ascii = renderMermaidASCII(
|
|
15
|
+
`erDiagram
|
|
16
|
+
WIDGET |o--|| GADGET : rel`,
|
|
17
|
+
{ useAscii: true },
|
|
18
|
+
)
|
|
19
|
+
expect(ascii).toContain('|o')
|
|
20
|
+
|
|
21
|
+
const unicode = renderMermaidASCII(`erDiagram
|
|
22
|
+
WIDGET |o--|| GADGET : rel`)
|
|
23
|
+
expect(unicode).toContain('│○')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('keeps the zero-one marker orientation on the right side', () => {
|
|
27
|
+
// Right-side markers sit at the right entity's left edge, pointing away
|
|
28
|
+
// from it: circle nearest the entity, line furthest: "o|".
|
|
29
|
+
const ascii = renderMermaidASCII(
|
|
30
|
+
`erDiagram
|
|
31
|
+
WIDGET ||--o| GADGET : rel`,
|
|
32
|
+
{ useAscii: true },
|
|
33
|
+
)
|
|
34
|
+
expect(ascii).toContain('o|')
|
|
35
|
+
|
|
36
|
+
const unicode = renderMermaidASCII(`erDiagram
|
|
37
|
+
WIDGET ||--o| GADGET : rel`)
|
|
38
|
+
expect(unicode).toContain('○│')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram: routed relationship lines get a corner glyph at each
|
|
3
|
+
// jog/detour turn (issue #414)
|
|
4
|
+
//
|
|
5
|
+
// renderErAscii drew every relationship line, jog, and detour purely out of
|
|
6
|
+
// plain '─'/'│' segments meeting at unmarked right angles, even at a point
|
|
7
|
+
// where the routed path actually changes direction. In a dense diagram this
|
|
8
|
+
// reads ambiguously: a viewer can't tell "this line turns here" from "this
|
|
9
|
+
// line ends here and an unrelated line begins right next to it".
|
|
10
|
+
//
|
|
11
|
+
// The fix draws a box-drawing corner glyph ('┌'/'┐'/'└'/'┘', or '+' in ASCII
|
|
12
|
+
// mode) at each of the three routing paths that can change direction:
|
|
13
|
+
// - the same-row detour beneath an obstructing entity
|
|
14
|
+
// (`obstructionBottom`/`detourY`)
|
|
15
|
+
// - a vertical relationship's horizontal jog (`needsJog`/`pickBandY`/`midY`)
|
|
16
|
+
// - a vertical relationship's multi-row-obstruction bypass
|
|
17
|
+
// (`multiRowObstruction`/`routingX`)
|
|
18
|
+
// through the same setCGuarded occupancy guard as every other relationship
|
|
19
|
+
// write, so a corner can never overwrite an entity box border or another
|
|
20
|
+
// relationship's label (the same corruption shape #391 fixed).
|
|
21
|
+
//
|
|
22
|
+
// Each assertion below pins an exact substring of the rendered output that
|
|
23
|
+
// was verified (via a before/after diff against the pre-fix renderer) to
|
|
24
|
+
// contain the new corner glyph at the exact cell the path turns, and to be
|
|
25
|
+
// completely absent from the pre-fix render. Reverting the fix in
|
|
26
|
+
// src/ascii/er-diagram.ts makes every one of these assertions fail — the
|
|
27
|
+
// dashes/lines render as before, without the corner character.
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
import { describe, it, expect } from 'vitest'
|
|
31
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
32
|
+
|
|
33
|
+
describe('ASCII ER relationship routing draws a corner glyph at each turn (issue #414)', () => {
|
|
34
|
+
it('draws corner glyphs at a same-row detour around an obstructing entity', () => {
|
|
35
|
+
// FIRST_ENTITY and THIRD_ENTITY are same-row, non-adjacent (MIDDLE_ENTITY
|
|
36
|
+
// sits between them), so "relates_to" must detour beneath
|
|
37
|
+
// MIDDLE_ENTITY's box. FOURTH_ENTITY/FIFTH_ENTITY exist only to keep this
|
|
38
|
+
// component's row width at 3 (sqrt-based row limit), same trick REPRO_*
|
|
39
|
+
// in ascii-er-relationship-label-corruption-350.test.ts uses.
|
|
40
|
+
const ascii = renderMermaidASCII(
|
|
41
|
+
`erDiagram
|
|
42
|
+
FIRST_ENTITY ||--|{ MIDDLE_ENTITY : contains
|
|
43
|
+
MIDDLE_ENTITY ||--o{ THIRD_ENTITY : includes
|
|
44
|
+
FIRST_ENTITY ||--o| THIRD_ENTITY : relates_to
|
|
45
|
+
FIRST_ENTITY ||--o{ FOURTH_ENTITY : owns
|
|
46
|
+
MIDDLE_ENTITY ||--o{ FIFTH_ENTITY : tracks`,
|
|
47
|
+
{ colorMode: 'none' },
|
|
48
|
+
)
|
|
49
|
+
// The detour's horizontal run opens with '└' (turning up out of the
|
|
50
|
+
// left vertical drop) and closes with '┘' (turning up into the right
|
|
51
|
+
// vertical drop) instead of plain '─' at both ends.
|
|
52
|
+
//
|
|
53
|
+
// MIDDLE_ENTITY's own vertical "one" marker for its "tracks"
|
|
54
|
+
// relationship no longer crosses this detour row directly — #390's
|
|
55
|
+
// flush-marker change (merged after this test was first written) puts
|
|
56
|
+
// the marker flush against MIDDLE_ENTITY's own border, one row above
|
|
57
|
+
// the detour line, rather than inset into the detour's own row the
|
|
58
|
+
// way the old inset positioning did. Checked separately below instead
|
|
59
|
+
// of assuming they share a row.
|
|
60
|
+
expect(ascii).toContain('└─────────────── tracks ────────────┘')
|
|
61
|
+
const lines = ascii.split('\n')
|
|
62
|
+
const detourLineIndex = lines.findIndex((l) =>
|
|
63
|
+
l.includes('└─────────────── tracks ────────────┘'),
|
|
64
|
+
)
|
|
65
|
+
expect(detourLineIndex).toBeGreaterThan(0)
|
|
66
|
+
// MIDDLE_ENTITY's "tracks" marker ('┼', flush against its own border)
|
|
67
|
+
// sits on the row immediately above the detour line.
|
|
68
|
+
expect(lines[detourLineIndex - 1]).toMatch(/┼/)
|
|
69
|
+
expect(ascii).toContain('relates_to')
|
|
70
|
+
expect(ascii).toContain('tracks')
|
|
71
|
+
expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("draws corner glyphs at a vertical relationship's horizontal jog", () => {
|
|
75
|
+
// USER and SESSION don't share a center column, so "opens" needs a
|
|
76
|
+
// horizontal jog partway down — the exact repro from
|
|
77
|
+
// ascii-er-jog-stray-line.test.ts's stray-duplicate-line fix.
|
|
78
|
+
const ascii = renderMermaidASCII(
|
|
79
|
+
`erDiagram
|
|
80
|
+
USER ||--o{ LOG_ENTRY : generates
|
|
81
|
+
USER ||..o{ SESSION : opens`,
|
|
82
|
+
{ colorMode: 'none' },
|
|
83
|
+
)
|
|
84
|
+
// The jog's dashed line ('╌', a non-identifying relationship) opens
|
|
85
|
+
// with a solid '┐' corner glyph at the turn instead of another dash.
|
|
86
|
+
// USER's own vertical "one" marker for this relationship ('┼', a
|
|
87
|
+
// crossing tick rather than '│', which would blend into the plain
|
|
88
|
+
// line, or a bare '─', which would sever it — see getCrowsFootChars'
|
|
89
|
+
// `vertical` param) sits flush against USER's own border, one row
|
|
90
|
+
// above the jog — not fused onto the same row as the jog's corner
|
|
91
|
+
// (#390's flush-marker change, merged after this test was first
|
|
92
|
+
// written, shifted the marker there).
|
|
93
|
+
const lines = ascii.split('\n')
|
|
94
|
+
const jogLineIndex = lines.findIndex((l) => l.includes('└┐ opens'))
|
|
95
|
+
expect(jogLineIndex).toBeGreaterThan(0)
|
|
96
|
+
expect(lines[jogLineIndex - 1]).toMatch(/┼/)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('draws the ASCII-mode corner glyph ("+") at the same jog, in useAscii mode', () => {
|
|
100
|
+
// Same repro and turn as the Unicode jog test above — getCornerChar's
|
|
101
|
+
// useAscii branch (a plain '+', matching getCrowsFootChars' own
|
|
102
|
+
// Unicode/ASCII split) has no Unicode-mode equivalent path, so it needs
|
|
103
|
+
// its own coverage rather than assuming the Unicode assertion implies it.
|
|
104
|
+
// Both adjacent corners use the same '+' glyph regardless of direction
|
|
105
|
+
// in ASCII mode, so the two-corner pair reads as '++' rather than
|
|
106
|
+
// Unicode's direction-distinct '└┐' — see the Unicode test above for
|
|
107
|
+
// why there are two adjacent corners here at all.
|
|
108
|
+
const ascii = renderMermaidASCII(
|
|
109
|
+
`erDiagram
|
|
110
|
+
USER ||--o{ LOG_ENTRY : generates
|
|
111
|
+
USER ||..o{ SESSION : opens`,
|
|
112
|
+
{ colorMode: 'none', useAscii: true },
|
|
113
|
+
)
|
|
114
|
+
// Both '+' characters are the jog's own two adjacent corner glyphs
|
|
115
|
+
// (ASCII mode uses a plain '+' for every corner direction — see the
|
|
116
|
+
// Unicode test above, which shows the same shape as '└┐'). USER's own
|
|
117
|
+
// vertical "one" marker sits on its own row above this line, flush
|
|
118
|
+
// against USER's border (#390's flush-marker change) — not fused onto
|
|
119
|
+
// this line the way an earlier version of this comment assumed.
|
|
120
|
+
expect(ascii).toContain('++ opens')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("draws corner glyphs at a vertical relationship's multi-row-obstruction bypass", () => {
|
|
124
|
+
// Same 7-entity, 3-row shape as the multi-row-obstruction regression in
|
|
125
|
+
// ascii-er-relationship-label-corruption-350.test.ts: A-G shares D's
|
|
126
|
+
// column two rows down, so "ag" must bypass through a free column with
|
|
127
|
+
// two short jogs (one leaving A's row, one entering G's row).
|
|
128
|
+
const ascii = renderMermaidASCII(
|
|
129
|
+
`erDiagram
|
|
130
|
+
A ||--o{ B : ab
|
|
131
|
+
B ||--o{ C : bc
|
|
132
|
+
C ||--o{ D : cd
|
|
133
|
+
D ||--o{ E : de
|
|
134
|
+
E ||--o{ F : ef
|
|
135
|
+
F ||--o{ G : fg
|
|
136
|
+
A ||--o{ G : ag
|
|
137
|
+
A {
|
|
138
|
+
string id
|
|
139
|
+
string name
|
|
140
|
+
}
|
|
141
|
+
D {
|
|
142
|
+
string id
|
|
143
|
+
string title
|
|
144
|
+
}
|
|
145
|
+
G {
|
|
146
|
+
string id
|
|
147
|
+
string label
|
|
148
|
+
}`,
|
|
149
|
+
{ colorMode: 'none' },
|
|
150
|
+
)
|
|
151
|
+
// Corner glyphs at both jogs of the bypass route. G is also the shared
|
|
152
|
+
// target of "fg" (from F, same row as G) and "ag" (this bypass) — since
|
|
153
|
+
// issue #453, each gets its own staggered attachment column instead of
|
|
154
|
+
// both landing on G's exact center, which shifts these exact strings
|
|
155
|
+
// from a single shared-center bypass to two side-by-side ones; the
|
|
156
|
+
// corner glyphs at the bypass's own turns are still there, just at the
|
|
157
|
+
// updated column.
|
|
158
|
+
expect(ascii).toContain('┌─│──────────────────┘')
|
|
159
|
+
expect(ascii).toContain('○╟─────────────────┘')
|
|
160
|
+
expect(ascii).toContain('ag')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('handles a bypass whose free column search finds a match to the left, not just to the right', () => {
|
|
164
|
+
// Same shape as the bypass test above, but D's box is widened enough
|
|
165
|
+
// that the free-column search (which tries lineX+offset before
|
|
166
|
+
// lineX-offset at every step — see columnClearOfBoxes' caller) exhausts
|
|
167
|
+
// rightward space and finds its match on the left side instead. This
|
|
168
|
+
// exercises the *other* branch of the horizDirAtLine/horizDirAtLower
|
|
169
|
+
// ternaries in the bypass block — every other test in this file only
|
|
170
|
+
// ever exercises the rightward case, since a plain sqrt-based row
|
|
171
|
+
// layout with same-sized entities always searches rightward-first
|
|
172
|
+
// successfully.
|
|
173
|
+
//
|
|
174
|
+
// In this specific diagram, the matched free column (routingX) lands
|
|
175
|
+
// off-canvas (negative — nothing to A's own left in this narrow
|
|
176
|
+
// layout), and the two corners at valid coordinates (A's own column,
|
|
177
|
+
// and G's own column) both land exactly on cells the crow's-foot
|
|
178
|
+
// markers already occupy — the marker legitimately wins there (same
|
|
179
|
+
// precedence as the rightward case: markers are drawn after corners,
|
|
180
|
+
// see drawCorner's own doc comment), so there's no *visible* new
|
|
181
|
+
// glyph to assert on here. What this test actually pins down is that
|
|
182
|
+
// reaching that branch doesn't corrupt anything or throw — content
|
|
183
|
+
// integrity, not a specific rendered character.
|
|
184
|
+
const ascii = renderMermaidASCII(
|
|
185
|
+
`erDiagram
|
|
186
|
+
A ||--o{ B : ab
|
|
187
|
+
B ||--o{ C : bc
|
|
188
|
+
C ||--o{ D : cd
|
|
189
|
+
D ||--o{ E : de
|
|
190
|
+
E ||--o{ F : ef
|
|
191
|
+
F ||--o{ G : fg
|
|
192
|
+
A ||--o{ G : ag
|
|
193
|
+
D {
|
|
194
|
+
string this_is_a_very_long_attribute_name_to_widen_the_box
|
|
195
|
+
}
|
|
196
|
+
G {
|
|
197
|
+
string id
|
|
198
|
+
}`,
|
|
199
|
+
{ colorMode: 'none' },
|
|
200
|
+
)
|
|
201
|
+
expect(ascii).toContain('ag')
|
|
202
|
+
expect(ascii).toContain('string id')
|
|
203
|
+
expect(ascii).toContain(
|
|
204
|
+
'string this_is_a_very_long_attribute_name_to_widen_the_box',
|
|
205
|
+
)
|
|
206
|
+
expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('does not corrupt content when the bypass free-column search matches lineX itself (content-integrity coverage only)', () => {
|
|
210
|
+
// The obstruction that triggers multiRowObstruction is detected within
|
|
211
|
+
// the narrow row-gap band (bandTop..bandBottom — see the comment above
|
|
212
|
+
// that check), but the free-column search below scans the *full*
|
|
213
|
+
// startY..endY span. When the obstruction is confined to the band and
|
|
214
|
+
// lineX's own column is otherwise clear across the fuller span, offset
|
|
215
|
+
// 0 of that search succeeds immediately at lineX itself — so
|
|
216
|
+
// `routingX === lineX`, skipping the "if (lineX !== routingX)" corner
|
|
217
|
+
// block entirely (every other bypass test in this file has
|
|
218
|
+
// routingX !== lineX, since a same-sized-entity layout's obstruction
|
|
219
|
+
// isn't usually confined that way). Found the same way as the
|
|
220
|
+
// leftward-bypass case above: varying A's own attribute width while
|
|
221
|
+
// instrumenting routingX locally, not by construction alone.
|
|
222
|
+
//
|
|
223
|
+
// When that block is skipped, nothing observably distinguishes this
|
|
224
|
+
// render from an equivalent diagram that never triggered
|
|
225
|
+
// multiRowObstruction at all — skipping the block draws no corner, so
|
|
226
|
+
// there is no glyph unique to this path to assert on (raised in review
|
|
227
|
+
// on this PR: the assertions below prove the branch doesn't corrupt
|
|
228
|
+
// content or throw when reached, not that it visibly changed anything —
|
|
229
|
+
// hence this test's name and scope are deliberately about integrity,
|
|
230
|
+
// not a specific rendered character).
|
|
231
|
+
const ascii = renderMermaidASCII(
|
|
232
|
+
`erDiagram
|
|
233
|
+
A ||--o{ B : ab
|
|
234
|
+
B ||--o{ C : bc
|
|
235
|
+
C ||--o{ D : cd
|
|
236
|
+
D ||--o{ E : de
|
|
237
|
+
E ||--o{ F : ef
|
|
238
|
+
F ||--o{ G : fg
|
|
239
|
+
A ||--o{ G : ag
|
|
240
|
+
A {
|
|
241
|
+
string x
|
|
242
|
+
}`,
|
|
243
|
+
{ colorMode: 'none' },
|
|
244
|
+
)
|
|
245
|
+
expect(ascii).toContain('ag')
|
|
246
|
+
expect(ascii).toContain('string x')
|
|
247
|
+
expect(ascii).not.toMatch(/[a-z]+[─│┊╌][a-z]+/)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram: a vertical relationship's jog must not leave a stray
|
|
3
|
+
// duplicate line at its original column
|
|
4
|
+
//
|
|
5
|
+
// renderErAscii's vertical-connection branch always drew the initial
|
|
6
|
+
// straight vertical line for the *entire* startY..endY span at the upper
|
|
7
|
+
// entity's own center column (lineX), even when a horizontal jog then
|
|
8
|
+
// redirects the path to the lower entity's center column (lowerCX). Below
|
|
9
|
+
// the jog row, that left two parallel vertical segments where only one
|
|
10
|
+
// path exists: the "real" one at lowerCX (leading into the lower entity),
|
|
11
|
+
// and a leftover one still running the rest of the way down at the
|
|
12
|
+
// *original* lineX — connected to nothing, since the path already turned.
|
|
13
|
+
// Spotted as an unexplained doubled "││" / "┊┊" glyph pair in a real
|
|
14
|
+
// catalog sample's rendered output.
|
|
15
|
+
//
|
|
16
|
+
// A bare "││"/"┊┊" substring is not on its own proof of this bug: a
|
|
17
|
+
// same-row (horizontal) connection's flush-against-the-border crow's-foot
|
|
18
|
+
// marker for "one" cardinality is also a lone "│", so a "one"-cardinality
|
|
19
|
+
// relationship starting right at an entity's own right border legitimately
|
|
20
|
+
// prints "│<border>│<marker>" on the *same row* as the rest of that
|
|
21
|
+
// entity's box — see issue #351's marker-inset removal. The actual bug
|
|
22
|
+
// this test guards against instead leaves a *connector-only* row: a row
|
|
23
|
+
// whose sole non-blank content is the doubled glyph pair, with no entity
|
|
24
|
+
// box or text sharing that row, since the leftover stem runs down through
|
|
25
|
+
// empty space between two entity rows.
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
import { describe, it, expect } from 'vitest'
|
|
29
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when some row's only non-whitespace content is a doubled line/dash
|
|
33
|
+
* glyph ("││" or "┊┊") — the signature of a leftover parallel vertical
|
|
34
|
+
* stem left behind by an unstopped jog. A legitimate box-border-flush-
|
|
35
|
+
* against-a-marker "││" always shares its row with other box/text
|
|
36
|
+
* content (the entity's own border and label), so it's excluded by
|
|
37
|
+
* requiring the rest of the row to be blank.
|
|
38
|
+
*/
|
|
39
|
+
function hasStrayDuplicateColumn(ascii: string): boolean {
|
|
40
|
+
return ascii.split('\n').some((line) => {
|
|
41
|
+
for (const pattern of ['││', '┊┊']) {
|
|
42
|
+
const col = line.indexOf(pattern)
|
|
43
|
+
if (col === -1) continue
|
|
44
|
+
const rest = line.slice(0, col) + line.slice(col + pattern.length)
|
|
45
|
+
if (rest.trim() === '') return true
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('ASCII ER vertical relationship jog does not leave a stray duplicate line', () => {
|
|
52
|
+
it('does not draw a leftover line at the original column once a relationship has jogged over', () => {
|
|
53
|
+
// Real catalog sample ("ER: Non-Identifying (Dashed) Relationship"):
|
|
54
|
+
// USER-SESSION needs a horizontal jog (their centers don't align), so
|
|
55
|
+
// its path runs straight down from USER, jogs sideways, then straight
|
|
56
|
+
// down into SESSION. Before this fix, the row directly below the jog
|
|
57
|
+
// showed two dotted-line glyphs side by side ("┊┊") instead of one —
|
|
58
|
+
// the real jogged path, plus a stray remnant of the pre-jog column
|
|
59
|
+
// that was never told to stop.
|
|
60
|
+
const ascii = renderMermaidASCII(
|
|
61
|
+
`erDiagram
|
|
62
|
+
USER ||--o{ LOG_ENTRY : generates
|
|
63
|
+
USER ||..o{ SESSION : opens`,
|
|
64
|
+
{ colorMode: 'none' },
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(hasStrayDuplicateColumn(ascii)).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('does not draw a leftover line for either of two independently-jogging relationships', () => {
|
|
71
|
+
// Real catalog sample ("ER: School Management Schema"): both
|
|
72
|
+
// STUDENT-COURSE ("enrolled") and TEACHER-ENROLLMENT ("teaches") jog
|
|
73
|
+
// over to reach their lower entity. Before this fix, the row right
|
|
74
|
+
// below each jog showed a doubled "│╟"/"││" pair on both sides at
|
|
75
|
+
// once — confirming the leftover-column bug wasn't specific to one
|
|
76
|
+
// relationship's geometry.
|
|
77
|
+
const ascii = renderMermaidASCII(
|
|
78
|
+
`erDiagram
|
|
79
|
+
STUDENT {
|
|
80
|
+
int id PK
|
|
81
|
+
string name
|
|
82
|
+
date dob
|
|
83
|
+
string grade
|
|
84
|
+
}
|
|
85
|
+
TEACHER {
|
|
86
|
+
int id PK
|
|
87
|
+
string name
|
|
88
|
+
string department
|
|
89
|
+
}
|
|
90
|
+
COURSE {
|
|
91
|
+
int id PK
|
|
92
|
+
string title
|
|
93
|
+
int teacher_id FK
|
|
94
|
+
int credits
|
|
95
|
+
}
|
|
96
|
+
ENROLLMENT {
|
|
97
|
+
int id PK
|
|
98
|
+
int student_id FK
|
|
99
|
+
int course_id FK
|
|
100
|
+
string semester
|
|
101
|
+
float grade
|
|
102
|
+
}
|
|
103
|
+
STUDENT ||--o{ COURSE : enrolled
|
|
104
|
+
TEACHER ||--o{ ENROLLMENT : teaches
|
|
105
|
+
COURSE ||--o{ ENROLLMENT : has`,
|
|
106
|
+
{ colorMode: 'none' },
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(hasStrayDuplicateColumn(ascii)).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram relationship label truncation/padding regression tests
|
|
3
|
+
// (GitHub issue #67)
|
|
4
|
+
//
|
|
5
|
+
// Relationship labels were clamped to a fixed 6-char inter-entity gap,
|
|
6
|
+
// silently truncating anything longer (e.g. "ordered in" → "ordere") and,
|
|
7
|
+
// even when a label did fit, leaving it flush against both entity boxes
|
|
8
|
+
// with zero padding. The cardinality glyph cluster (e.g. "││───○╟") was
|
|
9
|
+
// also crammed directly against the entity box border.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest'
|
|
13
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
14
|
+
|
|
15
|
+
const REPRO = `erDiagram
|
|
16
|
+
CUSTOMER ||--o{ ORDER : places
|
|
17
|
+
PRODUCT ||--o{ LINE_ITEM : "ordered in"`
|
|
18
|
+
|
|
19
|
+
describe('ASCII ER relationship label truncation and padding (issue #67)', () => {
|
|
20
|
+
it('renders a long quoted label in full instead of truncating it', () => {
|
|
21
|
+
const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
|
|
22
|
+
|
|
23
|
+
expect(ascii).toContain('ordered in')
|
|
24
|
+
// The old bug truncated to "ordere" — make sure that's gone.
|
|
25
|
+
expect(ascii).not.toMatch(/ordere(?!d)/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders a long label in full in Unicode mode too', () => {
|
|
29
|
+
const ascii = renderMermaidASCII(REPRO, {
|
|
30
|
+
colorMode: 'none',
|
|
31
|
+
useAscii: false,
|
|
32
|
+
})
|
|
33
|
+
expect(ascii).toContain('ordered in')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders a long label in full in pure-ASCII mode too', () => {
|
|
37
|
+
const ascii = renderMermaidASCII(REPRO, {
|
|
38
|
+
colorMode: 'none',
|
|
39
|
+
useAscii: true,
|
|
40
|
+
})
|
|
41
|
+
expect(ascii).toContain('ordered in')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('keeps at least 1 char of padding between the label and each entity box', () => {
|
|
45
|
+
const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
|
|
46
|
+
const lines = ascii.split('\n')
|
|
47
|
+
|
|
48
|
+
// "places" sits on the row directly below the CUSTOMER/ORDER connector line,
|
|
49
|
+
// between the two boxes' bottom borders. Find that row and confirm the
|
|
50
|
+
// label isn't flush against either "└" border character.
|
|
51
|
+
const placesLine = lines.find((l) => l.includes('places'))
|
|
52
|
+
expect(placesLine).toBeDefined()
|
|
53
|
+
const idx = placesLine!.indexOf('places')
|
|
54
|
+
// Character immediately before/after the label must not be a box border.
|
|
55
|
+
expect(placesLine![idx - 1]).not.toBe('┘')
|
|
56
|
+
expect(placesLine![idx - 1]).not.toBe('└')
|
|
57
|
+
expect(placesLine![idx + 'places'.length]).not.toBe('┌')
|
|
58
|
+
expect(placesLine![idx + 'places'.length]).not.toBe('└')
|
|
59
|
+
|
|
60
|
+
const orderedInLine = lines.find((l) => l.includes('ordered in'))
|
|
61
|
+
expect(orderedInLine).toBeDefined()
|
|
62
|
+
const idx2 = orderedInLine!.indexOf('ordered in')
|
|
63
|
+
expect(orderedInLine![idx2 - 1]).not.toBe('┘')
|
|
64
|
+
expect(orderedInLine![idx2 - 1]).not.toBe('└')
|
|
65
|
+
expect(orderedInLine![idx2 + 'ordered in'.length]).not.toBe('┌')
|
|
66
|
+
expect(orderedInLine![idx2 + 'ordered in'.length]).not.toBe('└')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('keeps at least 1 char of padding between the cardinality glyph cluster and each entity box', () => {
|
|
70
|
+
const ascii = renderMermaidASCII(
|
|
71
|
+
`erDiagram
|
|
72
|
+
CUSTOMER ||--o{ ORDER : places`,
|
|
73
|
+
{ colorMode: 'none' },
|
|
74
|
+
)
|
|
75
|
+
const lines = ascii.split('\n')
|
|
76
|
+
// The connector line row contains the crow's foot markers, e.g.
|
|
77
|
+
// "│ CUSTOMER │─│───○╟─│ ORDER │" — the marker must not sit directly
|
|
78
|
+
// against the "│" that closes the CUSTOMER box or opens the ORDER box.
|
|
79
|
+
const connectorLine = lines.find(
|
|
80
|
+
(l) => l.includes('CUSTOMER') && (l.includes('│') || l.includes('|')),
|
|
81
|
+
)
|
|
82
|
+
expect(connectorLine).toBeDefined()
|
|
83
|
+
|
|
84
|
+
// Locate the right border of the CUSTOMER box and left border of the ORDER box.
|
|
85
|
+
const customerEnd = connectorLine!.indexOf('CUSTOMER') + 'CUSTOMER'.length
|
|
86
|
+
const closingBorderIdx = connectorLine!.indexOf('│', customerEnd)
|
|
87
|
+
expect(closingBorderIdx).toBeGreaterThan(-1)
|
|
88
|
+
// The very next character must not itself be a cardinality marker glyph
|
|
89
|
+
// touching the border — there must be at least 1 separating cell.
|
|
90
|
+
const afterBorder = connectorLine![closingBorderIdx + 1]
|
|
91
|
+
expect(afterBorder).not.toBe('○')
|
|
92
|
+
expect(afterBorder).not.toBe('╟')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('does not widen the gap for short labels that already fit (stays compact)', () => {
|
|
96
|
+
const compact = renderMermaidASCII(
|
|
97
|
+
`erDiagram
|
|
98
|
+
A ||--o{ B : x`,
|
|
99
|
+
{ colorMode: 'none' },
|
|
100
|
+
)
|
|
101
|
+
// Sanity: still renders correctly and compactly (no huge gap for a 1-char label).
|
|
102
|
+
const lines = compact.split('\n')
|
|
103
|
+
const line = lines.find((l) => l.includes('A') && l.includes('│'))
|
|
104
|
+
expect(line).toBeDefined()
|
|
105
|
+
expect(line!.length).toBeLessThan(30)
|
|
106
|
+
expect(compact).toContain('x')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('full example diagram from the issue renders every entity and full labels', () => {
|
|
110
|
+
const ascii = renderMermaidASCII(REPRO, { colorMode: 'none' })
|
|
111
|
+
expect(ascii).toContain('CUSTOMER')
|
|
112
|
+
expect(ascii).toContain('ORDER')
|
|
113
|
+
expect(ascii).toContain('PRODUCT')
|
|
114
|
+
expect(ascii).toContain('LINE_ITEM')
|
|
115
|
+
expect(ascii).toContain('places')
|
|
116
|
+
expect(ascii).toContain('ordered in')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII ER diagram padding tests (issue #343)
|
|
3
|
+
//
|
|
4
|
+
// `-x`/`-y`/`-p` (AsciiRenderOptions.paddingX/paddingY/boxBorderPadding)
|
|
5
|
+
// previously only affected the flowchart/state grid layout — ER diagrams
|
|
6
|
+
// ignored them entirely (hGap/vGap/componentGap were fixed constants, and
|
|
7
|
+
// drawMultiBox was always called with its implicit default padding). This
|
|
8
|
+
// file verifies the fix, and that a render with no explicit padding option
|
|
9
|
+
// still renders byte-for-byte identical to before the fix (see the
|
|
10
|
+
// DEFAULT_PADDING_* comment in src/ascii/types.ts for why that matters).
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
15
|
+
|
|
16
|
+
// A single relationship with a short label — exercises the horizontal gap
|
|
17
|
+
// between entities without the label itself becoming the binding constraint
|
|
18
|
+
// (a long label widens the gap independently of hGap — see the
|
|
19
|
+
// `pairLabelWidth` widening in er-diagram.ts — which would make the
|
|
20
|
+
// hGap-driven shift non-linear across padding values).
|
|
21
|
+
const RELATED = `erDiagram
|
|
22
|
+
CUSTOMER ||--o{ ORDER : x`
|
|
23
|
+
|
|
24
|
+
// Two disconnected entities — exercises componentGap (vertical stacking).
|
|
25
|
+
const DISCONNECTED = `erDiagram
|
|
26
|
+
CUSTOMER {
|
|
27
|
+
string name
|
|
28
|
+
}
|
|
29
|
+
ORDER {
|
|
30
|
+
string id
|
|
31
|
+
}`
|
|
32
|
+
|
|
33
|
+
function colOf(ascii: string, lineIndex: number, needle: string): number {
|
|
34
|
+
const line = ascii.split('\n')[lineIndex]
|
|
35
|
+
if (line === undefined) throw new Error(`no line ${lineIndex}`)
|
|
36
|
+
const idx = line.indexOf(needle)
|
|
37
|
+
if (idx === -1) throw new Error(`"${needle}" not found on line ${lineIndex}`)
|
|
38
|
+
return idx
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function lineCount(ascii: string): number {
|
|
42
|
+
return ascii.split('\n').filter((l) => l.length > 0).length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function borderWidth(ascii: string, lineIndex: number): number {
|
|
46
|
+
const line = ascii.split('\n')[lineIndex]
|
|
47
|
+
const match = line ? /\+-+\+/.exec(line) : null
|
|
48
|
+
if (!match) throw new Error(`no border run on line ${lineIndex}: ${line}`)
|
|
49
|
+
return match[0].length
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('ASCII ER diagrams — padding (issue #343)', () => {
|
|
53
|
+
it('renders identically with no padding options and with explicit defaults', () => {
|
|
54
|
+
const withoutOptions = renderMermaidASCII(RELATED, { useAscii: true })
|
|
55
|
+
const withDefaults = renderMermaidASCII(RELATED, {
|
|
56
|
+
useAscii: true,
|
|
57
|
+
paddingX: 5,
|
|
58
|
+
paddingY: 5,
|
|
59
|
+
boxBorderPadding: 1,
|
|
60
|
+
})
|
|
61
|
+
expect(withDefaults).toBe(withoutOptions)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('paddingX widens the gap between related entities', () => {
|
|
65
|
+
const base = renderMermaidASCII(RELATED, { useAscii: true })
|
|
66
|
+
const wide = renderMermaidASCII(RELATED, { useAscii: true, paddingX: 25 })
|
|
67
|
+
const baseCol = colOf(base, 1, '| ORDER |')
|
|
68
|
+
const wideCol = colOf(wide, 1, '| ORDER |')
|
|
69
|
+
expect(wideCol).toBeGreaterThan(baseCol)
|
|
70
|
+
const mid = renderMermaidASCII(RELATED, { useAscii: true, paddingX: 15 })
|
|
71
|
+
const midCol = colOf(mid, 1, '| ORDER |')
|
|
72
|
+
expect(midCol - baseCol).toBe(wideCol - midCol)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('a very small paddingX does not shrink the gap below the safe minimum', () => {
|
|
76
|
+
// Unlike sequence/class diagrams, ER's horizontal gap has a floor equal
|
|
77
|
+
// to its own default (6) rather than a smaller generic minimum — a
|
|
78
|
+
// smaller gap lets the crow's-foot markers on each end overlap (see the
|
|
79
|
+
// hGap comment in er-diagram.ts and ascii-padding-edge-cases.test.ts for
|
|
80
|
+
// the regression test), so a very negative paddingX clamps to the same
|
|
81
|
+
// gap as the default rather than tightening further.
|
|
82
|
+
const base = renderMermaidASCII(RELATED, { useAscii: true })
|
|
83
|
+
const tight = renderMermaidASCII(RELATED, {
|
|
84
|
+
useAscii: true,
|
|
85
|
+
paddingX: -10,
|
|
86
|
+
})
|
|
87
|
+
expect(tight).toBe(base)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('paddingY widens the gap between disconnected components', () => {
|
|
91
|
+
const base = renderMermaidASCII(DISCONNECTED, { useAscii: true })
|
|
92
|
+
const tall = renderMermaidASCII(DISCONNECTED, {
|
|
93
|
+
useAscii: true,
|
|
94
|
+
paddingY: 25,
|
|
95
|
+
})
|
|
96
|
+
expect(lineCount(tall)).toBeGreaterThan(lineCount(base))
|
|
97
|
+
const mid = renderMermaidASCII(DISCONNECTED, {
|
|
98
|
+
useAscii: true,
|
|
99
|
+
paddingY: 15,
|
|
100
|
+
})
|
|
101
|
+
expect(lineCount(mid) - lineCount(base)).toBe(
|
|
102
|
+
lineCount(tall) - lineCount(mid),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('boxBorderPadding widens entity boxes by exactly 2 columns per unit', () => {
|
|
107
|
+
const base = renderMermaidASCII(RELATED, { useAscii: true })
|
|
108
|
+
const padded = renderMermaidASCII(RELATED, {
|
|
109
|
+
useAscii: true,
|
|
110
|
+
boxBorderPadding: 4,
|
|
111
|
+
})
|
|
112
|
+
expect(borderWidth(padded, 0) - borderWidth(base, 0)).toBe(6)
|
|
113
|
+
})
|
|
114
|
+
})
|