@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #182 — `drawMultiBox` (the multi-compartment
|
|
3
|
+
* boxes used by class and ER diagrams) measured and wrote text by UTF-16 code
|
|
4
|
+
* unit rather than display width, the same bug class #66 fixed for the
|
|
5
|
+
* single-box path.
|
|
6
|
+
*
|
|
7
|
+
* The fix has to cover three sites at once, which is why these tests exercise
|
|
8
|
+
* whole rendered diagrams rather than `drawMultiBox` alone:
|
|
9
|
+
*
|
|
10
|
+
* 1. `drawMultiBox`'s own sizing and per-character write loop, and
|
|
11
|
+
* 2/3. the box dimensions `class-diagram.ts` and `er-diagram.ts` each
|
|
12
|
+
* precompute to reserve grid space *before* drawing.
|
|
13
|
+
*
|
|
14
|
+
* Fixing only (1) would desync the drawn box from the space layout reserved
|
|
15
|
+
* for it, so the multi-box grid cases below (wide-character and ASCII
|
|
16
|
+
* siblings placed side by side) are the ones that would catch a partial fix.
|
|
17
|
+
*
|
|
18
|
+
* Width is measured with an oracle independent of `src/ascii/display-width.ts`
|
|
19
|
+
* — see the shared helper.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect } from 'vitest'
|
|
22
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
23
|
+
import {
|
|
24
|
+
terminalDisplayWidth,
|
|
25
|
+
assertUniformDisplayWidth,
|
|
26
|
+
} from './helpers/terminal-display-width.ts'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Assert that every border row and content row of a multi-compartment box
|
|
30
|
+
* puts its right-hand vertical border in the same terminal column — the
|
|
31
|
+
* direct symptom from the issue, content overflowing its own border.
|
|
32
|
+
*
|
|
33
|
+
* Only valid for single-box output: relationship connector lines also contain
|
|
34
|
+
* the vertical border character without being a box edge.
|
|
35
|
+
*/
|
|
36
|
+
function assertBordersAligned(ascii: string, useAscii: boolean): void {
|
|
37
|
+
const vChar = useAscii ? '|' : '│'
|
|
38
|
+
const rows = ascii.split('\n').filter((l) => l.includes(vChar))
|
|
39
|
+
expect(rows.length).toBeGreaterThan(0)
|
|
40
|
+
|
|
41
|
+
const columns = rows.map((row) =>
|
|
42
|
+
terminalDisplayWidth(row.slice(0, row.lastIndexOf(vChar))),
|
|
43
|
+
)
|
|
44
|
+
const expected = columns[0]
|
|
45
|
+
for (let i = 0; i < rows.length; i++) {
|
|
46
|
+
expect(
|
|
47
|
+
columns[i],
|
|
48
|
+
`row ${i} (${JSON.stringify(rows[i])}) has its right border at column ${columns[i]}, expected ${expected}`,
|
|
49
|
+
).toBe(expected)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Terminal-column gap between two boxes drawn side by side on the same row.
|
|
55
|
+
*
|
|
56
|
+
* This is the assertion that catches a *partial* fix. If `drawMultiBox` is
|
|
57
|
+
* corrected but a caller still reserves grid space using code-unit widths,
|
|
58
|
+
* the box drawn for wide-character content is wider than the space reserved
|
|
59
|
+
* for it, and the surplus eats into the gap separating it from its neighbour
|
|
60
|
+
* (and, for a wide enough box, into the neighbour itself). The rendered box
|
|
61
|
+
* still looks well-formed, so only the spacing reveals the desync.
|
|
62
|
+
*/
|
|
63
|
+
function siblingGap(ascii: string, useAscii: boolean): number {
|
|
64
|
+
// Matched on the top-border row rather than by corner character: in the
|
|
65
|
+
// ASCII charset all four corners are '+', so there is no distinct
|
|
66
|
+
// right-corner glyph to search for. Two runs of border characters
|
|
67
|
+
// separated by spaces is unambiguous in both charsets.
|
|
68
|
+
const border = useAscii ? '[+\\-]' : '[┌┐─]'
|
|
69
|
+
const twoBoxes = new RegExp(`^\\s*(${border}+)(\\s+)(${border}+)`)
|
|
70
|
+
|
|
71
|
+
for (const line of ascii.split('\n')) {
|
|
72
|
+
const match = twoBoxes.exec(line)
|
|
73
|
+
if (match) return terminalDisplayWidth(match[2]!)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect.fail('expected a row containing two side-by-side box borders')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('ASCII renderer — wide characters in multi-compartment boxes (issue #182)', () => {
|
|
80
|
+
for (const useAscii of [false, true]) {
|
|
81
|
+
const charset = useAscii ? 'ASCII charset' : 'Unicode charset'
|
|
82
|
+
|
|
83
|
+
describe(`class diagrams — ${charset}`, () => {
|
|
84
|
+
it('keeps a CJK class box within its own border', () => {
|
|
85
|
+
const ascii = renderMermaidASCII(
|
|
86
|
+
'classDiagram\n class Aクラス {\n +名前 name\n +取得する() void\n }',
|
|
87
|
+
{ useAscii, colorMode: 'none' },
|
|
88
|
+
)
|
|
89
|
+
assertUniformDisplayWidth(ascii)
|
|
90
|
+
assertBordersAligned(ascii, useAscii)
|
|
91
|
+
expect(ascii).toContain('名前')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not desync layout from drawing for wide/ASCII sibling boxes', () => {
|
|
95
|
+
// Two boxes side by side on the same level: one wide-character, one
|
|
96
|
+
// ASCII. A fix applied to drawMultiBox but not to class-diagram.ts's
|
|
97
|
+
// own sizing pass would overlap or misalign these.
|
|
98
|
+
const ascii = renderMermaidASCII(
|
|
99
|
+
'classDiagram\n class Base\n class 子クラスA {\n +属性値 v\n }\n class ChildB {\n +value v\n }\n Base <|-- 子クラスA\n Base <|-- ChildB',
|
|
100
|
+
{ useAscii, colorMode: 'none' },
|
|
101
|
+
)
|
|
102
|
+
assertUniformDisplayWidth(ascii)
|
|
103
|
+
expect(ascii).toContain('子クラスA')
|
|
104
|
+
expect(ascii).toContain('ChildB')
|
|
105
|
+
|
|
106
|
+
// The wide-character box must be separated from its ASCII sibling by
|
|
107
|
+
// the class renderer's full horizontal gap (hGap = 4). A caller still
|
|
108
|
+
// reserving code-unit width narrows this instead.
|
|
109
|
+
expect(siblingGap(ascii, useAscii)).toBe(4)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('centers a CJK relationship label without overrunning', () => {
|
|
113
|
+
const ascii = renderMermaidASCII(
|
|
114
|
+
'classDiagram\n class 動物 {\n +名前 name\n }\n class 犬 {\n +吠える() void\n }\n 動物 <|-- 犬 : 継承する',
|
|
115
|
+
{ useAscii, colorMode: 'none' },
|
|
116
|
+
)
|
|
117
|
+
assertUniformDisplayWidth(ascii)
|
|
118
|
+
expect(ascii).toContain('継承する')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('handles a non-BMP (surrogate-pair) member without splitting it', () => {
|
|
122
|
+
const ascii = renderMermaidASCII(
|
|
123
|
+
'classDiagram\n class Party {\n +🎉🎉 confetti\n }',
|
|
124
|
+
{ useAscii, colorMode: 'none' },
|
|
125
|
+
)
|
|
126
|
+
assertUniformDisplayWidth(ascii)
|
|
127
|
+
// A code-unit write loop would emit lone surrogate halves into
|
|
128
|
+
// adjacent cells; the pair must survive intact.
|
|
129
|
+
expect(ascii).toContain('🎉🎉')
|
|
130
|
+
expect(ascii).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/)
|
|
131
|
+
expect(ascii).not.toMatch(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe(`ER diagrams — ${charset}`, () => {
|
|
136
|
+
it('keeps a CJK entity box within its own border', () => {
|
|
137
|
+
const ascii = renderMermaidASCII(
|
|
138
|
+
'erDiagram\n 顧客 {\n string 名前\n int 年齢\n }',
|
|
139
|
+
{ useAscii, colorMode: 'none' },
|
|
140
|
+
)
|
|
141
|
+
assertUniformDisplayWidth(ascii)
|
|
142
|
+
assertBordersAligned(ascii, useAscii)
|
|
143
|
+
expect(ascii).toContain('顧客')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('does not desync layout from drawing for wide/ASCII sibling entities', () => {
|
|
147
|
+
const ascii = renderMermaidASCII(
|
|
148
|
+
'erDiagram\n 顧客 ||--o{ ORDER : places\n 顧客 {\n string 名前\n }\n ORDER {\n int number\n }',
|
|
149
|
+
{ useAscii, colorMode: 'none' },
|
|
150
|
+
)
|
|
151
|
+
assertUniformDisplayWidth(ascii)
|
|
152
|
+
expect(ascii).toContain('顧客')
|
|
153
|
+
expect(ascii).toContain('ORDER')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('places a CJK relationship label without breaking alignment', () => {
|
|
157
|
+
const ascii = renderMermaidASCII(
|
|
158
|
+
'erDiagram\n 顧客 ||--o{ 注文 : "発注する"\n 顧客 {\n string 名前\n }\n 注文 {\n int 番号\n }',
|
|
159
|
+
{ useAscii, colorMode: 'none' },
|
|
160
|
+
)
|
|
161
|
+
assertUniformDisplayWidth(ascii)
|
|
162
|
+
expect(ascii).toContain('発注する')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
it('leaves the common all-ASCII case intact', () => {
|
|
168
|
+
// Guard against the fix perturbing diagrams with no wide characters.
|
|
169
|
+
const single = renderMermaidASCII(
|
|
170
|
+
'classDiagram\n class Animal {\n +String name\n +eat() void\n }',
|
|
171
|
+
{ colorMode: 'none' },
|
|
172
|
+
)
|
|
173
|
+
assertUniformDisplayWidth(single)
|
|
174
|
+
assertBordersAligned(single, false)
|
|
175
|
+
expect(single).toContain('+ name: String')
|
|
176
|
+
|
|
177
|
+
const related = renderMermaidASCII(
|
|
178
|
+
'classDiagram\n class Animal {\n +String name\n }\n class Dog\n Animal <|-- Dog',
|
|
179
|
+
{ colorMode: 'none' },
|
|
180
|
+
)
|
|
181
|
+
assertUniformDisplayWidth(related)
|
|
182
|
+
expect(related).toContain('Dog')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
3
|
+
import { mkCanvas, canvasToString } from '../canvas.ts'
|
|
4
|
+
import {
|
|
5
|
+
drawMultilineTextCentered,
|
|
6
|
+
drawMultilineTextLeft,
|
|
7
|
+
} from '../multiline-utils.ts'
|
|
8
|
+
|
|
9
|
+
describe('ASCII multi-line labels', () => {
|
|
10
|
+
describe('flowchart nodes', () => {
|
|
11
|
+
it('renders multi-line node labels', () => {
|
|
12
|
+
const ascii = renderMermaidASCII('graph TD\n A[Line1<br>Line2]', {
|
|
13
|
+
useAscii: false,
|
|
14
|
+
})
|
|
15
|
+
expect(ascii).toContain('Line1')
|
|
16
|
+
expect(ascii).toContain('Line2')
|
|
17
|
+
// Lines should be on different rows
|
|
18
|
+
const lines = ascii.split('\n')
|
|
19
|
+
const line1Row = lines.findIndex((l) => l.includes('Line1'))
|
|
20
|
+
const line2Row = lines.findIndex((l) => l.includes('Line2'))
|
|
21
|
+
expect(line2Row).toBeGreaterThan(line1Row)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('handles 3+ line labels', () => {
|
|
25
|
+
const ascii = renderMermaidASCII('graph TD\n A[A<br>B<br>C]', {
|
|
26
|
+
useAscii: false,
|
|
27
|
+
})
|
|
28
|
+
expect(ascii).toContain('A')
|
|
29
|
+
expect(ascii).toContain('B')
|
|
30
|
+
expect(ascii).toContain('C')
|
|
31
|
+
// Verify vertical ordering
|
|
32
|
+
const lines = ascii.split('\n')
|
|
33
|
+
const aRow = lines.findIndex(
|
|
34
|
+
(l) => l.includes('A') && !l.includes('─') && !l.includes('-'),
|
|
35
|
+
)
|
|
36
|
+
const bRow = lines.findIndex((l) => l.includes('B'))
|
|
37
|
+
const cRow = lines.findIndex((l) => l.includes('C'))
|
|
38
|
+
expect(bRow).toBeGreaterThan(aRow)
|
|
39
|
+
expect(cRow).toBeGreaterThan(bRow)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders in ASCII mode (not Unicode)', () => {
|
|
43
|
+
const ascii = renderMermaidASCII('graph TD\n A[Line1<br>Line2]', {
|
|
44
|
+
useAscii: true,
|
|
45
|
+
})
|
|
46
|
+
expect(ascii).toContain('Line1')
|
|
47
|
+
expect(ascii).toContain('Line2')
|
|
48
|
+
// Should use ASCII box characters
|
|
49
|
+
expect(ascii).toContain('+')
|
|
50
|
+
expect(ascii).toContain('-')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('flowchart edge labels', () => {
|
|
55
|
+
it('renders multi-line edge labels', () => {
|
|
56
|
+
const ascii = renderMermaidASCII(
|
|
57
|
+
'graph TD\n A --> B\n A -->|Line1<br>Line2| C',
|
|
58
|
+
{ useAscii: false },
|
|
59
|
+
)
|
|
60
|
+
expect(ascii).toContain('Line1')
|
|
61
|
+
expect(ascii).toContain('Line2')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('flowchart subgraph labels', () => {
|
|
66
|
+
it('renders multi-line subgraph labels', () => {
|
|
67
|
+
const ascii = renderMermaidASCII(
|
|
68
|
+
`graph TD
|
|
69
|
+
subgraph sg [Group<br>Header]
|
|
70
|
+
A[Node]
|
|
71
|
+
end
|
|
72
|
+
`,
|
|
73
|
+
{ useAscii: false },
|
|
74
|
+
)
|
|
75
|
+
expect(ascii).toContain('Group')
|
|
76
|
+
expect(ascii).toContain('Header')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('sequence diagram', () => {
|
|
81
|
+
it('renders multi-line actor labels', () => {
|
|
82
|
+
const ascii = renderMermaidASCII(
|
|
83
|
+
`sequenceDiagram
|
|
84
|
+
participant A as Actor<br>One
|
|
85
|
+
A->>A: msg
|
|
86
|
+
`,
|
|
87
|
+
{ useAscii: false },
|
|
88
|
+
)
|
|
89
|
+
expect(ascii).toContain('Actor')
|
|
90
|
+
expect(ascii).toContain('One')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders multi-line message labels', () => {
|
|
94
|
+
const ascii = renderMermaidASCII(
|
|
95
|
+
`sequenceDiagram
|
|
96
|
+
participant A
|
|
97
|
+
participant B
|
|
98
|
+
A->>B: Line1<br>Line2
|
|
99
|
+
`,
|
|
100
|
+
{ useAscii: false },
|
|
101
|
+
)
|
|
102
|
+
expect(ascii).toContain('Line1')
|
|
103
|
+
expect(ascii).toContain('Line2')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('preserves existing note multi-line support', () => {
|
|
107
|
+
const ascii = renderMermaidASCII(
|
|
108
|
+
`sequenceDiagram
|
|
109
|
+
participant A
|
|
110
|
+
A->>A: self
|
|
111
|
+
Note over A: Note line 1<br>Note line 2
|
|
112
|
+
`,
|
|
113
|
+
{ useAscii: false },
|
|
114
|
+
)
|
|
115
|
+
expect(ascii).toContain('Note line 1')
|
|
116
|
+
expect(ascii).toContain('Note line 2')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('class diagram', () => {
|
|
121
|
+
it('renders multi-line class names', () => {
|
|
122
|
+
const ascii = renderMermaidASCII(
|
|
123
|
+
`classDiagram
|
|
124
|
+
class MyClass["Long<br>Name"]
|
|
125
|
+
`,
|
|
126
|
+
{ useAscii: false },
|
|
127
|
+
)
|
|
128
|
+
expect(ascii).toContain('Long')
|
|
129
|
+
expect(ascii).toContain('Name')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('renders multi-line relationship labels', () => {
|
|
133
|
+
const ascii = renderMermaidASCII(
|
|
134
|
+
`classDiagram
|
|
135
|
+
A --> B : uses<br>implements
|
|
136
|
+
`,
|
|
137
|
+
{ useAscii: false },
|
|
138
|
+
)
|
|
139
|
+
expect(ascii).toContain('uses')
|
|
140
|
+
expect(ascii).toContain('implements')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('ER diagram', () => {
|
|
145
|
+
it('renders multi-line entity names', () => {
|
|
146
|
+
const ascii = renderMermaidASCII(
|
|
147
|
+
`erDiagram
|
|
148
|
+
"Entity<br>Name" {
|
|
149
|
+
string id
|
|
150
|
+
}
|
|
151
|
+
`,
|
|
152
|
+
{ useAscii: false },
|
|
153
|
+
)
|
|
154
|
+
expect(ascii).toContain('Entity')
|
|
155
|
+
expect(ascii).toContain('Name')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('renders multi-line relationship labels', () => {
|
|
159
|
+
const ascii = renderMermaidASCII(
|
|
160
|
+
`erDiagram
|
|
161
|
+
A ||--o{ B : "has<br>many"
|
|
162
|
+
`,
|
|
163
|
+
{ useAscii: false },
|
|
164
|
+
)
|
|
165
|
+
expect(ascii).toContain('has')
|
|
166
|
+
expect(ascii).toContain('many')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('edge cases', () => {
|
|
171
|
+
it('handles empty lines from consecutive <br>', () => {
|
|
172
|
+
const ascii = renderMermaidASCII('graph TD\n A[Line1<br><br>Line3]', {
|
|
173
|
+
useAscii: false,
|
|
174
|
+
})
|
|
175
|
+
expect(ascii).toContain('Line1')
|
|
176
|
+
expect(ascii).toContain('Line3')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('handles single-line labels (no <br>)', () => {
|
|
180
|
+
const ascii = renderMermaidASCII('graph TD\n A[SingleLine]', {
|
|
181
|
+
useAscii: false,
|
|
182
|
+
})
|
|
183
|
+
expect(ascii).toContain('SingleLine')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('handles very long lines', () => {
|
|
187
|
+
const long = 'A'.repeat(30)
|
|
188
|
+
const ascii = renderMermaidASCII(`graph TD\n A[${long}<br>Short]`, {
|
|
189
|
+
useAscii: false,
|
|
190
|
+
})
|
|
191
|
+
expect(ascii).toContain(long)
|
|
192
|
+
expect(ascii).toContain('Short')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('handles mixed short and long lines', () => {
|
|
196
|
+
const ascii = renderMermaidASCII(
|
|
197
|
+
'graph TD\n A[Short<br>VeryLongSecondLine<br>Med]',
|
|
198
|
+
{ useAscii: false },
|
|
199
|
+
)
|
|
200
|
+
expect(ascii).toContain('Short')
|
|
201
|
+
expect(ascii).toContain('VeryLongSecondLine')
|
|
202
|
+
expect(ascii).toContain('Med')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('multiline-utils functions', () => {
|
|
207
|
+
it('splitLines splits on newlines', () => {
|
|
208
|
+
// Test through the rendering pipeline
|
|
209
|
+
const ascii = renderMermaidASCII('graph TD\n A[One<br>Two<br>Three]', {
|
|
210
|
+
useAscii: false,
|
|
211
|
+
})
|
|
212
|
+
const lines = ascii.split('\n')
|
|
213
|
+
// All three words should appear on separate lines
|
|
214
|
+
expect(lines.some((l) => l.includes('One'))).toBe(true)
|
|
215
|
+
expect(lines.some((l) => l.includes('Two'))).toBe(true)
|
|
216
|
+
expect(lines.some((l) => l.includes('Three'))).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('maxLineWidth uses longest line for box sizing', () => {
|
|
220
|
+
// Box should be wide enough for the longest line
|
|
221
|
+
const ascii = renderMermaidASCII('graph TD\n A[X<br>LongLine<br>Y]', {
|
|
222
|
+
useAscii: false,
|
|
223
|
+
})
|
|
224
|
+
// The box should contain LongLine without truncation
|
|
225
|
+
expect(ascii).toContain('LongLine')
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('drawMultilineTextCentered', () => {
|
|
230
|
+
it('draws a single line centered at the given point', () => {
|
|
231
|
+
const canvas = mkCanvas(10, 4)
|
|
232
|
+
drawMultilineTextCentered(canvas, 'Hi', 5, 2)
|
|
233
|
+
const lines = canvasToString(canvas).split('\n')
|
|
234
|
+
expect(lines[2]).toContain('Hi')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('centers multiple lines vertically around the point', () => {
|
|
238
|
+
const canvas = mkCanvas(12, 6)
|
|
239
|
+
drawMultilineTextCentered(canvas, 'One\nTwo\nThree', 6, 3)
|
|
240
|
+
const lines = canvasToString(canvas).split('\n')
|
|
241
|
+
const oneRow = lines.findIndex((l) => l.includes('One'))
|
|
242
|
+
const twoRow = lines.findIndex((l) => l.includes('Two'))
|
|
243
|
+
const threeRow = lines.findIndex((l) => l.includes('Three'))
|
|
244
|
+
expect(oneRow).toBeGreaterThanOrEqual(0)
|
|
245
|
+
expect(twoRow).toBe(oneRow + 1)
|
|
246
|
+
expect(threeRow).toBe(twoRow + 1)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('centers each line horizontally around the given x', () => {
|
|
250
|
+
const canvas = mkCanvas(20, 2)
|
|
251
|
+
drawMultilineTextCentered(canvas, 'Wide\nX', 10, 1)
|
|
252
|
+
const lines = canvasToString(canvas).split('\n')
|
|
253
|
+
const wideRow = lines.findIndex((l) => l.includes('Wide'))
|
|
254
|
+
const xRow = lines.findIndex((l) => l.includes('X'))
|
|
255
|
+
// 'Wide' (4 chars) centered on x=10 starts 2 columns left of center
|
|
256
|
+
expect(lines[wideRow]!.indexOf('Wide')).toBe(8)
|
|
257
|
+
// 'X' (1 char) centered on x=10 starts exactly at center
|
|
258
|
+
expect(lines[xRow]!.indexOf('X')).toBe(10)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('handles an empty label without throwing', () => {
|
|
262
|
+
const canvas = mkCanvas(6, 3)
|
|
263
|
+
expect(() => drawMultilineTextCentered(canvas, '', 3, 1)).not.toThrow()
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
describe('drawMultilineTextLeft', () => {
|
|
268
|
+
it('draws a single line starting at the given coordinate', () => {
|
|
269
|
+
const canvas = mkCanvas(10, 4)
|
|
270
|
+
drawMultilineTextLeft(canvas, 'Hello', 1, 1)
|
|
271
|
+
const lines = canvasToString(canvas).split('\n')
|
|
272
|
+
expect(lines[1]!.indexOf('Hello')).toBe(1)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('places each subsequent line one row below the previous', () => {
|
|
276
|
+
const canvas = mkCanvas(10, 5)
|
|
277
|
+
drawMultilineTextLeft(canvas, 'Alpha\nBeta', 0, 0)
|
|
278
|
+
const lines = canvasToString(canvas).split('\n')
|
|
279
|
+
expect(lines[0]!.indexOf('Alpha')).toBe(0)
|
|
280
|
+
expect(lines[1]!.indexOf('Beta')).toBe(0)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('handles an empty label without throwing', () => {
|
|
284
|
+
const canvas = mkCanvas(6, 3)
|
|
285
|
+
expect(() => drawMultilineTextLeft(canvas, '', 0, 0)).not.toThrow()
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII padding edge-case tests (issue #343 — CodeRabbit follow-up)
|
|
3
|
+
//
|
|
4
|
+
// #343 made paddingX/paddingY/boxBorderPadding functional for sequence,
|
|
5
|
+
// class, and ER diagrams. CodeRabbit's review of that PR found three ways
|
|
6
|
+
// an out-of-range padding value could corrupt output rather than just
|
|
7
|
+
// tighten/loosen spacing:
|
|
8
|
+
//
|
|
9
|
+
// 1. A negative boxBorderPadding could make measureMultiBox/drawMultiBox
|
|
10
|
+
// compute a negative box width for a short label (class/ER boxes, and
|
|
11
|
+
// sequence actor/note boxes, which derive their own width the same way).
|
|
12
|
+
// 2. A very small paddingX/paddingY on an ER diagram could shrink hGap/vGap
|
|
13
|
+
// below what a 2-cell crow's-foot marker needs, making same-row markers
|
|
14
|
+
// overlap or vertical markers land on the same row.
|
|
15
|
+
// 3. A very small paddingY on a sequence diagram could collapse rowGap to
|
|
16
|
+
// 0, so a block divider and the following message (or a block's closing
|
|
17
|
+
// border and the footer) would land on the same row and overwrite each
|
|
18
|
+
// other, rather than just rendering tightly.
|
|
19
|
+
//
|
|
20
|
+
// This file locks in the fixes for all three.
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest'
|
|
24
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
25
|
+
|
|
26
|
+
describe('ASCII padding edge cases (issue #343 follow-up)', () => {
|
|
27
|
+
it('a negative boxBorderPadding does not produce a negative-width class box', () => {
|
|
28
|
+
const cls = `classDiagram
|
|
29
|
+
class Foo`
|
|
30
|
+
// Should not throw, and every border run should still be a real box
|
|
31
|
+
// (at least 3 chars: two corners + at least one border char).
|
|
32
|
+
const ascii = renderMermaidASCII(cls, {
|
|
33
|
+
useAscii: true,
|
|
34
|
+
boxBorderPadding: -10,
|
|
35
|
+
})
|
|
36
|
+
const borderRuns = ascii.match(/\+-+\+/g) ?? []
|
|
37
|
+
expect(borderRuns.length).toBeGreaterThan(0)
|
|
38
|
+
for (const run of borderRuns) expect(run.length).toBeGreaterThanOrEqual(3)
|
|
39
|
+
expect(ascii).toContain('Foo')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('a negative boxBorderPadding does not produce a negative-width ER box', () => {
|
|
43
|
+
const er = `erDiagram
|
|
44
|
+
CUSTOMER {
|
|
45
|
+
string name
|
|
46
|
+
}`
|
|
47
|
+
const ascii = renderMermaidASCII(er, {
|
|
48
|
+
useAscii: true,
|
|
49
|
+
boxBorderPadding: -10,
|
|
50
|
+
})
|
|
51
|
+
const borderRuns = ascii.match(/\+-+\+/g) ?? []
|
|
52
|
+
expect(borderRuns.length).toBeGreaterThan(0)
|
|
53
|
+
for (const run of borderRuns) expect(run.length).toBeGreaterThanOrEqual(3)
|
|
54
|
+
expect(ascii).toContain('CUSTOMER')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('a negative boxBorderPadding does not produce a negative-width sequence actor box', () => {
|
|
58
|
+
const seq = `sequenceDiagram
|
|
59
|
+
participant A
|
|
60
|
+
participant B
|
|
61
|
+
A->>B: Hi`
|
|
62
|
+
const ascii = renderMermaidASCII(seq, {
|
|
63
|
+
useAscii: true,
|
|
64
|
+
boxBorderPadding: -10,
|
|
65
|
+
})
|
|
66
|
+
const borderRuns = ascii.match(/\+-+\+/g) ?? []
|
|
67
|
+
expect(borderRuns.length).toBeGreaterThan(0)
|
|
68
|
+
for (const run of borderRuns) expect(run.length).toBeGreaterThanOrEqual(3)
|
|
69
|
+
expect(ascii).toContain('A')
|
|
70
|
+
expect(ascii).toContain('B')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("a very negative paddingX keeps ER crow's-foot markers from overlapping", () => {
|
|
74
|
+
// Both endpoint markers ('|' for "one", '<' + 'o' for "zero-many") must
|
|
75
|
+
// still appear as distinct, non-overwriting characters in the connector
|
|
76
|
+
// segment between the two entity boxes. Extract that segment precisely
|
|
77
|
+
// (the text between "CUSTOMER |" and "| ORDER") rather than slicing from
|
|
78
|
+
// the line's first/last '|' — the "one" marker is itself a '|', so a
|
|
79
|
+
// naive first/last-pipe slice would silently include it as a false
|
|
80
|
+
// positive even when it has been overwritten by the other marker.
|
|
81
|
+
const er = `erDiagram
|
|
82
|
+
CUSTOMER ||--o{ ORDER : x`
|
|
83
|
+
const ascii = renderMermaidASCII(er, { useAscii: true, paddingX: -100 })
|
|
84
|
+
const line = ascii.split('\n').find((l) => l.includes('CUSTOMER'))
|
|
85
|
+
expect(line).toBeDefined()
|
|
86
|
+
const match = /CUSTOMER\s*\|(.*)\|\s*ORDER/.exec(line!)
|
|
87
|
+
expect(match).not.toBeNull()
|
|
88
|
+
const connector = match![1]!
|
|
89
|
+
expect(connector).toContain('|')
|
|
90
|
+
expect(connector).toContain('o')
|
|
91
|
+
expect(connector).toContain('<')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('a very negative paddingY keeps ER vertical relationship markers on separate rows', () => {
|
|
95
|
+
// A wraps to a second row relative to C (via B forcing a 2-column grid),
|
|
96
|
+
// producing a vertical connector whose upper/lower crow's-foot markers
|
|
97
|
+
// must land on different rows even when vGap is squeezed.
|
|
98
|
+
const er = `erDiagram
|
|
99
|
+
A ||--o{ B : r1
|
|
100
|
+
A ||--o{ C : r2`
|
|
101
|
+
const ascii = renderMermaidASCII(er, { useAscii: true, paddingY: -100 })
|
|
102
|
+
const lines = ascii.split('\n')
|
|
103
|
+
// Find the two marker rows for the A-C vertical relationship: one
|
|
104
|
+
// containing the label "r2" (upper marker) and, below it, one
|
|
105
|
+
// containing the zero-many marker glyphs.
|
|
106
|
+
const r2Idx = lines.findIndex((l) => l.includes('r2'))
|
|
107
|
+
expect(r2Idx).toBeGreaterThanOrEqual(0)
|
|
108
|
+
const markerIdx = lines.findIndex(
|
|
109
|
+
(l, i) => i > r2Idx && /[o<]/.test(l) && !l.includes('CUSTOMER'),
|
|
110
|
+
)
|
|
111
|
+
expect(markerIdx).toBeGreaterThan(r2Idx)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('very tight paddingY keeps a block divider from overwriting the next message', () => {
|
|
115
|
+
const seq = `sequenceDiagram
|
|
116
|
+
participant A
|
|
117
|
+
participant B
|
|
118
|
+
alt yes
|
|
119
|
+
A->>B: One
|
|
120
|
+
else no
|
|
121
|
+
A->>B: Two
|
|
122
|
+
end`
|
|
123
|
+
const ascii = renderMermaidASCII(seq, { useAscii: true, paddingY: -100 })
|
|
124
|
+
// Both branch labels must survive — a collision would silently drop
|
|
125
|
+
// "Two" by overwriting its row with the divider row.
|
|
126
|
+
expect(ascii).toContain('One')
|
|
127
|
+
expect(ascii).toContain('Two')
|
|
128
|
+
expect(ascii).toContain('[no]')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('very tight paddingY keeps a block-closing border from overwriting the footer', () => {
|
|
132
|
+
const seq = `sequenceDiagram
|
|
133
|
+
participant A
|
|
134
|
+
participant B
|
|
135
|
+
alt yes
|
|
136
|
+
A->>B: One
|
|
137
|
+
end`
|
|
138
|
+
const ascii = renderMermaidASCII(seq, { useAscii: true, paddingY: -100 })
|
|
139
|
+
const lines = ascii.split('\n')
|
|
140
|
+
// The two-actor header and footer boxes each draw a "+---+ +---+"-
|
|
141
|
+
// style top border and an identical-looking bottom border — 4 such rows
|
|
142
|
+
// total. A collision between the block's closing border and the
|
|
143
|
+
// footer's top border overwrites the footer's top border entirely
|
|
144
|
+
// (verified by sabotaging the fix: the count drops to 3, and the
|
|
145
|
+
// footer's content row — "| A | | B |" — appears directly under the
|
|
146
|
+
// block's closing border with no border row above it), rather than
|
|
147
|
+
// merely rendering close together, so counting these rows is a precise
|
|
148
|
+
// way to detect it instead of checking for adjacency.
|
|
149
|
+
const twoBoxBorderRows = lines.filter((l) =>
|
|
150
|
+
/^\s*\+-+\+\s+\+-+\+\s*$/.test(l),
|
|
151
|
+
)
|
|
152
|
+
expect(twoBoxBorderRows.length).toBe(4)
|
|
153
|
+
})
|
|
154
|
+
})
|