@zombie-mermaid/ascii-renderer 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.js +5392 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
- package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
- package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
- package/src/__tests__/ascii-canvas-write.test.ts +132 -0
- package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +150 -0
- package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
- package/src/__tests__/ascii-class-padding.test.ts +104 -0
- package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
- package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
- package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
- package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
- package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
- package/src/__tests__/ascii-edge-styles.test.ts +149 -0
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
- package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-er-padding.test.ts +114 -0
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
- package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
- package/src/__tests__/ascii-lane-search.test.ts +207 -0
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
- package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
- package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
- package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
- package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
- package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
- package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
- package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
- package/src/__tests__/ascii-shapes-special.test.ts +344 -0
- package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
- package/src/__tests__/ascii-shapes-state.test.ts +224 -0
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-territory-unit.test.ts +219 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/class-arrow-directions.test.ts +505 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-cell-styles.test.ts +278 -0
- package/src/__tests__/grid-occupancy.test.ts +240 -0
- package/src/__tests__/helpers/ascii-form.ts +142 -0
- package/src/__tests__/helpers/terminal-display-width.ts +74 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
- package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
- package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
- package/src/__tests__/testdata/ascii/comments.txt +23 -0
- package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
- package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
- package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
- package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
- package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
- package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
- package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
- package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
- package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/ascii/single_node.txt +8 -0
- package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
- package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
- package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
- package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
- package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
- package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
- package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
- package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
- package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
- package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
- package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
- package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
- package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
- package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
- package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
- package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
- package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
- package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
- package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
- package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
- package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
- package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
- package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
- package/src/__tests__/testdata/unicode/comments.txt +23 -0
- package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
- package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
- package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
- package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
- package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
- package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
- package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
- package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
- package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
- package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
- package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
- package/src/__tests__/testdata/unicode/single_node.txt +8 -0
- package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
- package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
- package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
- package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
- package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
- package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
- package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
- package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
- package/src/__tests__/xychart-ascii.test.ts +376 -0
- package/src/ansi.ts +490 -0
- package/src/canvas.ts +757 -0
- package/src/class-diagram.ts +2001 -0
- package/src/converter.ts +446 -0
- package/src/coords.ts +58 -0
- package/src/display-width.ts +151 -0
- package/src/draw-arrows.ts +593 -0
- package/src/draw-boxes.ts +267 -0
- package/src/draw-bundles.ts +611 -0
- package/src/draw-lines.ts +174 -0
- package/src/draw-subgraphs.ts +108 -0
- package/src/draw.ts +350 -0
- package/src/edge-bundling.ts +435 -0
- package/src/edge-cell-styles.ts +209 -0
- package/src/edge-routing.ts +1070 -0
- package/src/er-diagram.ts +1488 -0
- package/src/flowchart.ts +94 -0
- package/src/grid-occupancy.ts +234 -0
- package/src/grid.ts +1309 -0
- package/src/hyperlinks.ts +248 -0
- package/src/index.ts +163 -0
- package/src/lane-search.ts +68 -0
- package/src/multiline-utils.ts +82 -0
- package/src/pathfinder.ts +448 -0
- package/src/registry.ts +88 -0
- package/src/sequence.ts +1318 -0
- package/src/shapes/circle.ts +31 -0
- package/src/shapes/corners.ts +273 -0
- package/src/shapes/diamond.ts +31 -0
- package/src/shapes/hexagon.ts +35 -0
- package/src/shapes/index.ts +123 -0
- package/src/shapes/rectangle.ts +199 -0
- package/src/shapes/rounded.ts +31 -0
- package/src/shapes/special.ts +360 -0
- package/src/shapes/stadium.ts +122 -0
- package/src/shapes/state.ts +204 -0
- package/src/shapes/types.ts +78 -0
- package/src/territory.ts +136 -0
- package/src/types.ts +454 -0
- package/src/validate.ts +189 -0
- package/src/xychart.ts +1085 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zombie-mermaid/ascii-renderer",
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "ASCII and Unicode box-drawing renderer for zombie-mermaid diagrams.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "dist/index.cjs",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src/",
|
|
29
|
+
"dist/",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@zombie-mermaid/core": "workspace:*",
|
|
34
|
+
"@zombie-mermaid/mermaid-parser": "workspace:*"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for #1083: the last edge in a mixed-style fan-out got a
|
|
3
|
+
* bogus diagonal arrowhead (↘) instead of a straight one (▼), even though
|
|
4
|
+
* its line was perfectly vertical with zero horizontal drift.
|
|
5
|
+
*
|
|
6
|
+
* Root cause: that edge's route fell through to determinePath's Case-4
|
|
7
|
+
* direct fallback (see #418), producing a single non-axis-aligned grid
|
|
8
|
+
* segment. draw-lines.ts's drawLine draws that as an L (horizontal run,
|
|
9
|
+
* then vertical run) folded into one `linesDrawn` entry. drawArrowHead used
|
|
10
|
+
* to derive direction from that entry's *first* and *last* point, which
|
|
11
|
+
* spans both legs of the L and reads as diagonal — even when the actual
|
|
12
|
+
* final approach into the arrowhead is a plain vertical step. Fixed by
|
|
13
|
+
* deriving direction from the last two points (the final step) instead.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
+
|
|
18
|
+
const ARROWHEAD_CHARS = '▼▲◄►↖↗↘↙'
|
|
19
|
+
|
|
20
|
+
function arrowCharsInLastRowBeforeTargets(source: string): string[] {
|
|
21
|
+
const lines = renderMermaidASCII(source, { colorMode: 'none' }).split('\n')
|
|
22
|
+
const arrowRow = lines.find((l) =>
|
|
23
|
+
[...l].some((ch) => ARROWHEAD_CHARS.includes(ch)),
|
|
24
|
+
)!
|
|
25
|
+
return [...arrowRow].filter((ch) => ARROWHEAD_CHARS.includes(ch))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('arrowhead direction on a Case-4 direct-fallback (diagonal) path', () => {
|
|
29
|
+
it('renders every arrowhead as ▼ when thick is the last mixed-style edge', () => {
|
|
30
|
+
const source = `graph TD
|
|
31
|
+
A[Source] -->|solid| B[Target 1]
|
|
32
|
+
A -.->|dotted| C[Target 2]
|
|
33
|
+
A ==>|thick| D[Target 3]`
|
|
34
|
+
|
|
35
|
+
const arrows = arrowCharsInLastRowBeforeTargets(source)
|
|
36
|
+
expect(arrows).toEqual(['▼', '▼', '▼'])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders every arrowhead as ▼ regardless of which style is last', () => {
|
|
40
|
+
// Same three styles, reordered so `solid` (not `thick`) is the one that
|
|
41
|
+
// falls through to the Case-4 fallback — the bug followed *position*,
|
|
42
|
+
// not the specific style.
|
|
43
|
+
const source = `graph TD
|
|
44
|
+
A[Source] ==>|thick| B[Target 1]
|
|
45
|
+
A -.->|dotted| C[Target 2]
|
|
46
|
+
A -->|solid| D[Target 3]`
|
|
47
|
+
|
|
48
|
+
const arrows = arrowCharsInLastRowBeforeTargets(source)
|
|
49
|
+
expect(arrows).toEqual(['▼', '▼', '▼'])
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for `firstClaimWins` (packages/ascii-renderer/src/canvas.ts),
|
|
3
|
+
* added in response to a CodeRabbit finding on PR #1093 (the #1067 fix):
|
|
4
|
+
* https://github.com/dfadler/zombie-mermaid/pull/1093#discussion_r… (canvas.ts:446).
|
|
5
|
+
*
|
|
6
|
+
* `firstClaimWins` runs before `mergeCanvases` on the line-canvas layer to
|
|
7
|
+
* fix #1067's "one edge's style bleeds into another's" bug — but its
|
|
8
|
+
* original implementation suppressed a later canvas's character at any
|
|
9
|
+
* already-claimed cell purely by coordinate, with no regard for what the
|
|
10
|
+
* two characters actually were. That silently broke a genuine perpendicular
|
|
11
|
+
* crossing between two plain box-drawing edges (one edge's own `─`, another
|
|
12
|
+
* unrelated edge's own `│`): `mergeCanvases`'s existing junction-merge logic
|
|
13
|
+
* (`isJunctionChar` + `mergeJunctions`) would normally combine them into
|
|
14
|
+
* `┼`, but firstClaimWins now deleted the second character before
|
|
15
|
+
* mergeCanvases ever saw it, so the crossing silently became whichever edge
|
|
16
|
+
* drew first instead. The fix: only suppress a later cell when the two
|
|
17
|
+
* characters *wouldn't* form a meaningful junction merge (i.e. when at
|
|
18
|
+
* least one of them isn't a junction char at all — the actual #1067
|
|
19
|
+
* scenario, dashed vs. heavy characters, neither of which is in
|
|
20
|
+
* `JUNCTION_CHARS`).
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from 'vitest'
|
|
23
|
+
import { mkCanvas, firstClaimWins, mergeCanvases } from '../canvas.ts'
|
|
24
|
+
import type { Canvas } from '../types.ts'
|
|
25
|
+
|
|
26
|
+
function canvasWithChar(
|
|
27
|
+
width: number,
|
|
28
|
+
height: number,
|
|
29
|
+
x: number,
|
|
30
|
+
y: number,
|
|
31
|
+
c: string,
|
|
32
|
+
): Canvas {
|
|
33
|
+
const canvas = mkCanvas(width, height)
|
|
34
|
+
canvas[x]![y] = c
|
|
35
|
+
return canvas
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('firstClaimWins', () => {
|
|
39
|
+
it('preserves a genuine perpendicular crossing between two plain junction characters', () => {
|
|
40
|
+
// Edge A's own line draws '─' at (2,2); edge B's own (unrelated) line
|
|
41
|
+
// independently draws '│' at the very same cell — a real crossing, not
|
|
42
|
+
// a style conflict.
|
|
43
|
+
const horizontal = canvasWithChar(4, 4, 2, 2, '─')
|
|
44
|
+
const vertical = canvasWithChar(4, 4, 2, 2, '│')
|
|
45
|
+
|
|
46
|
+
const resolved = firstClaimWins([horizontal, vertical])
|
|
47
|
+
// Both canvases must still carry their own character at the shared
|
|
48
|
+
// cell — dropping either one would make it impossible for
|
|
49
|
+
// mergeCanvases's junction-merge logic below to ever see both.
|
|
50
|
+
expect(resolved[0]![2]![2]).toBe('─')
|
|
51
|
+
expect(resolved[1]![2]![2]).toBe('│')
|
|
52
|
+
|
|
53
|
+
const merged = mergeCanvases(
|
|
54
|
+
mkCanvas(4, 4),
|
|
55
|
+
{ x: 0, y: 0 },
|
|
56
|
+
false,
|
|
57
|
+
...resolved,
|
|
58
|
+
)
|
|
59
|
+
expect(merged[2]![2]).toBe('┼')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('still suppresses a later same-orientation duplicate (collinear overlap)', () => {
|
|
63
|
+
const first = canvasWithChar(4, 4, 1, 1, '─')
|
|
64
|
+
const second = canvasWithChar(4, 4, 1, 1, '─')
|
|
65
|
+
const resolved = firstClaimWins([first, second])
|
|
66
|
+
expect(resolved[0]![1]![1]).toBe('─')
|
|
67
|
+
expect(resolved[1]![1]![1]).toBe(' ')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('still suppresses a later non-junction character at an already-claimed cell (#1067)', () => {
|
|
71
|
+
// The original #1067 scenario: a dashed edge's own leg ('┄', not a
|
|
72
|
+
// junction char) sharing a cell with a heavy edge's own leg ('━', also
|
|
73
|
+
// not a junction char) — neither can form a meaningful merge, so first
|
|
74
|
+
// claim must still win.
|
|
75
|
+
const dashed = canvasWithChar(4, 4, 1, 1, '┄')
|
|
76
|
+
const heavy = canvasWithChar(4, 4, 1, 1, '━')
|
|
77
|
+
const resolved = firstClaimWins([dashed, heavy])
|
|
78
|
+
expect(resolved[0]![1]![1]).toBe('┄')
|
|
79
|
+
expect(resolved[1]![1]![1]).toBe(' ')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('suppresses a later cell when only one side is a junction character', () => {
|
|
83
|
+
const junction = canvasWithChar(4, 4, 1, 1, '│')
|
|
84
|
+
const heavy = canvasWithChar(4, 4, 1, 1, '━')
|
|
85
|
+
const resolved = firstClaimWins([junction, heavy])
|
|
86
|
+
expect(resolved[0]![1]![1]).toBe('│')
|
|
87
|
+
expect(resolved[1]![1]![1]).toBe(' ')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression for a bug found while investigating PR #1093 (the #1067 fix):
|
|
3
|
+
* `setCanvasSizeToGrid`/`setRoleCanvasSizeToGrid` sized the canvas from
|
|
4
|
+
* `columnWidth`/`rowHeight` alone, with no way to account for
|
|
5
|
+
* `graph.offsetX`/`offsetY` — the shift `offsetDrawingForSubgraphs` applies
|
|
6
|
+
* whenever a subgraph's own border padding would otherwise push its content
|
|
7
|
+
* negative. Every `gridToDrawingCoord` call (including edge line-drawing)
|
|
8
|
+
* adds that offset to its result, but `createMapping` sized the canvas
|
|
9
|
+
* *before* the offset was known, leaving it exactly `offsetX`/`offsetY` too
|
|
10
|
+
* small. Node boxes were retroactively shifted into the correct position,
|
|
11
|
+
* but nothing widened the canvas array to match — so any edge line whose
|
|
12
|
+
* drawing coordinate landed in that unreserved margin was silently dropped
|
|
13
|
+
* by `write()`'s out-of-bounds clip. This surfaced as a fully disconnected
|
|
14
|
+
* edge (a dangling corner glyph with no line connecting it to its node) in
|
|
15
|
+
* the "State: Composite States" sample's `Error --> Idle : retry` edge,
|
|
16
|
+
* once the #1067 chain-overlap fix rerouted that edge through a column
|
|
17
|
+
* further right than anything else in the diagram — see that sample's
|
|
18
|
+
* regenerated baseline in this same PR.
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect } from 'vitest'
|
|
21
|
+
import {
|
|
22
|
+
mkCanvas,
|
|
23
|
+
mkRoleCanvas,
|
|
24
|
+
setCanvasSizeToGrid,
|
|
25
|
+
setRoleCanvasSizeToGrid,
|
|
26
|
+
getCanvasSize,
|
|
27
|
+
} from '../canvas.ts'
|
|
28
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
29
|
+
|
|
30
|
+
describe('setCanvasSizeToGrid / setRoleCanvasSizeToGrid — offset', () => {
|
|
31
|
+
it('reserves extra width/height for a nonzero offset', () => {
|
|
32
|
+
const canvas = mkCanvas(0, 0)
|
|
33
|
+
const columnWidth = new Map([
|
|
34
|
+
[0, 3],
|
|
35
|
+
[1, 4],
|
|
36
|
+
])
|
|
37
|
+
const rowHeight = new Map([
|
|
38
|
+
[0, 2],
|
|
39
|
+
[1, 5],
|
|
40
|
+
])
|
|
41
|
+
setCanvasSizeToGrid(canvas, columnWidth, rowHeight, 6, 3)
|
|
42
|
+
const [maxX, maxY] = getCanvasSize(canvas)
|
|
43
|
+
// columnWidth sums to 7, plus an offset of 6 => max valid index 12.
|
|
44
|
+
expect(maxX).toBe(12)
|
|
45
|
+
// rowHeight sums to 7, plus an offset of 3 => max valid index 9.
|
|
46
|
+
expect(maxY).toBe(9)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('matches the un-offset canvas size when offset is omitted (default 0)', () => {
|
|
50
|
+
const canvas = mkCanvas(0, 0)
|
|
51
|
+
const columnWidth = new Map([[0, 5]])
|
|
52
|
+
const rowHeight = new Map([[0, 4]])
|
|
53
|
+
setCanvasSizeToGrid(canvas, columnWidth, rowHeight)
|
|
54
|
+
const [maxX, maxY] = getCanvasSize(canvas)
|
|
55
|
+
expect(maxX).toBe(4)
|
|
56
|
+
expect(maxY).toBe(3)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('role canvas sizing accepts the same offset', () => {
|
|
60
|
+
const roleCanvas = mkRoleCanvas(0, 0)
|
|
61
|
+
const columnWidth = new Map([[0, 3]])
|
|
62
|
+
const rowHeight = new Map([[0, 2]])
|
|
63
|
+
setRoleCanvasSizeToGrid(roleCanvas, columnWidth, rowHeight, 5, 1)
|
|
64
|
+
expect(roleCanvas.length - 1).toBe(7)
|
|
65
|
+
expect((roleCanvas[0]?.length ?? 1) - 1).toBe(2)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('subgraph-offset edges reach their node border (#1093 regression)', () => {
|
|
70
|
+
it("draws the retry edge's connector all the way into Error's box, with no gap", () => {
|
|
71
|
+
const src = `stateDiagram-v2
|
|
72
|
+
[*] --> Idle
|
|
73
|
+
Idle --> Processing : submit
|
|
74
|
+
state Processing {
|
|
75
|
+
parse --> validate
|
|
76
|
+
validate --> execute
|
|
77
|
+
}
|
|
78
|
+
Processing --> Complete : done
|
|
79
|
+
Processing --> Error : fail
|
|
80
|
+
Error --> Idle : retry
|
|
81
|
+
Complete --> [*]`
|
|
82
|
+
|
|
83
|
+
const lines = renderMermaidASCII(src, { colorMode: 'none' }).split('\n')
|
|
84
|
+
const errorRow = lines.find((l) => l.includes('Error'))
|
|
85
|
+
expect(errorRow).toBeDefined()
|
|
86
|
+
|
|
87
|
+
// Error's own right border ('├', where the outgoing retry edge exits)
|
|
88
|
+
// must be immediately followed by a drawn connector character, not a
|
|
89
|
+
// gap — a disconnected edge leaves that cell (and the next) blank.
|
|
90
|
+
const portIndex = errorRow!.indexOf('├')
|
|
91
|
+
expect(portIndex).toBeGreaterThan(-1)
|
|
92
|
+
const afterPort = errorRow!.slice(portIndex + 1)
|
|
93
|
+
expect(afterPort).toMatch(/^[─┘└┐┌│]/)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct unit tests for `write()` (src/ascii/canvas.ts) — specifically its
|
|
3
|
+
* clip boundary in all four directions.
|
|
4
|
+
*
|
|
5
|
+
* These previously didn't exist: an adversarial review of PR #180
|
|
6
|
+
* instrumented the clip condition and ran the full suite (1318 passing
|
|
7
|
+
* tests at the time) and found the clip branch was never exercised — 0
|
|
8
|
+
* clip events fired. That gap is exactly how a real border-corruption
|
|
9
|
+
* regression in `drawSubgraphLabel` (src/ascii/draw-subgraphs.ts, see
|
|
10
|
+
* ascii-subgraph-label-border-clip.test.ts) got through green CI: no test
|
|
11
|
+
* distinguished `write()`'s clip semantics from the per-call-site guards it
|
|
12
|
+
* replaced.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { mkCanvas, mkRoleCanvas, write, getCanvasSize } from '../canvas.ts'
|
|
16
|
+
import type { CharRole } from '../types.ts'
|
|
17
|
+
|
|
18
|
+
describe('canvas.ts write()', () => {
|
|
19
|
+
it('writes within bounds, including exactly on the inclusive max edge', () => {
|
|
20
|
+
const canvas = mkCanvas(2, 2) // valid indices 0..2 in both axes
|
|
21
|
+
const [maxX, maxY] = getCanvasSize(canvas)
|
|
22
|
+
expect([maxX, maxY]).toEqual([2, 2])
|
|
23
|
+
|
|
24
|
+
write(canvas, 0, 0, 'A')
|
|
25
|
+
write(canvas, maxX, maxY, 'B')
|
|
26
|
+
write(canvas, maxX, 0, 'C')
|
|
27
|
+
write(canvas, 0, maxY, 'D')
|
|
28
|
+
|
|
29
|
+
expect(canvas[0]![0]).toBe('A')
|
|
30
|
+
expect(canvas[maxX]![maxY]).toBe('B')
|
|
31
|
+
expect(canvas[maxX]![0]).toBe('C')
|
|
32
|
+
expect(canvas[0]![maxY]).toBe('D')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('is a no-op at x = -1 (just past the low edge)', () => {
|
|
36
|
+
const canvas = mkCanvas(2, 2)
|
|
37
|
+
// No cell to inspect at x = -1; assert the call doesn't throw and
|
|
38
|
+
// leaves the rest of the canvas untouched.
|
|
39
|
+
expect(() => write(canvas, -1, 1, 'X')).not.toThrow()
|
|
40
|
+
for (const col of canvas) {
|
|
41
|
+
expect(col.every((c) => c === ' ')).toBe(true)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('is a no-op at x = maxX + 1 (just past the high edge)', () => {
|
|
46
|
+
const canvas = mkCanvas(2, 2)
|
|
47
|
+
const [maxX] = getCanvasSize(canvas)
|
|
48
|
+
expect(() => write(canvas, maxX + 1, 1, 'X')).not.toThrow()
|
|
49
|
+
for (const col of canvas) {
|
|
50
|
+
expect(col.every((c) => c === ' ')).toBe(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('writes at x = maxX (on the inclusive edge, not clipped)', () => {
|
|
55
|
+
const canvas = mkCanvas(2, 2)
|
|
56
|
+
const [maxX] = getCanvasSize(canvas)
|
|
57
|
+
write(canvas, maxX, 1, 'X')
|
|
58
|
+
expect(canvas[maxX]![1]).toBe('X')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('is a no-op at y = -1 (just past the low edge)', () => {
|
|
62
|
+
const canvas = mkCanvas(2, 2)
|
|
63
|
+
expect(() => write(canvas, 1, -1, 'X')).not.toThrow()
|
|
64
|
+
for (const col of canvas) {
|
|
65
|
+
expect(col.every((c) => c === ' ')).toBe(true)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('is a no-op at y = maxY + 1 (just past the high edge)', () => {
|
|
70
|
+
const canvas = mkCanvas(2, 2)
|
|
71
|
+
const [, maxY] = getCanvasSize(canvas)
|
|
72
|
+
expect(() => write(canvas, 1, maxY + 1, 'X')).not.toThrow()
|
|
73
|
+
for (const col of canvas) {
|
|
74
|
+
expect(col.every((c) => c === ' ')).toBe(true)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('writes at y = maxY (on the inclusive edge, not clipped)', () => {
|
|
79
|
+
const canvas = mkCanvas(2, 2)
|
|
80
|
+
const [, maxY] = getCanvasSize(canvas)
|
|
81
|
+
write(canvas, 1, maxY, 'X')
|
|
82
|
+
expect(canvas[1]![maxY]).toBe('X')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('never grows the canvas on an out-of-range write in any direction', () => {
|
|
86
|
+
const canvas = mkCanvas(2, 2)
|
|
87
|
+
const before = getCanvasSize(canvas)
|
|
88
|
+
write(canvas, -1, 1, 'X')
|
|
89
|
+
write(canvas, 5, 1, 'X')
|
|
90
|
+
write(canvas, 1, -1, 'X')
|
|
91
|
+
write(canvas, 1, 5, 'X')
|
|
92
|
+
expect(getCanvasSize(canvas)).toEqual(before)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('out-of-range writes are a no-op on the role canvas too, when roleTracking is provided', () => {
|
|
96
|
+
const canvas = mkCanvas(2, 2)
|
|
97
|
+
const roleCanvas = mkRoleCanvas(2, 2)
|
|
98
|
+
const role: CharRole = 'border'
|
|
99
|
+
|
|
100
|
+
write(canvas, -1, 1, 'X', { role, roleCanvas })
|
|
101
|
+
write(canvas, 5, 1, 'X', { role, roleCanvas })
|
|
102
|
+
write(canvas, 1, -1, 'X', { role, roleCanvas })
|
|
103
|
+
write(canvas, 1, 5, 'X', { role, roleCanvas })
|
|
104
|
+
|
|
105
|
+
for (const col of roleCanvas) {
|
|
106
|
+
expect(col.every((r) => r === null)).toBe(true)
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('records the role on the role canvas for an in-range write', () => {
|
|
111
|
+
const canvas = mkCanvas(2, 2)
|
|
112
|
+
const roleCanvas = mkRoleCanvas(2, 2)
|
|
113
|
+
const role: CharRole = 'border'
|
|
114
|
+
|
|
115
|
+
write(canvas, 1, 1, 'X', { role, roleCanvas })
|
|
116
|
+
|
|
117
|
+
expect(canvas[1]![1]).toBe('X')
|
|
118
|
+
expect(roleCanvas[1]![1]).toBe('border')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not touch the role canvas when roleTracking is omitted', () => {
|
|
122
|
+
const canvas = mkCanvas(2, 2)
|
|
123
|
+
const roleCanvas = mkRoleCanvas(2, 2)
|
|
124
|
+
|
|
125
|
+
write(canvas, 1, 1, 'X')
|
|
126
|
+
|
|
127
|
+
expect(canvas[1]![1]).toBe('X')
|
|
128
|
+
for (const col of roleCanvas) {
|
|
129
|
+
expect(col.every((r) => r === null)).toBe(true)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for #1067 ("System Architecture" form-judge finding): an
|
|
3
|
+
* edge chained through a shared intermediate node (`C(Mobile App) -->
|
|
4
|
+
* B(API Gateway)` then `B --> E(User Service)`) could render as one
|
|
5
|
+
* unbroken line straight from Mobile App to User Service, even though the
|
|
6
|
+
* source has no direct `Mobile App --> User Service` edge.
|
|
7
|
+
*
|
|
8
|
+
* Root cause: both edges are independently routed (LR direction, so
|
|
9
|
+
* `analyzeEdgeBundles` never groups anything — see its own "LR routing
|
|
10
|
+
* handles merging naturally at corners" comment), and their corners
|
|
11
|
+
* happened to land on the exact same grid cell: `Mobile App --> API
|
|
12
|
+
* Gateway`'s incoming leg travels up API Gateway's own column to the same
|
|
13
|
+
* row `API Gateway --> User Service`'s outgoing leg travels down, before
|
|
14
|
+
* turning right toward User Service. The two legs are collinear, so the
|
|
15
|
+
* drawn result reads as one continuous connector across a shared row.
|
|
16
|
+
*
|
|
17
|
+
* Fix: `edge-cell-styles.ts`'s `findUnrelatedOverlap` (scoped to true
|
|
18
|
+
* *chain* pairs — one edge's `to` equal to the other's `from`, not a
|
|
19
|
+
* fan-in/fan-out sibling pair, which legitimately shares trunk cells; see
|
|
20
|
+
* that module's doc for why the broader "shares neither endpoint" version
|
|
21
|
+
* broke the `ampersand_lhs_and_rhs` golden file) flags a chain pair that
|
|
22
|
+
* shares 2+ open cells beyond their common node's own border, and
|
|
23
|
+
* `rerouteAroundStyleConflicts` (grid.ts) reroutes one of them around the
|
|
24
|
+
* conflicting cell — the same mechanism already used for cross-style
|
|
25
|
+
* conflicts, extended to this same-style chain case.
|
|
26
|
+
*/
|
|
27
|
+
import { describe, it, expect } from 'vitest'
|
|
28
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
29
|
+
|
|
30
|
+
const SYSTEM_ARCHITECTURE = `graph LR
|
|
31
|
+
subgraph clients [Client Layer]
|
|
32
|
+
A([Web App]) --> B[API Gateway]
|
|
33
|
+
C([Mobile App]) --> B
|
|
34
|
+
end
|
|
35
|
+
subgraph services [Service Layer]
|
|
36
|
+
B --> D[Auth Service]
|
|
37
|
+
B --> E[User Service]
|
|
38
|
+
B --> F[Order Service]
|
|
39
|
+
end
|
|
40
|
+
subgraph data [Data Layer]
|
|
41
|
+
D --> G[(Auth DB)]
|
|
42
|
+
E --> H[(User DB)]
|
|
43
|
+
F --> I[(Order DB)]
|
|
44
|
+
F --> J([Message Queue])
|
|
45
|
+
end`
|
|
46
|
+
|
|
47
|
+
describe('chained edges through a shared intermediate node stay visually distinct (#1067)', () => {
|
|
48
|
+
const lines = renderMermaidASCII(SYSTEM_ARCHITECTURE, { colorMode: 'none' })
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((l) => l.replace(/\s+$/, ''))
|
|
51
|
+
|
|
52
|
+
it('renders every label and does not throw', () => {
|
|
53
|
+
const out = lines.join('\n')
|
|
54
|
+
for (const label of [
|
|
55
|
+
'Web App',
|
|
56
|
+
'Mobile App',
|
|
57
|
+
'API Gateway',
|
|
58
|
+
'Auth Service',
|
|
59
|
+
'User Service',
|
|
60
|
+
'Order Service',
|
|
61
|
+
]) {
|
|
62
|
+
expect(out).toContain(label)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("leaves a visual break between Mobile App's own row and User Service — they must not read as one connected line", () => {
|
|
67
|
+
const mobileRow = lines.find((l) => l.includes('Mobile App'))
|
|
68
|
+
expect(mobileRow).toBeDefined()
|
|
69
|
+
// Start just past Mobile App's own right border connector ('├' or
|
|
70
|
+
// '┤'), and end at User Service's own left border ('│ User Service').
|
|
71
|
+
// Both boxes' own padding ("Mobile App │" / "│ User Service")
|
|
72
|
+
// contributes a harmless blank cell or two right next to the label
|
|
73
|
+
// text itself — excluding both box borders from the scanned range
|
|
74
|
+
// means only the *connector* in between is checked, not incidental box
|
|
75
|
+
// padding (see the sabotage check on this test, which caught exactly
|
|
76
|
+
// this false pass on an earlier, looser version of this assertion).
|
|
77
|
+
const afterLabel = mobileRow!.indexOf('Mobile App') + 'Mobile App'.length
|
|
78
|
+
const start = mobileRow!.slice(afterLabel).search(/[├┤]/) + afterLabel + 1
|
|
79
|
+
const end = mobileRow!.indexOf('│ User Service')
|
|
80
|
+
expect(end).toBeGreaterThan(start)
|
|
81
|
+
const between = mobileRow!.slice(start, end)
|
|
82
|
+
|
|
83
|
+
// The bug produced a fully packed run of box-drawing characters (no
|
|
84
|
+
// blank cell at all) all the way from Mobile App's own border to User
|
|
85
|
+
// Service's — i.e. one unbroken connector. A genuine visual break
|
|
86
|
+
// requires at least one blank cell somewhere in between.
|
|
87
|
+
expect(between).toMatch(/ /)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("does not draw an arrowhead into User Service on Mobile App's own row", () => {
|
|
91
|
+
// User Service's real incoming edge (from API Gateway) is routed on a
|
|
92
|
+
// different row; if Mobile App's row also shows an arrowhead landing
|
|
93
|
+
// directly on User Service's border, the two are still effectively
|
|
94
|
+
// fused into what reads as one edge.
|
|
95
|
+
const mobileRow = lines.find((l) => l.includes('Mobile App'))!
|
|
96
|
+
const userServiceBoxStart = mobileRow.indexOf('│ User Service')
|
|
97
|
+
expect(userServiceBoxStart).toBeGreaterThan(-1)
|
|
98
|
+
expect(mobileRow[userServiceBoxStart - 1]).not.toBe('►')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: ASCII-charset rendering (`{ useAscii: true }`) never drew
|
|
3
|
+
* a junction character where an edge exits a node's border — the border
|
|
4
|
+
* stayed a plain run of dashes even at the column where a connector
|
|
5
|
+
* dropped/branched from it, while Unicode mode correctly drew a T-junction
|
|
6
|
+
* ('┬'/'┴'/'├'/'┤') there. Root cause: `drawBoxStart` (draw-arrows.ts) and
|
|
7
|
+
* the fan-in box-start connector (draw-bundles.ts) early-returned/guarded
|
|
8
|
+
* on `useAscii`, skipping junction placement entirely instead of writing
|
|
9
|
+
* the ASCII equivalent ('+').
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from 'vitest'
|
|
12
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
13
|
+
|
|
14
|
+
describe('ASCII vs Unicode charset: box-start border junctions', () => {
|
|
15
|
+
it('draws a "+" junction on a plain top-down edge, matching the Unicode T-junction', () => {
|
|
16
|
+
const src = `flowchart TD
|
|
17
|
+
A[Hello World] --> B[Done]`
|
|
18
|
+
|
|
19
|
+
const unicode = renderMermaidASCII(src, { useAscii: false })
|
|
20
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
21
|
+
|
|
22
|
+
expect(unicode).toContain('└──────┬──────┘')
|
|
23
|
+
// Before the fix this rendered as a plain, uninterrupted
|
|
24
|
+
// '+-------------+' with no junction at all.
|
|
25
|
+
expect(ascii).toContain('+------+------+')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('draws a "+" junction on a left-right (LR) edge exit, matching Unicode', () => {
|
|
29
|
+
const src = `flowchart LR
|
|
30
|
+
A[Hello] --> B[World]`
|
|
31
|
+
|
|
32
|
+
const unicode = renderMermaidASCII(src, { useAscii: false })
|
|
33
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
34
|
+
|
|
35
|
+
expect(unicode).toContain('├')
|
|
36
|
+
|
|
37
|
+
// The row with the arrow must have a '+' where Unicode has '├', not a
|
|
38
|
+
// plain '|' vertical border with no junction.
|
|
39
|
+
const unicodeRow = unicode.split('\n').find((l) => l.includes('├'))!
|
|
40
|
+
const asciiRow = ascii.split('\n').find((l) => l.includes('+---->'))
|
|
41
|
+
expect(asciiRow).toBeDefined()
|
|
42
|
+
const junctionCol = unicodeRow.indexOf('├')
|
|
43
|
+
expect(asciiRow![junctionCol]).toBe('+')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('draws "+" junctions on a fan-in bundle box-start connector in ASCII mode', () => {
|
|
47
|
+
const src = `flowchart TD
|
|
48
|
+
A[One] --> C[Target]
|
|
49
|
+
B[Two] --> C`
|
|
50
|
+
|
|
51
|
+
const unicode = renderMermaidASCII(src, { useAscii: false })
|
|
52
|
+
const ascii = renderMermaidASCII(src, { useAscii: true })
|
|
53
|
+
|
|
54
|
+
// Unicode shows T-junctions where each source box's bottom border
|
|
55
|
+
// meets the fan-in connector line.
|
|
56
|
+
expect(unicode).toMatch(/[┬┴├┤]/)
|
|
57
|
+
|
|
58
|
+
// Each source box's bottom border must show an interior '+' junction
|
|
59
|
+
// where the fan-in connector drops from it, not a plain dash run.
|
|
60
|
+
expect(ascii).toContain('+----+---+')
|
|
61
|
+
expect(ascii).toContain('+--+--+')
|
|
62
|
+
})
|
|
63
|
+
})
|