@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,61 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram note tests
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
7
|
+
|
|
8
|
+
describe('ASCII sequence diagrams – pre-message notes', () => {
|
|
9
|
+
it('renders a note placed before the first message (regression)', () => {
|
|
10
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
11
|
+
participant A as Alice
|
|
12
|
+
participant B as Bob
|
|
13
|
+
Note over A: note 1
|
|
14
|
+
A->>B: Hello`)
|
|
15
|
+
expect(result).toContain('note 1')
|
|
16
|
+
expect(result).toContain('Hello')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders a notes-only diagram (0 messages) without crashing', () => {
|
|
20
|
+
const result = renderMermaidASCII(`sequenceDiagram
|
|
21
|
+
participant A
|
|
22
|
+
Note over A: lonely note`)
|
|
23
|
+
expect(result).toContain('lonely note')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('ASCII sequence diagrams – note box vs. lifeline collision (regression)', () => {
|
|
28
|
+
// When a "Note over A,B" box is computed wide enough that its own right
|
|
29
|
+
// padding column lands exactly on B's lifeline x-position, the lifeline
|
|
30
|
+
// (drawn in an earlier pass) must not leak through as a second border
|
|
31
|
+
// character next to the note's real right border. 8-9 'x' characters is
|
|
32
|
+
// the exact width, given these actors' default spacing, that reproduces
|
|
33
|
+
// the collision — shorter or longer text misses the colliding column.
|
|
34
|
+
for (const n of [8, 9]) {
|
|
35
|
+
it(`does not double up border characters for a ${n}-char note spanning two actors`, () => {
|
|
36
|
+
const noteText = 'x'.repeat(n)
|
|
37
|
+
const result = renderMermaidASCII(
|
|
38
|
+
`sequenceDiagram
|
|
39
|
+
participant A
|
|
40
|
+
participant B
|
|
41
|
+
A->>B: hi
|
|
42
|
+
Note over A,B: ${noteText}`,
|
|
43
|
+
{ colorMode: 'none' },
|
|
44
|
+
)
|
|
45
|
+
const lines = result.split('\n')
|
|
46
|
+
const contentLine = lines.find((l) => l.includes(noteText))
|
|
47
|
+
expect(contentLine).toBeDefined()
|
|
48
|
+
|
|
49
|
+
// No two border/line-drawing characters should sit directly adjacent
|
|
50
|
+
// with no space between them anywhere in the note's rows — that
|
|
51
|
+
// shape (e.g. "││") only occurs when a stale lifeline character
|
|
52
|
+
// leaks through an unblanked padding column next to a real border.
|
|
53
|
+
const noteLineIndex = lines.indexOf(contentLine!)
|
|
54
|
+
const topBorderLine = lines[noteLineIndex - 1]!
|
|
55
|
+
const bottomBorderLine = lines[noteLineIndex + 1]!
|
|
56
|
+
for (const line of [topBorderLine, contentLine!, bottomBorderLine]) {
|
|
57
|
+
expect(line).not.toMatch(/[│|┌┐└┘][│|┌┐└┘]/)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram padding tests (issue #343)
|
|
3
|
+
//
|
|
4
|
+
// `-x`/`-y`/`-p` (AsciiRenderOptions.paddingX/paddingY/boxBorderPadding)
|
|
5
|
+
// previously only affected the flowchart/state grid layout — sequence
|
|
6
|
+
// diagrams ignored them entirely and always used fixed internal spacing
|
|
7
|
+
// constants. This file verifies the fix: padding now visibly widens/
|
|
8
|
+
// tightens lifeline spacing, actor-box size, and vertical row spacing,
|
|
9
|
+
// while a render with no explicit padding option still renders byte-for-
|
|
10
|
+
// byte identical to before the fix (see the DEFAULT_PADDING_* comment in
|
|
11
|
+
// src/ascii/types.ts for why that matters).
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
16
|
+
|
|
17
|
+
const SEQ = `sequenceDiagram
|
|
18
|
+
participant A
|
|
19
|
+
participant B
|
|
20
|
+
A->>B: Hi`
|
|
21
|
+
|
|
22
|
+
/** Column index of the first occurrence of `needle` on a given line. */
|
|
23
|
+
function colOf(ascii: string, lineIndex: number, needle: string): number {
|
|
24
|
+
const line = ascii.split('\n')[lineIndex]
|
|
25
|
+
if (line === undefined) throw new Error(`no line ${lineIndex}`)
|
|
26
|
+
const idx = line.indexOf(needle)
|
|
27
|
+
if (idx === -1) throw new Error(`"${needle}" not found on line ${lineIndex}`)
|
|
28
|
+
return idx
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Width (in chars) of the first `+---+`-style border run on a line. */
|
|
32
|
+
function borderWidth(ascii: string, lineIndex: number): number {
|
|
33
|
+
const line = ascii.split('\n')[lineIndex]
|
|
34
|
+
if (line === undefined) throw new Error(`no line ${lineIndex}`)
|
|
35
|
+
const match = /\+-+\+/.exec(line)
|
|
36
|
+
if (!match) throw new Error(`no border run on line ${lineIndex}: ${line}`)
|
|
37
|
+
return match[0].length
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function lineCount(ascii: string): number {
|
|
41
|
+
return ascii.split('\n').filter((l) => l.length > 0).length
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('ASCII sequence diagrams — padding (issue #343)', () => {
|
|
45
|
+
it('renders identically with no padding options and with explicit defaults', () => {
|
|
46
|
+
const withoutOptions = renderMermaidASCII(SEQ, { useAscii: true })
|
|
47
|
+
const withDefaults = renderMermaidASCII(SEQ, {
|
|
48
|
+
useAscii: true,
|
|
49
|
+
paddingX: 5,
|
|
50
|
+
paddingY: 5,
|
|
51
|
+
boxBorderPadding: 1,
|
|
52
|
+
})
|
|
53
|
+
expect(withDefaults).toBe(withoutOptions)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('paddingX widens the gap between lifelines (actor B shifts right)', () => {
|
|
57
|
+
const base = renderMermaidASCII(SEQ, { useAscii: true })
|
|
58
|
+
const wide = renderMermaidASCII(SEQ, { useAscii: true, paddingX: 25 })
|
|
59
|
+
const baseCol = colOf(base, 1, '| B |')
|
|
60
|
+
const wideCol = colOf(wide, 1, '| B |')
|
|
61
|
+
expect(wideCol).toBeGreaterThan(baseCol)
|
|
62
|
+
// Two independent step sizes from two different starting points produce
|
|
63
|
+
// the same shift — demonstrates paddingX scales lifeline spacing
|
|
64
|
+
// linearly rather than just "doing something" once.
|
|
65
|
+
const mid = renderMermaidASCII(SEQ, { useAscii: true, paddingX: 15 })
|
|
66
|
+
const midCol = colOf(mid, 1, '| B |')
|
|
67
|
+
expect(midCol - baseCol).toBe(wideCol - midCol)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('a very small paddingX tightens lifeline spacing below the default', () => {
|
|
71
|
+
const base = renderMermaidASCII(SEQ, { useAscii: true })
|
|
72
|
+
const tight = renderMermaidASCII(SEQ, { useAscii: true, paddingX: -10 })
|
|
73
|
+
expect(colOf(tight, 1, '| B |')).toBeLessThan(colOf(base, 1, '| B |'))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('paddingY adds extra blank rows (taller diagram)', () => {
|
|
77
|
+
const base = renderMermaidASCII(SEQ, { useAscii: true })
|
|
78
|
+
const tall = renderMermaidASCII(SEQ, { useAscii: true, paddingY: 25 })
|
|
79
|
+
expect(lineCount(tall)).toBeGreaterThan(lineCount(base))
|
|
80
|
+
// Same linearity check as paddingX above.
|
|
81
|
+
const mid = renderMermaidASCII(SEQ, { useAscii: true, paddingY: 15 })
|
|
82
|
+
expect(lineCount(mid) - lineCount(base)).toBe(
|
|
83
|
+
lineCount(tall) - lineCount(mid),
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('boxBorderPadding widens actor boxes by exactly 2 columns per unit', () => {
|
|
88
|
+
const base = renderMermaidASCII(SEQ, { useAscii: true })
|
|
89
|
+
const padded = renderMermaidASCII(SEQ, {
|
|
90
|
+
useAscii: true,
|
|
91
|
+
boxBorderPadding: 4,
|
|
92
|
+
})
|
|
93
|
+
// boxWidth = labelWidth + 2*boxBorderPadding + 2 (border chars) — going
|
|
94
|
+
// from the default (1) to 4 adds 2*(4-1) = 6 columns to each actor box.
|
|
95
|
+
expect(borderWidth(padded, 0) - borderWidth(base, 0)).toBe(6)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('boxBorderPadding also widens note boxes and shifts note content', () => {
|
|
99
|
+
const withNote = `sequenceDiagram
|
|
100
|
+
participant A
|
|
101
|
+
Note over A: hello`
|
|
102
|
+
const base = renderMermaidASCII(withNote, { useAscii: true })
|
|
103
|
+
const padded = renderMermaidASCII(withNote, {
|
|
104
|
+
useAscii: true,
|
|
105
|
+
boxBorderPadding: 4,
|
|
106
|
+
})
|
|
107
|
+
// The note's own top border is the second '+---+' run rendered (the
|
|
108
|
+
// first belongs to the actor header box), so isolate it before
|
|
109
|
+
// comparing width.
|
|
110
|
+
const noteBorder = (ascii: string) => {
|
|
111
|
+
const line = ascii.split('\n')[4] // note top border row
|
|
112
|
+
const match = line ? /\+-+\+/.exec(line) : null
|
|
113
|
+
if (!match) throw new Error(`no note border found: ${line}`)
|
|
114
|
+
return match[0].length
|
|
115
|
+
}
|
|
116
|
+
expect(noteBorder(padded) - noteBorder(base)).toBe(6)
|
|
117
|
+
expect(colOf(padded, 5, 'hello')).toBeGreaterThan(colOf(base, 5, 'hello'))
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII sequence diagram — self-arrow regression tests (issue #68)
|
|
3
|
+
//
|
|
4
|
+
// Two bugs in the ASCII sequence-diagram renderer's handling of self-arrows
|
|
5
|
+
// (A->>A: ...):
|
|
6
|
+
// 1. <br/> in a self-arrow label wrote the raw string (with an embedded
|
|
7
|
+
// newline) onto a single canvas row instead of splitting it onto
|
|
8
|
+
// multiple rows, corrupting every column to the right for the rest of
|
|
9
|
+
// the diagram.
|
|
10
|
+
// 2. A self-arrow inside an alt/loop/opt block could be drawn outside the
|
|
11
|
+
// block's wall because the wall's width was computed from lifeline
|
|
12
|
+
// positions only, ignoring the self-arrow's loop glyphs and label.
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
17
|
+
|
|
18
|
+
describe('ASCII sequence diagrams – self-arrow labels (issue #68)', () => {
|
|
19
|
+
it('splits <br/> in a self-arrow label across rows without corrupting the canvas', () => {
|
|
20
|
+
const result = renderMermaidASCII(
|
|
21
|
+
`
|
|
22
|
+
sequenceDiagram
|
|
23
|
+
participant A
|
|
24
|
+
participant B
|
|
25
|
+
A->>A: line one<br/>line two
|
|
26
|
+
`,
|
|
27
|
+
{ useAscii: false },
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const lines = result.split('\n')
|
|
31
|
+
const line1Idx = lines.findIndex((l) => l.includes('line one'))
|
|
32
|
+
const line2Idx = lines.findIndex((l) => l.includes('line two'))
|
|
33
|
+
|
|
34
|
+
expect(line1Idx).toBeGreaterThan(-1)
|
|
35
|
+
expect(line2Idx).toBeGreaterThan(-1)
|
|
36
|
+
// Each line of the label gets its own row, immediately below the last.
|
|
37
|
+
expect(line2Idx).toBe(line1Idx + 1)
|
|
38
|
+
|
|
39
|
+
// A corrupted canvas dumps the raw "line one\nline two" string onto one
|
|
40
|
+
// cell: the terminal newline forces "line two" to print flush against
|
|
41
|
+
// column 0 (no box-drawing indentation) instead of as a properly
|
|
42
|
+
// positioned canvas row.
|
|
43
|
+
expect(lines[line2Idx]!.startsWith(' ')).toBe(true)
|
|
44
|
+
|
|
45
|
+
// B's lifeline must still be intact on both label rows — a corrupted
|
|
46
|
+
// canvas shifts every column right of the embedded newline down one
|
|
47
|
+
// row for the rest of the diagram, destroying the B column.
|
|
48
|
+
expect(lines[line1Idx]).toContain('│')
|
|
49
|
+
expect(lines[line2Idx]).toContain('│')
|
|
50
|
+
|
|
51
|
+
// The footer actor boxes must render correctly and stay aligned with
|
|
52
|
+
// the header boxes (proves later rows weren't shifted by the newline).
|
|
53
|
+
const bottomBorders = (result.match(/└───┘/g) || []).length
|
|
54
|
+
expect(bottomBorders).toBe(2) // one footer box per actor (A, B)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('handles 3+ line self-arrow labels', () => {
|
|
58
|
+
const result = renderMermaidASCII(
|
|
59
|
+
`
|
|
60
|
+
sequenceDiagram
|
|
61
|
+
participant A
|
|
62
|
+
A->>A: one<br/>two<br/>three
|
|
63
|
+
`,
|
|
64
|
+
{ useAscii: false },
|
|
65
|
+
)
|
|
66
|
+
const lines = result.split('\n')
|
|
67
|
+
const idxOne = lines.findIndex((l) => l.includes('one'))
|
|
68
|
+
const idxTwo = lines.findIndex((l) => l.includes('two'))
|
|
69
|
+
const idxThree = lines.findIndex((l) => l.includes('three'))
|
|
70
|
+
expect(idxTwo).toBe(idxOne + 1)
|
|
71
|
+
expect(idxThree).toBe(idxTwo + 1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("extends an alt block's wall past a self-arrow's loop and label", () => {
|
|
75
|
+
const result = renderMermaidASCII(
|
|
76
|
+
`
|
|
77
|
+
sequenceDiagram
|
|
78
|
+
participant A
|
|
79
|
+
participant B
|
|
80
|
+
alt some condition
|
|
81
|
+
A->>A: a fairly long self-arrow label here
|
|
82
|
+
end
|
|
83
|
+
`,
|
|
84
|
+
{ useAscii: false },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// The block header label must not be clipped by an undersized wall.
|
|
88
|
+
expect(result).toContain('alt [some condition]')
|
|
89
|
+
|
|
90
|
+
// The self-arrow's loop corners must stay intact — a wall drawn too
|
|
91
|
+
// far left overwrites them with the wall's vertical-bar character.
|
|
92
|
+
expect(result).toContain('├───┐')
|
|
93
|
+
expect(result).toContain('◀───┘')
|
|
94
|
+
|
|
95
|
+
// The label and the block's right wall must coexist on the same row,
|
|
96
|
+
// with the wall to the right of (not through) the label.
|
|
97
|
+
const labelLine = result
|
|
98
|
+
.split('\n')
|
|
99
|
+
.find((l) => l.includes('a fairly long self-arrow label here'))
|
|
100
|
+
expect(labelLine).toBeDefined()
|
|
101
|
+
expect(labelLine!.trimEnd().endsWith('│')).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("extends a loop block's wall past a self-arrow's loop and label", () => {
|
|
105
|
+
const result = renderMermaidASCII(
|
|
106
|
+
`
|
|
107
|
+
sequenceDiagram
|
|
108
|
+
participant A
|
|
109
|
+
loop every request
|
|
110
|
+
A->>A: a fairly long self-arrow label for the loop block
|
|
111
|
+
end
|
|
112
|
+
`,
|
|
113
|
+
{ useAscii: false },
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(result).toContain('loop [every request]')
|
|
117
|
+
expect(result).toContain('├───┐')
|
|
118
|
+
expect(result).toContain('◀───┘')
|
|
119
|
+
|
|
120
|
+
const labelLine = result
|
|
121
|
+
.split('\n')
|
|
122
|
+
.find((l) =>
|
|
123
|
+
l.includes('a fairly long self-arrow label for the loop block'),
|
|
124
|
+
)
|
|
125
|
+
expect(labelLine).toBeDefined()
|
|
126
|
+
expect(labelLine!.trimEnd().endsWith('│')).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Self-message autonumber badge (issue #342)
|
|
132
|
+
//
|
|
133
|
+
// `autonumber` consumed a sequence number for a self-message but never drew
|
|
134
|
+
// the badge glyph on the loop — the counter still advanced correctly, but a
|
|
135
|
+
// numbered self-message and an unnumbered one looked identical. Fixed by
|
|
136
|
+
// widening the loop (see `selfLoopWidth` in sequence.ts) to make room for the
|
|
137
|
+
// badge digits at the start of the top arm, without disturbing the corner
|
|
138
|
+
// glyphs (├/┐ on top, ◀/┘ on bottom).
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
describe('ASCII sequence diagrams – self-message autonumber badge (issue #342)', () => {
|
|
142
|
+
it('draws the autonumber badge on a self-message loop', () => {
|
|
143
|
+
const result = renderMermaidASCII(
|
|
144
|
+
`sequenceDiagram
|
|
145
|
+
autonumber
|
|
146
|
+
A->>A: Reflect`,
|
|
147
|
+
{ useAscii: false },
|
|
148
|
+
)
|
|
149
|
+
const lines = result.split('\n')
|
|
150
|
+
// The badge digit "1" sits on the loop's top arm, between the left
|
|
151
|
+
// junction and the top-right corner. (Actor boxes also contain "┐", so
|
|
152
|
+
// match on the junction character "├" to find the loop row specifically.)
|
|
153
|
+
const topArmLine = lines.find((l) => l.includes('├'))
|
|
154
|
+
expect(topArmLine).toBeDefined()
|
|
155
|
+
expect(topArmLine).toMatch(/├1─+┐/)
|
|
156
|
+
// The loop's corner glyphs must stay intact — a corrupted badge
|
|
157
|
+
// placement would overwrite the corner instead of just the dashes.
|
|
158
|
+
expect(result).toContain('┐')
|
|
159
|
+
expect(result).toContain('┘')
|
|
160
|
+
expect(result).toContain('Reflect')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('keeps numbering in sequence across self-messages mixed with normal messages', () => {
|
|
164
|
+
const result = renderMermaidASCII(
|
|
165
|
+
`sequenceDiagram
|
|
166
|
+
participant A
|
|
167
|
+
participant B
|
|
168
|
+
autonumber
|
|
169
|
+
A->>B: First
|
|
170
|
+
A->>A: Self
|
|
171
|
+
A->>B: Third`,
|
|
172
|
+
{ useAscii: false },
|
|
173
|
+
)
|
|
174
|
+
const lines = result.split('\n')
|
|
175
|
+
|
|
176
|
+
// First normal message numbered 1.
|
|
177
|
+
const firstArrowLine = lines.find((l) => l.includes('▶') && l.includes('1'))
|
|
178
|
+
expect(firstArrowLine).toBeDefined()
|
|
179
|
+
|
|
180
|
+
// Self-message numbered 2, badge drawn on its loop's top arm.
|
|
181
|
+
const selfTopArmLine = lines.find((l) => l.includes('├'))
|
|
182
|
+
expect(selfTopArmLine).toBeDefined()
|
|
183
|
+
expect(selfTopArmLine).toMatch(/├2─+┐/)
|
|
184
|
+
|
|
185
|
+
// Third message (also A->>B) numbered 3, appearing after the self
|
|
186
|
+
// message's rows.
|
|
187
|
+
const selfIdx = lines.indexOf(selfTopArmLine ?? '')
|
|
188
|
+
const thirdArrowLine = lines
|
|
189
|
+
.slice(selfIdx + 1)
|
|
190
|
+
.find((l) => l.includes('▶') && l.includes('3'))
|
|
191
|
+
expect(thirdArrowLine).toBeDefined()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('renders a self-message without autonumber unchanged (no stray digits, no regression)', () => {
|
|
195
|
+
const result = renderMermaidASCII(
|
|
196
|
+
`sequenceDiagram
|
|
197
|
+
A->>A: Reflect`,
|
|
198
|
+
{ useAscii: false },
|
|
199
|
+
)
|
|
200
|
+
// No autonumber active — the loop must render exactly as before: plain
|
|
201
|
+
// corners with no digit inserted into the top arm.
|
|
202
|
+
expect(result).toContain('├───┐')
|
|
203
|
+
expect(result).toContain('◀───┘')
|
|
204
|
+
expect(result).toContain('Reflect')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { diamondRenderer } from '../shapes/diamond.ts'
|
|
3
|
+
import { canvasToString } from '../canvas.ts'
|
|
4
|
+
import type { ShapeRenderOptions } from '../shapes/types.ts'
|
|
5
|
+
|
|
6
|
+
describe('diamondRenderer.render', () => {
|
|
7
|
+
it('draws unicode diamond corner markers around a short label', () => {
|
|
8
|
+
const options: ShapeRenderOptions = { useAscii: false, padding: 1 }
|
|
9
|
+
const dimensions = diamondRenderer.getDimensions('Yes', options)
|
|
10
|
+
const canvas = diamondRenderer.render('Yes', dimensions, options)
|
|
11
|
+
|
|
12
|
+
const text = canvasToString(canvas)
|
|
13
|
+
const lines = text.split('\n')
|
|
14
|
+
|
|
15
|
+
expect(lines[0]!.startsWith('◇')).toBe(true)
|
|
16
|
+
expect(lines[0]!.endsWith('◇')).toBe(true)
|
|
17
|
+
expect(lines[lines.length - 1]!.startsWith('◇')).toBe(true)
|
|
18
|
+
expect(lines[lines.length - 1]!.endsWith('◇')).toBe(true)
|
|
19
|
+
expect(text).toContain('Yes')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('draws ascii diamond corner markers around a multi-line label', () => {
|
|
23
|
+
const options: ShapeRenderOptions = { useAscii: true, padding: 1 }
|
|
24
|
+
const label = 'Retry?\nCheck'
|
|
25
|
+
const dimensions = diamondRenderer.getDimensions(label, options)
|
|
26
|
+
const canvas = diamondRenderer.render(label, dimensions, options)
|
|
27
|
+
|
|
28
|
+
const text = canvasToString(canvas)
|
|
29
|
+
const lines = text.split('\n')
|
|
30
|
+
|
|
31
|
+
expect(lines[0]!.startsWith('<')).toBe(true)
|
|
32
|
+
expect(lines[0]!.endsWith('>')).toBe(true)
|
|
33
|
+
expect(lines[lines.length - 1]!.startsWith('<')).toBe(true)
|
|
34
|
+
expect(lines[lines.length - 1]!.endsWith('>')).toBe(true)
|
|
35
|
+
expect(text).toContain('Retry?')
|
|
36
|
+
expect(text).toContain('Check')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getBoxDimensions,
|
|
4
|
+
renderBox,
|
|
5
|
+
getBoxAttachmentPoint,
|
|
6
|
+
rectangleRenderer,
|
|
7
|
+
} from '../shapes/rectangle.ts'
|
|
8
|
+
import { canvasToString } from '../canvas.ts'
|
|
9
|
+
import { getCorners } from '../shapes/corners.ts'
|
|
10
|
+
import {
|
|
11
|
+
Up,
|
|
12
|
+
Down,
|
|
13
|
+
Left,
|
|
14
|
+
Right,
|
|
15
|
+
UpperLeft,
|
|
16
|
+
UpperRight,
|
|
17
|
+
LowerLeft,
|
|
18
|
+
LowerRight,
|
|
19
|
+
Middle,
|
|
20
|
+
} from '../types.ts'
|
|
21
|
+
import type { ShapeRenderOptions } from '../shapes/types.ts'
|
|
22
|
+
|
|
23
|
+
const optsP0: ShapeRenderOptions = { useAscii: false, padding: 0 }
|
|
24
|
+
const optsP1: ShapeRenderOptions = { useAscii: false, padding: 1 }
|
|
25
|
+
const optsAsciiP0: ShapeRenderOptions = { useAscii: true, padding: 0 }
|
|
26
|
+
|
|
27
|
+
describe('rectangle shape: getBoxDimensions', () => {
|
|
28
|
+
it('sizes a single-line label with zero padding', () => {
|
|
29
|
+
const dims = getBoxDimensions('Hi', optsP0)
|
|
30
|
+
expect(dims.width).toBe(4)
|
|
31
|
+
expect(dims.height).toBe(3)
|
|
32
|
+
expect(dims.labelArea).toEqual({ x: 1, y: 1, width: 2, height: 1 })
|
|
33
|
+
expect(dims.gridColumns).toEqual([1, 2, 1])
|
|
34
|
+
expect(dims.gridRows).toEqual([1, 1, 1])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('grows width and height with padding', () => {
|
|
38
|
+
const dims = getBoxDimensions('Hi', optsP1)
|
|
39
|
+
expect(dims.width).toBe(6)
|
|
40
|
+
// rawInnerHeight = 1 + 2*1 = 3 (odd) -> innerHeight stays 3
|
|
41
|
+
expect(dims.height).toBe(5)
|
|
42
|
+
expect(dims.gridColumns).toEqual([1, 4, 1])
|
|
43
|
+
expect(dims.gridRows).toEqual([1, 3, 1])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('bumps innerHeight to odd when line count makes it even', () => {
|
|
47
|
+
const dims = getBoxDimensions('One\nTwo', optsP0)
|
|
48
|
+
// lineCount=2, rawInnerHeight=2 (even) -> innerHeight=3
|
|
49
|
+
expect(dims.gridRows).toEqual([1, 3, 1])
|
|
50
|
+
expect(dims.height).toBe(5)
|
|
51
|
+
expect(dims.labelArea.height).toBe(2)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('keeps innerHeight odd when line count already makes it odd', () => {
|
|
55
|
+
const dims = getBoxDimensions('One\nTwo\nThree', optsP0)
|
|
56
|
+
// lineCount=3, rawInnerHeight=3 (odd) -> innerHeight stays 3
|
|
57
|
+
expect(dims.gridRows).toEqual([1, 3, 1])
|
|
58
|
+
expect(dims.height).toBe(5)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('uses the widest line to size width for multi-line labels', () => {
|
|
62
|
+
const dims = getBoxDimensions('Short\nA much longer line', optsP0)
|
|
63
|
+
const expectedWidth = 'A much longer line'.length + 2
|
|
64
|
+
expect(dims.width).toBe(expectedWidth)
|
|
65
|
+
expect(dims.labelArea.width).toBe('A much longer line'.length)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('handles an empty label', () => {
|
|
69
|
+
const dims = getBoxDimensions('', optsP0)
|
|
70
|
+
expect(dims.labelArea.width).toBe(0)
|
|
71
|
+
expect(dims.labelArea.height).toBe(1)
|
|
72
|
+
expect(dims.width).toBe(2)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('rectangle shape: renderBox', () => {
|
|
77
|
+
it('draws unicode borders and corners for a single-line label', () => {
|
|
78
|
+
const dims = getBoxDimensions('Hi', optsP0)
|
|
79
|
+
const corners = getCorners('rectangle', false)
|
|
80
|
+
const canvas = renderBox('Hi', dims, corners, false)
|
|
81
|
+
const text = canvasToString(canvas)
|
|
82
|
+
expect(text).toContain('┌')
|
|
83
|
+
expect(text).toContain('┐')
|
|
84
|
+
expect(text).toContain('└')
|
|
85
|
+
expect(text).toContain('┘')
|
|
86
|
+
expect(text).toContain('─')
|
|
87
|
+
expect(text).toContain('│')
|
|
88
|
+
expect(text).toContain('Hi')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('draws ascii borders and corners for a single-line label', () => {
|
|
92
|
+
const dims = getBoxDimensions('Hi', optsAsciiP0)
|
|
93
|
+
const corners = getCorners('rectangle', true)
|
|
94
|
+
const canvas = renderBox('Hi', dims, corners, true)
|
|
95
|
+
const text = canvasToString(canvas)
|
|
96
|
+
expect(text).toContain('+')
|
|
97
|
+
expect(text).toContain('-')
|
|
98
|
+
expect(text).toContain('|')
|
|
99
|
+
expect(text).not.toContain('┌')
|
|
100
|
+
expect(text).toContain('Hi')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('centers a single-line label horizontally and vertically', () => {
|
|
104
|
+
const dims = getBoxDimensions('Hi', optsP1)
|
|
105
|
+
const corners = getCorners('rectangle', false)
|
|
106
|
+
const canvas = renderBox('Hi', dims, corners, false)
|
|
107
|
+
const lines = canvasToString(canvas).split('\n')
|
|
108
|
+
const labelRow = lines.findIndex((l) => l.includes('Hi'))
|
|
109
|
+
expect(labelRow).toBeGreaterThan(0)
|
|
110
|
+
expect(labelRow).toBeLessThan(lines.length - 1)
|
|
111
|
+
const col = lines[labelRow]!.indexOf('Hi')
|
|
112
|
+
expect(col).toBeGreaterThan(0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('stacks a multi-line label across separate rows in order', () => {
|
|
116
|
+
const dims = getBoxDimensions('One\nTwo\nThree', optsP0)
|
|
117
|
+
const corners = getCorners('rectangle', false)
|
|
118
|
+
const canvas = renderBox('One\nTwo\nThree', dims, corners, false)
|
|
119
|
+
const lines = canvasToString(canvas).split('\n')
|
|
120
|
+
const oneRow = lines.findIndex((l) => l.includes('One'))
|
|
121
|
+
const twoRow = lines.findIndex((l) => l.includes('Two'))
|
|
122
|
+
const threeRow = lines.findIndex((l) => l.includes('Three'))
|
|
123
|
+
expect(oneRow).toBeGreaterThan(-1)
|
|
124
|
+
expect(twoRow).toBeGreaterThan(oneRow)
|
|
125
|
+
expect(threeRow).toBeGreaterThan(twoRow)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('centers an even number of label lines around the middle row', () => {
|
|
129
|
+
const dims = getBoxDimensions('One\nTwo', optsP0)
|
|
130
|
+
const corners = getCorners('rectangle', false)
|
|
131
|
+
const canvas = renderBox('One\nTwo', dims, corners, false)
|
|
132
|
+
const lines = canvasToString(canvas).split('\n')
|
|
133
|
+
const oneRow = lines.findIndex((l) => l.includes('One'))
|
|
134
|
+
const twoRow = lines.findIndex((l) => l.includes('Two'))
|
|
135
|
+
expect(twoRow).toBe(oneRow + 1)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('clips label characters that fall outside the canvas bounds', () => {
|
|
139
|
+
const tinyDims = getBoxDimensions('', optsP0)
|
|
140
|
+
const corners = getCorners('rectangle', false)
|
|
141
|
+
const canvas = renderBox('Hello World', tinyDims, corners, false)
|
|
142
|
+
const text = canvasToString(canvas)
|
|
143
|
+
expect(text).not.toContain('Hello World')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('uses custom corner characters when provided', () => {
|
|
147
|
+
const dims = getBoxDimensions('Hi', optsP0)
|
|
148
|
+
const canvas = renderBox(
|
|
149
|
+
'Hi',
|
|
150
|
+
dims,
|
|
151
|
+
{ tl: '<', tr: '>', bl: '<', br: '>' },
|
|
152
|
+
false,
|
|
153
|
+
)
|
|
154
|
+
const text = canvasToString(canvas)
|
|
155
|
+
expect(text).toContain('<')
|
|
156
|
+
expect(text).toContain('>')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('rectangle shape: getBoxAttachmentPoint', () => {
|
|
161
|
+
const dims = getBoxDimensions('Hi', optsP1)
|
|
162
|
+
const base = { x: 10, y: 20 }
|
|
163
|
+
|
|
164
|
+
it('returns the top-center point for Up', () => {
|
|
165
|
+
expect(getBoxAttachmentPoint(Up, dims, base)).toEqual({
|
|
166
|
+
x: base.x + Math.floor(dims.width / 2),
|
|
167
|
+
y: base.y,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns the bottom-center point for Down', () => {
|
|
172
|
+
expect(getBoxAttachmentPoint(Down, dims, base)).toEqual({
|
|
173
|
+
x: base.x + Math.floor(dims.width / 2),
|
|
174
|
+
y: base.y + dims.height - 1,
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('returns the left-middle point for Left', () => {
|
|
179
|
+
expect(getBoxAttachmentPoint(Left, dims, base)).toEqual({
|
|
180
|
+
x: base.x,
|
|
181
|
+
y: base.y + Math.floor(dims.height / 2),
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('returns the right-middle point for Right', () => {
|
|
186
|
+
expect(getBoxAttachmentPoint(Right, dims, base)).toEqual({
|
|
187
|
+
x: base.x + dims.width - 1,
|
|
188
|
+
y: base.y + Math.floor(dims.height / 2),
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns the top-left corner for UpperLeft', () => {
|
|
193
|
+
expect(getBoxAttachmentPoint(UpperLeft, dims, base)).toEqual({
|
|
194
|
+
x: base.x,
|
|
195
|
+
y: base.y,
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('returns the top-right corner for UpperRight', () => {
|
|
200
|
+
expect(getBoxAttachmentPoint(UpperRight, dims, base)).toEqual({
|
|
201
|
+
x: base.x + dims.width - 1,
|
|
202
|
+
y: base.y,
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('returns the bottom-left corner for LowerLeft', () => {
|
|
207
|
+
expect(getBoxAttachmentPoint(LowerLeft, dims, base)).toEqual({
|
|
208
|
+
x: base.x,
|
|
209
|
+
y: base.y + dims.height - 1,
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('returns the bottom-right corner for LowerRight', () => {
|
|
214
|
+
expect(getBoxAttachmentPoint(LowerRight, dims, base)).toEqual({
|
|
215
|
+
x: base.x + dims.width - 1,
|
|
216
|
+
y: base.y + dims.height - 1,
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('falls back to the center point for Middle', () => {
|
|
221
|
+
expect(getBoxAttachmentPoint(Middle, dims, base)).toEqual({
|
|
222
|
+
x: base.x + Math.floor(dims.width / 2),
|
|
223
|
+
y: base.y + Math.floor(dims.height / 2),
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('rectangleRenderer', () => {
|
|
229
|
+
it('delegates getDimensions to getBoxDimensions', () => {
|
|
230
|
+
expect(rectangleRenderer.getDimensions('Hi', optsP0)).toEqual(
|
|
231
|
+
getBoxDimensions('Hi', optsP0),
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('renders unicode rectangle corners by default', () => {
|
|
236
|
+
const dims = rectangleRenderer.getDimensions('Hi', optsP0)
|
|
237
|
+
const canvas = rectangleRenderer.render('Hi', dims, optsP0)
|
|
238
|
+
const text = canvasToString(canvas)
|
|
239
|
+
expect(text).toContain('┌')
|
|
240
|
+
expect(text).toContain('┘')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('renders ascii rectangle corners when useAscii is set', () => {
|
|
244
|
+
const dims = rectangleRenderer.getDimensions('Hi', optsAsciiP0)
|
|
245
|
+
const canvas = rectangleRenderer.render('Hi', dims, optsAsciiP0)
|
|
246
|
+
const text = canvasToString(canvas)
|
|
247
|
+
expect(text).toContain('+')
|
|
248
|
+
expect(text).not.toContain('┌')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('delegates getAttachmentPoint to getBoxAttachmentPoint', () => {
|
|
252
|
+
const dims = rectangleRenderer.getDimensions('Hi', optsP0)
|
|
253
|
+
const base = { x: 3, y: 4 }
|
|
254
|
+
expect(rectangleRenderer.getAttachmentPoint(Down, dims, base)).toEqual(
|
|
255
|
+
getBoxAttachmentPoint(Down, dims, base),
|
|
256
|
+
)
|
|
257
|
+
})
|
|
258
|
+
})
|