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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +27 -0
  3. package/README.md +223 -2
  4. package/bin/otmd.tsx +99 -0
  5. package/native/Cargo.lock +3708 -0
  6. package/native/Cargo.toml +28 -0
  7. package/native/build.rs +3 -0
  8. package/native/src/lib.rs +880 -0
  9. package/package.json +67 -4
  10. package/prebuilds/linux-x64-gnu/opentui_markdown_render.node +4 -0
  11. package/src/app.tsx +180 -0
  12. package/src/cli.ts +63 -0
  13. package/src/config/index.ts +46 -0
  14. package/src/config/load.ts +337 -0
  15. package/src/config/resolve.ts +36 -0
  16. package/src/config/theme.ts +110 -0
  17. package/src/config/types.ts +105 -0
  18. package/src/core/document.ts +44 -0
  19. package/src/core/frame.ts +144 -0
  20. package/src/core/image.ts +9 -0
  21. package/src/core/index.ts +6 -0
  22. package/src/core/layout.ts +27 -0
  23. package/src/core/math.ts +170 -0
  24. package/src/core/parse.ts +208 -0
  25. package/src/core/tables.ts +58 -0
  26. package/src/graphics.ts +85 -0
  27. package/src/headings.ts +77 -0
  28. package/src/index.ts +107 -0
  29. package/src/math/index.ts +86 -0
  30. package/src/math/native.ts +39 -0
  31. package/src/math/renderer.ts +84 -0
  32. package/src/math/validate.ts +44 -0
  33. package/src/media.tsx +267 -0
  34. package/src/merman/class/dagre-layout.ts +171 -0
  35. package/src/merman/class/diagram.ts +8 -0
  36. package/src/merman/class/drawing.ts +343 -0
  37. package/src/merman/class/labels.ts +38 -0
  38. package/src/merman/class/layout.ts +531 -0
  39. package/src/merman/class/parser.ts +308 -0
  40. package/src/merman/class/render-grid.ts +18 -0
  41. package/src/merman/class/style.ts +27 -0
  42. package/src/merman/class/types.ts +57 -0
  43. package/src/merman/core/canvas.ts +228 -0
  44. package/src/merman/core/color/style.ts +56 -0
  45. package/src/merman/core/drawing.ts +307 -0
  46. package/src/merman/core/geometry.ts +347 -0
  47. package/src/merman/core/mermaid.ts +38 -0
  48. package/src/merman/core/render-grid.ts +51 -0
  49. package/src/merman/core/spatial.ts +243 -0
  50. package/src/merman/core/text-lines.ts +32 -0
  51. package/src/merman/core/text.ts +44 -0
  52. package/src/merman/detect.ts +19 -0
  53. package/src/merman/diagnostics.ts +16 -0
  54. package/src/merman/diagram.ts +268 -0
  55. package/src/merman/flowchart/drawing.ts +433 -0
  56. package/src/merman/flowchart/labels.ts +144 -0
  57. package/src/merman/flowchart/layout.ts +1057 -0
  58. package/src/merman/flowchart/options.ts +12 -0
  59. package/src/merman/flowchart/parser.ts +477 -0
  60. package/src/merman/flowchart/render.ts +10 -0
  61. package/src/merman/flowchart/routing.ts +1429 -0
  62. package/src/merman/flowchart/style.ts +79 -0
  63. package/src/merman/flowchart/types.ts +63 -0
  64. package/src/merman/gantt/diagram.ts +8 -0
  65. package/src/merman/gantt/drawing.ts +193 -0
  66. package/src/merman/gantt/parser.ts +198 -0
  67. package/src/merman/gantt/render-grid.ts +17 -0
  68. package/src/merman/gantt/style.ts +36 -0
  69. package/src/merman/gantt/types.ts +45 -0
  70. package/src/merman/gitgraph/diagram.ts +8 -0
  71. package/src/merman/gitgraph/drawing.ts +234 -0
  72. package/src/merman/gitgraph/parser.ts +234 -0
  73. package/src/merman/gitgraph/render-grid.ts +17 -0
  74. package/src/merman/gitgraph/style.ts +37 -0
  75. package/src/merman/gitgraph/types.ts +36 -0
  76. package/src/merman/palette.ts +60 -0
  77. package/src/merman/sequence/diagram.ts +8 -0
  78. package/src/merman/sequence/drawing.ts +288 -0
  79. package/src/merman/sequence/options.ts +8 -0
  80. package/src/merman/sequence/parser.ts +303 -0
  81. package/src/merman/sequence/placement.ts +692 -0
  82. package/src/merman/sequence/render-grid.ts +22 -0
  83. package/src/merman/sequence/style.ts +81 -0
  84. package/src/merman/sequence/types.ts +74 -0
  85. package/src/merman/state/diagram.ts +8 -0
  86. package/src/merman/state/drawing.ts +363 -0
  87. package/src/merman/state/endpoint.ts +15 -0
  88. package/src/merman/state/layout.ts +1134 -0
  89. package/src/merman/state/note.ts +59 -0
  90. package/src/merman/state/options.ts +11 -0
  91. package/src/merman/state/parser.ts +245 -0
  92. package/src/merman/state/render-grid.ts +22 -0
  93. package/src/merman/state/routing.ts +1522 -0
  94. package/src/merman/state/search.ts +103 -0
  95. package/src/merman/state/style.ts +52 -0
  96. package/src/merman/state/types.ts +63 -0
  97. package/src/merman/state/visible-model.ts +77 -0
  98. package/src/merman/timeline/diagram.ts +8 -0
  99. package/src/merman/timeline/drawing.ts +109 -0
  100. package/src/merman/timeline/parser.ts +119 -0
  101. package/src/merman/timeline/render-grid.ts +17 -0
  102. package/src/merman/timeline/style.ts +36 -0
  103. package/src/merman/timeline/types.ts +31 -0
  104. package/src/merman.ts +3 -0
  105. package/src/statusline.tsx +183 -0
  106. package/src/theme/colors.ts +11 -0
  107. package/src/theme/defaults.ts +190 -0
  108. package/src/theme/index.ts +7 -0
  109. package/src/theme/load.ts +39 -0
  110. package/src/theme/resolve.ts +167 -0
  111. package/src/theme/syntax.ts +96 -0
  112. package/src/theme/types.ts +151 -0
  113. package/src/view/code.tsx +27 -0
  114. package/src/view/document-slots.ts +111 -0
  115. package/src/view/document-table.tsx +98 -0
  116. package/src/view/document.tsx +195 -0
  117. package/src/view/frame.tsx +83 -0
  118. package/src/view/inline-flow.tsx +113 -0
  119. package/src/view/inline-layout.ts +149 -0
  120. package/src/view/inline-math.ts +42 -0
  121. package/src/view/inline-style.ts +103 -0
  122. package/src/view/math.tsx +85 -0
  123. package/src/view/mermaid.tsx +128 -0
  124. package/src/view/table.ts +100 -0
  125. package/themes/dark.json +32 -0
  126. package/themes/example.jsonc +98 -0
  127. package/themes/light.json +32 -0
  128. package/themes/solarized-dark.json +32 -0
  129. 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
+ }