@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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #205 — ASCII renderer box borders misaligned by
|
|
3
|
+
* zero-width Unicode combining marks.
|
|
4
|
+
*
|
|
5
|
+
* `displayWidth()` used to add 1 column for every code point that isn't
|
|
6
|
+
* "wide" (CJK etc), including combining marks (general categories Mn/Me,
|
|
7
|
+
* e.g. U+0300–U+036F COMBINING ACUTE ACCENT). A real terminal gives such a
|
|
8
|
+
* mark 0 columns — it attaches to the preceding base character. A decomposed
|
|
9
|
+
* string like "café" (e + U+0301) used to measure as 5 columns; a real
|
|
10
|
+
* terminal renders it as 4.
|
|
11
|
+
*
|
|
12
|
+
* `terminalDisplayWidth` re-implements the "how many terminal columns does
|
|
13
|
+
* this string occupy" calculation independently of
|
|
14
|
+
* `src/ascii/display-width.ts`, so these tests verify the *rendered output*
|
|
15
|
+
* against an authority separate from the implementation under test.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect } from 'vitest'
|
|
18
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
19
|
+
import { displayWidth } from '../display-width.ts'
|
|
20
|
+
import {
|
|
21
|
+
terminalDisplayWidth,
|
|
22
|
+
assertUniformDisplayWidth,
|
|
23
|
+
} from './helpers/terminal-display-width.ts'
|
|
24
|
+
|
|
25
|
+
// "café", decomposed: c a f e + COMBINING ACUTE ACCENT (U+0301) — 5 code
|
|
26
|
+
// points, 4 grapheme clusters / terminal columns.
|
|
27
|
+
const DECOMPOSED_CAFE = 'caf' + 'e' + '́'
|
|
28
|
+
|
|
29
|
+
describe('ASCII renderer — combining mark box alignment (issue #205)', () => {
|
|
30
|
+
it('displayWidth("café") (decomposed) is 4, not 5', () => {
|
|
31
|
+
expect([...DECOMPOSED_CAFE].length).toBe(5)
|
|
32
|
+
expect(displayWidth(DECOMPOSED_CAFE)).toBe(4)
|
|
33
|
+
expect(terminalDisplayWidth(DECOMPOSED_CAFE)).toBe(4)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
for (const useAscii of [false, true]) {
|
|
37
|
+
describe(useAscii ? 'ASCII charset' : 'Unicode charset', () => {
|
|
38
|
+
it('renders a flowchart node with a decomposed label at uniform row width', () => {
|
|
39
|
+
const mermaid = `flowchart TD\n A[${DECOMPOSED_CAFE}] --> B[ok]`
|
|
40
|
+
const ascii = renderMermaidASCII(mermaid, {
|
|
41
|
+
useAscii,
|
|
42
|
+
colorMode: 'none',
|
|
43
|
+
})
|
|
44
|
+
assertUniformDisplayWidth(ascii)
|
|
45
|
+
expect(ascii).toContain(DECOMPOSED_CAFE)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it('renders a class diagram compartment box with a decomposed member name at uniform width', () => {
|
|
51
|
+
const mermaid = `classDiagram\n class Person {\n +String ${DECOMPOSED_CAFE}\n }`
|
|
52
|
+
const ascii = renderMermaidASCII(mermaid, { colorMode: 'none' })
|
|
53
|
+
assertUniformDisplayWidth(ascii)
|
|
54
|
+
expect(ascii).toContain(DECOMPOSED_CAFE)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders an ER diagram compartment box with a decomposed attribute name at uniform width', () => {
|
|
58
|
+
const mermaid = `erDiagram\n ITEM {\n string ${DECOMPOSED_CAFE}\n }`
|
|
59
|
+
const ascii = renderMermaidASCII(mermaid, { colorMode: 'none' })
|
|
60
|
+
assertUniformDisplayWidth(ascii)
|
|
61
|
+
expect(ascii).toContain(DECOMPOSED_CAFE)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('does not perturb the all-ASCII case', () => {
|
|
65
|
+
const ascii = renderMermaidASCII('flowchart TD\n A[plain] --> B[ok]', {
|
|
66
|
+
colorMode: 'none',
|
|
67
|
+
})
|
|
68
|
+
assertUniformDisplayWidth(ascii)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { addCoordsOverlay } from '../coords.ts'
|
|
3
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
4
|
+
|
|
5
|
+
describe('addCoordsOverlay', () => {
|
|
6
|
+
it('prepends a two-row column ruler and a row-index gutter', () => {
|
|
7
|
+
const rendered = 'AB\nCD'
|
|
8
|
+
const result = addCoordsOverlay(rendered)
|
|
9
|
+
const lines = result.split('\n')
|
|
10
|
+
|
|
11
|
+
// Two ruler rows on top, then one gutter-prefixed row per original line.
|
|
12
|
+
expect(lines).toHaveLength(4)
|
|
13
|
+
expect(lines[0]).toBe(' 00')
|
|
14
|
+
expect(lines[1]).toBe(' 01')
|
|
15
|
+
expect(lines[2]).toBe('0 AB')
|
|
16
|
+
expect(lines[3]).toBe('1 CD')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('pads the row gutter to fit multi-digit row indices', () => {
|
|
20
|
+
const rendered = Array.from({ length: 11 }, (_, i) => `row${i}`).join('\n')
|
|
21
|
+
const result = addCoordsOverlay(rendered)
|
|
22
|
+
const lines = result.split('\n')
|
|
23
|
+
|
|
24
|
+
// Row gutter width is 2 (since max row index is 10), so row 0's label
|
|
25
|
+
// is right-aligned as " 0" and row 10's as "10".
|
|
26
|
+
expect(lines[2]?.startsWith(' 0 ')).toBe(true)
|
|
27
|
+
expect(lines[lines.length - 1]?.startsWith('10 ')).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('handles an empty string without throwing', () => {
|
|
31
|
+
expect(() => addCoordsOverlay('')).not.toThrow()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('measures width by visible characters, not raw length with ANSI codes', () => {
|
|
35
|
+
// A 2-char-wide line wrapped in a color escape sequence is still only
|
|
36
|
+
// 2 columns wide on screen — the ruler must match that, not the ~10
|
|
37
|
+
// raw characters the escape codes add.
|
|
38
|
+
const colored = '\x1b[37mAB\x1b[0m'
|
|
39
|
+
const result = addCoordsOverlay(colored)
|
|
40
|
+
const lines = result.split('\n')
|
|
41
|
+
expect(lines[0]).toBe(' 00')
|
|
42
|
+
expect(lines[1]).toBe(' 01')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('renderMermaidASCII – showCoords', () => {
|
|
47
|
+
const DIAGRAM = 'graph LR\n A --> B'
|
|
48
|
+
|
|
49
|
+
it('is off by default (no ruler in output)', () => {
|
|
50
|
+
const result = renderMermaidASCII(DIAGRAM)
|
|
51
|
+
// Without the overlay, the very first line is drawing content, not a
|
|
52
|
+
// row of digit-only ruler characters starting at the left margin.
|
|
53
|
+
expect(result.split('\n')[0]).not.toMatch(/^\s*0123456789/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('overlays coordinate rulers when showCoords is true', () => {
|
|
57
|
+
const plain = renderMermaidASCII(DIAGRAM)
|
|
58
|
+
const withCoords = renderMermaidASCII(DIAGRAM, { showCoords: true })
|
|
59
|
+
|
|
60
|
+
expect(withCoords).not.toBe(plain)
|
|
61
|
+
expect(withCoords.split('\n').length).toBe(plain.split('\n').length + 2)
|
|
62
|
+
expect(withCoords).toContain('0123456789')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for GitHub issue #86 — ASCII: stray ├ character on
|
|
3
|
+
* flowchart decision-node edge labels (LR, Unicode).
|
|
4
|
+
*
|
|
5
|
+
* Repro (from the issue): a decision node with two outgoing labeled edges,
|
|
6
|
+
* rendered LR with the default Unicode box-drawing charset. The box-start
|
|
7
|
+
* connector for the first ("Yes") edge landed on a blank grid cell — one
|
|
8
|
+
* that had no real vertical border line above or below it — and still
|
|
9
|
+
* unconditionally emitted a ├ tee character there, producing a stray,
|
|
10
|
+
* disconnected junction glyph instead of a plain horizontal line.
|
|
11
|
+
*
|
|
12
|
+
* Not reproducible in TD direction or with the ASCII charset (`useAscii:
|
|
13
|
+
* true`), since ASCII mode skips box-start connectors entirely.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
+
import { findOrphanedJunctions } from '../validate.ts'
|
|
18
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { join, dirname } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const ISSUE_86_REPRO = `flowchart LR
|
|
23
|
+
A{Decision} -->|Yes| B[Do thing]
|
|
24
|
+
A -->|No| C[Other thing]`
|
|
25
|
+
|
|
26
|
+
describe('issue #86: stray ├ on decision-node edge labels (LR, Unicode)', () => {
|
|
27
|
+
it('does not emit an orphaned ├ where the "Yes" edge exits the decision box', () => {
|
|
28
|
+
const out = renderMermaidASCII(ISSUE_86_REPRO, { colorMode: 'none' })
|
|
29
|
+
|
|
30
|
+
const orphans = findOrphanedJunctions(out)
|
|
31
|
+
expect(orphans).toEqual([])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('places the "Yes" edge box-start exactly on the decision box\'s real right border, not drifted onto a blank cell', () => {
|
|
35
|
+
// A sibling fix (issue #64 bug 5, box-start connector drift) re-anchors
|
|
36
|
+
// horizontal connectors to the node's *actual* rendered border column
|
|
37
|
+
// instead of a grid-centered position that a sibling label could widen.
|
|
38
|
+
// Combined with this fix's validation, the "Yes" connector now lands
|
|
39
|
+
// exactly on the decision box's real border — so the correct output
|
|
40
|
+
// *is* a genuine ├ junction there (replacing what's a plain │ on every
|
|
41
|
+
// other row of that same border column), not a stray one. The
|
|
42
|
+
// structural check above (findOrphanedJunctions) is what actually
|
|
43
|
+
// proves it's not orphaned; this test locks in that the junction's
|
|
44
|
+
// column lines up with the box's own border on adjacent rows, which is
|
|
45
|
+
// what "not drifted" means concretely.
|
|
46
|
+
const out = renderMermaidASCII(ISSUE_86_REPRO, { colorMode: 'none' })
|
|
47
|
+
const lines = out.split('\n')
|
|
48
|
+
const yesLineIdx = lines.findIndex((l) => l.includes('Yes'))
|
|
49
|
+
expect(yesLineIdx).toBeGreaterThanOrEqual(0)
|
|
50
|
+
|
|
51
|
+
const yesRow = lines[yesLineIdx]!
|
|
52
|
+
const junctionCol = yesRow.indexOf('├')
|
|
53
|
+
expect(junctionCol).toBeGreaterThanOrEqual(0)
|
|
54
|
+
|
|
55
|
+
// The same column on the rows immediately above/below must be the
|
|
56
|
+
// decision box's own plain vertical border — confirming the junction
|
|
57
|
+
// sits exactly on a real border line, not on a blank/drifted cell.
|
|
58
|
+
expect(lines[yesLineIdx - 1]?.[junctionCol]).toBe('│')
|
|
59
|
+
expect(lines[yesLineIdx + 1]?.[junctionCol]).toBe('│')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('still renders a genuine ├ junction for a normal single-edge box-start', () => {
|
|
63
|
+
// Sanity check: the fix must not turn into a false negative that
|
|
64
|
+
// removes legitimate tee/junction characters where a real vertical
|
|
65
|
+
// border line does exist above and below.
|
|
66
|
+
const out = renderMermaidASCII(
|
|
67
|
+
`flowchart LR
|
|
68
|
+
A[Box A] --> B[Box B]`,
|
|
69
|
+
{ colorMode: 'none' },
|
|
70
|
+
)
|
|
71
|
+
expect(out).toContain('├')
|
|
72
|
+
expect(findOrphanedJunctions(out)).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('still renders a genuine ┬ junction for a TD fan-out box-start', () => {
|
|
76
|
+
// Labeled edges opt out of edge bundling (see analyzeEdgeBundles'
|
|
77
|
+
// canBundle check), so both edges go through the plain drawArrow /
|
|
78
|
+
// drawBoxStart path exercised by this fix rather than the separate
|
|
79
|
+
// fan-out bundle trunk/junction logic.
|
|
80
|
+
const out = renderMermaidASCII(
|
|
81
|
+
`flowchart TD
|
|
82
|
+
A[Box A] -->|one| B[Box B]
|
|
83
|
+
A -->|two| C[Box C]`,
|
|
84
|
+
{ colorMode: 'none' },
|
|
85
|
+
)
|
|
86
|
+
expect(out).toContain('┬')
|
|
87
|
+
expect(findOrphanedJunctions(out)).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('has no orphaned junctions across all Unicode golden fixtures', () => {
|
|
91
|
+
const dir = join(
|
|
92
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
93
|
+
'testdata',
|
|
94
|
+
'unicode',
|
|
95
|
+
)
|
|
96
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.txt'))
|
|
97
|
+
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
const content = readFileSync(join(dir, file), 'utf-8')
|
|
100
|
+
const expected = content.split('\n---\n')[1] ?? content.split('---')[1]
|
|
101
|
+
if (!expected) continue
|
|
102
|
+
const orphans = findOrphanedJunctions(expected)
|
|
103
|
+
expect(orphans, `orphaned junction(s) in fixture: ${file}`).toEqual([])
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for `src/ascii/display-width.ts`'s grapheme-cluster-based
|
|
3
|
+
* width algorithm, covering both bugs it fixes:
|
|
4
|
+
*
|
|
5
|
+
* - #205: a decomposed combining mark (base char + Mn/Me code point) must
|
|
6
|
+
* contribute 0 columns of its own, not 1.
|
|
7
|
+
* - #214: a composed multi-code-point grapheme cluster (ZWJ emoji sequence,
|
|
8
|
+
* flag via regional indicators, skin-tone modifier) must be measured
|
|
9
|
+
* once per cluster, not once per code point.
|
|
10
|
+
*
|
|
11
|
+
* The render-level regression suites (`ascii-combining-mark-width.test.ts`,
|
|
12
|
+
* `ascii-emoji-cluster-width.test.ts`) verify these against a fully
|
|
13
|
+
* independent oracle; this file tests the module's own public API directly,
|
|
14
|
+
* including the degenerate lone-combining-mark case that a full diagram
|
|
15
|
+
* render wouldn't naturally exercise.
|
|
16
|
+
*
|
|
17
|
+
* Strings are built from explicit `\u` code point escapes rather than typed
|
|
18
|
+
* accented/emoji literals, so the decomposed/precomposed and
|
|
19
|
+
* single-cluster/multi-code-point distinctions under test can't be silently
|
|
20
|
+
* altered by source-file Unicode normalization.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from 'vitest'
|
|
23
|
+
import {
|
|
24
|
+
charDisplayWidth,
|
|
25
|
+
displayWidth,
|
|
26
|
+
toDisplayCells,
|
|
27
|
+
WIDE_CHAR_PLACEHOLDER,
|
|
28
|
+
} from '../display-width.ts'
|
|
29
|
+
|
|
30
|
+
const COMBINING_ACUTE = '́'
|
|
31
|
+
const COMBINING_GRAVE = '̀'
|
|
32
|
+
// "café", decomposed: c a f e + COMBINING ACUTE ACCENT (5 code points).
|
|
33
|
+
const DECOMPOSED_CAFE = 'caf' + 'e' + COMBINING_ACUTE
|
|
34
|
+
// "café", precomposed: c a f é, where é is the single code point U+00E9
|
|
35
|
+
// (4 code points).
|
|
36
|
+
const PRECOMPOSED_CAFE = 'caf' + 'é'
|
|
37
|
+
|
|
38
|
+
// 👨👩👧 — MAN, ZWJ, WOMAN, ZWJ, GIRL (5 code points, 1 grapheme cluster).
|
|
39
|
+
const ZWJ_FAMILY = '\u{1F468}' + '' + '\u{1F469}' + '' + '\u{1F467}'
|
|
40
|
+
// 🇺🇸 — REGIONAL INDICATOR U + REGIONAL INDICATOR S (2 code points, 1 cluster).
|
|
41
|
+
const FLAG_US = '\u{1F1FA}' + '\u{1F1F8}'
|
|
42
|
+
// 👍🏽 — THUMBS UP + MEDIUM SKIN TONE modifier (2 code points, 1 cluster).
|
|
43
|
+
const THUMB_MEDIUM = '\u{1F44D}' + '\u{1F3FD}'
|
|
44
|
+
|
|
45
|
+
describe('displayWidth — combining marks (issue #205)', () => {
|
|
46
|
+
it('measures a decomposed "café" (e + U+0301) as 4 columns', () => {
|
|
47
|
+
expect([...DECOMPOSED_CAFE].length).toBe(5) // 5 code points
|
|
48
|
+
expect(displayWidth(DECOMPOSED_CAFE)).toBe(4)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('measures the precomposed form identically', () => {
|
|
52
|
+
expect([...PRECOMPOSED_CAFE].length).toBe(4) // 4 code points already
|
|
53
|
+
expect(displayWidth(PRECOMPOSED_CAFE)).toBe(4)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not add width for multiple stacked combining marks on one base', () => {
|
|
57
|
+
// "e" + combining acute + combining grave — still one grapheme cluster.
|
|
58
|
+
const stacked = 'e' + COMBINING_ACUTE + COMBINING_GRAVE
|
|
59
|
+
expect(displayWidth(stacked)).toBe(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('treats a lone combining mark with no base character as zero-width', () => {
|
|
63
|
+
expect(charDisplayWidth(COMBINING_ACUTE)).toBe(0)
|
|
64
|
+
expect(displayWidth(COMBINING_ACUTE)).toBe(0)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('produces no grid cell for a lone combining mark', () => {
|
|
68
|
+
expect(toDisplayCells(COMBINING_ACUTE)).toEqual([])
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('displayWidth — composed grapheme clusters (issue #214)', () => {
|
|
73
|
+
it('measures a ZWJ family emoji as 2 columns, not 8', () => {
|
|
74
|
+
expect([...ZWJ_FAMILY].length).toBe(5) // 5 code points
|
|
75
|
+
expect(displayWidth(ZWJ_FAMILY)).toBe(2)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('measures a regional-indicator flag as 2 columns, not 4', () => {
|
|
79
|
+
expect([...FLAG_US].length).toBe(2)
|
|
80
|
+
expect(displayWidth(FLAG_US)).toBe(2)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('measures a skin-tone-modified emoji as 2 columns, not 4', () => {
|
|
84
|
+
expect([...THUMB_MEDIUM].length).toBe(2)
|
|
85
|
+
expect(displayWidth(THUMB_MEDIUM)).toBe(2)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('still measures plain CJK correctly (contrast case)', () => {
|
|
89
|
+
expect(displayWidth('日本')).toBe(4) // 日本
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('measures a mixed string combining marks and emoji clusters', () => {
|
|
93
|
+
const text = DECOMPOSED_CAFE + ' ' + ZWJ_FAMILY
|
|
94
|
+
expect(displayWidth(text)).toBe(4 + 1 + 2)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('displayWidth — VS16 emoji-presentation override', () => {
|
|
99
|
+
// ▶️ / ◀️ — the arrowhead glyphs excluded from `isWideChar` as narrow
|
|
100
|
+
// Geometric Shapes characters (see text-metrics.ts), explicitly forced to
|
|
101
|
+
// emoji presentation via a trailing U+FE0F VARIATION SELECTOR-16.
|
|
102
|
+
const RIGHT_ARROW_VS16 = '▶' + '\u{FE0F}'
|
|
103
|
+
const LEFT_ARROW_VS16 = '◀' + '\u{FE0F}'
|
|
104
|
+
|
|
105
|
+
it('measures VS16-suffixed arrowheads as 2 columns despite the base character being narrow', () => {
|
|
106
|
+
expect(charDisplayWidth(RIGHT_ARROW_VS16)).toBe(2)
|
|
107
|
+
expect(charDisplayWidth(LEFT_ARROW_VS16)).toBe(2)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('still measures the bare (VS16-less) arrowheads as narrow', () => {
|
|
111
|
+
expect(charDisplayWidth('▶')).toBe(1)
|
|
112
|
+
expect(charDisplayWidth('◀')).toBe(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('keeps cell count in sync with displayWidth for a VS16 arrowhead', () => {
|
|
116
|
+
expect(toDisplayCells(RIGHT_ARROW_VS16)).toEqual([
|
|
117
|
+
RIGHT_ARROW_VS16,
|
|
118
|
+
WIDE_CHAR_PLACEHOLDER,
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('toDisplayCells — grid-cell/column-count invariant', () => {
|
|
124
|
+
it('keeps cell count in sync with displayWidth for a decomposed mark', () => {
|
|
125
|
+
expect(toDisplayCells(DECOMPOSED_CAFE).length).toBe(
|
|
126
|
+
displayWidth(DECOMPOSED_CAFE),
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('keeps cell count in sync with displayWidth for a ZWJ emoji', () => {
|
|
131
|
+
expect(toDisplayCells(ZWJ_FAMILY).length).toBe(displayWidth(ZWJ_FAMILY))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('writes the whole composed cluster into a single cell, followed by a placeholder', () => {
|
|
135
|
+
const cells = toDisplayCells(ZWJ_FAMILY)
|
|
136
|
+
expect(cells).toEqual([ZWJ_FAMILY, WIDE_CHAR_PLACEHOLDER])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('writes a base character + combining mark into a single cell with no placeholder', () => {
|
|
140
|
+
const decomposedE = 'e' + COMBINING_ACUTE
|
|
141
|
+
const cells = toDisplayCells(decomposedE)
|
|
142
|
+
expect(cells).toEqual([decomposedE])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('leaves plain ASCII behavior unchanged', () => {
|
|
146
|
+
expect(toDisplayCells('ab')).toEqual(['a', 'b'])
|
|
147
|
+
expect(displayWidth('ab')).toBe(2)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('leaves plain CJK behavior unchanged', () => {
|
|
151
|
+
const cells = toDisplayCells('日') // 日
|
|
152
|
+
expect(cells).toEqual(['日', WIDE_CHAR_PLACEHOLDER])
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII arrow-drawing coverage — no-arrow edges, bidirectional arrowheads,
|
|
3
|
+
// and upward labeled edges (src/ascii/draw-arrows.ts)
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
8
|
+
import { unicodeArrowChar, asciiArrowChar } from '../draw-arrows.ts'
|
|
9
|
+
import {
|
|
10
|
+
Up,
|
|
11
|
+
Down,
|
|
12
|
+
Left,
|
|
13
|
+
Right,
|
|
14
|
+
UpperRight,
|
|
15
|
+
UpperLeft,
|
|
16
|
+
LowerRight,
|
|
17
|
+
LowerLeft,
|
|
18
|
+
Middle,
|
|
19
|
+
} from '../types.ts'
|
|
20
|
+
|
|
21
|
+
describe('ASCII arrow drawing: no-arrow edges', () => {
|
|
22
|
+
it('renders a solid no-arrow edge without an arrowhead in unicode mode', () => {
|
|
23
|
+
const result = renderMermaidASCII(`
|
|
24
|
+
graph LR
|
|
25
|
+
A --- B
|
|
26
|
+
`)
|
|
27
|
+
expect(result).toContain('─')
|
|
28
|
+
expect(result).not.toMatch(/[►◄▲▼]/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders a solid no-arrow edge without an arrowhead in ascii mode', () => {
|
|
32
|
+
const result = renderMermaidASCII(
|
|
33
|
+
`
|
|
34
|
+
graph LR
|
|
35
|
+
A --- B
|
|
36
|
+
`,
|
|
37
|
+
{ useAscii: true },
|
|
38
|
+
)
|
|
39
|
+
expect(result).not.toMatch(/[<>^v]/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders a dotted no-arrow edge without an arrowhead', () => {
|
|
43
|
+
const result = renderMermaidASCII(`
|
|
44
|
+
graph LR
|
|
45
|
+
A -.- B
|
|
46
|
+
`)
|
|
47
|
+
expect(result).toContain('┄')
|
|
48
|
+
expect(result).not.toMatch(/[►◄▲▼]/)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders a thick no-arrow edge without an arrowhead', () => {
|
|
52
|
+
const result = renderMermaidASCII(`
|
|
53
|
+
graph LR
|
|
54
|
+
A === B
|
|
55
|
+
`)
|
|
56
|
+
expect(result).toContain('━')
|
|
57
|
+
expect(result).not.toMatch(/[►◄▲▼]/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('renders a vertical no-arrow edge without an arrowhead', () => {
|
|
61
|
+
const result = renderMermaidASCII(`
|
|
62
|
+
graph TD
|
|
63
|
+
A --- B
|
|
64
|
+
`)
|
|
65
|
+
expect(result).not.toMatch(/[►◄▲▼]/)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('renders a labeled no-arrow edge', () => {
|
|
69
|
+
const result = renderMermaidASCII(`
|
|
70
|
+
graph LR
|
|
71
|
+
A ---|connects| B
|
|
72
|
+
`)
|
|
73
|
+
expect(result).toContain('connects')
|
|
74
|
+
expect(result).not.toMatch(/[►◄▲▼]/)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('ASCII arrow drawing: bidirectional edges', () => {
|
|
79
|
+
it('draws arrowheads at both ends of a solid bidirectional edge (LR, unicode)', () => {
|
|
80
|
+
const result = renderMermaidASCII(`
|
|
81
|
+
graph LR
|
|
82
|
+
A <--> B
|
|
83
|
+
`)
|
|
84
|
+
expect(result).toContain('◄')
|
|
85
|
+
expect(result).toContain('►')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('draws arrowheads at both ends of a solid bidirectional edge (LR, ascii)', () => {
|
|
89
|
+
const result = renderMermaidASCII(
|
|
90
|
+
`
|
|
91
|
+
graph LR
|
|
92
|
+
A <--> B
|
|
93
|
+
`,
|
|
94
|
+
{ useAscii: true },
|
|
95
|
+
)
|
|
96
|
+
expect(result).toContain('<')
|
|
97
|
+
expect(result).toContain('>')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('draws arrowheads at both ends of a vertical bidirectional edge (TD, unicode)', () => {
|
|
101
|
+
const result = renderMermaidASCII(`
|
|
102
|
+
graph TD
|
|
103
|
+
A <--> B
|
|
104
|
+
`)
|
|
105
|
+
expect(result).toContain('▲')
|
|
106
|
+
expect(result).toContain('▼')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('draws arrowheads at both ends of a vertical bidirectional edge (TD, ascii)', () => {
|
|
110
|
+
const result = renderMermaidASCII(
|
|
111
|
+
`
|
|
112
|
+
graph TD
|
|
113
|
+
A <--> B
|
|
114
|
+
`,
|
|
115
|
+
{ useAscii: true },
|
|
116
|
+
)
|
|
117
|
+
expect(result).toContain('^')
|
|
118
|
+
expect(result).toContain('v')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('draws arrowheads at both ends of a bidirectional edge reversed by layout (BT)', () => {
|
|
122
|
+
const result = renderMermaidASCII(`
|
|
123
|
+
graph BT
|
|
124
|
+
A <--> B
|
|
125
|
+
`)
|
|
126
|
+
expect(result).toContain('▲')
|
|
127
|
+
expect(result).toContain('▼')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('draws arrowheads on a dotted bidirectional edge', () => {
|
|
131
|
+
const result = renderMermaidASCII(`
|
|
132
|
+
graph LR
|
|
133
|
+
A <-.-> B
|
|
134
|
+
`)
|
|
135
|
+
expect(result).toContain('┄')
|
|
136
|
+
expect(result).toContain('◄')
|
|
137
|
+
expect(result).toContain('►')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('draws arrowheads on a thick bidirectional edge', () => {
|
|
141
|
+
const result = renderMermaidASCII(`
|
|
142
|
+
graph LR
|
|
143
|
+
A <==> B
|
|
144
|
+
`)
|
|
145
|
+
expect(result).toContain('━')
|
|
146
|
+
expect(result).toContain('◄')
|
|
147
|
+
expect(result).toContain('►')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('draws both arrowheads on a bidirectional back-edge routed around other nodes', () => {
|
|
151
|
+
const result = renderMermaidASCII(`
|
|
152
|
+
graph LR
|
|
153
|
+
A --> B
|
|
154
|
+
B --> C
|
|
155
|
+
C <--> A
|
|
156
|
+
`)
|
|
157
|
+
expect(result).toContain('►')
|
|
158
|
+
expect(result).toContain('▲')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('draws a labeled bidirectional edge', () => {
|
|
162
|
+
const result = renderMermaidASCII(`
|
|
163
|
+
graph LR
|
|
164
|
+
A <-->|sync| B
|
|
165
|
+
`)
|
|
166
|
+
expect(result).toContain('sync')
|
|
167
|
+
expect(result).toContain('◄')
|
|
168
|
+
expect(result).toContain('►')
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('unicodeArrowChar / asciiArrowChar: direction-to-glyph mapping', () => {
|
|
173
|
+
// Direct unit coverage for every Direction, including LowerLeft — the
|
|
174
|
+
// ELK/pathfinder layout this module draws from only produces a diagonal
|
|
175
|
+
// direction via determinePath's rare Case-4 fallback, and every
|
|
176
|
+
// hand-written or gallery diagram that triggers it happens to need the
|
|
177
|
+
// target down-and-*right* of its source, never down-and-left. See
|
|
178
|
+
// issue #1062's drawArrowHead refactor.
|
|
179
|
+
it('maps all four orthogonal directions to filled triangles (unicode)', () => {
|
|
180
|
+
expect(unicodeArrowChar(Up)).toBe('▲')
|
|
181
|
+
expect(unicodeArrowChar(Down)).toBe('▼')
|
|
182
|
+
expect(unicodeArrowChar(Left)).toBe('◄')
|
|
183
|
+
expect(unicodeArrowChar(Right)).toBe('►')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('maps all four diagonal directions to Arrows-block glyphs (unicode)', () => {
|
|
187
|
+
expect(unicodeArrowChar(UpperRight)).toBe('↗')
|
|
188
|
+
expect(unicodeArrowChar(UpperLeft)).toBe('↖')
|
|
189
|
+
expect(unicodeArrowChar(LowerRight)).toBe('↘')
|
|
190
|
+
expect(unicodeArrowChar(LowerLeft)).toBe('↙')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('returns undefined for Middle (no arrowhead glyph)', () => {
|
|
194
|
+
expect(unicodeArrowChar(Middle)).toBeUndefined()
|
|
195
|
+
expect(asciiArrowChar(Middle)).toBeUndefined()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('maps all four orthogonal directions to ASCII carets (ascii mode)', () => {
|
|
199
|
+
expect(asciiArrowChar(Up)).toBe('^')
|
|
200
|
+
expect(asciiArrowChar(Down)).toBe('v')
|
|
201
|
+
expect(asciiArrowChar(Left)).toBe('<')
|
|
202
|
+
expect(asciiArrowChar(Right)).toBe('>')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('has no diagonal glyphs in ASCII mode', () => {
|
|
206
|
+
expect(asciiArrowChar(UpperRight)).toBeUndefined()
|
|
207
|
+
expect(asciiArrowChar(UpperLeft)).toBeUndefined()
|
|
208
|
+
expect(asciiArrowChar(LowerRight)).toBeUndefined()
|
|
209
|
+
expect(asciiArrowChar(LowerLeft)).toBeUndefined()
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('ASCII arrow drawing: upward edge labels', () => {
|
|
214
|
+
it('offsets the label on an edge that routes upward', () => {
|
|
215
|
+
const result = renderMermaidASCII(`
|
|
216
|
+
graph TD
|
|
217
|
+
A --> B
|
|
218
|
+
B -->|back| A
|
|
219
|
+
`)
|
|
220
|
+
expect(result).toContain('back')
|
|
221
|
+
expect(result).toContain('▲')
|
|
222
|
+
expect(result).toContain('▼')
|
|
223
|
+
})
|
|
224
|
+
})
|