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,234 @@
|
|
|
1
|
+
import { DiagramCanvas } from "../core/canvas.js"
|
|
2
|
+
import { diagramTextWidth } from "../core/text.js"
|
|
3
|
+
import type { GitGraphGrid } from "./render-grid.js"
|
|
4
|
+
import type { GitGraphCellStyle, GitGraphCommit, GitGraphDiagram, GitGraphDiagramRenderOptions } from "./types.js"
|
|
5
|
+
|
|
6
|
+
interface BranchSpan {
|
|
7
|
+
first: number
|
|
8
|
+
last: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Connections {
|
|
12
|
+
up?: boolean
|
|
13
|
+
down?: boolean
|
|
14
|
+
left?: boolean
|
|
15
|
+
right?: boolean
|
|
16
|
+
style: GitGraphCellStyle
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LANE_WIDTH = 2
|
|
20
|
+
const LABEL_GAP = 2
|
|
21
|
+
|
|
22
|
+
export function drawGitGraphDiagramGrid(
|
|
23
|
+
diagram: GitGraphDiagram,
|
|
24
|
+
_options: GitGraphDiagramRenderOptions = {},
|
|
25
|
+
): GitGraphGrid {
|
|
26
|
+
if (diagram.commits.length === 0) return new DiagramCanvas(0, 0)
|
|
27
|
+
const laneByBranch = new Map(diagram.branches.map((branch, index) => [branch.name, index]))
|
|
28
|
+
const commitById = new Map(diagram.commits.map((commit) => [commit.id, commit]))
|
|
29
|
+
const spans = branchSpans(diagram, commitById)
|
|
30
|
+
const heads = branchHeads(diagram)
|
|
31
|
+
const graphWidth = (diagram.branches.length - 1) * LANE_WIDTH + 1
|
|
32
|
+
let labelWidth = 0
|
|
33
|
+
for (const commit of diagram.commits) labelWidth = Math.max(labelWidth, diagramTextWidth(commitLabel(commit, heads)))
|
|
34
|
+
const forks = diagram.commits.map((commit) => isFork(commit, laneByBranch, commitById))
|
|
35
|
+
const height = diagram.commits.length + forks.filter(Boolean).length
|
|
36
|
+
const grid: GitGraphGrid = new DiagramCanvas(graphWidth + LABEL_GAP + labelWidth, height)
|
|
37
|
+
|
|
38
|
+
let row = 0
|
|
39
|
+
diagram.commits.forEach((commit, index) => {
|
|
40
|
+
if (forks[index]) {
|
|
41
|
+
drawTransitionRow(grid, spans, laneByBranch, commitById, commit, index, row)
|
|
42
|
+
row += 1
|
|
43
|
+
}
|
|
44
|
+
drawCommitRow(grid, diagram, spans, laneByBranch, commitById, commit, index, row)
|
|
45
|
+
grid.setText(graphWidth + LABEL_GAP, row, commitLabel(commit, heads), "label")
|
|
46
|
+
row += 1
|
|
47
|
+
})
|
|
48
|
+
return grid
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function drawTransitionRow(
|
|
52
|
+
grid: GitGraphGrid,
|
|
53
|
+
spans: Map<string, BranchSpan>,
|
|
54
|
+
laneByBranch: Map<string, number>,
|
|
55
|
+
commitById: Map<string, GitGraphCommit>,
|
|
56
|
+
commit: GitGraphCommit,
|
|
57
|
+
index: number,
|
|
58
|
+
y: number,
|
|
59
|
+
): void {
|
|
60
|
+
const cells = new Map<number, Connections>()
|
|
61
|
+
for (const [branch, span] of spans) {
|
|
62
|
+
if (span.first >= index || span.last < index) continue
|
|
63
|
+
const lane = laneByBranch.get(branch)!
|
|
64
|
+
connect(cells, lane * LANE_WIDTH, { up: true, down: true }, branchStyle(lane))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lane = laneByBranch.get(commit.branch)!
|
|
68
|
+
const firstParent = commit.parents[0] === undefined ? undefined : commitById.get(commit.parents[0])
|
|
69
|
+
if (firstParent && firstParent.branch !== commit.branch) {
|
|
70
|
+
const parentLane = laneByBranch.get(firstParent.branch)!
|
|
71
|
+
connectHorizontal(
|
|
72
|
+
cells,
|
|
73
|
+
parentLane,
|
|
74
|
+
lane,
|
|
75
|
+
{ sourceUp: true, sourceDown: true, targetDown: true },
|
|
76
|
+
branchStyle(lane),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
paintConnections(grid, cells, y)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function drawCommitRow(
|
|
83
|
+
grid: GitGraphGrid,
|
|
84
|
+
diagram: GitGraphDiagram,
|
|
85
|
+
spans: Map<string, BranchSpan>,
|
|
86
|
+
laneByBranch: Map<string, number>,
|
|
87
|
+
commitById: Map<string, GitGraphCommit>,
|
|
88
|
+
commit: GitGraphCommit,
|
|
89
|
+
index: number,
|
|
90
|
+
y: number,
|
|
91
|
+
): void {
|
|
92
|
+
const cells = new Map<number, Connections>()
|
|
93
|
+
for (const branch of diagram.branches) {
|
|
94
|
+
const span = spans.get(branch.name)
|
|
95
|
+
if (!span || span.first > index || (span.last <= index && branch.name !== commit.branch)) continue
|
|
96
|
+
const lane = laneByBranch.get(branch.name)!
|
|
97
|
+
connect(cells, lane * LANE_WIDTH, { up: index > 0, down: span.last > index }, branchStyle(lane))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lane = laneByBranch.get(commit.branch)!
|
|
101
|
+
const secondParent = commit.parents[1] === undefined ? undefined : commitById.get(commit.parents[1])
|
|
102
|
+
if (secondParent) {
|
|
103
|
+
const parentLane = laneByBranch.get(secondParent.branch)!
|
|
104
|
+
connectHorizontal(cells, lane, parentLane, { sourceUp: true, targetUp: true }, branchStyle(parentLane))
|
|
105
|
+
}
|
|
106
|
+
paintConnections(grid, cells, y)
|
|
107
|
+
grid.setCell(lane * LANE_WIDTH, y, commitGlyph(commit), commitStyle(commit))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function connectHorizontal(
|
|
111
|
+
cells: Map<number, Connections>,
|
|
112
|
+
sourceLane: number,
|
|
113
|
+
targetLane: number,
|
|
114
|
+
vertical: { sourceUp?: boolean; sourceDown?: boolean; targetUp?: boolean; targetDown?: boolean },
|
|
115
|
+
style: GitGraphCellStyle,
|
|
116
|
+
): void {
|
|
117
|
+
if (sourceLane === targetLane) return
|
|
118
|
+
const source = sourceLane * LANE_WIDTH
|
|
119
|
+
const target = targetLane * LANE_WIDTH
|
|
120
|
+
const direction = Math.sign(target - source)
|
|
121
|
+
connect(
|
|
122
|
+
cells,
|
|
123
|
+
source,
|
|
124
|
+
{ ...verticalAt(vertical.sourceUp, vertical.sourceDown), ...(direction > 0 ? { right: true } : { left: true }) },
|
|
125
|
+
style,
|
|
126
|
+
)
|
|
127
|
+
for (let x = source + direction; x !== target; x += direction) {
|
|
128
|
+
connect(cells, x, { left: true, right: true }, style)
|
|
129
|
+
}
|
|
130
|
+
connect(
|
|
131
|
+
cells,
|
|
132
|
+
target,
|
|
133
|
+
{ ...verticalAt(vertical.targetUp, vertical.targetDown), ...(direction > 0 ? { left: true } : { right: true }) },
|
|
134
|
+
style,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function verticalAt(up: boolean | undefined, down: boolean | undefined): Pick<Connections, "up" | "down"> {
|
|
139
|
+
return { ...(up ? { up: true } : {}), ...(down ? { down: true } : {}) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function connect(
|
|
143
|
+
cells: Map<number, Connections>,
|
|
144
|
+
x: number,
|
|
145
|
+
additions: Omit<Connections, "style">,
|
|
146
|
+
style: GitGraphCellStyle,
|
|
147
|
+
): void {
|
|
148
|
+
const current = cells.get(x)
|
|
149
|
+
cells.set(x, { ...current, ...additions, style: current?.style ?? style })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function paintConnections(grid: GitGraphGrid, cells: Map<number, Connections>, y: number): void {
|
|
153
|
+
for (const [x, connections] of cells) grid.setCell(x, y, connectionGlyph(connections), connections.style)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function connectionGlyph({ up, down, left, right }: Connections): string {
|
|
157
|
+
const mask = `${up ? 1 : 0}${down ? 1 : 0}${left ? 1 : 0}${right ? 1 : 0}`
|
|
158
|
+
const glyphs: Record<string, string> = {
|
|
159
|
+
"1100": "│",
|
|
160
|
+
"0011": "─",
|
|
161
|
+
"0101": "╭",
|
|
162
|
+
"0110": "╮",
|
|
163
|
+
"1001": "╰",
|
|
164
|
+
"1010": "╯",
|
|
165
|
+
"1101": "├",
|
|
166
|
+
"1110": "┤",
|
|
167
|
+
"0111": "┬",
|
|
168
|
+
"1011": "┴",
|
|
169
|
+
"1111": "┼",
|
|
170
|
+
"1000": "│",
|
|
171
|
+
"0100": "│",
|
|
172
|
+
"0010": "─",
|
|
173
|
+
"0001": "─",
|
|
174
|
+
}
|
|
175
|
+
return glyphs[mask] ?? " "
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function branchSpans(diagram: GitGraphDiagram, commitById: Map<string, GitGraphCommit>): Map<string, BranchSpan> {
|
|
179
|
+
const spans = new Map<string, BranchSpan>()
|
|
180
|
+
diagram.commits.forEach((commit, index) => {
|
|
181
|
+
const span = spans.get(commit.branch)
|
|
182
|
+
if (span) span.last = index
|
|
183
|
+
else spans.set(commit.branch, { first: index, last: index })
|
|
184
|
+
for (const parentId of commit.parents) {
|
|
185
|
+
const parent = commitById.get(parentId)
|
|
186
|
+
if (!parent || parent.branch === commit.branch) continue
|
|
187
|
+
const parentSpan = spans.get(parent.branch)
|
|
188
|
+
if (parentSpan) parentSpan.last = Math.max(parentSpan.last, index)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
return spans
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function branchHeads(diagram: GitGraphDiagram): Map<string, string[]> {
|
|
195
|
+
const heads = new Map<string, string[]>()
|
|
196
|
+
for (const branch of diagram.branches) {
|
|
197
|
+
if (branch.head === undefined) continue
|
|
198
|
+
const names = heads.get(branch.head) ?? []
|
|
199
|
+
names.push(branch.name)
|
|
200
|
+
heads.set(branch.head, names)
|
|
201
|
+
}
|
|
202
|
+
return heads
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isFork(
|
|
206
|
+
commit: GitGraphCommit,
|
|
207
|
+
laneByBranch: Map<string, number>,
|
|
208
|
+
commitById: Map<string, GitGraphCommit>,
|
|
209
|
+
): boolean {
|
|
210
|
+
const parent = commit.parents[0] === undefined ? undefined : commitById.get(commit.parents[0])
|
|
211
|
+
return parent !== undefined && laneByBranch.get(parent.branch) !== laneByBranch.get(commit.branch)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function commitGlyph(commit: GitGraphCommit): string {
|
|
215
|
+
if (commit.type === "REVERSE") return "⊗"
|
|
216
|
+
if (commit.type === "HIGHLIGHT") return "◆"
|
|
217
|
+
return commit.parents.length > 1 ? "◎" : "●"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function commitStyle(commit: GitGraphCommit): GitGraphCellStyle {
|
|
221
|
+
if (commit.type === "REVERSE") return "reverse"
|
|
222
|
+
if (commit.type === "HIGHLIGHT") return "highlight"
|
|
223
|
+
return commit.parents.length > 1 ? "merge" : "commit"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function commitLabel(commit: GitGraphCommit, heads: Map<string, string[]>): string {
|
|
227
|
+
const subject = commit.message ?? commit.id
|
|
228
|
+
const decorations = [...(heads.get(commit.id) ?? []), ...commit.tags].map((value) => `[${value}]`)
|
|
229
|
+
return decorations.length === 0 ? subject : `${subject} ${decorations.join(" ")}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function branchStyle(lane: number): GitGraphCellStyle {
|
|
233
|
+
return `branch${lane % 8}` as GitGraphCellStyle
|
|
234
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { firstMeaningfulMermaidLine, meaningfulNumberedMermaidLines, stripMermaidQuotes } from "../core/mermaid.js"
|
|
2
|
+
import { MermaidSyntaxError } from "../diagnostics.js"
|
|
3
|
+
import type { GitGraphBranch, GitGraphCommit, GitGraphCommitType, GitGraphDiagram, GitGraphDirection } from "./types.js"
|
|
4
|
+
|
|
5
|
+
const HEADER_RE = /^gitGraph(?:\s+(LR|TB|BT))?\s*:?$/i
|
|
6
|
+
const ACCESSIBILITY_RE = /^acc(?:Title|Descr)(?::|\s|$)/i
|
|
7
|
+
|
|
8
|
+
export function isMermaidGitGraphDiagram(content: string): boolean {
|
|
9
|
+
return HEADER_RE.test(firstMeaningfulMermaidLine(content) ?? "")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseMermaidGitGraphDiagram(content: string): GitGraphDiagram {
|
|
13
|
+
const firstLine = firstMeaningfulMermaidLine(content)
|
|
14
|
+
if (!HEADER_RE.test(firstLine ?? "")) throw syntaxError(1, firstLine ?? "", "GitGraph header is required")
|
|
15
|
+
const branches: GitGraphBranch[] = [{ name: "main", order: 0 }]
|
|
16
|
+
const commits: GitGraphCommit[] = []
|
|
17
|
+
const heads = new Map<string, string | undefined>([["main", undefined]])
|
|
18
|
+
const ids = new Set<string>()
|
|
19
|
+
let direction: GitGraphDirection = "LR"
|
|
20
|
+
let currentBranch = "main"
|
|
21
|
+
let generatedId = 1
|
|
22
|
+
let inAccessibilityDescription = false
|
|
23
|
+
let headerSeen = false
|
|
24
|
+
const nextId = (): string => {
|
|
25
|
+
while (ids.has(`commit-${generatedId}`)) generatedId++
|
|
26
|
+
return `commit-${generatedId++}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const source of meaningfulNumberedMermaidLines(content)) {
|
|
30
|
+
const line = stripComment(source.text)
|
|
31
|
+
if (inAccessibilityDescription) {
|
|
32
|
+
if (line === "}") inAccessibilityDescription = false
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (/^accDescr\s*\{$/i.test(line)) {
|
|
36
|
+
inAccessibilityDescription = true
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
if (!line || ACCESSIBILITY_RE.test(line) || /^title(?:\s|$)/i.test(line)) continue
|
|
40
|
+
|
|
41
|
+
const header = line.match(HEADER_RE)
|
|
42
|
+
if (header) {
|
|
43
|
+
if (headerSeen) throw syntaxError(source.lineNumber, line, "GitGraph header can only appear once")
|
|
44
|
+
headerSeen = true
|
|
45
|
+
direction = (header[1]?.toUpperCase() as GitGraphDirection | undefined) ?? "LR"
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [command = "", ...rest] = tokenize(line)
|
|
50
|
+
const operation = command.toLowerCase()
|
|
51
|
+
if (operation === "commit") {
|
|
52
|
+
const shorthandMessage = rest[0]?.match(/^(["']).*\1$/) ? stripMermaidQuotes(rest.shift()!) : undefined
|
|
53
|
+
const attributes = parseAttributes(rest, source.lineNumber, line, ["id", "msg", "tag", "type"])
|
|
54
|
+
const id = single(attributes, "id", source.lineNumber, line) ?? nextId()
|
|
55
|
+
if (!id) throw syntaxError(source.lineNumber, line, "GitGraph commit id cannot be empty")
|
|
56
|
+
if (ids.has(id)) throw syntaxError(source.lineNumber, line, `Duplicate commit id "${id}"`)
|
|
57
|
+
const type = parseCommitType(single(attributes, "type", source.lineNumber, line), source.lineNumber, line)
|
|
58
|
+
const parent = heads.get(currentBranch)
|
|
59
|
+
const message = single(attributes, "msg", source.lineNumber, line) ?? shorthandMessage
|
|
60
|
+
const commit: GitGraphCommit = {
|
|
61
|
+
id,
|
|
62
|
+
...(message === undefined ? {} : { message }),
|
|
63
|
+
tags: attributes.get("tag") ?? [],
|
|
64
|
+
type,
|
|
65
|
+
branch: currentBranch,
|
|
66
|
+
parents: parent === undefined ? [] : [parent],
|
|
67
|
+
}
|
|
68
|
+
commits.push(commit)
|
|
69
|
+
ids.add(id)
|
|
70
|
+
heads.set(currentBranch, id)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (operation === "branch") {
|
|
75
|
+
if (rest.length === 0) throw syntaxError(source.lineNumber, line, "GitGraph branch name cannot be empty")
|
|
76
|
+
const name = stripMermaidQuotes(rest[0]!)
|
|
77
|
+
if (!name) throw syntaxError(source.lineNumber, line, "GitGraph branch name cannot be empty")
|
|
78
|
+
if (heads.has(name)) throw syntaxError(source.lineNumber, line, `Duplicate branch "${name}"`)
|
|
79
|
+
const attributes = parseAttributes(rest.slice(1), source.lineNumber, line, ["order"])
|
|
80
|
+
const orderValue = single(attributes, "order", source.lineNumber, line)
|
|
81
|
+
const order = orderValue === undefined ? undefined : Number(orderValue)
|
|
82
|
+
if (order !== undefined && (!Number.isInteger(order) || order < 0)) {
|
|
83
|
+
throw syntaxError(source.lineNumber, line, "GitGraph branch order must be a non-negative integer")
|
|
84
|
+
}
|
|
85
|
+
branches.push({ name, ...(order === undefined ? {} : { order }) })
|
|
86
|
+
heads.set(name, heads.get(currentBranch))
|
|
87
|
+
currentBranch = name
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (operation === "checkout" || operation === "switch") {
|
|
92
|
+
if (rest.length !== 1) throw syntaxError(source.lineNumber, line, `GitGraph ${operation} requires one branch`)
|
|
93
|
+
const name = stripMermaidQuotes(rest[0]!)
|
|
94
|
+
if (!heads.has(name)) throw syntaxError(source.lineNumber, line, `Unknown branch "${name}"`)
|
|
95
|
+
currentBranch = name
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (operation === "merge") {
|
|
100
|
+
if (rest.length === 0) throw syntaxError(source.lineNumber, line, "GitGraph merge requires a branch")
|
|
101
|
+
const branch = stripMermaidQuotes(rest[0]!)
|
|
102
|
+
if (!heads.has(branch)) throw syntaxError(source.lineNumber, line, `Unknown branch "${branch}"`)
|
|
103
|
+
if (branch === currentBranch)
|
|
104
|
+
throw syntaxError(source.lineNumber, line, "GitGraph cannot merge a branch into itself")
|
|
105
|
+
const currentHead = heads.get(currentBranch)
|
|
106
|
+
const mergedHead = heads.get(branch)
|
|
107
|
+
if (currentHead === undefined)
|
|
108
|
+
throw syntaxError(source.lineNumber, line, `Branch "${currentBranch}" has no commits`)
|
|
109
|
+
if (mergedHead === undefined) throw syntaxError(source.lineNumber, line, `Branch "${branch}" has no commits`)
|
|
110
|
+
if (currentHead === mergedHead)
|
|
111
|
+
throw syntaxError(source.lineNumber, line, `Branches already share head "${mergedHead}"`)
|
|
112
|
+
const attributes = parseAttributes(rest.slice(1), source.lineNumber, line, ["id", "tag", "type"])
|
|
113
|
+
const id = single(attributes, "id", source.lineNumber, line) ?? nextId()
|
|
114
|
+
if (ids.has(id)) throw syntaxError(source.lineNumber, line, `Duplicate commit id "${id}"`)
|
|
115
|
+
const commit: GitGraphCommit = {
|
|
116
|
+
id,
|
|
117
|
+
tags: attributes.get("tag") ?? [],
|
|
118
|
+
type: parseCommitType(single(attributes, "type", source.lineNumber, line), source.lineNumber, line),
|
|
119
|
+
branch: currentBranch,
|
|
120
|
+
parents: [currentHead, mergedHead],
|
|
121
|
+
}
|
|
122
|
+
commits.push(commit)
|
|
123
|
+
ids.add(id)
|
|
124
|
+
heads.set(currentBranch, id)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (operation === "cherry-pick") {
|
|
129
|
+
const attributes = parseAttributes(rest, source.lineNumber, line, ["id"])
|
|
130
|
+
const sourceId = single(attributes, "id", source.lineNumber, line)
|
|
131
|
+
const picked = commits.find((commit) => commit.id === sourceId)
|
|
132
|
+
if (!picked) throw syntaxError(source.lineNumber, line, `Unknown cherry-pick commit "${sourceId ?? ""}"`)
|
|
133
|
+
const parent = heads.get(currentBranch)
|
|
134
|
+
if (!parent) throw syntaxError(source.lineNumber, line, `Branch "${currentBranch}" has no commits`)
|
|
135
|
+
const id = nextId()
|
|
136
|
+
commits.push({ id, message: picked.message ?? `cherry-pick ${picked.id}`, tags: [], type: "NORMAL",
|
|
137
|
+
branch: currentBranch, parents: [parent] })
|
|
138
|
+
ids.add(id)
|
|
139
|
+
heads.set(currentBranch, id)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
throw syntaxError(source.lineNumber, line)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const resolvedBranches = branches.map((branch) => {
|
|
146
|
+
const head = heads.get(branch.name)
|
|
147
|
+
return { ...branch, ...(head === undefined ? {} : { head }) }
|
|
148
|
+
})
|
|
149
|
+
return { direction, branches: orderBranches(resolvedBranches), commits }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function tokenize(line: string): string[] {
|
|
153
|
+
const tokens: string[] = []
|
|
154
|
+
let token = ""
|
|
155
|
+
let quote: '"' | "'" | undefined
|
|
156
|
+
for (const char of line) {
|
|
157
|
+
if ((char === '"' || char === "'") && (quote === undefined || quote === char)) {
|
|
158
|
+
quote = quote === char ? undefined : char
|
|
159
|
+
token += char
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (/\s/.test(char) && quote === undefined) {
|
|
163
|
+
if (token) tokens.push(token)
|
|
164
|
+
token = ""
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
token += char
|
|
168
|
+
}
|
|
169
|
+
if (quote !== undefined) return [line]
|
|
170
|
+
if (token) tokens.push(token)
|
|
171
|
+
return tokens
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseAttributes(
|
|
175
|
+
tokens: string[],
|
|
176
|
+
lineNumber: number,
|
|
177
|
+
line: string,
|
|
178
|
+
allowed: readonly string[],
|
|
179
|
+
): Map<string, string[]> {
|
|
180
|
+
const result = new Map<string, string[]>()
|
|
181
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
182
|
+
const keyToken = tokens[index]!
|
|
183
|
+
const separator = keyToken.indexOf(":")
|
|
184
|
+
const key = (separator < 0 ? keyToken : keyToken.slice(0, separator)).toLowerCase()
|
|
185
|
+
if (!allowed.includes(key)) throw syntaxError(lineNumber, line, `Unsupported GitGraph attribute "${key}"`)
|
|
186
|
+
const inline = separator < 0 ? "" : keyToken.slice(separator + 1)
|
|
187
|
+
const valueToken = inline || tokens[++index]
|
|
188
|
+
if (valueToken === undefined) throw syntaxError(lineNumber, line, `GitGraph attribute "${key}" requires a value`)
|
|
189
|
+
const values = result.get(key) ?? []
|
|
190
|
+
values.push(stripMermaidQuotes(valueToken))
|
|
191
|
+
result.set(key, values)
|
|
192
|
+
}
|
|
193
|
+
return result
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function single(attributes: Map<string, string[]>, key: string, lineNumber: number, line: string): string | undefined {
|
|
197
|
+
const values = attributes.get(key)
|
|
198
|
+
if (values && values.length > 1) throw syntaxError(lineNumber, line, `GitGraph attribute "${key}" cannot repeat`)
|
|
199
|
+
return values?.[0]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseCommitType(value: string | undefined, lineNumber: number, line: string): GitGraphCommitType {
|
|
203
|
+
if (value === undefined) return "NORMAL"
|
|
204
|
+
const type = value.toUpperCase()
|
|
205
|
+
if (type === "NORMAL" || type === "REVERSE" || type === "HIGHLIGHT") return type
|
|
206
|
+
throw syntaxError(lineNumber, line, `Unknown GitGraph commit type "${value}"`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function orderBranches(branches: GitGraphBranch[]): GitGraphBranch[] {
|
|
210
|
+
const main = branches[0]!
|
|
211
|
+
const rest = branches.slice(1).map((branch, index) => ({ branch, index }))
|
|
212
|
+
const unordered = rest.filter(({ branch }) => branch.order === undefined)
|
|
213
|
+
const ordered = rest
|
|
214
|
+
.filter(({ branch }) => branch.order !== undefined)
|
|
215
|
+
.sort((left, right) => left.branch.order! - right.branch.order! || left.index - right.index)
|
|
216
|
+
return [main, ...unordered.map(({ branch }) => branch), ...ordered.map(({ branch }) => branch)]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function stripComment(value: string): string {
|
|
220
|
+
let quote: '"' | "'" | undefined
|
|
221
|
+
for (let index = 0; index < value.length - 1; index += 1) {
|
|
222
|
+
const char = value[index]
|
|
223
|
+
if ((char === '"' || char === "'") && (quote === undefined || quote === char)) {
|
|
224
|
+
quote = quote === char ? undefined : char
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (quote === undefined && char === "%" && value[index + 1] === "%") return value.slice(0, index).trim()
|
|
228
|
+
}
|
|
229
|
+
return value.trim()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function syntaxError(lineNumber: number, sourceLine: string, reason?: string): MermaidSyntaxError {
|
|
233
|
+
return new MermaidSyntaxError("gitGraph", lineNumber, sourceLine, reason)
|
|
234
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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 type { GitGraphStyleColors } from "./style.js"
|
|
5
|
+
import type { GitGraphCellStyle } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export type GitGraphGrid = DiagramCanvas<GitGraphCellStyle>
|
|
8
|
+
|
|
9
|
+
export function renderGitGraphGridText(grid: GitGraphGrid): string {
|
|
10
|
+
return grid.toString({ trimBottom: true })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function renderGitGraphGridStyledText(grid: GitGraphGrid, colors: GitGraphStyleColors): StyledText {
|
|
14
|
+
return renderDiagramGridStyledText(grid, (run) => (run.style ? colors[run.style] : undefined), undefined, {
|
|
15
|
+
trimBottom: true,
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { RGBA } from "@opentui/core"
|
|
2
|
+
import { rgba, type DiagramRgb } from "../core/color/style.js"
|
|
3
|
+
import type { GitGraphCellStyle } from "./types.js"
|
|
4
|
+
|
|
5
|
+
const BRANCH_RGB = [
|
|
6
|
+
[134, 225, 200],
|
|
7
|
+
[230, 177, 126],
|
|
8
|
+
[154, 184, 169],
|
|
9
|
+
[198, 160, 246],
|
|
10
|
+
[126, 189, 230],
|
|
11
|
+
[225, 134, 166],
|
|
12
|
+
[190, 210, 120],
|
|
13
|
+
[180, 180, 210],
|
|
14
|
+
] as const satisfies readonly DiagramRgb[]
|
|
15
|
+
|
|
16
|
+
export type GitGraphStyleColors = Required<Record<GitGraphCellStyle, RGBA>>
|
|
17
|
+
|
|
18
|
+
export function resolveGitGraphStyleColors(
|
|
19
|
+
colors: Partial<Record<"primary" | "secondary" | "muted" | "warning" | "text", RGBA | undefined>> = {},
|
|
20
|
+
): GitGraphStyleColors {
|
|
21
|
+
const rail = colors.muted ?? rgba([111, 138, 126])
|
|
22
|
+
return {
|
|
23
|
+
branch0: rail,
|
|
24
|
+
branch1: rail,
|
|
25
|
+
branch2: rail,
|
|
26
|
+
branch3: rail,
|
|
27
|
+
branch4: rail,
|
|
28
|
+
branch5: rail,
|
|
29
|
+
branch6: rail,
|
|
30
|
+
branch7: rail,
|
|
31
|
+
commit: colors.primary ?? rgba(BRANCH_RGB[0]),
|
|
32
|
+
merge: colors.secondary ?? rgba(BRANCH_RGB[2]),
|
|
33
|
+
highlight: colors.warning ?? rgba(BRANCH_RGB[1]),
|
|
34
|
+
reverse: colors.warning ?? rgba(BRANCH_RGB[5]),
|
|
35
|
+
label: colors.text ?? rgba([228, 239, 232]),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type GitGraphDirection = "LR" | "TB" | "BT"
|
|
2
|
+
export type GitGraphCommitType = "NORMAL" | "REVERSE" | "HIGHLIGHT"
|
|
3
|
+
|
|
4
|
+
export interface GitGraphBranch {
|
|
5
|
+
name: string
|
|
6
|
+
order?: number
|
|
7
|
+
head?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GitGraphCommit {
|
|
11
|
+
id: string
|
|
12
|
+
message?: string
|
|
13
|
+
tags: string[]
|
|
14
|
+
type: GitGraphCommitType
|
|
15
|
+
branch: string
|
|
16
|
+
parents: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GitGraphDiagram {
|
|
20
|
+
direction: GitGraphDirection
|
|
21
|
+
branches: GitGraphBranch[]
|
|
22
|
+
commits: GitGraphCommit[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GitGraphDiagramRenderOptions {
|
|
26
|
+
/** Parsed for Mermaid compatibility. Git graphs always use a vertical terminal layout. */
|
|
27
|
+
direction?: GitGraphDirection
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type GitGraphCellStyle =
|
|
31
|
+
| `branch${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7}`
|
|
32
|
+
| "commit"
|
|
33
|
+
| "merge"
|
|
34
|
+
| "highlight"
|
|
35
|
+
| "reverse"
|
|
36
|
+
| "label"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RGBA } from "@opentui/core"
|
|
2
|
+
import type { Theme } from "../theme/types"
|
|
3
|
+
import { blendColor } from "./core/color/style.js"
|
|
4
|
+
|
|
5
|
+
export interface OpenCodeDiagramPaletteInput {
|
|
6
|
+
readonly text: RGBA
|
|
7
|
+
readonly subdued: RGBA
|
|
8
|
+
readonly info: RGBA
|
|
9
|
+
readonly success: RGBA
|
|
10
|
+
readonly warning: RGBA
|
|
11
|
+
readonly background: RGBA
|
|
12
|
+
readonly accent: {
|
|
13
|
+
readonly soft: RGBA
|
|
14
|
+
readonly clear: RGBA
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type OpenCodeDiagramPalette = ReturnType<typeof createOpenCodeDiagramPalette>
|
|
19
|
+
|
|
20
|
+
export function createOpenCodeDiagramPalette(input: OpenCodeDiagramPaletteInput) {
|
|
21
|
+
const secondary = blendColor(input.text, input.subdued, 0.5)
|
|
22
|
+
const muted = blendColor(input.text, input.subdued, 0.7)
|
|
23
|
+
return {
|
|
24
|
+
text: input.text,
|
|
25
|
+
primary: input.text,
|
|
26
|
+
secondary,
|
|
27
|
+
muted,
|
|
28
|
+
warning: input.info,
|
|
29
|
+
background: input.background,
|
|
30
|
+
request: input.success,
|
|
31
|
+
response: input.warning,
|
|
32
|
+
note: input.text,
|
|
33
|
+
noteBackground: blendColor(input.background, input.subdued, 0.25),
|
|
34
|
+
boxText: input.text,
|
|
35
|
+
boxBorder: muted,
|
|
36
|
+
line: input.subdued,
|
|
37
|
+
labelBackground: RGBA.fromValues(input.text.r, input.text.g, input.text.b, 0.08),
|
|
38
|
+
group: input.accent.soft,
|
|
39
|
+
groupText: input.accent.soft,
|
|
40
|
+
marker: secondary,
|
|
41
|
+
noteBorder: input.accent.soft,
|
|
42
|
+
noteText: input.accent.clear,
|
|
43
|
+
noteConnector: input.accent.soft,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveOpenCodeDiagramPalette(theme: Theme) {
|
|
48
|
+
return createOpenCodeDiagramPalette({
|
|
49
|
+
text: theme.text.base,
|
|
50
|
+
subdued: theme.text.muted,
|
|
51
|
+
info: theme.text.feedback.info,
|
|
52
|
+
success: theme.text.feedback.success,
|
|
53
|
+
warning: theme.text.feedback.warning,
|
|
54
|
+
background: theme.background.base,
|
|
55
|
+
accent: {
|
|
56
|
+
soft: theme.accent,
|
|
57
|
+
clear: theme.accent,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { drawSequenceDiagramGrid } from "./drawing.js"
|
|
2
|
+
import { parseMermaidSequenceDiagram } from "./parser.js"
|
|
3
|
+
import { renderSequenceGridText } from "./render-grid.js"
|
|
4
|
+
import type { SequenceDiagramRenderOptions } from "./types.js"
|
|
5
|
+
|
|
6
|
+
export function renderSequenceDiagram(content: string, options: SequenceDiagramRenderOptions = {}): string {
|
|
7
|
+
return renderSequenceGridText(drawSequenceDiagramGrid(parseMermaidSequenceDiagram(content), options))
|
|
8
|
+
}
|