@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independently re-implemented terminal display-width calculation, shared by
|
|
3
|
+
* the ASCII wide-character regression suites (issues #66, #182, #205, #214).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately does NOT import from `src/ascii/display-width.ts` or
|
|
6
|
+
* `packages/core/src/text-metrics.ts`: these suites verify rendered output against an
|
|
7
|
+
* authority separate from the implementation under test, so that a bug in the
|
|
8
|
+
* renderer's own width helper cannot make its tests pass by agreeing with
|
|
9
|
+
* itself. Mirrors the East Asian Width ranges a real monospace terminal uses
|
|
10
|
+
* to decide whether a code point renders as 1 or 2 columns.
|
|
11
|
+
*
|
|
12
|
+
* Measures per grapheme cluster (`Intl.Segmenter`), not per code point: a
|
|
13
|
+
* real terminal gives zero columns to a combining mark (it attaches to the
|
|
14
|
+
* preceding base character — issue #205) and exactly one glyph's worth of
|
|
15
|
+
* columns to a composed multi-code-point sequence like a ZWJ emoji, flag, or
|
|
16
|
+
* skin-tone modifier (issue #214), not one glyph's worth per code point.
|
|
17
|
+
*/
|
|
18
|
+
import { expect } from 'vitest'
|
|
19
|
+
|
|
20
|
+
const EMOJI_PATTERN = /\p{Extended_Pictographic}|\p{Emoji_Presentation}/u
|
|
21
|
+
/** Unicode general categories Mn (Nonspacing_Mark) and Me (Enclosing_Mark). */
|
|
22
|
+
const COMBINING_MARK_PATTERN = /\p{Mn}|\p{Me}/u
|
|
23
|
+
|
|
24
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, {
|
|
25
|
+
granularity: 'grapheme',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function isWideCodePoint(ch: string): boolean {
|
|
29
|
+
const code = ch.codePointAt(0)!
|
|
30
|
+
return (
|
|
31
|
+
(code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
|
|
32
|
+
(code >= 0x2e80 && code <= 0xa4cf) || // CJK radicals .. Yi
|
|
33
|
+
(code >= 0xac00 && code <= 0xd7a3) || // Hangul syllables
|
|
34
|
+
(code >= 0xf900 && code <= 0xfaff) || // CJK compatibility ideographs
|
|
35
|
+
(code >= 0xff00 && code <= 0xff60) || // Fullwidth forms
|
|
36
|
+
(code >= 0xffe0 && code <= 0xffe6) || // Fullwidth signs
|
|
37
|
+
code >= 0x20000 || // CJK extension B+
|
|
38
|
+
EMOJI_PATTERN.test(ch)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Number of terminal columns one grapheme cluster occupies: 0, 1, or 2. */
|
|
43
|
+
function clusterWidth(cluster: string): number {
|
|
44
|
+
let sawWide = false
|
|
45
|
+
let sawNonMark = false
|
|
46
|
+
for (const ch of cluster) {
|
|
47
|
+
if (isWideCodePoint(ch)) sawWide = true
|
|
48
|
+
if (!COMBINING_MARK_PATTERN.test(ch)) sawNonMark = true
|
|
49
|
+
}
|
|
50
|
+
if (sawWide) return 2
|
|
51
|
+
return sawNonMark ? 1 : 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Number of terminal columns `text` occupies. */
|
|
55
|
+
export function terminalDisplayWidth(text: string): number {
|
|
56
|
+
let width = 0
|
|
57
|
+
for (const { segment } of graphemeSegmenter.segment(text)) {
|
|
58
|
+
width += clusterWidth(segment)
|
|
59
|
+
}
|
|
60
|
+
return width
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** All rendered lines should occupy the same number of terminal columns. */
|
|
64
|
+
export function assertUniformDisplayWidth(ascii: string): void {
|
|
65
|
+
const lines = ascii.split('\n')
|
|
66
|
+
const widths = lines.map(terminalDisplayWidth)
|
|
67
|
+
const expected = widths[0]
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
expect(
|
|
70
|
+
widths[i],
|
|
71
|
+
`line ${i} (${JSON.stringify(lines[i])}) has display width ${widths[i]}, expected ${expected}`,
|
|
72
|
+
).toBe(expected)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getPath,
|
|
4
|
+
heuristic,
|
|
5
|
+
mergePath,
|
|
6
|
+
createPathBudget,
|
|
7
|
+
DEFAULT_PATH_BUDGET,
|
|
8
|
+
} from '../pathfinder.ts'
|
|
9
|
+
import type { GridCoord, PathBudget } from '../types.ts'
|
|
10
|
+
import { createGrid, placeBlock, type Grid } from '../grid-occupancy.ts'
|
|
11
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to build an occupied grid from a list of coordinates, going through
|
|
15
|
+
* the real `Grid` abstraction rather than hand-rolling a raw Map/Set — a
|
|
16
|
+
* size-1 "block" reserves exactly the one requested cell.
|
|
17
|
+
*/
|
|
18
|
+
function buildGrid(occupied: GridCoord[]): Grid {
|
|
19
|
+
const grid = createGrid()
|
|
20
|
+
for (const c of occupied) {
|
|
21
|
+
placeBlock(grid, c, 1)
|
|
22
|
+
}
|
|
23
|
+
return grid
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('pathfinder', () => {
|
|
27
|
+
describe('getPath', () => {
|
|
28
|
+
it('finds a straight-line path on an empty grid', () => {
|
|
29
|
+
const grid = buildGrid([])
|
|
30
|
+
const path = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 3 })
|
|
31
|
+
expect(path).not.toBeNull()
|
|
32
|
+
expect(path![0]).toEqual({ x: 0, y: 0 })
|
|
33
|
+
expect(path![path!.length - 1]).toEqual({ x: 0, y: 3 })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('routes around an obstacle', () => {
|
|
37
|
+
// Block the straight path at (0,1)
|
|
38
|
+
const grid = buildGrid([{ x: 0, y: 1 }])
|
|
39
|
+
const path = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 2 })
|
|
40
|
+
expect(path).not.toBeNull()
|
|
41
|
+
expect(path![0]).toEqual({ x: 0, y: 0 })
|
|
42
|
+
expect(path![path!.length - 1]).toEqual({ x: 0, y: 2 })
|
|
43
|
+
// Path must detour (length > 3)
|
|
44
|
+
expect(path!.length).toBeGreaterThan(3)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns null when destination is completely enclosed', () => {
|
|
48
|
+
// Surround (2,2) with occupied cells on all 4 sides
|
|
49
|
+
const grid = buildGrid([
|
|
50
|
+
{ x: 1, y: 2 },
|
|
51
|
+
{ x: 3, y: 2 },
|
|
52
|
+
{ x: 2, y: 1 },
|
|
53
|
+
{ x: 2, y: 3 },
|
|
54
|
+
])
|
|
55
|
+
const path = getPath(grid, { x: 0, y: 0 }, { x: 2, y: 2 })
|
|
56
|
+
// Destination is occupied-surrounded; A* should return null within iteration limit
|
|
57
|
+
// (not throw RangeError: Map maximum size exceeded)
|
|
58
|
+
expect(path).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns null for unreachable destination without exhausting memory', () => {
|
|
62
|
+
// Create a wall that blocks access to the target region
|
|
63
|
+
const occupied: GridCoord[] = []
|
|
64
|
+
for (let i = 0; i < 100; i++) {
|
|
65
|
+
occupied.push({ x: 5, y: i }) // vertical wall at x=5
|
|
66
|
+
}
|
|
67
|
+
const grid = buildGrid(occupied)
|
|
68
|
+
// Target is behind the wall; only reachable via y<0 (disallowed) or y>=100 (very far)
|
|
69
|
+
getPath(grid, { x: 0, y: 50 }, { x: 10, y: 50 })
|
|
70
|
+
// Should find a path going around the wall (via y>=100), or return null if
|
|
71
|
+
// the iteration limit is hit. Either outcome is acceptable; the key is no
|
|
72
|
+
// crash / unbounded memory growth.
|
|
73
|
+
expect(true).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('heuristic', () => {
|
|
78
|
+
it('returns 0 for same point', () => {
|
|
79
|
+
expect(heuristic({ x: 3, y: 5 }, { x: 3, y: 5 })).toBe(0)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('returns manhattan distance for axis-aligned points', () => {
|
|
83
|
+
expect(heuristic({ x: 0, y: 0 }, { x: 0, y: 5 })).toBe(5)
|
|
84
|
+
expect(heuristic({ x: 0, y: 0 }, { x: 3, y: 0 })).toBe(3)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('adds +1 penalty for diagonal displacement', () => {
|
|
88
|
+
expect(heuristic({ x: 0, y: 0 }, { x: 2, y: 3 })).toBe(6) // 2+3+1
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('mergePath', () => {
|
|
93
|
+
it('removes collinear intermediate points', () => {
|
|
94
|
+
const path: GridCoord[] = [
|
|
95
|
+
{ x: 0, y: 0 },
|
|
96
|
+
{ x: 1, y: 0 },
|
|
97
|
+
{ x: 2, y: 0 },
|
|
98
|
+
{ x: 2, y: 1 },
|
|
99
|
+
]
|
|
100
|
+
const merged = mergePath(path)
|
|
101
|
+
expect(merged).toEqual([
|
|
102
|
+
{ x: 0, y: 0 },
|
|
103
|
+
{ x: 2, y: 0 },
|
|
104
|
+
{ x: 2, y: 1 },
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns short paths unchanged', () => {
|
|
109
|
+
expect(mergePath([{ x: 0, y: 0 }])).toEqual([{ x: 0, y: 0 }])
|
|
110
|
+
expect(
|
|
111
|
+
mergePath([
|
|
112
|
+
{ x: 0, y: 0 },
|
|
113
|
+
{ x: 1, y: 0 },
|
|
114
|
+
]),
|
|
115
|
+
).toEqual([
|
|
116
|
+
{ x: 0, y: 0 },
|
|
117
|
+
{ x: 1, y: 0 },
|
|
118
|
+
])
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('PathBudget (render-wide iteration budget)', () => {
|
|
123
|
+
// These test the deterministic mechanism that actually bounds total
|
|
124
|
+
// pathfinding work for a render — not wall-clock time. See the
|
|
125
|
+
// ascii-edge-routing-fixes.test.ts "much larger dense fan-in" test
|
|
126
|
+
// (PR #143) for the wall-clock version of this same regression guard,
|
|
127
|
+
// which flaked under CPU contention because it measures how long the
|
|
128
|
+
// work took rather than how much work happened. Asserting on
|
|
129
|
+
// budget.remaining instead is immune to machine speed/load entirely.
|
|
130
|
+
|
|
131
|
+
it('createPathBudget defaults to DEFAULT_PATH_BUDGET', () => {
|
|
132
|
+
expect(createPathBudget()).toEqual({ remaining: DEFAULT_PATH_BUDGET })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('createPathBudget accepts a custom total', () => {
|
|
136
|
+
expect(createPathBudget(500)).toEqual({ remaining: 500 })
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('decrements a shared budget cumulatively across multiple getPath calls', () => {
|
|
140
|
+
const grid = buildGrid([])
|
|
141
|
+
const budget = createPathBudget(1000)
|
|
142
|
+
const initial = budget.remaining
|
|
143
|
+
|
|
144
|
+
getPath(grid, { x: 0, y: 0 }, { x: 0, y: 20 }, budget)
|
|
145
|
+
const afterFirst = budget.remaining
|
|
146
|
+
expect(afterFirst).toBeLessThan(initial)
|
|
147
|
+
|
|
148
|
+
getPath(grid, { x: 0, y: 0 }, { x: 0, y: 20 }, budget)
|
|
149
|
+
const afterSecond = budget.remaining
|
|
150
|
+
// Same search run twice costs the same iterations each time, and the
|
|
151
|
+
// budget is cumulative across calls, not reset per call — so the
|
|
152
|
+
// second call must spend down further from where the first left off.
|
|
153
|
+
expect(afterSecond).toBeLessThan(afterFirst)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('returns null immediately once the budget is exhausted, without spending further iterations', () => {
|
|
157
|
+
const grid = buildGrid([])
|
|
158
|
+
const budget: PathBudget = { remaining: 0 }
|
|
159
|
+
const result = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 5 }, budget)
|
|
160
|
+
expect(result).toBeNull()
|
|
161
|
+
// The exhausted-budget check is the first thing getPath does, before
|
|
162
|
+
// the search loop runs at all, so remaining stays unchanged.
|
|
163
|
+
expect(budget.remaining).toBe(0)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('never lets budget.remaining go negative', () => {
|
|
167
|
+
const grid = buildGrid([])
|
|
168
|
+
const budget = createPathBudget(3) // deliberately smaller than this search needs
|
|
169
|
+
getPath(grid, { x: 0, y: 0 }, { x: 0, y: 50 }, budget)
|
|
170
|
+
expect(budget.remaining).toBeGreaterThanOrEqual(0)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('bounds total pathfinding work across many edges to the budget, regardless of edge count', () => {
|
|
174
|
+
// Structural equivalent of the "much larger dense fan-in" wall-clock
|
|
175
|
+
// test: 120 fan-in edges, each independently getting its own
|
|
176
|
+
// unbounded allowance, is what caused the real OOM crashes this
|
|
177
|
+
// budget exists to prevent (see pathfinder.ts's PathBudget doc
|
|
178
|
+
// comment). Here, a deliberately small shared budget stands in for
|
|
179
|
+
// "more total demand than the budget allows" — the same shape of
|
|
180
|
+
// problem as 120 real edges against the production default.
|
|
181
|
+
const grid = buildGrid([])
|
|
182
|
+
const budget = createPathBudget(500)
|
|
183
|
+
for (let i = 0; i < 120; i++) {
|
|
184
|
+
getPath(grid, { x: i, y: 0 }, { x: i, y: 40 }, budget)
|
|
185
|
+
}
|
|
186
|
+
// Total demand (120 searches x ~40+ iterations each) far exceeds the
|
|
187
|
+
// 500-iteration budget, so it must land at exactly 0 — proving later
|
|
188
|
+
// calls were actually cut off partway through, not merely that none
|
|
189
|
+
// of them individually happened to exceed it.
|
|
190
|
+
expect(budget.remaining).toBe(0)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe('dense graph regression', () => {
|
|
195
|
+
it('does not crash on dense TD graph with multiple fan-in bundles', () => {
|
|
196
|
+
// Regression test for: RangeError: Map maximum size exceeded
|
|
197
|
+
// https://github.com/lukilabs/beautiful-mermaid/issues/64
|
|
198
|
+
const code = `graph TD
|
|
199
|
+
A["AAA<br>(keita)"] --> C["CCC"]
|
|
200
|
+
B["BBB<br>(yuriko)"] --> C
|
|
201
|
+
C --> D["DDDD"]
|
|
202
|
+
D --> E["EEEE"]
|
|
203
|
+
|
|
204
|
+
A1["1 / 2"] --> A
|
|
205
|
+
A2["3 / 4"] --> A
|
|
206
|
+
A3["5 / 6"] --> A
|
|
207
|
+
A4["XXX<br>(YYY ZZZ)"] --> A
|
|
208
|
+
|
|
209
|
+
B1["77 77<br>(7 / 7 / 7)"] --> B
|
|
210
|
+
B2["88-88<br>(99 99)"] --> B
|
|
211
|
+
B3["111s 222s"] --> B
|
|
212
|
+
|
|
213
|
+
D --> F{"F?"}
|
|
214
|
+
F -->|Yes| G["High level<br>Tr"]
|
|
215
|
+
F -->|No| H["Dumb Tr<br>S"]`
|
|
216
|
+
|
|
217
|
+
// Should not throw (previously threw RangeError: Map maximum size exceeded)
|
|
218
|
+
const result = renderMermaidASCII(code, { useAscii: false })
|
|
219
|
+
expect(result).toBeDefined()
|
|
220
|
+
expect(result.length).toBeGreaterThan(0)
|
|
221
|
+
// Verify most node labels appear (some may be visually clipped by edge routing)
|
|
222
|
+
for (const label of [
|
|
223
|
+
'CCC',
|
|
224
|
+
'DDDD',
|
|
225
|
+
'EEEE',
|
|
226
|
+
'1 / 2',
|
|
227
|
+
'3 / 4',
|
|
228
|
+
'5 / 6',
|
|
229
|
+
'XXX',
|
|
230
|
+
'77 77',
|
|
231
|
+
'88-88',
|
|
232
|
+
'111s 222s',
|
|
233
|
+
'F?',
|
|
234
|
+
]) {
|
|
235
|
+
expect(result).toContain(label)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
graph LR
|
|
2
|
+
A --> B
|
|
3
|
+
B --> C
|
|
4
|
+
A --> C
|
|
5
|
+
B --> D
|
|
6
|
+
C --> D
|
|
7
|
+
---
|
|
8
|
+
+---+ +---+ +---+
|
|
9
|
+
| | | | | |
|
|
10
|
+
| A +---->| B +---->| D |
|
|
11
|
+
| | | | | |
|
|
12
|
+
+-+-+ +-+-+ +---+
|
|
13
|
+
| | ^
|
|
14
|
+
| | |
|
|
15
|
+
| | |
|
|
16
|
+
| | |
|
|
17
|
+
| v |
|
|
18
|
+
| +---+ |
|
|
19
|
+
| | | |
|
|
20
|
+
+------>| C +-------+
|
|
21
|
+
| |
|
|
22
|
+
+---+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
graph LR
|
|
2
|
+
A --> B
|
|
3
|
+
B --> C
|
|
4
|
+
A --> C
|
|
5
|
+
B --> D
|
|
6
|
+
D --> C
|
|
7
|
+
---
|
|
8
|
+
+---+ +---+ +---+
|
|
9
|
+
| | | | | |
|
|
10
|
+
| A +---->| B +--+->+ D |
|
|
11
|
+
| | | | | | |
|
|
12
|
+
+-+-+ +-+-+ | +---+
|
|
13
|
+
| | |
|
|
14
|
+
| | |
|
|
15
|
+
| +----+
|
|
16
|
+
| |
|
|
17
|
+
| v
|
|
18
|
+
| +---+
|
|
19
|
+
| | |
|
|
20
|
+
+------>| C |
|
|
21
|
+
| |
|
|
22
|
+
+---+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
paddingX=5
|
|
2
|
+
paddingY=3
|
|
3
|
+
graph LR
|
|
4
|
+
A --> B & C
|
|
5
|
+
B --> C & D
|
|
6
|
+
D --> C
|
|
7
|
+
---
|
|
8
|
+
+---+ +---+ +---+
|
|
9
|
+
| | | | | |
|
|
10
|
+
| A +---->| B +--+->+ D |
|
|
11
|
+
| | | | | | |
|
|
12
|
+
+-+-+ +-+-+ | +---+
|
|
13
|
+
| | |
|
|
14
|
+
| +----+
|
|
15
|
+
| v
|
|
16
|
+
| +---+
|
|
17
|
+
| | |
|
|
18
|
+
+------>| C |
|
|
19
|
+
| |
|
|
20
|
+
+---+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
classDiagram
|
|
2
|
+
A <|-- B : inherits
|
|
3
|
+
C *-- D : owns
|
|
4
|
+
E o-- F : has
|
|
5
|
+
G --> H : uses
|
|
6
|
+
I ..> J : depends
|
|
7
|
+
K ..|> L : implements
|
|
8
|
+
---
|
|
9
|
+
+---+ +---+ +---+ +---+ +---+ +---+
|
|
10
|
+
| A | | C | | E | | G | | I | | K |
|
|
11
|
+
+---+ +---+ +---+ +---+ +---+ +---+
|
|
12
|
+
^ * o | : :
|
|
13
|
+
inherits owns has uses depends implements
|
|
14
|
+
| | | v v v
|
|
15
|
+
+---+ +---+ +---+ +---+ +---+ +---+
|
|
16
|
+
| B | | D | | F | | H | | J | | L |
|
|
17
|
+
+---+ +---+ +---+ +---+ +---+ +---+
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
classDiagram
|
|
2
|
+
class Shape {
|
|
3
|
+
<<abstract>>
|
|
4
|
+
+draw() void
|
|
5
|
+
}
|
|
6
|
+
class Circle {
|
|
7
|
+
+radius int
|
|
8
|
+
+draw() void
|
|
9
|
+
}
|
|
10
|
+
Shape <|-- Circle
|
|
11
|
+
---
|
|
12
|
+
+----------------+
|
|
13
|
+
| <<abstract>> |
|
|
14
|
+
| Shape |
|
|
15
|
+
+----------------+
|
|
16
|
+
| + draw(): void |
|
|
17
|
+
+----------------+
|
|
18
|
+
^
|
|
19
|
+
|
|
|
20
|
+
|
|
|
21
|
+
+----------------+
|
|
22
|
+
| Circle |
|
|
23
|
+
+----------------+
|
|
24
|
+
| + int: radius |
|
|
25
|
+
+----------------+
|
|
26
|
+
| + draw(): void |
|
|
27
|
+
+----------------+
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
classDiagram
|
|
2
|
+
Animal <|-- Dog
|
|
3
|
+
Animal : +String name
|
|
4
|
+
Dog : +bark() void
|
|
5
|
+
---
|
|
6
|
+
+----------------+
|
|
7
|
+
| Animal |
|
|
8
|
+
+----------------+
|
|
9
|
+
| + name: String |
|
|
10
|
+
+----------------+
|
|
11
|
+
^
|
|
12
|
+
|
|
|
13
|
+
|
|
|
14
|
+
+----------------+
|
|
15
|
+
| Dog |
|
|
16
|
+
+----------------+
|
|
17
|
+
| + bark(): void |
|
|
18
|
+
+----------------+
|
|
19
|
+
|
|
20
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
classDiagram
|
|
2
|
+
class BankAccount {
|
|
3
|
+
+String owner
|
|
4
|
+
-int balance
|
|
5
|
+
+deposit(int amount) void
|
|
6
|
+
+withdraw(int amount) bool
|
|
7
|
+
-validate() bool
|
|
8
|
+
}
|
|
9
|
+
---
|
|
10
|
+
+------------------------------+
|
|
11
|
+
| BankAccount |
|
|
12
|
+
+------------------------------+
|
|
13
|
+
| + owner: String |
|
|
14
|
+
| - balance: int |
|
|
15
|
+
+------------------------------+
|
|
16
|
+
| + deposit(int amount): void |
|
|
17
|
+
| + withdraw(int amount): bool |
|
|
18
|
+
| - validate(): bool |
|
|
19
|
+
+------------------------------+
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
graph LR
|
|
2
|
+
%% This is a comment
|
|
3
|
+
A --> B
|
|
4
|
+
%% Another comment
|
|
5
|
+
B --> C
|
|
6
|
+
A --> C
|
|
7
|
+
%% Final comment
|
|
8
|
+
---
|
|
9
|
+
+---+ +---+
|
|
10
|
+
| | | |
|
|
11
|
+
| A +---->| B |
|
|
12
|
+
| | | |
|
|
13
|
+
+-+-+ +-+-+
|
|
14
|
+
| |
|
|
15
|
+
| |
|
|
16
|
+
| |
|
|
17
|
+
| |
|
|
18
|
+
| v
|
|
19
|
+
| +---+
|
|
20
|
+
| | |
|
|
21
|
+
+------>| C |
|
|
22
|
+
| |
|
|
23
|
+
+---+
|