zombie-mermaid 1.1.3
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 +21 -0
- package/README.md +153 -0
- package/dist/cli.js +11194 -0
- package/dist/index.cjs +11060 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +344 -0
- package/dist/index.d.ts +344 -0
- package/dist/index.js +11014 -0
- package/dist/index.js.map +1 -0
- package/package.json +104 -0
- package/src/__tests__/ascii-ansi.test.ts +498 -0
- package/src/__tests__/ascii-canvas-write.test.ts +137 -0
- package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
- package/src/__tests__/ascii-cjk-width.test.ts +185 -0
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +171 -0
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
- package/src/__tests__/ascii-draw-bundles.test.ts +638 -0
- package/src/__tests__/ascii-edge-bundling-routing.test.ts +210 -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-styles.test.ts +149 -0
- package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
- package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
- package/src/__tests__/ascii-grid-deep-chain.test.ts +64 -0
- package/src/__tests__/ascii-multiline.test.ts +288 -0
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
- package/src/__tests__/ascii-sequence-notes.test.ts +25 -0
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +128 -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 +296 -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-subgraph-bbox.test.ts +135 -0
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +89 -0
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
- package/src/__tests__/ascii-validate.test.ts +220 -0
- package/src/__tests__/ascii.test.ts +325 -0
- package/src/__tests__/browser.test.ts +34 -0
- package/src/__tests__/class-arrow-directions.test.ts +413 -0
- package/src/__tests__/class-integration.test.ts +118 -0
- package/src/__tests__/class-parser.test.ts +241 -0
- package/src/__tests__/cli-diagrams.test.ts +101 -0
- package/src/__tests__/cli-e2e.test.ts +213 -0
- package/src/__tests__/cli-entrypoint.test.ts +146 -0
- package/src/__tests__/cli-render.test.ts +175 -0
- package/src/__tests__/cli-test-helpers.ts +26 -0
- package/src/__tests__/cli.test.ts +204 -0
- package/src/__tests__/diagram-type.test.ts +28 -0
- package/src/__tests__/draw-lines.test.ts +93 -0
- package/src/__tests__/edge-approach-direction.test.ts +295 -0
- package/src/__tests__/er-integration.test.ts +528 -0
- package/src/__tests__/er-parser.test.ts +297 -0
- package/src/__tests__/grid-occupancy.test.ts +80 -0
- package/src/__tests__/integration.test.ts +595 -0
- package/src/__tests__/layout-disconnected.test.ts +384 -0
- package/src/__tests__/layout-engine.test.ts +88 -0
- package/src/__tests__/linkstyle.test.ts +133 -0
- package/src/__tests__/multiline-labels.test.ts +912 -0
- package/src/__tests__/nested-subgraph-direction-crossing.test.ts +304 -0
- package/src/__tests__/parser.test.ts +1187 -0
- package/src/__tests__/pathfinder.test.ts +239 -0
- package/src/__tests__/render-options-overrides.test.ts +250 -0
- package/src/__tests__/renderer.test.ts +743 -0
- package/src/__tests__/sequence-integration.test.ts +121 -0
- package/src/__tests__/sequence-layout.test.ts +861 -0
- package/src/__tests__/sequence-parser.test.ts +313 -0
- package/src/__tests__/styles.test.ts +201 -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 +17 -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 +37 -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 +17 -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__/text-metrics.test.ts +298 -0
- package/src/__tests__/theme.test.ts +108 -0
- package/src/__tests__/xychart-ascii.test.ts +327 -0
- package/src/__tests__/xychart-colors.test.ts +148 -0
- package/src/__tests__/xychart-integration.test.ts +137 -0
- package/src/__tests__/xychart-layout.test.ts +167 -0
- package/src/ascii/ansi.ts +500 -0
- package/src/ascii/canvas.ts +633 -0
- package/src/ascii/class-diagram.ts +765 -0
- package/src/ascii/converter.ts +393 -0
- package/src/ascii/display-width.ts +65 -0
- package/src/ascii/draw-arrows.ts +468 -0
- package/src/ascii/draw-boxes.ts +231 -0
- package/src/ascii/draw-bundles.ts +488 -0
- package/src/ascii/draw-lines.ts +165 -0
- package/src/ascii/draw-subgraphs.ts +93 -0
- package/src/ascii/draw.ts +303 -0
- package/src/ascii/edge-bundling.ts +343 -0
- package/src/ascii/edge-routing.ts +431 -0
- package/src/ascii/er-diagram.ts +565 -0
- package/src/ascii/grid-occupancy.ts +136 -0
- package/src/ascii/grid.ts +894 -0
- package/src/ascii/index.ts +166 -0
- package/src/ascii/multiline-utils.ts +82 -0
- package/src/ascii/pathfinder.ts +446 -0
- package/src/ascii/sequence.ts +578 -0
- package/src/ascii/shapes/circle.ts +31 -0
- package/src/ascii/shapes/corners.ts +130 -0
- package/src/ascii/shapes/diamond.ts +31 -0
- package/src/ascii/shapes/hexagon.ts +31 -0
- package/src/ascii/shapes/index.ts +119 -0
- package/src/ascii/shapes/rectangle.ts +199 -0
- package/src/ascii/shapes/rounded.ts +31 -0
- package/src/ascii/shapes/special.ts +310 -0
- package/src/ascii/shapes/stadium.ts +122 -0
- package/src/ascii/shapes/state.ts +204 -0
- package/src/ascii/shapes/types.ts +78 -0
- package/src/ascii/types.ts +373 -0
- package/src/ascii/validate.ts +189 -0
- package/src/ascii/xychart.ts +1060 -0
- package/src/browser.ts +36 -0
- package/src/class/layout.ts +274 -0
- package/src/class/parser.ts +347 -0
- package/src/class/renderer.ts +449 -0
- package/src/class/types.ts +121 -0
- package/src/cli/parse-args.ts +121 -0
- package/src/cli/render.ts +101 -0
- package/src/cli.ts +82 -0
- package/src/diagram-type.ts +32 -0
- package/src/elk-instance.ts +159 -0
- package/src/er/layout.ts +229 -0
- package/src/er/parser.ts +253 -0
- package/src/er/renderer.ts +474 -0
- package/src/er/types.ts +99 -0
- package/src/index.ts +174 -0
- package/src/layout-engine/constants.ts +19 -0
- package/src/layout-engine/edge-bundling.ts +287 -0
- package/src/layout-engine/from-elk.ts +616 -0
- package/src/layout-engine/layer-alignment.ts +170 -0
- package/src/layout-engine/to-elk.ts +645 -0
- package/src/layout-engine.ts +70 -0
- package/src/layout.ts +8 -0
- package/src/multiline-utils.ts +246 -0
- package/src/parser.ts +833 -0
- package/src/renderer.ts +810 -0
- package/src/sequence/layout.ts +561 -0
- package/src/sequence/parser.ts +283 -0
- package/src/sequence/renderer.ts +410 -0
- package/src/sequence/types.ts +146 -0
- package/src/shape-clipping.ts +197 -0
- package/src/styles.ts +118 -0
- package/src/text-metrics.ts +291 -0
- package/src/theme.ts +498 -0
- package/src/types.ts +198 -0
- package/src/xychart/colors.ts +177 -0
- package/src/xychart/layout.ts +682 -0
- package/src/xychart/parser.ts +125 -0
- package/src/xychart/renderer.ts +657 -0
- package/src/xychart/types.ts +150 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage-focused tests for src/xychart/layout.ts.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the horizontal layout path (previously untested), numeric
|
|
5
|
+
* x-axis ranges (category label interpolation), multi-series bar groups,
|
|
6
|
+
* and axis-title branch combinations that xychart-integration.test.ts
|
|
7
|
+
* doesn't reach.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from 'vitest'
|
|
10
|
+
import { renderMermaid } from '../index.ts'
|
|
11
|
+
|
|
12
|
+
describe('xychart – horizontal layout', () => {
|
|
13
|
+
it('lays out a single-series horizontal bar chart', async () => {
|
|
14
|
+
const svg = await renderMermaid(`xychart-beta horizontal
|
|
15
|
+
x-axis [A, B, C]
|
|
16
|
+
y-axis 0 --> 100
|
|
17
|
+
bar [10, 50, 90]`)
|
|
18
|
+
expect(svg).toContain('data-value="10"')
|
|
19
|
+
expect(svg).toContain('data-value="90"')
|
|
20
|
+
expect(svg).toContain('data-label="A"')
|
|
21
|
+
expect(svg).toContain('data-label="C"')
|
|
22
|
+
expect(svg).not.toContain('NaN')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('lays out a horizontal chart with title, axis titles, multiple bar series, and a line series', async () => {
|
|
26
|
+
const svg = await renderMermaid(`xychart-beta horizontal
|
|
27
|
+
title "Horizontal Combo"
|
|
28
|
+
x-axis "Category" [A, B, C]
|
|
29
|
+
y-axis "Value" 0 --> 100
|
|
30
|
+
bar [10, 50, 90]
|
|
31
|
+
bar [20, 40, 60]
|
|
32
|
+
line [15, 45, 75]`)
|
|
33
|
+
expect(svg).toContain('Horizontal Combo')
|
|
34
|
+
expect(svg).toContain('Category')
|
|
35
|
+
expect(svg).toContain('Value')
|
|
36
|
+
expect(svg).toContain('Bar 1')
|
|
37
|
+
expect(svg).toContain('Bar 2')
|
|
38
|
+
expect(svg).toContain('Line 1')
|
|
39
|
+
expect(svg).toContain('data-value="20"')
|
|
40
|
+
expect(svg).toContain('data-value="60"')
|
|
41
|
+
expect(svg).toContain('data-value="15"')
|
|
42
|
+
expect(svg).not.toContain('NaN')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('lays out a horizontal chart with only a line series (no bars)', async () => {
|
|
46
|
+
const svg = await renderMermaid(`xychart-beta horizontal
|
|
47
|
+
x-axis [X, Y]
|
|
48
|
+
y-axis 0 --> 10
|
|
49
|
+
line [3, 8]`)
|
|
50
|
+
expect(svg).toContain('data-value="3"')
|
|
51
|
+
expect(svg).toContain('data-value="8"')
|
|
52
|
+
expect(svg).toContain('data-label="X"')
|
|
53
|
+
expect(svg).not.toContain('NaN')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('lays out a horizontal chart with negative values', async () => {
|
|
57
|
+
const svg = await renderMermaid(`xychart-beta horizontal
|
|
58
|
+
x-axis [A, B, C]
|
|
59
|
+
y-axis -50 --> 50
|
|
60
|
+
bar [-30, 20, 40]`)
|
|
61
|
+
expect(svg).toContain('data-value="-30"')
|
|
62
|
+
expect(svg).toContain('data-value="40"')
|
|
63
|
+
expect(svg).not.toContain('NaN')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('xychart – numeric x-axis range (category interpolation)', () => {
|
|
68
|
+
it('interpolates category labels across a numeric x-axis range with a title', async () => {
|
|
69
|
+
const svg = await renderMermaid(`xychart-beta
|
|
70
|
+
x-axis "Distance" 0 --> 17
|
|
71
|
+
y-axis "Value" 0 --> 100
|
|
72
|
+
bar [10, 40, 70, 90]`)
|
|
73
|
+
expect(svg).toContain('Distance')
|
|
74
|
+
expect(svg).toContain('data-label="0"')
|
|
75
|
+
expect(svg).toContain('data-label="17"')
|
|
76
|
+
expect(svg).toContain('data-label="5.7"')
|
|
77
|
+
expect(svg).toContain('data-label="11"')
|
|
78
|
+
expect(svg).not.toContain('NaN')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('falls back to numeric index labels when x-axis is unspecified', async () => {
|
|
82
|
+
const svg = await renderMermaid(`xychart-beta
|
|
83
|
+
y-axis 0 --> 100
|
|
84
|
+
bar [10, 20, 30]`)
|
|
85
|
+
expect(svg).toContain('data-value="10"')
|
|
86
|
+
expect(svg).toContain('data-label="1"')
|
|
87
|
+
expect(svg).toContain('data-label="3"')
|
|
88
|
+
expect(svg).not.toContain('NaN')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('handles a single-point series on a numeric x-axis range', async () => {
|
|
92
|
+
const svg = await renderMermaid(`xychart-beta
|
|
93
|
+
x-axis 0 --> 10
|
|
94
|
+
y-axis 0 --> 50
|
|
95
|
+
bar [25]`)
|
|
96
|
+
expect(svg).toContain('data-value="25"')
|
|
97
|
+
expect(svg).toContain('data-label="0"')
|
|
98
|
+
expect(svg).not.toContain('NaN')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('xychart – vertical layout branch coverage', () => {
|
|
103
|
+
it('lays out a chart without a y-axis title (single series, no legend)', async () => {
|
|
104
|
+
const svg = await renderMermaid(`xychart-beta
|
|
105
|
+
x-axis [A, B]
|
|
106
|
+
y-axis 0 --> 10
|
|
107
|
+
bar [3, 8]`)
|
|
108
|
+
expect(svg).toContain('data-value="3"')
|
|
109
|
+
expect(svg).toContain('data-value="8"')
|
|
110
|
+
expect(svg).not.toContain('class="xychart-axis-title"')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('lays out a chart with two bar series and a line series (multi-bar grouping)', async () => {
|
|
114
|
+
const svg = await renderMermaid(`xychart-beta
|
|
115
|
+
title "Multi Bar"
|
|
116
|
+
x-axis [Q1, Q2, Q3]
|
|
117
|
+
y-axis "Sales" 0 --> 200
|
|
118
|
+
bar [50, 80, 120]
|
|
119
|
+
bar [30, 60, 90]
|
|
120
|
+
line [40, 70, 100]`)
|
|
121
|
+
expect(svg).toContain('Multi Bar')
|
|
122
|
+
expect(svg).toContain('Bar 1')
|
|
123
|
+
expect(svg).toContain('Bar 2')
|
|
124
|
+
expect(svg).toContain('Line 1')
|
|
125
|
+
expect(svg).toContain('data-value="120"')
|
|
126
|
+
expect(svg).toContain('data-value="90"')
|
|
127
|
+
expect(svg).not.toContain('NaN')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('lays out a chart with negative values (baseline clamped to zero)', async () => {
|
|
131
|
+
const svg = await renderMermaid(`xychart-beta
|
|
132
|
+
x-axis [A, B, C]
|
|
133
|
+
y-axis -50 --> 50
|
|
134
|
+
bar [-30, 20, 40]`)
|
|
135
|
+
expect(svg).toContain('data-value="-30"')
|
|
136
|
+
expect(svg).toContain('data-value="40"')
|
|
137
|
+
expect(svg).not.toContain('NaN')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('renders a chart with no series at all', async () => {
|
|
141
|
+
const svg = await renderMermaid(`xychart-beta
|
|
142
|
+
x-axis 0 --> 10
|
|
143
|
+
y-axis 0 --> 100`)
|
|
144
|
+
expect(svg).toContain('<svg')
|
|
145
|
+
expect(svg).not.toContain('NaN')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('handles a degenerate y-axis range where min equals max', async () => {
|
|
149
|
+
const svg = await renderMermaid(`xychart-beta
|
|
150
|
+
x-axis [A, B]
|
|
151
|
+
y-axis 5 --> 5
|
|
152
|
+
bar [5, 5]`)
|
|
153
|
+
expect(svg).toContain('<svg')
|
|
154
|
+
expect(svg).toContain('data-value="5"')
|
|
155
|
+
expect(svg).not.toContain('NaN')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('picks a tick interval equal to the magnitude for a small range', async () => {
|
|
159
|
+
const svg = await renderMermaid(`xychart-beta
|
|
160
|
+
x-axis [A, B]
|
|
161
|
+
y-axis 0 --> 6
|
|
162
|
+
bar [2, 5]`)
|
|
163
|
+
expect(svg).toContain('data-label="A"')
|
|
164
|
+
expect(svg).toContain('data-value="2"')
|
|
165
|
+
expect(svg).not.toContain('NaN')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — color utilities
|
|
3
|
+
//
|
|
4
|
+
// Provides color output for themed ASCII diagrams.
|
|
5
|
+
// Supports ANSI terminal modes (16/256/truecolor) and HTML <span> tags
|
|
6
|
+
// for browser rendering.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import type { CharRole, AsciiTheme, ColorMode } from './types.ts'
|
|
10
|
+
import type { DiagramColors } from '../theme.ts'
|
|
11
|
+
import { MIX } from '../theme.ts'
|
|
12
|
+
|
|
13
|
+
declare const document: unknown
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Default theme — matches SVG theme colors for consistency
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default ASCII theme derived from the SVG renderer's color palette.
|
|
21
|
+
* Uses the same mixing ratios to maintain visual consistency.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_ASCII_THEME: AsciiTheme = {
|
|
24
|
+
fg: '#27272a', // zinc-800 — primary text
|
|
25
|
+
border: '#a1a1aa', // zinc-400 — node borders (12% mix)
|
|
26
|
+
line: '#71717a', // zinc-500 — edge lines (35% mix)
|
|
27
|
+
arrow: '#52525b', // zinc-600 — arrowheads (60% mix)
|
|
28
|
+
corner: '#71717a', // same as line
|
|
29
|
+
junction: '#a1a1aa', // same as border
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// DiagramColors → AsciiTheme bridge
|
|
34
|
+
//
|
|
35
|
+
// Converts SVG DiagramColors into an AsciiTheme using the same MIX ratios
|
|
36
|
+
// that the SVG renderer uses via CSS color-mix(). This ensures visual
|
|
37
|
+
// consistency between SVG and ASCII output for any theme.
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/** Mix fg into bg at a given percentage (replicates CSS color-mix(in srgb)). */
|
|
41
|
+
function mixColors(fg: string, bg: string, pct: number): string {
|
|
42
|
+
const f = parseHex(fg),
|
|
43
|
+
b = parseHex(bg)
|
|
44
|
+
const mix = (a: number, z: number) =>
|
|
45
|
+
Math.round(a * (pct / 100) + z * (1 - pct / 100))
|
|
46
|
+
const r = mix(f.r, b.r),
|
|
47
|
+
g = mix(f.g, b.g),
|
|
48
|
+
bl = mix(f.b, b.b)
|
|
49
|
+
return '#' + [r, g, bl].map((c) => c.toString(16).padStart(2, '0')).join('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Derive an AsciiTheme from SVG DiagramColors using the same mixing ratios.
|
|
54
|
+
* Honors optional enrichment colors (line, accent, border) when present,
|
|
55
|
+
* otherwise falls back to color-mix derivation — matching SVG behavior.
|
|
56
|
+
*/
|
|
57
|
+
export function diagramColorsToAsciiTheme(colors: DiagramColors): AsciiTheme {
|
|
58
|
+
const line = colors.line ?? mixColors(colors.fg, colors.bg, MIX.line)
|
|
59
|
+
const border =
|
|
60
|
+
colors.border ?? mixColors(colors.fg, colors.bg, MIX.nodeStroke)
|
|
61
|
+
return {
|
|
62
|
+
fg: colors.fg,
|
|
63
|
+
border,
|
|
64
|
+
line,
|
|
65
|
+
arrow: colors.accent ?? mixColors(colors.fg, colors.bg, MIX.arrow),
|
|
66
|
+
accent: colors.accent,
|
|
67
|
+
bg: colors.bg,
|
|
68
|
+
corner: line,
|
|
69
|
+
junction: border,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Color mode detection
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect the best color mode for the current environment.
|
|
79
|
+
*
|
|
80
|
+
* Terminal detection order:
|
|
81
|
+
* 1. COLORTERM=truecolor or COLORTERM=24bit → truecolor
|
|
82
|
+
* 2. TERM contains "256color" → ansi256
|
|
83
|
+
* 3. TERM is set and not "dumb" → ansi16
|
|
84
|
+
*
|
|
85
|
+
* Browser: returns 'html' (uses <span> tags with inline styles).
|
|
86
|
+
* Unknown/piped: returns 'none'.
|
|
87
|
+
*/
|
|
88
|
+
export function detectColorMode(): ColorMode {
|
|
89
|
+
// Check if we're in a Node.js-like environment with process object
|
|
90
|
+
// Use globalThis to safely check for process without TypeScript errors
|
|
91
|
+
const proc = (
|
|
92
|
+
globalThis as {
|
|
93
|
+
process?: {
|
|
94
|
+
stdout?: { isTTY?: boolean }
|
|
95
|
+
env?: Record<string, string | undefined>
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
).process
|
|
99
|
+
|
|
100
|
+
if (proc) {
|
|
101
|
+
// Check if stdout is a TTY (not piped/redirected)
|
|
102
|
+
if (!proc.stdout?.isTTY) {
|
|
103
|
+
return 'none'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const colorTerm = proc.env?.COLORTERM?.toLowerCase() ?? ''
|
|
107
|
+
const term = proc.env?.TERM?.toLowerCase() ?? ''
|
|
108
|
+
|
|
109
|
+
// True color support
|
|
110
|
+
if (colorTerm === 'truecolor' || colorTerm === '24bit') {
|
|
111
|
+
return 'truecolor'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 256 color support
|
|
115
|
+
if (term.includes('256color') || term.includes('256')) {
|
|
116
|
+
return 'ansi256'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Basic color support
|
|
120
|
+
if (term && term !== 'dumb') {
|
|
121
|
+
return 'ansi16'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return 'none'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No process object → browser environment → use HTML color output
|
|
128
|
+
if (typeof document !== 'undefined') {
|
|
129
|
+
return 'html'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return 'none'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Hex color parsing
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse a hex color string to RGB values.
|
|
141
|
+
* Supports both 3-char (#RGB) and 6-char (#RRGGBB) formats.
|
|
142
|
+
*/
|
|
143
|
+
function parseHex(hex: string): { r: number; g: number; b: number } {
|
|
144
|
+
const h = hex.replace('#', '')
|
|
145
|
+
if (h.length === 3) {
|
|
146
|
+
// .charAt() always returns a string (never undefined) — unlike bracket
|
|
147
|
+
// indexing, it isn't subject to noUncheckedIndexedAccess.
|
|
148
|
+
const c0 = h.charAt(0)
|
|
149
|
+
const c1 = h.charAt(1)
|
|
150
|
+
const c2 = h.charAt(2)
|
|
151
|
+
return {
|
|
152
|
+
r: parseInt(c0 + c0, 16),
|
|
153
|
+
g: parseInt(c1 + c1, 16),
|
|
154
|
+
b: parseInt(c2 + c2, 16),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
159
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
160
|
+
b: parseInt(h.substring(4, 6), 16),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// ANSI escape code generation
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/** ANSI escape sequence prefix */
|
|
169
|
+
const ESC = '\x1b['
|
|
170
|
+
/** Reset all attributes */
|
|
171
|
+
const RESET = `${ESC}0m`
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate ANSI foreground color escape sequence for 24-bit true color.
|
|
175
|
+
* Format: ESC[38;2;R;G;Bm
|
|
176
|
+
*/
|
|
177
|
+
function truecolorFg(hex: string): string {
|
|
178
|
+
const { r, g, b } = parseHex(hex)
|
|
179
|
+
return `${ESC}38;2;${r};${g};${b}m`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Find the closest 256-color palette index for an RGB color.
|
|
184
|
+
* The 256-color palette has:
|
|
185
|
+
* - 0-15: Standard colors (duplicates of 16-color)
|
|
186
|
+
* - 16-231: 6x6x6 color cube (216 colors)
|
|
187
|
+
* - 232-255: Grayscale ramp (24 shades)
|
|
188
|
+
*/
|
|
189
|
+
function rgbTo256(r: number, g: number, b: number): number {
|
|
190
|
+
// Check if it's close to grayscale
|
|
191
|
+
const avg = (r + g + b) / 3
|
|
192
|
+
const maxDiff = Math.max(
|
|
193
|
+
Math.abs(r - avg),
|
|
194
|
+
Math.abs(g - avg),
|
|
195
|
+
Math.abs(b - avg),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if (maxDiff < 10) {
|
|
199
|
+
// Use grayscale ramp (232-255)
|
|
200
|
+
// Each step is ~10.625 (256/24)
|
|
201
|
+
const gray = Math.round((avg / 255) * 23)
|
|
202
|
+
return 232 + Math.min(23, Math.max(0, gray))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Use 6x6x6 color cube (16-231)
|
|
206
|
+
// Each channel maps to 0-5: 0, 95, 135, 175, 215, 255
|
|
207
|
+
const toIndex = (v: number): number => {
|
|
208
|
+
if (v < 48) return 0
|
|
209
|
+
if (v < 115) return 1
|
|
210
|
+
return Math.min(5, Math.floor((v - 35) / 40))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ri = toIndex(r)
|
|
214
|
+
const gi = toIndex(g)
|
|
215
|
+
const bi = toIndex(b)
|
|
216
|
+
|
|
217
|
+
return 16 + 36 * ri + 6 * gi + bi
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generate ANSI foreground color escape sequence for 256-color mode.
|
|
222
|
+
* Format: ESC[38;5;Nm
|
|
223
|
+
*/
|
|
224
|
+
function ansi256Fg(hex: string): string {
|
|
225
|
+
const { r, g, b } = parseHex(hex)
|
|
226
|
+
const index = rgbTo256(r, g, b)
|
|
227
|
+
return `${ESC}38;5;${index}m`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Map an RGB color to the closest 16-color ANSI code.
|
|
232
|
+
* Returns the foreground color escape sequence.
|
|
233
|
+
*
|
|
234
|
+
* Standard 16 colors:
|
|
235
|
+
* 0=black, 1=red, 2=green, 3=yellow, 4=blue, 5=magenta, 6=cyan, 7=white
|
|
236
|
+
* 8-15 = bright versions
|
|
237
|
+
*/
|
|
238
|
+
function ansi16Fg(hex: string): string {
|
|
239
|
+
const { r, g, b } = parseHex(hex)
|
|
240
|
+
const luma = 0.299 * r + 0.587 * g + 0.114 * b
|
|
241
|
+
|
|
242
|
+
// Determine brightness (use bright colors for better visibility)
|
|
243
|
+
const bright = luma > 100 ? 0 : 60 // 60 = bright variant offset
|
|
244
|
+
|
|
245
|
+
// Determine base color based on dominant channel
|
|
246
|
+
let code: number
|
|
247
|
+
if (r > 180 && g < 100 && b < 100)
|
|
248
|
+
code = 31 // red
|
|
249
|
+
else if (g > 180 && r < 100 && b < 100)
|
|
250
|
+
code = 32 // green
|
|
251
|
+
else if (r > 150 && g > 150 && b < 100)
|
|
252
|
+
code = 33 // yellow
|
|
253
|
+
else if (b > 180 && r < 100 && g < 100)
|
|
254
|
+
code = 34 // blue
|
|
255
|
+
else if (r > 150 && b > 150 && g < 100)
|
|
256
|
+
code = 35 // magenta
|
|
257
|
+
else if (g > 150 && b > 150 && r < 100)
|
|
258
|
+
code = 36 // cyan
|
|
259
|
+
else if (luma > 200)
|
|
260
|
+
code = 37 // white
|
|
261
|
+
else if (luma < 50)
|
|
262
|
+
code = 30 // black
|
|
263
|
+
else code = 37 // default to white for grays
|
|
264
|
+
|
|
265
|
+
return `${ESC}${code + bright}m`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// HTML color output (for browser rendering)
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
/** Escape characters that would break HTML output. */
|
|
273
|
+
function escapeHtml(text: string): string {
|
|
274
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Wrap text in a <span> with an inline color style. */
|
|
278
|
+
function htmlSpan(hex: string, text: string): string {
|
|
279
|
+
return `<span style="color:${hex}">${escapeHtml(text)}</span>`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Role → color mapping
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the color for a character role from the theme.
|
|
288
|
+
*/
|
|
289
|
+
function getRoleColor(role: CharRole, theme: AsciiTheme): string {
|
|
290
|
+
switch (role) {
|
|
291
|
+
case 'text':
|
|
292
|
+
return theme.fg
|
|
293
|
+
case 'border':
|
|
294
|
+
return theme.border
|
|
295
|
+
case 'line':
|
|
296
|
+
return theme.line
|
|
297
|
+
case 'arrow':
|
|
298
|
+
return theme.arrow
|
|
299
|
+
case 'corner':
|
|
300
|
+
return theme.corner ?? theme.line
|
|
301
|
+
case 'junction':
|
|
302
|
+
return theme.junction ?? theme.border
|
|
303
|
+
default:
|
|
304
|
+
return theme.fg
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Generate the ANSI escape sequence for a role color.
|
|
310
|
+
*/
|
|
311
|
+
export function getAnsiColor(
|
|
312
|
+
role: CharRole,
|
|
313
|
+
theme: AsciiTheme,
|
|
314
|
+
mode: ColorMode,
|
|
315
|
+
): string {
|
|
316
|
+
if (mode === 'none') return ''
|
|
317
|
+
|
|
318
|
+
const hex = getRoleColor(role, theme)
|
|
319
|
+
|
|
320
|
+
switch (mode) {
|
|
321
|
+
case 'truecolor':
|
|
322
|
+
return truecolorFg(hex)
|
|
323
|
+
case 'ansi256':
|
|
324
|
+
return ansi256Fg(hex)
|
|
325
|
+
case 'ansi16':
|
|
326
|
+
return ansi16Fg(hex)
|
|
327
|
+
default:
|
|
328
|
+
return ''
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get the ANSI reset sequence.
|
|
334
|
+
*/
|
|
335
|
+
export function getAnsiReset(mode: ColorMode): string {
|
|
336
|
+
return mode === 'none' ? '' : RESET
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Wrap a character with ANSI color codes based on its role.
|
|
341
|
+
*/
|
|
342
|
+
export function colorizeChar(
|
|
343
|
+
char: string,
|
|
344
|
+
role: CharRole | null,
|
|
345
|
+
theme: AsciiTheme,
|
|
346
|
+
mode: ColorMode,
|
|
347
|
+
): string {
|
|
348
|
+
if (mode === 'none' || role === null || char === ' ') {
|
|
349
|
+
return char
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const colorCode = getAnsiColor(role, theme, mode)
|
|
353
|
+
return `${colorCode}${char}${RESET}`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Colorize an entire line efficiently by grouping consecutive same-role characters.
|
|
358
|
+
* This reduces the number of escape sequences (ANSI) or span tags (HTML) in the output.
|
|
359
|
+
*/
|
|
360
|
+
export function colorizeLine(
|
|
361
|
+
chars: string[],
|
|
362
|
+
roles: (CharRole | null)[],
|
|
363
|
+
theme: AsciiTheme,
|
|
364
|
+
mode: ColorMode,
|
|
365
|
+
): string {
|
|
366
|
+
if (mode === 'none') {
|
|
367
|
+
return chars.join('')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (mode === 'html') {
|
|
371
|
+
return colorizeLineHtml(chars, roles, theme)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let result = ''
|
|
375
|
+
let currentRole: CharRole | null = null
|
|
376
|
+
let buffer = ''
|
|
377
|
+
|
|
378
|
+
for (const [i, char] of chars.entries()) {
|
|
379
|
+
const role = roles[i] ?? null
|
|
380
|
+
|
|
381
|
+
// Whitespace doesn't need coloring
|
|
382
|
+
if (char === ' ') {
|
|
383
|
+
// Flush any buffered characters (with or without color)
|
|
384
|
+
if (buffer.length > 0) {
|
|
385
|
+
if (currentRole !== null) {
|
|
386
|
+
result += getAnsiColor(currentRole, theme, mode) + buffer + RESET
|
|
387
|
+
} else {
|
|
388
|
+
result += buffer
|
|
389
|
+
}
|
|
390
|
+
buffer = ''
|
|
391
|
+
currentRole = null
|
|
392
|
+
}
|
|
393
|
+
result += char
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Same role as previous — accumulate
|
|
398
|
+
if (role === currentRole) {
|
|
399
|
+
buffer += char
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Role changed — flush buffer (with or without color) and start new
|
|
404
|
+
if (buffer.length > 0) {
|
|
405
|
+
if (currentRole !== null) {
|
|
406
|
+
result += getAnsiColor(currentRole, theme, mode) + buffer + RESET
|
|
407
|
+
} else {
|
|
408
|
+
result += buffer
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
buffer = char
|
|
412
|
+
currentRole = role
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Flush remaining buffer
|
|
416
|
+
if (buffer.length > 0 && currentRole !== null) {
|
|
417
|
+
result += getAnsiColor(currentRole, theme, mode) + buffer + RESET
|
|
418
|
+
} else if (buffer.length > 0) {
|
|
419
|
+
result += buffer
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* HTML-specific line colorization.
|
|
427
|
+
* Groups consecutive same-role characters into <span> tags with inline color styles.
|
|
428
|
+
* Whitespace is emitted bare (no wrapping) to keep output compact.
|
|
429
|
+
*/
|
|
430
|
+
function colorizeLineHtml(
|
|
431
|
+
chars: string[],
|
|
432
|
+
roles: (CharRole | null)[],
|
|
433
|
+
theme: AsciiTheme,
|
|
434
|
+
): string {
|
|
435
|
+
let result = ''
|
|
436
|
+
let currentRole: CharRole | null = null
|
|
437
|
+
let buffer = ''
|
|
438
|
+
|
|
439
|
+
const flush = () => {
|
|
440
|
+
if (buffer.length === 0) return
|
|
441
|
+
if (currentRole !== null) {
|
|
442
|
+
result += htmlSpan(getRoleColor(currentRole, theme), buffer)
|
|
443
|
+
} else {
|
|
444
|
+
result += escapeHtml(buffer)
|
|
445
|
+
}
|
|
446
|
+
buffer = ''
|
|
447
|
+
currentRole = null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const [i, char] of chars.entries()) {
|
|
451
|
+
const role = roles[i] ?? null
|
|
452
|
+
|
|
453
|
+
if (char === ' ') {
|
|
454
|
+
flush()
|
|
455
|
+
result += ' '
|
|
456
|
+
continue
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (role === currentRole) {
|
|
460
|
+
buffer += char
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
flush()
|
|
465
|
+
buffer = char
|
|
466
|
+
currentRole = role
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
flush()
|
|
470
|
+
return result
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Colorize a text string with a direct hex color.
|
|
475
|
+
* Used by renderers that need per-cell color control (e.g. multi-series xychart).
|
|
476
|
+
* Handles all output modes: ANSI (16/256/truecolor) and HTML.
|
|
477
|
+
*/
|
|
478
|
+
export function colorizeText(
|
|
479
|
+
text: string,
|
|
480
|
+
hex: string,
|
|
481
|
+
mode: ColorMode,
|
|
482
|
+
): string {
|
|
483
|
+
if (mode === 'none' || text.length === 0) return text
|
|
484
|
+
if (mode === 'html') return htmlSpan(hex, text)
|
|
485
|
+
let code: string
|
|
486
|
+
switch (mode) {
|
|
487
|
+
case 'truecolor':
|
|
488
|
+
code = truecolorFg(hex)
|
|
489
|
+
break
|
|
490
|
+
case 'ansi256':
|
|
491
|
+
code = ansi256Fg(hex)
|
|
492
|
+
break
|
|
493
|
+
case 'ansi16':
|
|
494
|
+
code = ansi16Fg(hex)
|
|
495
|
+
break
|
|
496
|
+
default:
|
|
497
|
+
return text
|
|
498
|
+
}
|
|
499
|
+
return `${code}${text}${RESET}`
|
|
500
|
+
}
|