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,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
+ }