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,12 @@
|
|
|
1
|
+
import type { BorderStyle } from "@opentui/core"
|
|
2
|
+
import type { FlowchartDirection } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export interface FlowchartDiagramRenderOptions {
|
|
5
|
+
compact?: boolean
|
|
6
|
+
direction?: FlowchartDirection
|
|
7
|
+
borderStyle?: BorderStyle
|
|
8
|
+
minNodeGap?: number
|
|
9
|
+
minRankGap?: number
|
|
10
|
+
/** Target rendered width. Oversized horizontal layouts fold vertically and broad vertical ranks wrap. */
|
|
11
|
+
layoutMaxWidth?: number
|
|
12
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowchartDiagram,
|
|
3
|
+
FlowchartDirection,
|
|
4
|
+
FlowchartEdge,
|
|
5
|
+
FlowchartEdgeStyle,
|
|
6
|
+
FlowchartNode,
|
|
7
|
+
FlowchartSubgraph,
|
|
8
|
+
} from "./types.js"
|
|
9
|
+
import { MermaidSyntaxError } from "../diagnostics.js"
|
|
10
|
+
import {
|
|
11
|
+
decodeMermaidText,
|
|
12
|
+
firstMeaningfulMermaidLine,
|
|
13
|
+
meaningfulNumberedMermaidLines,
|
|
14
|
+
stripMermaidQuotes as stripQuotes,
|
|
15
|
+
type NumberedMermaidLine,
|
|
16
|
+
} from "../core/mermaid.js"
|
|
17
|
+
|
|
18
|
+
const DEFAULT_DIRECTION = "TD" satisfies FlowchartDirection
|
|
19
|
+
const FLOWCHART_HEADER_RE = /^(flowchart|graph)(?:\s+(TB|TD|BT|LR|RL))?$/i
|
|
20
|
+
const ID_RE = "[A-Za-z_][A-Za-z0-9_.-]*"
|
|
21
|
+
const SUBGRAPH_RE = /^subgraph\s+(.+)$/i
|
|
22
|
+
const SUBGRAPH_WITH_LABEL_RE = new RegExp(`^(${ID_RE})\\s*\\[(.+)\\]$`)
|
|
23
|
+
const SUBGRAPH_DIRECTION_RE = /^direction\s+(TB|TD|BT|LR|RL)$/i
|
|
24
|
+
const IGNORED_PRESENTATION_RE = /^(?:classDef|class|style|linkStyle)\b/i
|
|
25
|
+
const DATABASE_NODE_RE = new RegExp(`^(${ID_RE})\\[\\((.+)\\)\\]$`)
|
|
26
|
+
const SUBROUTINE_NODE_RE = new RegExp(`^(${ID_RE})\\[\\[(.+)\\]\\]$`)
|
|
27
|
+
const ROUNDED_BRACKET_NODE_RE = new RegExp(`^(${ID_RE})\\(\\[(.+)\\]\\)$`)
|
|
28
|
+
const ROUNDED_NODE_RE = new RegExp(`^(${ID_RE})\\((.+)\\)$`)
|
|
29
|
+
const HEXAGON_NODE_RE = new RegExp(`^(${ID_RE})\\{\\{(.+)\\}\\}$`)
|
|
30
|
+
const PARALLELOGRAM_NODE_RE = new RegExp(`^(${ID_RE})\\[/(.+)/\\]$`)
|
|
31
|
+
const DECISION_NODE_RE = new RegExp(`^(${ID_RE})\\{(.+)\\}$`)
|
|
32
|
+
const BOX_NODE_RE = new RegExp(`^(${ID_RE})\\[(.+)\\]$`)
|
|
33
|
+
const ID_ONLY_RE = new RegExp(`^${ID_RE}$`)
|
|
34
|
+
const EXPLICIT_NODE_SHAPE_RE = new RegExp(`^${ID_RE}(?:\\[|\\(|\\{)`)
|
|
35
|
+
const CIRCLE_NODE_RE = new RegExp(`^(${ID_RE})\\(\\((.+)\\)\\)$`)
|
|
36
|
+
const MAX_FLOWCHART_LINE_LENGTH = 10_000
|
|
37
|
+
const EDGE_OPERATOR_RE =
|
|
38
|
+
/(-\.(?!->)(.+?)\.(?:->|-))|(--|==|-\.)\s+(.+?)\s+(-->|==>|\.->|-\.->|\.-)|(<-->|-->|==>|-\.->|---|~~~)\s*(?:\|([^|]*)\|\s*)?/dg
|
|
39
|
+
|
|
40
|
+
function normalizeDirection(value?: string): FlowchartDirection {
|
|
41
|
+
const upper = value?.toUpperCase()
|
|
42
|
+
if (upper === "TB" || upper === "TD" || upper === "BT" || upper === "LR" || upper === "RL") return upper
|
|
43
|
+
return DEFAULT_DIRECTION
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeSubgraphId(value: string, index: number): string {
|
|
47
|
+
const stripped = stripQuotes(value)
|
|
48
|
+
return ID_ONLY_RE.test(stripped) ? stripped : `subgraph_${index + 1}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseSubgraphToken(token: string, index: number): Pick<FlowchartSubgraph, "id" | "label"> {
|
|
52
|
+
const trimmed = token
|
|
53
|
+
.trim()
|
|
54
|
+
.replace(/\s*:::.*$/, "")
|
|
55
|
+
.replace(/;$/, "")
|
|
56
|
+
const withLabel = trimmed.match(SUBGRAPH_WITH_LABEL_RE)
|
|
57
|
+
if (withLabel) {
|
|
58
|
+
return { id: withLabel[1]!, label: stripQuotes(withLabel[2]!) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const label = stripQuotes(trimmed)
|
|
62
|
+
return { id: normalizeSubgraphId(trimmed, index), label }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseNodeToken(token: string): FlowchartNode {
|
|
66
|
+
const trimmed = token.trim().replace(/;$/, "")
|
|
67
|
+
const database = trimmed.match(DATABASE_NODE_RE)
|
|
68
|
+
if (database) return { id: database[1]!, label: stripQuotes(database[2]!), shape: "database" }
|
|
69
|
+
|
|
70
|
+
const subroutine = trimmed.match(SUBROUTINE_NODE_RE)
|
|
71
|
+
if (subroutine) return { id: subroutine[1]!, label: stripQuotes(subroutine[2]!), shape: "subroutine" }
|
|
72
|
+
|
|
73
|
+
const roundedBracket = trimmed.match(ROUNDED_BRACKET_NODE_RE)
|
|
74
|
+
if (roundedBracket) return { id: roundedBracket[1]!, label: stripQuotes(roundedBracket[2]!), shape: "rounded" }
|
|
75
|
+
|
|
76
|
+
const circle = trimmed.match(CIRCLE_NODE_RE)
|
|
77
|
+
if (circle) return { id: circle[1]!, label: stripQuotes(circle[2]!), shape: "circle" }
|
|
78
|
+
|
|
79
|
+
const rounded = trimmed.match(ROUNDED_NODE_RE)
|
|
80
|
+
if (rounded) return { id: rounded[1]!, label: stripQuotes(rounded[2]!), shape: "rounded" }
|
|
81
|
+
|
|
82
|
+
const hexagon = trimmed.match(HEXAGON_NODE_RE)
|
|
83
|
+
if (hexagon) return { id: hexagon[1]!, label: stripQuotes(hexagon[2]!), shape: "hexagon" }
|
|
84
|
+
|
|
85
|
+
const parallelogram = trimmed.match(PARALLELOGRAM_NODE_RE)
|
|
86
|
+
if (parallelogram) return { id: parallelogram[1]!, label: stripQuotes(parallelogram[2]!), shape: "parallelogram" }
|
|
87
|
+
|
|
88
|
+
const decision = trimmed.match(DECISION_NODE_RE)
|
|
89
|
+
if (decision) return { id: decision[1]!, label: stripQuotes(decision[2]!), shape: "decision" }
|
|
90
|
+
|
|
91
|
+
const box = trimmed.match(BOX_NODE_RE)
|
|
92
|
+
if (box) return { id: box[1]!, label: stripQuotes(box[2]!), shape: "box" }
|
|
93
|
+
|
|
94
|
+
return { id: trimmed, label: trimmed, shape: "box" }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isSupportedNodeToken(token: string): boolean {
|
|
98
|
+
const trimmed = stripNodeToken(token)
|
|
99
|
+
return (
|
|
100
|
+
ID_ONLY_RE.test(trimmed) ||
|
|
101
|
+
DATABASE_NODE_RE.test(trimmed) ||
|
|
102
|
+
SUBROUTINE_NODE_RE.test(trimmed) ||
|
|
103
|
+
ROUNDED_BRACKET_NODE_RE.test(trimmed) ||
|
|
104
|
+
CIRCLE_NODE_RE.test(trimmed) ||
|
|
105
|
+
ROUNDED_NODE_RE.test(trimmed) ||
|
|
106
|
+
HEXAGON_NODE_RE.test(trimmed) ||
|
|
107
|
+
PARALLELOGRAM_NODE_RE.test(trimmed) ||
|
|
108
|
+
DECISION_NODE_RE.test(trimmed) ||
|
|
109
|
+
BOX_NODE_RE.test(trimmed)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasExplicitNodeShape(token: string): boolean {
|
|
114
|
+
return EXPLICIT_NODE_SHAPE_RE.test(token.trim())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureNode(nodes: Map<string, FlowchartNode>, token: string): FlowchartNode {
|
|
118
|
+
const node = parseNodeToken(token)
|
|
119
|
+
const existing = nodes.get(node.id)
|
|
120
|
+
if (!existing) {
|
|
121
|
+
nodes.set(node.id, node)
|
|
122
|
+
return node
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasExplicitNodeShape(token)) {
|
|
126
|
+
existing.label = node.label
|
|
127
|
+
existing.shape = node.shape
|
|
128
|
+
}
|
|
129
|
+
return existing
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function addNodeToSubgraph(subgraph: FlowchartSubgraph | undefined, nodeId: string): void {
|
|
133
|
+
if (!subgraph || subgraph.nodeIds.includes(nodeId)) return
|
|
134
|
+
subgraph.nodeIds.push(nodeId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripNodeToken(token: string): string {
|
|
138
|
+
return token
|
|
139
|
+
.replace(/\s*:::.*$/, "")
|
|
140
|
+
.replace(/;$/, "")
|
|
141
|
+
.trim()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Split an `&`-joined node group, leaving `&` inside labels (brackets or quotes) untouched. */
|
|
145
|
+
function splitNodeGroup(token: string): string[] {
|
|
146
|
+
const groups: string[] = []
|
|
147
|
+
const stack: string[] = []
|
|
148
|
+
let quote: '"' | "'" | undefined
|
|
149
|
+
let start = 0
|
|
150
|
+
const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
|
|
151
|
+
|
|
152
|
+
for (let index = 0; index < token.length; index++) {
|
|
153
|
+
const character = token[index]!
|
|
154
|
+
if (quote) {
|
|
155
|
+
if (character === quote && token[index - 1] !== "\\") quote = undefined
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
if (character === '"' || character === "'") {
|
|
159
|
+
quote = character
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (character in closes) {
|
|
163
|
+
stack.push(character)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (stack.length > 0 && character === closes[stack.at(-1)!]) {
|
|
167
|
+
stack.pop()
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
if (stack.length === 0 && character === "&") {
|
|
171
|
+
groups.push(token.slice(start, index))
|
|
172
|
+
start = index + 1
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
groups.push(token.slice(start))
|
|
176
|
+
return groups
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function edgeStyleFromArrow(...arrows: string[]): FlowchartEdgeStyle | undefined {
|
|
180
|
+
if (arrows.some((arrow) => arrow.includes("=="))) return "thick"
|
|
181
|
+
if (arrows.some((arrow) => arrow.includes("."))) return "dashed"
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createEdge(
|
|
186
|
+
from: string,
|
|
187
|
+
to: string,
|
|
188
|
+
label: string,
|
|
189
|
+
style: FlowchartEdgeStyle | undefined,
|
|
190
|
+
arrowhead: boolean,
|
|
191
|
+
sourceArrowhead: boolean,
|
|
192
|
+
): FlowchartEdge {
|
|
193
|
+
const edge: FlowchartEdge = style ? { from, to, label, style } : { from, to, label }
|
|
194
|
+
if (!arrowhead) edge.arrowhead = false
|
|
195
|
+
if (sourceArrowhead) edge.sourceArrowhead = true
|
|
196
|
+
return edge
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface ParsedEdgeOperator {
|
|
200
|
+
index: number
|
|
201
|
+
end: number
|
|
202
|
+
label: string
|
|
203
|
+
style: FlowchartEdgeStyle | undefined
|
|
204
|
+
arrowhead: boolean
|
|
205
|
+
sourceArrowhead: boolean
|
|
206
|
+
orderOnly: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseEdgeOperators(line: string): ParsedEdgeOperator[] {
|
|
210
|
+
return [...maskNodeLabelOperators(line).matchAll(EDGE_OPERATOR_RE)].map((match) => {
|
|
211
|
+
const inlineDashedArrow = match[1]
|
|
212
|
+
const startArrow = inlineDashedArrow ?? match[3] ?? match[6]!
|
|
213
|
+
const endArrow = inlineDashedArrow ?? match[5] ?? match[6]!
|
|
214
|
+
const labelGroup = match[2] ? 2 : match[4] ? 4 : match[7] ? 7 : undefined
|
|
215
|
+
const labelRange = labelGroup === undefined ? undefined : match.indices?.[labelGroup]
|
|
216
|
+
return {
|
|
217
|
+
index: match.index,
|
|
218
|
+
end: match.index + match[0].length,
|
|
219
|
+
label: stripQuotes(labelRange ? line.slice(labelRange[0], labelRange[1]) : ""),
|
|
220
|
+
style: edgeStyleFromArrow(startArrow, endArrow),
|
|
221
|
+
arrowhead: endArrow === "~~~" || endArrow.endsWith(">"),
|
|
222
|
+
sourceArrowhead: startArrow.startsWith("<"),
|
|
223
|
+
orderOnly: endArrow === "~~~",
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function maskNodeLabelOperators(line: string): string {
|
|
229
|
+
const characters = line.split("")
|
|
230
|
+
const stack: string[] = []
|
|
231
|
+
let quote: '"' | "'" | undefined
|
|
232
|
+
const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
|
|
233
|
+
|
|
234
|
+
for (let index = 0; index < characters.length; index++) {
|
|
235
|
+
const character = characters[index]!
|
|
236
|
+
if (quote) {
|
|
237
|
+
if (character === quote && characters[index - 1] !== "\\") quote = undefined
|
|
238
|
+
else if (/[<>=.-]/.test(character)) characters[index] = " "
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
if (character === '"' || character === "'") {
|
|
242
|
+
quote = character
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (character in closes) {
|
|
246
|
+
stack.push(character)
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
if (stack.length > 0 && character === closes[stack.at(-1)!]) {
|
|
250
|
+
stack.pop()
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if (stack.length > 0 && /[<>=.-]/.test(character)) characters[index] = " "
|
|
254
|
+
}
|
|
255
|
+
return characters.join("")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function splitStatements(line: string): string[] {
|
|
259
|
+
const statements: string[] = []
|
|
260
|
+
const stack: string[] = []
|
|
261
|
+
let quote: '"' | "'" | undefined
|
|
262
|
+
let edgeLabel = false
|
|
263
|
+
const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
|
|
264
|
+
let start = 0
|
|
265
|
+
|
|
266
|
+
for (let index = 0; index < line.length; index++) {
|
|
267
|
+
const character = line[index]!
|
|
268
|
+
if (quote) {
|
|
269
|
+
if (character === quote && line[index - 1] !== "\\") quote = undefined
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
if (character === '"' || character === "'") {
|
|
273
|
+
quote = character
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (character in closes) {
|
|
277
|
+
stack.push(character)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
if (stack.length > 0 && character === closes[stack.at(-1)!]) {
|
|
281
|
+
stack.pop()
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
if (stack.length === 0 && character === "|") {
|
|
285
|
+
edgeLabel = !edgeLabel
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
if (character === ";" && stack.length === 0 && !edgeLabel) {
|
|
289
|
+
const statement = line.slice(start, index).trim()
|
|
290
|
+
if (statement) statements.push(statement)
|
|
291
|
+
start = index + 1
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const last = line.slice(start).trim()
|
|
295
|
+
if (last) statements.push(last)
|
|
296
|
+
return statements
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function stripInlineComment(line: string): string {
|
|
300
|
+
const stack: string[] = []
|
|
301
|
+
let quote: '"' | "'" | undefined
|
|
302
|
+
let edgeLabel = false
|
|
303
|
+
const closes: Record<string, string> = { "[": "]", "(": ")", "{": "}" }
|
|
304
|
+
for (let index = 0; index < line.length - 1; index++) {
|
|
305
|
+
const character = line[index]!
|
|
306
|
+
if (quote) {
|
|
307
|
+
if (character === quote && line[index - 1] !== "\\") quote = undefined
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if (character === '"' || character === "'") {
|
|
311
|
+
quote = character
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
if (character in closes) {
|
|
315
|
+
stack.push(character)
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
if (stack.length && character === closes[stack.at(-1)!]) {
|
|
319
|
+
stack.pop()
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
if (character === "|" && stack.length === 0) {
|
|
323
|
+
edgeLabel = !edgeLabel
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
if (character === "%" && line[index + 1] === "%" && stack.length === 0 && !edgeLabel) {
|
|
327
|
+
return line.slice(0, index).trimEnd()
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return line
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function* flowchartStatements(content: string): Generator<NumberedMermaidLine> {
|
|
334
|
+
for (const source of meaningfulNumberedMermaidLines(content)) {
|
|
335
|
+
if (source.text.length > MAX_FLOWCHART_LINE_LENGTH) {
|
|
336
|
+
throw new MermaidSyntaxError("flowchart", source.lineNumber, source.text, "Flowchart statement is too long")
|
|
337
|
+
}
|
|
338
|
+
for (const text of splitStatements(stripInlineComment(source.text))) {
|
|
339
|
+
// The rest of a presentation directive may contain CSS semicolons, not
|
|
340
|
+
// Mermaid statements. Terminal colours come from the theme instead.
|
|
341
|
+
if (IGNORED_PRESENTATION_RE.test(text)) break
|
|
342
|
+
yield { ...source, text }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function isMermaidFlowchartDiagram(content: string): boolean {
|
|
348
|
+
const first = stripInlineComment(firstMeaningfulMermaidLine(content) ?? "").trim()
|
|
349
|
+
return FLOWCHART_HEADER_RE.test(first) || (first.includes(";") && FLOWCHART_HEADER_RE.test(splitStatements(first)[0] ?? ""))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function parseMermaidFlowchartDiagram(content: string): FlowchartDiagram {
|
|
353
|
+
const nodes = new Map<string, FlowchartNode>()
|
|
354
|
+
const edges: FlowchartEdge[] = []
|
|
355
|
+
const subgraphs: FlowchartSubgraph[] = []
|
|
356
|
+
const subgraphStack: Array<{ subgraph: FlowchartSubgraph; lineNumber: number; sourceLine: string }> = []
|
|
357
|
+
let direction: FlowchartDirection = DEFAULT_DIRECTION
|
|
358
|
+
|
|
359
|
+
for (const source of flowchartStatements(content)) {
|
|
360
|
+
const line = source.text
|
|
361
|
+
const header = line.match(FLOWCHART_HEADER_RE)
|
|
362
|
+
if (header) {
|
|
363
|
+
direction = normalizeDirection(header[2])
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Mermaid CSS styling does not apply to terminal theme rendering.
|
|
368
|
+
if (IGNORED_PRESENTATION_RE.test(line)) continue
|
|
369
|
+
|
|
370
|
+
const subgraphMatch = line.match(SUBGRAPH_RE)
|
|
371
|
+
if (subgraphMatch) {
|
|
372
|
+
const parsed = parseSubgraphToken(subgraphMatch[1]!, subgraphs.length)
|
|
373
|
+
const subgraph: FlowchartSubgraph = {
|
|
374
|
+
...parsed,
|
|
375
|
+
nodeIds: [],
|
|
376
|
+
parentId: subgraphStack[subgraphStack.length - 1]?.subgraph.id,
|
|
377
|
+
}
|
|
378
|
+
subgraphs.push(subgraph)
|
|
379
|
+
subgraphStack.push({ subgraph, lineNumber: source.lineNumber, sourceLine: line })
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (/^end$/i.test(line)) {
|
|
384
|
+
if (subgraphStack.length === 0) {
|
|
385
|
+
throw new MermaidSyntaxError("flowchart", source.lineNumber, line, 'Unexpected "end" without an open subgraph')
|
|
386
|
+
}
|
|
387
|
+
subgraphStack.pop()
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const currentSubgraph = subgraphStack[subgraphStack.length - 1]?.subgraph
|
|
392
|
+
|
|
393
|
+
const subgraphDirection = line.match(SUBGRAPH_DIRECTION_RE)
|
|
394
|
+
if (subgraphDirection) {
|
|
395
|
+
if (!currentSubgraph) {
|
|
396
|
+
throw new MermaidSyntaxError(
|
|
397
|
+
"flowchart",
|
|
398
|
+
source.lineNumber,
|
|
399
|
+
line,
|
|
400
|
+
'A "direction" statement requires an open subgraph',
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
currentSubgraph.direction = normalizeDirection(subgraphDirection[1])
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const edgeOperators = parseEdgeOperators(line)
|
|
408
|
+
if (edgeOperators.length > 0) {
|
|
409
|
+
// Each chain position may be an `&` group (`A & B --> C`), so endpoints are lists of node tokens.
|
|
410
|
+
const nodeGroups = [
|
|
411
|
+
line.slice(0, edgeOperators[0]!.index),
|
|
412
|
+
...edgeOperators.map((operator, index) =>
|
|
413
|
+
line.slice(operator.end, edgeOperators[index + 1]?.index ?? line.length),
|
|
414
|
+
),
|
|
415
|
+
].map((group) => splitNodeGroup(group).map(stripNodeToken))
|
|
416
|
+
|
|
417
|
+
if (nodeGroups.every((group) => group.every((token) => token.length > 0))) {
|
|
418
|
+
const unsupportedEndpoint = nodeGroups.find((group, index) => {
|
|
419
|
+
const orderOnlyEndpoint = edgeOperators[index - 1]?.orderOnly || edgeOperators[index]?.orderOnly
|
|
420
|
+
return group.some(
|
|
421
|
+
(stripped) =>
|
|
422
|
+
!(orderOnlyEndpoint && subgraphs.some((subgraph) => subgraph.id === stripped)) &&
|
|
423
|
+
!isSupportedNodeToken(stripped),
|
|
424
|
+
)
|
|
425
|
+
})
|
|
426
|
+
if (unsupportedEndpoint) throw new MermaidSyntaxError("flowchart", source.lineNumber, line)
|
|
427
|
+
const chainNodeIds = nodeGroups.map((group, index) => {
|
|
428
|
+
const orderOnlyEndpoint = edgeOperators[index - 1]?.orderOnly || edgeOperators[index]?.orderOnly
|
|
429
|
+
return group.map((stripped) => {
|
|
430
|
+
if (orderOnlyEndpoint && subgraphs.some((subgraph) => subgraph.id === stripped)) return stripped
|
|
431
|
+
return ensureNode(nodes, stripped).id
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
for (const nodeId of chainNodeIds.flat()) {
|
|
435
|
+
if (nodes.has(nodeId)) addNodeToSubgraph(currentSubgraph, nodeId)
|
|
436
|
+
}
|
|
437
|
+
for (let index = 0; index < edgeOperators.length; index++) {
|
|
438
|
+
const operator = edgeOperators[index]!
|
|
439
|
+
for (const from of chainNodeIds[index]!) {
|
|
440
|
+
for (const to of chainNodeIds[index + 1]!) {
|
|
441
|
+
const edge = createEdge(
|
|
442
|
+
from,
|
|
443
|
+
to,
|
|
444
|
+
operator.label,
|
|
445
|
+
operator.style,
|
|
446
|
+
operator.arrowhead,
|
|
447
|
+
operator.sourceArrowhead,
|
|
448
|
+
)
|
|
449
|
+
edges.push(operator.orderOnly ? { ...edge, orderOnly: true } : edge)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const nodeGroup = splitNodeGroup(line)
|
|
458
|
+
if (nodeGroup.every(isSupportedNodeToken)) {
|
|
459
|
+
for (const token of nodeGroup) addNodeToSubgraph(currentSubgraph, ensureNode(nodes, stripNodeToken(token)).id)
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
throw new MermaidSyntaxError("flowchart", source.lineNumber, line)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const unclosedSubgraph = subgraphStack[subgraphStack.length - 1]
|
|
467
|
+
if (unclosedSubgraph) {
|
|
468
|
+
throw new MermaidSyntaxError(
|
|
469
|
+
"flowchart",
|
|
470
|
+
unclosedSubgraph.lineNumber,
|
|
471
|
+
unclosedSubgraph.sourceLine,
|
|
472
|
+
'Unclosed subgraph; expected "end"',
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { direction, nodes: [...nodes.values()], edges, subgraphs }
|
|
477
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { drawFlowchartDiagramGrid } from "./drawing.js"
|
|
2
|
+
import type { FlowchartDiagramRenderOptions } from "./options.js"
|
|
3
|
+
import { parseMermaidFlowchartDiagram } from "./parser.js"
|
|
4
|
+
|
|
5
|
+
export function renderFlowchartDiagram(content: string, options: FlowchartDiagramRenderOptions = {}): string {
|
|
6
|
+
return drawFlowchartDiagramGrid(parseMermaidFlowchartDiagram(content), options).toString({
|
|
7
|
+
trimTop: true,
|
|
8
|
+
trimBottom: true,
|
|
9
|
+
})
|
|
10
|
+
}
|