opentui-markdown-render 0.0.0-stage → 0.2.0
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/NOTICE +27 -0
- package/README.md +223 -2
- package/bin/otmd.tsx +99 -0
- package/native/Cargo.lock +3708 -0
- package/native/Cargo.toml +28 -0
- package/native/build.rs +3 -0
- package/native/src/lib.rs +880 -0
- package/package.json +67 -4
- package/prebuilds/linux-x64-gnu/opentui_markdown_render.node +4 -0
- package/src/app.tsx +180 -0
- package/src/cli.ts +63 -0
- package/src/config/index.ts +46 -0
- package/src/config/load.ts +337 -0
- package/src/config/resolve.ts +36 -0
- package/src/config/theme.ts +110 -0
- package/src/config/types.ts +105 -0
- package/src/core/document.ts +44 -0
- package/src/core/frame.ts +144 -0
- package/src/core/image.ts +9 -0
- package/src/core/index.ts +6 -0
- package/src/core/layout.ts +27 -0
- package/src/core/math.ts +170 -0
- package/src/core/parse.ts +208 -0
- package/src/core/tables.ts +58 -0
- package/src/graphics.ts +85 -0
- package/src/headings.ts +77 -0
- package/src/index.ts +107 -0
- package/src/math/index.ts +86 -0
- package/src/math/native.ts +39 -0
- package/src/math/renderer.ts +84 -0
- package/src/math/validate.ts +44 -0
- package/src/media.tsx +267 -0
- package/src/merman/class/dagre-layout.ts +171 -0
- package/src/merman/class/diagram.ts +8 -0
- package/src/merman/class/drawing.ts +343 -0
- package/src/merman/class/labels.ts +38 -0
- package/src/merman/class/layout.ts +531 -0
- package/src/merman/class/parser.ts +308 -0
- package/src/merman/class/render-grid.ts +18 -0
- package/src/merman/class/style.ts +27 -0
- package/src/merman/class/types.ts +57 -0
- package/src/merman/core/canvas.ts +228 -0
- package/src/merman/core/color/style.ts +56 -0
- package/src/merman/core/drawing.ts +307 -0
- package/src/merman/core/geometry.ts +347 -0
- package/src/merman/core/mermaid.ts +38 -0
- package/src/merman/core/render-grid.ts +51 -0
- package/src/merman/core/spatial.ts +243 -0
- package/src/merman/core/text-lines.ts +32 -0
- package/src/merman/core/text.ts +44 -0
- package/src/merman/detect.ts +19 -0
- package/src/merman/diagnostics.ts +16 -0
- package/src/merman/diagram.ts +268 -0
- package/src/merman/flowchart/drawing.ts +433 -0
- package/src/merman/flowchart/labels.ts +144 -0
- package/src/merman/flowchart/layout.ts +1057 -0
- package/src/merman/flowchart/options.ts +12 -0
- package/src/merman/flowchart/parser.ts +477 -0
- package/src/merman/flowchart/render.ts +10 -0
- package/src/merman/flowchart/routing.ts +1429 -0
- package/src/merman/flowchart/style.ts +79 -0
- package/src/merman/flowchart/types.ts +63 -0
- package/src/merman/gantt/diagram.ts +8 -0
- package/src/merman/gantt/drawing.ts +193 -0
- package/src/merman/gantt/parser.ts +198 -0
- package/src/merman/gantt/render-grid.ts +17 -0
- package/src/merman/gantt/style.ts +36 -0
- package/src/merman/gantt/types.ts +45 -0
- package/src/merman/gitgraph/diagram.ts +8 -0
- package/src/merman/gitgraph/drawing.ts +234 -0
- package/src/merman/gitgraph/parser.ts +234 -0
- package/src/merman/gitgraph/render-grid.ts +17 -0
- package/src/merman/gitgraph/style.ts +37 -0
- package/src/merman/gitgraph/types.ts +36 -0
- package/src/merman/palette.ts +60 -0
- package/src/merman/sequence/diagram.ts +8 -0
- package/src/merman/sequence/drawing.ts +288 -0
- package/src/merman/sequence/options.ts +8 -0
- package/src/merman/sequence/parser.ts +303 -0
- package/src/merman/sequence/placement.ts +692 -0
- package/src/merman/sequence/render-grid.ts +22 -0
- package/src/merman/sequence/style.ts +81 -0
- package/src/merman/sequence/types.ts +74 -0
- package/src/merman/state/diagram.ts +8 -0
- package/src/merman/state/drawing.ts +363 -0
- package/src/merman/state/endpoint.ts +15 -0
- package/src/merman/state/layout.ts +1134 -0
- package/src/merman/state/note.ts +59 -0
- package/src/merman/state/options.ts +11 -0
- package/src/merman/state/parser.ts +245 -0
- package/src/merman/state/render-grid.ts +22 -0
- package/src/merman/state/routing.ts +1522 -0
- package/src/merman/state/search.ts +103 -0
- package/src/merman/state/style.ts +52 -0
- package/src/merman/state/types.ts +63 -0
- package/src/merman/state/visible-model.ts +77 -0
- package/src/merman/timeline/diagram.ts +8 -0
- package/src/merman/timeline/drawing.ts +109 -0
- package/src/merman/timeline/parser.ts +119 -0
- package/src/merman/timeline/render-grid.ts +17 -0
- package/src/merman/timeline/style.ts +36 -0
- package/src/merman/timeline/types.ts +31 -0
- package/src/merman.ts +3 -0
- package/src/statusline.tsx +183 -0
- package/src/theme/colors.ts +11 -0
- package/src/theme/defaults.ts +190 -0
- package/src/theme/index.ts +7 -0
- package/src/theme/load.ts +39 -0
- package/src/theme/resolve.ts +167 -0
- package/src/theme/syntax.ts +96 -0
- package/src/theme/types.ts +151 -0
- package/src/view/code.tsx +27 -0
- package/src/view/document-slots.ts +111 -0
- package/src/view/document-table.tsx +98 -0
- package/src/view/document.tsx +195 -0
- package/src/view/frame.tsx +83 -0
- package/src/view/inline-flow.tsx +113 -0
- package/src/view/inline-layout.ts +149 -0
- package/src/view/inline-math.ts +42 -0
- package/src/view/inline-style.ts +103 -0
- package/src/view/math.tsx +85 -0
- package/src/view/mermaid.tsx +128 -0
- package/src/view/table.ts +100 -0
- package/themes/dark.json +32 -0
- package/themes/example.jsonc +98 -0
- package/themes/light.json +32 -0
- package/themes/solarized-dark.json +32 -0
- package/themes/solarized-light.json +32 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StyledText } from "@opentui/core"
|
|
2
|
+
import type { DiagramCanvas } from "../core/canvas.js"
|
|
3
|
+
import { renderDiagramGridStyledText } from "../core/render-grid.js"
|
|
4
|
+
import { sequenceStyleBackgroundColor, sequenceStyleColor, type SequenceStyleColors } from "./style.js"
|
|
5
|
+
import type { SequenceCellStyle } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export type SequenceGrid = DiagramCanvas<SequenceCellStyle>
|
|
8
|
+
|
|
9
|
+
export function renderSequenceGridText(grid: SequenceGrid): string {
|
|
10
|
+
return grid.toString()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderSequenceGridStyledText(
|
|
14
|
+
grid: SequenceGrid,
|
|
15
|
+
colors: Parameters<typeof sequenceStyleColor>[1],
|
|
16
|
+
): StyledText {
|
|
17
|
+
return renderDiagramGridStyledText(
|
|
18
|
+
grid,
|
|
19
|
+
(run) => sequenceStyleColor(run.style, colors),
|
|
20
|
+
(run) => sequenceStyleBackgroundColor(run.style, colors as Required<SequenceStyleColors>),
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RGBA } from "@opentui/core"
|
|
2
|
+
import {
|
|
3
|
+
createColorRampTheme,
|
|
4
|
+
DIAGRAM_FADE_STEPS,
|
|
5
|
+
numberedStyleKeys,
|
|
6
|
+
rgba,
|
|
7
|
+
type DiagramRgb,
|
|
8
|
+
} from "../core/color/style.js"
|
|
9
|
+
import type { FadeStyle, LifelineRampStyle, SequenceCellStyle } from "./types.js"
|
|
10
|
+
|
|
11
|
+
export interface SequenceStyleColors {
|
|
12
|
+
participant?: RGBA
|
|
13
|
+
lifeline?: RGBA
|
|
14
|
+
group?: RGBA
|
|
15
|
+
request?: RGBA
|
|
16
|
+
response?: RGBA
|
|
17
|
+
fragment?: RGBA
|
|
18
|
+
fragmentLabelBg?: RGBA
|
|
19
|
+
note?: RGBA
|
|
20
|
+
noteBg?: RGBA
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SEQUENCE_FADE_STEPS = DIAGRAM_FADE_STEPS
|
|
24
|
+
const LIFELINE_RAMP_STYLES = [
|
|
25
|
+
"lifelineRamp1",
|
|
26
|
+
"lifelineRamp2",
|
|
27
|
+
"lifelineRamp3",
|
|
28
|
+
] as const satisfies readonly LifelineRampStyle[]
|
|
29
|
+
|
|
30
|
+
const DEFAULT_THEME_RGB = {
|
|
31
|
+
participant: [228, 239, 232],
|
|
32
|
+
lifeline: [111, 138, 126],
|
|
33
|
+
group: [76, 99, 89],
|
|
34
|
+
request: [134, 225, 200],
|
|
35
|
+
response: [230, 177, 126],
|
|
36
|
+
fragment: [154, 184, 169],
|
|
37
|
+
fragmentLabelBg: [28, 43, 36],
|
|
38
|
+
noteFg: [215, 229, 221],
|
|
39
|
+
noteBg: [36, 56, 47],
|
|
40
|
+
} as const satisfies Record<string, DiagramRgb>
|
|
41
|
+
|
|
42
|
+
export function resolveSequenceStyleColors(
|
|
43
|
+
colors: SequenceStyleColors = {},
|
|
44
|
+
): Required<SequenceStyleColors> & Record<FadeStyle | LifelineRampStyle, RGBA> {
|
|
45
|
+
const participant = colors.participant ?? rgba(DEFAULT_THEME_RGB.participant)
|
|
46
|
+
const lifeline = colors.lifeline ?? rgba(DEFAULT_THEME_RGB.lifeline)
|
|
47
|
+
const request = colors.request ?? rgba(DEFAULT_THEME_RGB.request)
|
|
48
|
+
const response = colors.response ?? rgba(DEFAULT_THEME_RGB.response)
|
|
49
|
+
return {
|
|
50
|
+
participant,
|
|
51
|
+
lifeline,
|
|
52
|
+
group: colors.group ?? rgba(DEFAULT_THEME_RGB.group),
|
|
53
|
+
request,
|
|
54
|
+
response,
|
|
55
|
+
fragment: colors.fragment ?? rgba(DEFAULT_THEME_RGB.fragment),
|
|
56
|
+
fragmentLabelBg: colors.fragmentLabelBg ?? rgba(DEFAULT_THEME_RGB.fragmentLabelBg),
|
|
57
|
+
note: colors.note ?? rgba(DEFAULT_THEME_RGB.noteFg),
|
|
58
|
+
noteBg: colors.noteBg ?? rgba(DEFAULT_THEME_RGB.noteBg),
|
|
59
|
+
...createColorRampTheme(numberedStyleKeys("requestFade", SEQUENCE_FADE_STEPS), lifeline, request),
|
|
60
|
+
...createColorRampTheme(numberedStyleKeys("responseFade", SEQUENCE_FADE_STEPS), lifeline, response),
|
|
61
|
+
...createColorRampTheme(LIFELINE_RAMP_STYLES, participant, lifeline),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function sequenceStyleColor(
|
|
66
|
+
style: SequenceCellStyle | undefined,
|
|
67
|
+
colors: Required<SequenceStyleColors> & Record<FadeStyle | LifelineRampStyle, RGBA>,
|
|
68
|
+
): RGBA | undefined {
|
|
69
|
+
if (style === "noteBadge") return colors.note
|
|
70
|
+
if (style === "fragmentLabel") return colors.fragment
|
|
71
|
+
return style ? colors[style] : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function sequenceStyleBackgroundColor(
|
|
75
|
+
style: SequenceCellStyle | undefined,
|
|
76
|
+
colors: Required<SequenceStyleColors>,
|
|
77
|
+
): RGBA | undefined {
|
|
78
|
+
if (style === "note" || style === "noteBadge") return colors.noteBg
|
|
79
|
+
if (style === "fragmentLabel") return colors.fragmentLabelBg
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { BorderStyle } from "@opentui/core"
|
|
2
|
+
|
|
3
|
+
export interface SequenceParticipant {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SequenceParticipantGroup {
|
|
9
|
+
label: string
|
|
10
|
+
participantIds: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SequenceMessage {
|
|
14
|
+
from: string
|
|
15
|
+
to: string
|
|
16
|
+
label: string
|
|
17
|
+
style: "solid" | "dashed"
|
|
18
|
+
head?: SequenceArrowHead
|
|
19
|
+
number?: number
|
|
20
|
+
activate?: string
|
|
21
|
+
deactivate?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SequenceArrowHead = "open" | "cross" | "async"
|
|
25
|
+
|
|
26
|
+
export interface SequenceNote {
|
|
27
|
+
over: string[]
|
|
28
|
+
label: string
|
|
29
|
+
position?: "over" | "left" | "right"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SequenceActivation {
|
|
33
|
+
participant: string
|
|
34
|
+
active: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SequenceFragment {
|
|
38
|
+
kind: "alt" | "else" | "loop" | "opt" | "par" | "and" | "critical" | "option" | "break" | "end"
|
|
39
|
+
label: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SequenceStep =
|
|
43
|
+
| { type: "message"; message: SequenceMessage }
|
|
44
|
+
| { type: "note"; note: SequenceNote }
|
|
45
|
+
| { type: "activation"; activation: SequenceActivation }
|
|
46
|
+
| { type: "fragment"; fragment: SequenceFragment }
|
|
47
|
+
|
|
48
|
+
export interface SequenceDiagram {
|
|
49
|
+
participants: SequenceParticipant[]
|
|
50
|
+
messages: SequenceMessage[]
|
|
51
|
+
steps: SequenceStep[]
|
|
52
|
+
groups: SequenceParticipantGroup[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SequenceDiagramRenderOptions {
|
|
56
|
+
compact?: boolean
|
|
57
|
+
minParticipantGap?: number
|
|
58
|
+
fragmentBorderStyle?: BorderStyle
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type MessageStyle = "request" | "response"
|
|
62
|
+
export type FadeStyle = `${MessageStyle}Fade${1 | 2 | 3 | 4 | 5}`
|
|
63
|
+
export type LifelineRampStyle = `lifelineRamp${1 | 2 | 3}`
|
|
64
|
+
export type SequenceCellStyle =
|
|
65
|
+
| "participant"
|
|
66
|
+
| "lifeline"
|
|
67
|
+
| "group"
|
|
68
|
+
| MessageStyle
|
|
69
|
+
| FadeStyle
|
|
70
|
+
| LifelineRampStyle
|
|
71
|
+
| "fragment"
|
|
72
|
+
| "fragmentLabel"
|
|
73
|
+
| "note"
|
|
74
|
+
| "noteBadge"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { drawStateDiagramGrid } from "./drawing.js"
|
|
2
|
+
import { parseMermaidStateDiagram } from "./parser.js"
|
|
3
|
+
import { renderStateGridText } from "./render-grid.js"
|
|
4
|
+
import type { StateDiagramRenderOptions } from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function renderStateDiagram(content: string, options: StateDiagramRenderOptions = {}): string {
|
|
7
|
+
return renderStateGridText(drawStateDiagramGrid(parseMermaidStateDiagram(content), options))
|
|
8
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { BorderChars, type BorderCharacters, type BorderStyle } from "@opentui/core"
|
|
2
|
+
import { DiagramCanvas, DiagramCanvasSizeError, type DiagramCanvasCell } from "../core/canvas.js"
|
|
3
|
+
import { directionBetween, orthogonalPathPoints, type DiagramDirection } from "../core/geometry.js"
|
|
4
|
+
import {
|
|
5
|
+
diagramArrowHead,
|
|
6
|
+
diagramLineGlyph,
|
|
7
|
+
drawDiagramFrame,
|
|
8
|
+
fillDiagramFrameInterior,
|
|
9
|
+
mergeDiagramLineGlyph,
|
|
10
|
+
} from "../core/drawing.js"
|
|
11
|
+
import { DIAGRAM_LABEL_PADDING_X, diagramTextWidth } from "../core/text.js"
|
|
12
|
+
import {
|
|
13
|
+
createStateDiagramLayout,
|
|
14
|
+
expandCompositeBoundsForFeedback,
|
|
15
|
+
expandCompositeBoundsForInternalTransitions,
|
|
16
|
+
separateExternalBoundsFromComposites,
|
|
17
|
+
translateStateDiagramLayout,
|
|
18
|
+
type StateDiagramBoxBounds as BoxBounds,
|
|
19
|
+
type StateDiagramNoteBounds as StateNoteBounds,
|
|
20
|
+
} from "./layout.js"
|
|
21
|
+
import { stateDiagramNoteConnector } from "./note.js"
|
|
22
|
+
import { DEFAULT_STATE_ARROW_HEAD_STYLE, DEFAULT_STATE_BORDER_STYLE, normalizeStateMinStateGap } from "./options.js"
|
|
23
|
+
import type { StateGrid } from "./render-grid.js"
|
|
24
|
+
import {
|
|
25
|
+
createStateTransitionJunctionPlans,
|
|
26
|
+
createStateTransitionRenderPlans,
|
|
27
|
+
measureStateTransitionLabel,
|
|
28
|
+
type StateTransitionRenderPlan,
|
|
29
|
+
} from "./routing.js"
|
|
30
|
+
import { createStateSearchBudget } from "./search.js"
|
|
31
|
+
import { NOTE_CONNECTOR_RAMP_STYLES, STATE_DEPARTURE_RAMP_STYLES } from "./style.js"
|
|
32
|
+
import type {
|
|
33
|
+
StateCellStyle,
|
|
34
|
+
StateDiagram,
|
|
35
|
+
StateDiagramArrowHeadStyle,
|
|
36
|
+
StateDiagramRenderOptions,
|
|
37
|
+
StateDiagramState,
|
|
38
|
+
} from "./types.js"
|
|
39
|
+
import { isHiddenCompositeMarker, prepareVisibleStateDiagram } from "./visible-model.js"
|
|
40
|
+
|
|
41
|
+
type StateCell = DiagramCanvasCell<StateCellStyle>
|
|
42
|
+
|
|
43
|
+
function translateTransitionPlans(
|
|
44
|
+
plans: readonly StateTransitionRenderPlan[],
|
|
45
|
+
dx: number,
|
|
46
|
+
dy: number,
|
|
47
|
+
): StateTransitionRenderPlan[] {
|
|
48
|
+
return plans.map((plan) => ({
|
|
49
|
+
...plan,
|
|
50
|
+
cells: plan.cells.map((cell) => ({ ...cell, x: cell.x + dx, y: cell.y + dy })),
|
|
51
|
+
path: plan.path.map(([x, y]) => [x + dx, y + dy]),
|
|
52
|
+
label: plan.label ? { ...plan.label, x: plan.label.x + dx, y: plan.label.y + dy } : undefined,
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeGrid(width: number, height: number): StateGrid {
|
|
57
|
+
return new DiagramCanvas(width, height, {
|
|
58
|
+
mergeCell: (existing, incoming): StateCell => {
|
|
59
|
+
const existingIsTransition = existing.style === "transition" || existing.style?.startsWith("stateDepartureRamp")
|
|
60
|
+
const incomingIsTransition = incoming.style === "transition" || incoming.style?.startsWith("stateDepartureRamp")
|
|
61
|
+
const shouldMerge = incomingIsTransition && existingIsTransition
|
|
62
|
+
return {
|
|
63
|
+
...incoming,
|
|
64
|
+
char: shouldMerge
|
|
65
|
+
? (mergeDiagramLineGlyph(existing.char, incoming.char, "rounded") ?? incoming.char)
|
|
66
|
+
: incoming.char,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setCell(grid: StateGrid, x: number, y: number, char: string, style?: StateCellStyle): void {
|
|
73
|
+
grid.setCell(x, y, char, style)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setText(grid: StateGrid, x: number, y: number, text: string, style?: StateCellStyle): void {
|
|
77
|
+
grid.setText(x, y, text, style)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function drawBox(
|
|
81
|
+
grid: StateGrid,
|
|
82
|
+
state: StateDiagramState,
|
|
83
|
+
bounds: BoxBounds,
|
|
84
|
+
lines: string[],
|
|
85
|
+
borderStyle: BorderStyle,
|
|
86
|
+
): void {
|
|
87
|
+
if (isHiddenCompositeMarker(state)) return
|
|
88
|
+
if (state.kind === "fork" || state.kind === "join") return
|
|
89
|
+
|
|
90
|
+
if (state.kind !== "state") {
|
|
91
|
+
setCell(grid, bounds.left, bounds.top, state.label, state.kind)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
fillDiagramFrameInterior(bounds, (x, y) => setCell(grid, x, y, " ", "state"))
|
|
95
|
+
drawStateFrame(grid, bounds, BorderChars[borderStyle], "stateBorder")
|
|
96
|
+
lines.forEach((line, index) => {
|
|
97
|
+
setText(grid, bounds.left + 2, bounds.top + 1 + index, line, "state")
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function drawStateFrame(grid: StateGrid, bounds: BoxBounds, chars: BorderCharacters, style: StateCellStyle): void {
|
|
102
|
+
drawDiagramFrame(bounds, chars, (x, y, char) => setCell(grid, x, y, char, style))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function drawContainerFrame(
|
|
106
|
+
grid: StateGrid,
|
|
107
|
+
bounds: BoxBounds,
|
|
108
|
+
label: string,
|
|
109
|
+
chars: BorderCharacters,
|
|
110
|
+
style: StateCellStyle,
|
|
111
|
+
): void {
|
|
112
|
+
drawDiagramFrame(bounds, chars, (x, y, char) => setCell(grid, x, y, char, style))
|
|
113
|
+
drawContainerLabel(grid, bounds, label, style)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function drawContainerLabel(grid: StateGrid, bounds: BoxBounds, label: string, style: StateCellStyle): void {
|
|
117
|
+
if (label) setText(grid, bounds.left + 2, bounds.top, ` ${label} `, style)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function noteConnectorGlyph(directions: ReadonlySet<DiagramDirection>): string {
|
|
121
|
+
const chars = BorderChars.double
|
|
122
|
+
const up = directions.has("up")
|
|
123
|
+
const down = directions.has("down")
|
|
124
|
+
const left = directions.has("left")
|
|
125
|
+
const right = directions.has("right")
|
|
126
|
+
if (up && down && left && right) return chars.cross
|
|
127
|
+
if (up && down && right) return chars.leftT
|
|
128
|
+
if (up && down && left) return chars.rightT
|
|
129
|
+
if (left && right && down) return chars.topT
|
|
130
|
+
if (left && right && up) return chars.bottomT
|
|
131
|
+
if (up && right) return chars.bottomLeft
|
|
132
|
+
if (up && left) return chars.bottomRight
|
|
133
|
+
if (down && right) return chars.topLeft
|
|
134
|
+
if (down && left) return chars.topRight
|
|
135
|
+
if (up || down) return chars.vertical
|
|
136
|
+
return chars.horizontal
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function drawNote(grid: StateGrid, bounds: StateNoteBounds, target: BoxBounds): void {
|
|
140
|
+
const chars = BorderChars.double
|
|
141
|
+
const connector = stateDiagramNoteConnector(bounds, target)
|
|
142
|
+
const points = orthogonalPathPoints(connector.points)
|
|
143
|
+
for (const [index, point] of points.entries()) {
|
|
144
|
+
const directions = new Set<DiagramDirection>()
|
|
145
|
+
const previous = points[index - 1]
|
|
146
|
+
const next = points[index + 1]
|
|
147
|
+
if (previous) directions.add(directionBetween(point, previous)!)
|
|
148
|
+
if (next) directions.add(directionBetween(point, next)!)
|
|
149
|
+
const distanceFromNote = points.length - index - 1
|
|
150
|
+
const style = distanceFromNote < 3 ? NOTE_CONNECTOR_RAMP_STYLES[2 - distanceFromNote]! : "noteConnector"
|
|
151
|
+
setCell(grid, point.x, point.y, noteConnectorGlyph(directions), style)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
drawContainerFrame(grid, bounds, "", chars, "noteBorder")
|
|
155
|
+
setCell(
|
|
156
|
+
grid,
|
|
157
|
+
bounds.note.position === "right" ? bounds.left : bounds.left + bounds.width - 1,
|
|
158
|
+
connector.connectorY,
|
|
159
|
+
bounds.note.position === "right" ? chars.rightT : chars.leftT,
|
|
160
|
+
"noteBorder",
|
|
161
|
+
)
|
|
162
|
+
bounds.lines.forEach((line, index) => setText(grid, bounds.left + 2, bounds.top + 1 + index, line, "noteText"))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function drawTransitionRenderPlan(
|
|
166
|
+
grid: StateGrid,
|
|
167
|
+
plan: StateTransitionRenderPlan,
|
|
168
|
+
arrowHeadStyle: StateDiagramArrowHeadStyle,
|
|
169
|
+
rampDeparture: boolean,
|
|
170
|
+
): void {
|
|
171
|
+
const departure = new Map(
|
|
172
|
+
rampDeparture
|
|
173
|
+
? plan.path.slice(0, 3).map(([x, y], index) => [`${x}:${y}`, STATE_DEPARTURE_RAMP_STYLES[index]!])
|
|
174
|
+
: [],
|
|
175
|
+
)
|
|
176
|
+
for (const cell of plan.cells) {
|
|
177
|
+
const char = cell.arrowDirection ? diagramArrowHead(cell.arrowDirection, arrowHeadStyle) : cell.char
|
|
178
|
+
setCell(grid, cell.x, cell.y, char, departure.get(`${cell.x}:${cell.y}`) ?? "transition")
|
|
179
|
+
}
|
|
180
|
+
if (plan.label) {
|
|
181
|
+
plan.label.lines.forEach((line, index) => {
|
|
182
|
+
const y = plan.label!.y + index
|
|
183
|
+
const leftX = plan.label!.x - DIAGRAM_LABEL_PADDING_X
|
|
184
|
+
if (grid.getCell(leftX, y)?.char === " ") setCell(grid, leftX, y, " ", "label")
|
|
185
|
+
setText(grid, plan.label!.x, y, line, "label")
|
|
186
|
+
const rightX = plan.label!.x + diagramTextWidth(line) + DIAGRAM_LABEL_PADDING_X - 1
|
|
187
|
+
if (grid.getCell(rightX, y)?.char === " ") setCell(grid, rightX, y, " ", "label")
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function drawTransitionJunctionPlans(
|
|
193
|
+
grid: StateGrid,
|
|
194
|
+
diagram: StateDiagram,
|
|
195
|
+
bounds: Map<string, BoxBounds>,
|
|
196
|
+
renderPlans: readonly StateTransitionRenderPlan[],
|
|
197
|
+
): void {
|
|
198
|
+
for (const plan of createStateTransitionJunctionPlans(diagram, bounds, renderPlans)) {
|
|
199
|
+
const parallel = plan.kind === "fork" || plan.kind === "join"
|
|
200
|
+
const style = plan.kind === "choice" ? "choice" : "transition"
|
|
201
|
+
const bar = diagram.direction === "LR" || diagram.direction === "RL" ? "┃" : "━"
|
|
202
|
+
const char = plan.kind === "choice" ? "◆" : parallel ? bar : diagramLineGlyph(plan.connections, "rounded")
|
|
203
|
+
setCell(grid, plan.bounds.left, plan.bounds.top, char, style)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function drawStateDiagramGrid(sourceDiagram: StateDiagram, options: StateDiagramRenderOptions = {}): StateGrid {
|
|
208
|
+
return createStateDiagramDrawing(sourceDiagram, options).grid
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function createStateDiagramDrawing(sourceDiagram: StateDiagram, options: StateDiagramRenderOptions = {}) {
|
|
212
|
+
const direction = options.direction ?? sourceDiagram.direction
|
|
213
|
+
if (direction !== "LR" && direction !== "RL") {
|
|
214
|
+
return createStateDiagramDrawingWithDirection(sourceDiagram, options, direction)
|
|
215
|
+
}
|
|
216
|
+
if (options.layoutMaxWidth === undefined || !Number.isFinite(options.layoutMaxWidth)) {
|
|
217
|
+
return createStateDiagramDrawingWithDirection(sourceDiagram, options, direction)
|
|
218
|
+
}
|
|
219
|
+
const fallbackDirection = direction === "RL" ? "BT" : "TB"
|
|
220
|
+
const maxWidth = Math.max(1, Math.trunc(options.layoutMaxWidth))
|
|
221
|
+
const drawing = (() => {
|
|
222
|
+
try {
|
|
223
|
+
return createStateDiagramDrawingWithDirection(sourceDiagram, options, direction)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof DiagramCanvasSizeError) return undefined
|
|
226
|
+
throw error
|
|
227
|
+
}
|
|
228
|
+
})()
|
|
229
|
+
if (!drawing) return createStateDiagramDrawingWithDirection(sourceDiagram, options, fallbackDirection)
|
|
230
|
+
if (drawing.grid.getTextSize({ trimTop: true, trimBottom: true }).width <= maxWidth) {
|
|
231
|
+
return drawing
|
|
232
|
+
}
|
|
233
|
+
const fallback = createStateDiagramDrawingWithDirection(sourceDiagram, options, fallbackDirection)
|
|
234
|
+
if (
|
|
235
|
+
fallback.grid.getTextSize({ trimTop: true, trimBottom: true }).width >=
|
|
236
|
+
drawing.grid.getTextSize({ trimTop: true, trimBottom: true }).width
|
|
237
|
+
) {
|
|
238
|
+
return drawing
|
|
239
|
+
}
|
|
240
|
+
return fallback
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createStateDiagramDrawingWithDirection(
|
|
244
|
+
sourceDiagram: StateDiagram,
|
|
245
|
+
options: StateDiagramRenderOptions,
|
|
246
|
+
direction: StateDiagram["direction"],
|
|
247
|
+
) {
|
|
248
|
+
const diagram = prepareVisibleStateDiagram(
|
|
249
|
+
direction === sourceDiagram.direction ? sourceDiagram : { ...sourceDiagram, direction },
|
|
250
|
+
)
|
|
251
|
+
const borderStyle = options.borderStyle ?? DEFAULT_STATE_BORDER_STYLE
|
|
252
|
+
const arrowHeadStyle = options.arrowHeadStyle ?? DEFAULT_STATE_ARROW_HEAD_STYLE
|
|
253
|
+
const minStateGap = normalizeStateMinStateGap(options.minStateGap)
|
|
254
|
+
const searchBudget = createStateSearchBudget()
|
|
255
|
+
const layout = createStateDiagramLayout(diagram, {
|
|
256
|
+
minStateGap,
|
|
257
|
+
searchBudget,
|
|
258
|
+
})
|
|
259
|
+
const { bounds, sizes, compositeBounds, noteBounds } = layout
|
|
260
|
+
let allBounds = [...bounds.values(), ...noteBounds]
|
|
261
|
+
let maxY = Math.max(0, ...allBounds.map((bound) => bound.top + bound.height))
|
|
262
|
+
const feedbackLaneY = maxY + 3
|
|
263
|
+
const feedbackTopY = Math.min(0, ...allBounds.map((bound) => bound.top)) - 3
|
|
264
|
+
expandCompositeBoundsForFeedback(diagram, bounds, compositeBounds, feedbackLaneY)
|
|
265
|
+
let transitionPlans: StateTransitionRenderPlan[] = []
|
|
266
|
+
const separationAttempts = diagram.states.length + diagram.composites.length + 1
|
|
267
|
+
for (let attempt = 0; attempt < separationAttempts; attempt++) {
|
|
268
|
+
transitionPlans = createStateTransitionRenderPlans(diagram, bounds, feedbackLaneY, {
|
|
269
|
+
feedbackTopY,
|
|
270
|
+
noteBounds,
|
|
271
|
+
searchBudget,
|
|
272
|
+
})
|
|
273
|
+
expandCompositeBoundsForInternalTransitions(diagram, compositeBounds, transitionPlans)
|
|
274
|
+
if (!separateExternalBoundsFromComposites(diagram, layout)) break
|
|
275
|
+
if (attempt === separationAttempts - 1) throw new Error("State composite separation did not converge")
|
|
276
|
+
}
|
|
277
|
+
const connectorPoints = noteBounds.flatMap((bound) => bound.connector?.points ?? [])
|
|
278
|
+
const contentLeft = Math.min(
|
|
279
|
+
0,
|
|
280
|
+
...[...bounds.values(), ...noteBounds].map((bound) => bound.left),
|
|
281
|
+
...connectorPoints.map((point) => point.x),
|
|
282
|
+
...transitionPlans.flatMap((plan) => [
|
|
283
|
+
...plan.cells.map((cell) => cell.x),
|
|
284
|
+
...(plan.label ? [plan.label.x - DIAGRAM_LABEL_PADDING_X] : []),
|
|
285
|
+
]),
|
|
286
|
+
)
|
|
287
|
+
const contentTop = Math.min(
|
|
288
|
+
0,
|
|
289
|
+
...[...bounds.values(), ...noteBounds].map((bound) => bound.top),
|
|
290
|
+
...connectorPoints.map((point) => point.y),
|
|
291
|
+
...transitionPlans.flatMap((plan) => [...plan.cells.map((cell) => cell.y), ...(plan.label ? [plan.label.y] : [])]),
|
|
292
|
+
)
|
|
293
|
+
if (contentLeft < 0 || contentTop < 0) {
|
|
294
|
+
translateStateDiagramLayout(layout, -contentLeft, -contentTop)
|
|
295
|
+
transitionPlans = translateTransitionPlans(transitionPlans, -contentLeft, -contentTop)
|
|
296
|
+
}
|
|
297
|
+
allBounds = [...bounds.values(), ...noteBounds]
|
|
298
|
+
const translatedConnectorPoints = noteBounds.flatMap((bound) => bound.connector?.points ?? [])
|
|
299
|
+
const maxX = Math.max(
|
|
300
|
+
0,
|
|
301
|
+
...allBounds.map((bound) => bound.left + bound.width),
|
|
302
|
+
...translatedConnectorPoints.map((point) => point.x + 1),
|
|
303
|
+
)
|
|
304
|
+
maxY = Math.max(
|
|
305
|
+
0,
|
|
306
|
+
...allBounds.map((bound) => bound.top + bound.height),
|
|
307
|
+
...translatedConnectorPoints.map((point) => point.y + 1),
|
|
308
|
+
)
|
|
309
|
+
const transitionLabelSizes = diagram.transitions.map((transition) => measureStateTransitionLabel(transition.label))
|
|
310
|
+
const maxTransitionLabelWidth = Math.max(0, ...transitionLabelSizes.map((size) => size.width))
|
|
311
|
+
const maxTransitionLabelLines = Math.max(0, ...transitionLabelSizes.map((size) => size.height))
|
|
312
|
+
const transitionRight = Math.max(
|
|
313
|
+
maxX,
|
|
314
|
+
...transitionPlans.flatMap((plan) => [
|
|
315
|
+
...plan.cells.map((cell) => cell.x + 1),
|
|
316
|
+
...(plan.label
|
|
317
|
+
? [plan.label.x + measureStateTransitionLabel(plan.route.transition.label).width + DIAGRAM_LABEL_PADDING_X]
|
|
318
|
+
: []),
|
|
319
|
+
]),
|
|
320
|
+
)
|
|
321
|
+
const transitionBottom = Math.max(
|
|
322
|
+
maxY,
|
|
323
|
+
...transitionPlans.flatMap((plan) => [
|
|
324
|
+
...plan.cells.map((cell) => cell.y + 1),
|
|
325
|
+
...(plan.label ? [plan.label.y + plan.label.lines.length] : []),
|
|
326
|
+
]),
|
|
327
|
+
)
|
|
328
|
+
const grid = makeGrid(
|
|
329
|
+
Math.max(maxX + Math.max(24, maxTransitionLabelWidth + 4), transitionRight + 2),
|
|
330
|
+
Math.max(maxY + 8 + maxTransitionLabelLines, transitionBottom + 2),
|
|
331
|
+
)
|
|
332
|
+
for (const composite of diagram.composites) {
|
|
333
|
+
const bound = compositeBounds.get(composite.id)
|
|
334
|
+
if (!bound) continue
|
|
335
|
+
drawContainerFrame(grid, bound, composite.label, BorderChars[borderStyle], "composite")
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const state of diagram.states) {
|
|
339
|
+
const bound = bounds.get(state.id)
|
|
340
|
+
const size = sizes.get(state.id)
|
|
341
|
+
if (!bound || !size) continue
|
|
342
|
+
drawBox(grid, state, bound, size.lines, borderStyle)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const plan of transitionPlans) {
|
|
346
|
+
const source = diagram.states.find((state) => state.id === plan.route.transition.from)
|
|
347
|
+
drawTransitionRenderPlan(grid, plan, arrowHeadStyle, source?.kind === "state")
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
drawTransitionJunctionPlans(grid, diagram, bounds, transitionPlans)
|
|
351
|
+
|
|
352
|
+
for (const composite of diagram.composites) {
|
|
353
|
+
const bound = compositeBounds.get(composite.id)
|
|
354
|
+
if (bound) drawContainerLabel(grid, bound, composite.label, "compositeLabel")
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const noteBound of noteBounds) {
|
|
358
|
+
const target = bounds.get(noteBound.note.target)
|
|
359
|
+
if (target) drawNote(grid, noteBound, target)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { grid, diagram, layout, transitionPlans }
|
|
363
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function stateDiagramStartMarkerId(scope?: string): string {
|
|
2
|
+
return scope ? `${scope}.__start` : "__start"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function stateDiagramEndMarkerId(scope?: string): string {
|
|
6
|
+
return scope ? `${scope}.__end` : "__end"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function stateDiagramMarkerId(position: "from" | "to", scope?: string): string {
|
|
10
|
+
return position === "from" ? stateDiagramStartMarkerId(scope) : stateDiagramEndMarkerId(scope)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeStateDiagramEndpoint(value: string, position: "from" | "to", scope?: string): string {
|
|
14
|
+
return value === "[*]" ? stateDiagramMarkerId(position, scope) : value
|
|
15
|
+
}
|