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,79 @@
1
+ import { RGBA, type StyledText } from "@opentui/core"
2
+ import type { DiagramCanvas } from "../core/canvas.js"
3
+ import { renderDiagramGridStyledTextByStyle } from "../core/render-grid.js"
4
+ import {
5
+ createColorRampTheme,
6
+ DIAGRAM_FADE_STEPS,
7
+ numberedStyleKeys,
8
+ rgba,
9
+ type DiagramFadeStep,
10
+ type DiagramRgb,
11
+ } from "../core/color/style.js"
12
+
13
+ export type FlowchartBaseCellStyle =
14
+ | "node"
15
+ | "nodeBorder"
16
+ | "database"
17
+ | "databaseBorder"
18
+ | "edge"
19
+ | "label"
20
+ | "group"
21
+ | "groupLabel"
22
+ export type FlowchartNodeEdgeFadeStyle = `nodeEdgeFade${DiagramFadeStep}`
23
+ export type FlowchartDatabaseEdgeFadeStyle = `databaseEdgeFade${DiagramFadeStep}`
24
+ export type FlowchartEdgeFadeStyle = FlowchartNodeEdgeFadeStyle | FlowchartDatabaseEdgeFadeStyle
25
+ export type FlowchartCellStyle = FlowchartBaseCellStyle | FlowchartEdgeFadeStyle
26
+ export interface FlowchartCellMetadata {
27
+ attributes?: number
28
+ }
29
+ export type FlowchartGrid = DiagramCanvas<FlowchartCellStyle, FlowchartCellMetadata>
30
+ export type FlowchartStyleColors = Required<Record<FlowchartCellStyle, RGBA>>
31
+ export type FlowchartStyleBackgroundColors = Partial<Record<FlowchartCellStyle, RGBA>>
32
+ export const DEFAULT_THEME_RGB = {
33
+ node: [228, 239, 232],
34
+ nodeBorder: [141, 163, 151],
35
+ database: [228, 239, 232],
36
+ databaseBorder: [141, 163, 151],
37
+ edge: [134, 225, 200],
38
+ label: [134, 225, 200],
39
+ group: [76, 99, 89],
40
+ groupLabel: [76, 99, 89],
41
+ } as const satisfies Record<FlowchartBaseCellStyle, DiagramRgb>
42
+
43
+ export const NODE_EDGE_FADE_STYLES = numberedStyleKeys("nodeEdgeFade", DIAGRAM_FADE_STEPS)
44
+ export const DATABASE_EDGE_FADE_STYLES = numberedStyleKeys("databaseEdgeFade", DIAGRAM_FADE_STEPS)
45
+
46
+ export function resolveFlowchartStyleColors(
47
+ colors: Partial<Record<FlowchartCellStyle, RGBA | undefined>> = {},
48
+ ): FlowchartStyleColors {
49
+ const node = colors.node ?? rgba(DEFAULT_THEME_RGB.node)
50
+ const nodeBorder = colors.nodeBorder ?? rgba(DEFAULT_THEME_RGB.nodeBorder)
51
+ const database = colors.database ?? rgba(DEFAULT_THEME_RGB.database)
52
+ const databaseBorder = colors.databaseBorder ?? rgba(DEFAULT_THEME_RGB.databaseBorder)
53
+ const edge = colors.edge ?? rgba(DEFAULT_THEME_RGB.edge)
54
+ return {
55
+ node,
56
+ nodeBorder,
57
+ database,
58
+ databaseBorder,
59
+ edge,
60
+ label: colors.label ?? rgba(DEFAULT_THEME_RGB.label),
61
+ group: colors.group ?? rgba(DEFAULT_THEME_RGB.group),
62
+ groupLabel: colors.groupLabel ?? colors.group ?? rgba(DEFAULT_THEME_RGB.groupLabel),
63
+ ...createColorRampTheme(NODE_EDGE_FADE_STYLES, nodeBorder, edge),
64
+ ...createColorRampTheme(DATABASE_EDGE_FADE_STYLES, databaseBorder, edge),
65
+ }
66
+ }
67
+
68
+ export function renderGridStyledText(
69
+ grid: FlowchartGrid,
70
+ colors: FlowchartStyleColors,
71
+ backgrounds?: FlowchartStyleBackgroundColors,
72
+ ): StyledText {
73
+ return renderDiagramGridStyledTextByStyle(grid, colors, backgrounds, {
74
+ key: (cell) => [cell.style, cell.attributes],
75
+ attributes: (run) => run.cell.attributes,
76
+ trimTop: true,
77
+ trimBottom: true,
78
+ })
79
+ }
@@ -0,0 +1,63 @@
1
+ import type { DiagramAxis, DiagramBounds, DiagramDirection, DiagramPoint } from "../core/geometry.js"
2
+
3
+ export type FlowchartDirection = "TB" | "TD" | "BT" | "LR" | "RL"
4
+ export type FlowchartNodeShape = "box" | "rounded" | "circle" | "database" | "decision" | "hexagon" | "parallelogram" | "subroutine"
5
+ export type FlowchartEdgeStyle = "thick" | "dashed"
6
+
7
+ export interface FlowchartNode {
8
+ id: string
9
+ label: string
10
+ shape: FlowchartNodeShape
11
+ }
12
+
13
+ export interface FlowchartEdge {
14
+ from: string
15
+ to: string
16
+ label: string
17
+ style?: FlowchartEdgeStyle
18
+ arrowhead?: false
19
+ sourceArrowhead?: true
20
+ orderOnly?: boolean
21
+ }
22
+
23
+ export interface FlowchartSubgraph {
24
+ id: string
25
+ label: string
26
+ nodeIds: string[]
27
+ parentId?: string
28
+ direction?: FlowchartDirection
29
+ }
30
+
31
+ export interface FlowchartDiagram {
32
+ direction: FlowchartDirection
33
+ nodes: FlowchartNode[]
34
+ edges: FlowchartEdge[]
35
+ subgraphs?: FlowchartSubgraph[]
36
+ }
37
+
38
+ export interface FlowchartNodeSize {
39
+ width: number
40
+ height: number
41
+ lines: string[]
42
+ }
43
+
44
+ export interface FlowchartNodeBounds extends FlowchartNodeSize, DiagramBounds {
45
+ id: string
46
+ }
47
+
48
+ export interface FlowchartSubgraphBounds extends DiagramBounds {
49
+ id: string
50
+ label: string
51
+ labelSide: "top" | "bottom"
52
+ }
53
+
54
+ export type FlowchartPoint = DiagramPoint
55
+
56
+ export interface FlowchartEdgeRoute {
57
+ edge: FlowchartEdge
58
+ points: FlowchartPoint[]
59
+ labelAxis?: DiagramAxis
60
+ labelPoint?: FlowchartPoint
61
+ }
62
+
63
+ export type FlowchartEdgeDirection = DiagramDirection
@@ -0,0 +1,8 @@
1
+ import { drawGanttDiagramGrid } from "./drawing.js"
2
+ import { parseMermaidGanttDiagram } from "./parser.js"
3
+ import { renderGanttGridText } from "./render-grid.js"
4
+ import type { GanttDiagramRenderOptions } from "./types.js"
5
+
6
+ export function renderGanttDiagram(content: string, options: GanttDiagramRenderOptions = {}): string {
7
+ return renderGanttGridText(drawGanttDiagramGrid(parseMermaidGanttDiagram(content), options))
8
+ }
@@ -0,0 +1,193 @@
1
+ import { DiagramCanvas } from "../core/canvas.js"
2
+ import { diagramTextWidth } from "../core/text.js"
3
+ import type { GanttGrid } from "./render-grid.js"
4
+ import type {
5
+ GanttCellStyle,
6
+ GanttDiagram,
7
+ GanttDiagramRenderOptions,
8
+ GanttLabelLayout,
9
+ GanttLineStyle,
10
+ GanttRenderStyle,
11
+ GanttTask,
12
+ } from "./types.js"
13
+
14
+ const LABEL_GAP = 2
15
+ const MIN_CHART_WIDTH = 24
16
+ const MAX_CHART_WIDTH = 64
17
+
18
+ export function drawGanttDiagramGrid(diagram: GanttDiagram, options: GanttDiagramRenderOptions = {}): GanttGrid {
19
+ if (diagram.entries.length === 0) return new DiagramCanvas(0, 0)
20
+ const labels = diagram.entries.map((_, index) => entryLabel(diagram, index, options.labels ?? "left"))
21
+ const labelWidth = Math.max(...labels.map(diagramTextWidth))
22
+ const entryRows: number[] = []
23
+ let bodyHeight = 0
24
+ diagram.entries.forEach((entry, index) => {
25
+ if ((options.sections ?? "spaced") === "spaced" && entry.type === "section" && index > 0) bodyHeight += 1
26
+ entryRows.push(bodyHeight)
27
+ bodyHeight += 1
28
+ })
29
+ if (diagram.tasks.length === 0) {
30
+ const grid: GanttGrid = new DiagramCanvas(labelWidth, bodyHeight)
31
+ diagram.entries.forEach((entry, index) => {
32
+ if (entry.type === "section") grid.setText(0, entryRows[index]!, entry.section.label, "section")
33
+ })
34
+ return grid
35
+ }
36
+ const available = (options.layoutMaxWidth ?? 120) - labelWidth - LABEL_GAP
37
+ const chartWidth = Math.max(MIN_CHART_WIDTH, Math.min(MAX_CHART_WIDTH, available))
38
+ const starts = diagram.tasks.map((task) => task.start)
39
+ const ends = diagram.tasks.map((task) => task.end)
40
+ const minimum = Math.min(...starts)
41
+ const maximum = Math.max(...ends)
42
+ const span = Math.max(1, maximum - minimum)
43
+ const titleHeight = diagram.title ? 2 : 0
44
+ const axisHeight = 2
45
+ const grid: GanttGrid = new DiagramCanvas(labelWidth + LABEL_GAP + chartWidth, titleHeight + axisHeight + bodyHeight)
46
+ const chartX = labelWidth + LABEL_GAP
47
+
48
+ if (diagram.title)
49
+ grid.setText(
50
+ Math.max(0, chartX + Math.floor((chartWidth - diagramTextWidth(diagram.title)) / 2)),
51
+ 0,
52
+ diagram.title,
53
+ "title",
54
+ )
55
+ drawAxis(grid, chartX, titleHeight, chartWidth, minimum, span, diagram.axisFormat, diagram.tickInterval)
56
+
57
+ diagram.entries.forEach((entry, index) => {
58
+ const y = titleHeight + axisHeight + entryRows[index]!
59
+ if (entry.type === "section") {
60
+ grid.setText(0, y, labels[index]!, "section")
61
+ return
62
+ }
63
+ const label = labels[index]!
64
+ const labelX = options.labels === "right" ? labelWidth - diagramTextWidth(label) : 0
65
+ grid.setText(labelX, y, label, entry.task.state)
66
+ drawTask(grid, entry.task, chartX, y, chartWidth, minimum, span, options)
67
+ })
68
+ return grid
69
+ }
70
+
71
+ function drawAxis(
72
+ grid: GanttGrid,
73
+ x: number,
74
+ y: number,
75
+ width: number,
76
+ minimum: number,
77
+ span: number,
78
+ format: string,
79
+ interval?: number,
80
+ ): void {
81
+ for (let offset = 0; offset < width; offset++) grid.setCell(x + offset, y + 1, "─", "axis")
82
+ const step = interval ? interval * Math.max(1, Math.ceil(span / (interval * 12))) : tickStep(span)
83
+ const ticks: number[] = []
84
+ for (let value = Math.ceil(minimum / step) * step; value <= minimum + span; value += step) ticks.push(value)
85
+ if (ticks.length === 0) ticks.push(minimum, minimum + span)
86
+ for (const value of ticks) {
87
+ const offset = Math.round(((value - minimum) / span) * (width - 1))
88
+ const label = formatTime(value, format)
89
+ const labelX = Math.max(
90
+ x,
91
+ Math.min(x + width - diagramTextWidth(label), x + offset - Math.floor(diagramTextWidth(label) / 2)),
92
+ )
93
+ grid.setText(labelX, y, label, "axis")
94
+ grid.setCell(x + offset, y + 1, "┬", "axis")
95
+ }
96
+ }
97
+
98
+ function tickStep(span: number): number {
99
+ const steps = [
100
+ 1_000, 5_000, 10_000, 30_000, 60_000, 300_000, 900_000, 3_600_000, 21_600_000, 43_200_000, 86_400_000, 172_800_000,
101
+ 604_800_000, 2_592_000_000, 31_536_000_000,
102
+ ]
103
+ return steps.find((step) => step >= span / 4) ?? steps.at(-1)!
104
+ }
105
+
106
+ function drawTask(
107
+ grid: GanttGrid,
108
+ task: GanttTask,
109
+ x: number,
110
+ y: number,
111
+ width: number,
112
+ minimum: number,
113
+ span: number,
114
+ options: GanttDiagramRenderOptions,
115
+ ): void {
116
+ const style: GanttRenderStyle = options.style ?? "track"
117
+ const line = lineGlyph(options.line ?? "thin")
118
+ const start = Math.round(((task.start - minimum) / span) * (width - 1))
119
+ const end = Math.round(((task.end - minimum) / span) * (width - 1))
120
+ if (task.state === "milestone" || start === end) {
121
+ grid.setCell(x + start, y, "◆", task.state)
122
+ return
123
+ }
124
+ if (style === "track") {
125
+ for (let offset = 0; offset < width; offset++) {
126
+ grid.setCell(x + offset, y, (options.track ?? "line") === "line" ? line : "·", trackCellStyle(options))
127
+ }
128
+ }
129
+ const glyph = style === "block" ? "█" : line
130
+ for (let offset = start; offset <= end; offset++) grid.setCell(x + offset, y, glyph, task.state)
131
+ if (style === "block") return
132
+ if (style === "capsule" || style === "track") {
133
+ if (style === "track") {
134
+ if (options.endpoints === "points") {
135
+ grid.setCell(x + start, y, "●", trackCellStyle(options))
136
+ grid.setCell(x + end, y, "●", trackCellStyle(options))
137
+ }
138
+ return
139
+ }
140
+ const caps = capGlyphs(options.line ?? "thin")
141
+ grid.setCell(x + start, y, caps.start, task.state)
142
+ grid.setCell(x + end, y, caps.end, task.state)
143
+ return
144
+ }
145
+ if (style === "points") {
146
+ grid.setCell(x + start, y, "●", task.state)
147
+ grid.setCell(x + end, y, "●", task.state)
148
+ return
149
+ }
150
+ grid.setCell(x + start, y, "┣", task.state)
151
+ grid.setCell(x + end, y, "┫", task.state)
152
+ }
153
+
154
+ function entryLabel(diagram: GanttDiagram, index: number, layout: GanttLabelLayout): string {
155
+ const entry = diagram.entries[index]!
156
+ if (entry.type === "section" || layout !== "tree") {
157
+ return entry.type === "section" ? entry.section.label : entry.task.label
158
+ }
159
+ const last = diagram.entries[index + 1]?.type !== "task"
160
+ return ` ${last ? "└" : "├"}─ ${entry.task.label}`
161
+ }
162
+
163
+ function lineGlyph(line: GanttLineStyle): string {
164
+ if (line === "thin") return "─"
165
+ if (line === "double") return "═"
166
+ if (line === "dashed") return "╌"
167
+ return "━"
168
+ }
169
+
170
+ function capGlyphs(line: GanttLineStyle): { start: string; end: string } {
171
+ if (line === "thin" || line === "dashed") return { start: "╶", end: "╴" }
172
+ if (line === "double") return { start: "╞", end: "╡" }
173
+ return { start: "╺", end: "╸" }
174
+ }
175
+
176
+ function trackCellStyle(options: GanttDiagramRenderOptions): "trackMedium" | "trackDim" | "trackFaint" {
177
+ if (options.trackTone === "medium") return "trackMedium"
178
+ if (options.trackTone === "dim") return "trackDim"
179
+ return "trackFaint"
180
+ }
181
+
182
+ function formatTime(value: number, format: string): string {
183
+ const date = new Date(value)
184
+ const parts: Record<string, string> = {
185
+ "%Y": String(date.getUTCFullYear()),
186
+ "%m": String(date.getUTCMonth() + 1).padStart(2, "0"),
187
+ "%d": String(date.getUTCDate()).padStart(2, "0"),
188
+ "%H": String(date.getUTCHours()).padStart(2, "0"),
189
+ "%M": String(date.getUTCMinutes()).padStart(2, "0"),
190
+ "%S": String(date.getUTCSeconds()).padStart(2, "0"),
191
+ }
192
+ return Object.entries(parts).reduce((result, [token, replacement]) => result.replaceAll(token, replacement), format)
193
+ }
@@ -0,0 +1,198 @@
1
+ import { firstMeaningfulMermaidLine, meaningfulNumberedMermaidLines, stripMermaidQuotes } from "../core/mermaid.js"
2
+ import { MermaidSyntaxError } from "../diagnostics.js"
3
+ import type { GanttDiagram, GanttEntry, GanttSection, GanttTask, GanttTaskState } from "./types.js"
4
+
5
+ const HEADER_RE = /^gantt$/i
6
+ const DIRECTIVE_RE = /^(title|dateFormat|axisFormat|tickInterval|excludes|todayMarker)\s+(.+)$/i
7
+ const SECTION_RE = /^section(?:\s+(.+))?$/i
8
+ const ACCESSIBILITY_RE = /^acc(?:Title|Descr)(?::|\s|$)/i
9
+ const TASK_STATES = new Set(["active", "done", "crit", "milestone", "vert"])
10
+
11
+ export function isMermaidGanttDiagram(content: string): boolean {
12
+ return HEADER_RE.test(firstMeaningfulMermaidLine(content) ?? "")
13
+ }
14
+
15
+ export function parseMermaidGanttDiagram(content: string): GanttDiagram {
16
+ const tasks: GanttTask[] = []
17
+ const entries: GanttEntry[] = []
18
+ const tasksById = new Map<string, GanttTask>()
19
+ let title: string | undefined
20
+ let dateFormat = "YYYY-MM-DD"
21
+ let axisFormat = "%Y-%m-%d"
22
+ let explicitAxisFormat = false
23
+ let tickInterval: number | undefined
24
+ let section: GanttSection | undefined
25
+ let headerSeen = false
26
+ let inAccessibilityDescription = false
27
+
28
+ for (const source of meaningfulNumberedMermaidLines(content)) {
29
+ const line = stripComment(source.text)
30
+ if (inAccessibilityDescription) {
31
+ if (line === "}") inAccessibilityDescription = false
32
+ continue
33
+ }
34
+ if (/^accDescr\s*\{$/i.test(line)) {
35
+ inAccessibilityDescription = true
36
+ continue
37
+ }
38
+ if (ACCESSIBILITY_RE.test(line)) continue
39
+ if (!line) continue
40
+ if (HEADER_RE.test(line)) {
41
+ if (headerSeen) throw syntaxError(source.lineNumber, line, "Gantt header can only appear once")
42
+ headerSeen = true
43
+ continue
44
+ }
45
+ if (!headerSeen) throw syntaxError(source.lineNumber, line, "Gantt header is required")
46
+
47
+ const sectionMatch = line.match(SECTION_RE)
48
+ if (sectionMatch) {
49
+ if (!sectionMatch[1]) throw syntaxError(source.lineNumber, line, "Gantt section cannot be empty")
50
+ section = { label: stripMermaidQuotes(sectionMatch[1]) }
51
+ entries.push({ type: "section", section })
52
+ continue
53
+ }
54
+
55
+ const directive = line.match(DIRECTIVE_RE)
56
+ if (directive) {
57
+ const name = directive[1]!.toLowerCase()
58
+ const value = directive[2]!.trim()
59
+ if (name === "title") title = stripMermaidQuotes(value)
60
+ if (name === "dateformat") {
61
+ dateFormat = value
62
+ if (!explicitAxisFormat && (value === "HH:mm" || value === "HH:mm:ss")) {
63
+ axisFormat = value === "HH:mm" ? "%H:%M" : "%H:%M:%S"
64
+ }
65
+ }
66
+ if (name === "axisformat") {
67
+ axisFormat = value
68
+ explicitAxisFormat = true
69
+ }
70
+ if (name === "tickinterval") {
71
+ const duration = value.match(/^(\d+)\s*(ms|s|m|h|d|w|seconds?|minutes?|hours?|days?|weeks?)$/i)
72
+ const units: Record<string, number> = {
73
+ ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
74
+ second: 1_000, minute: 60_000, hour: 3_600_000, day: 86_400_000, week: 604_800_000,
75
+ }
76
+ if (!duration || Number(duration[1]) <= 0) throw syntaxError(source.lineNumber, line, `Unsupported tick interval "${value}"`)
77
+ const unit = duration[2]!.toLowerCase()
78
+ tickInterval = Number(duration[1]) * (units[unit] ?? units[unit.slice(0, -1)]!)!
79
+ if (!Number.isSafeInteger(tickInterval)) throw syntaxError(source.lineNumber, line, `Unsupported tick interval "${value}"`)
80
+ }
81
+ if (!["title", "dateformat", "axisformat", "tickinterval"].includes(name)) {
82
+ throw syntaxError(source.lineNumber, line, `${directive[1]} is not supported`)
83
+ }
84
+ continue
85
+ }
86
+
87
+ const separator = line.indexOf(":")
88
+ if (separator < 1) throw syntaxError(source.lineNumber, line)
89
+ const label = stripMermaidQuotes(line.slice(0, separator))
90
+ const fields = line
91
+ .slice(separator + 1)
92
+ .split(",")
93
+ .map((field) => field.trim())
94
+ .filter(Boolean)
95
+ const flags = new Set<string>()
96
+ while (fields[0] && TASK_STATES.has(fields[0].toLowerCase())) flags.add(fields.shift()!.toLowerCase())
97
+ if (fields.length < 2 || fields.length > 3) {
98
+ throw syntaxError(source.lineNumber, line, "Gantt tasks require a start and end or duration")
99
+ }
100
+ const id = fields.length === 3 ? fields.shift() : undefined
101
+ if (id && tasksById.has(id)) throw syntaxError(source.lineNumber, line, `Duplicate task id "${id}"`)
102
+ const start = parseStart(fields[0]!, dateFormat, tasksById, source.lineNumber, line)
103
+ const end = parseEnd(fields[1]!, start, dateFormat, source.lineNumber, line)
104
+ if (end < start) throw syntaxError(source.lineNumber, line, "Gantt task cannot end before it starts")
105
+ const task: GanttTask = {
106
+ label,
107
+ ...(id ? { id } : {}),
108
+ ...(section ? { section } : {}),
109
+ start,
110
+ end,
111
+ state: taskState(flags),
112
+ }
113
+ tasks.push(task)
114
+ entries.push({ type: "task", task })
115
+ if (id) tasksById.set(id, task)
116
+ }
117
+
118
+ if (!headerSeen) throw new MermaidSyntaxError("gantt", 1, "", "Gantt header is required")
119
+ return { ...(title === undefined ? {} : { title }), dateFormat, axisFormat,
120
+ ...(tickInterval === undefined ? {} : { tickInterval }), tasks, entries }
121
+ }
122
+
123
+ function parseStart(
124
+ value: string,
125
+ format: string,
126
+ tasksById: Map<string, GanttTask>,
127
+ lineNumber: number,
128
+ sourceLine: string,
129
+ ): number {
130
+ const relative = value.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i)
131
+ if (relative && (format === "HH:mm" || format === "HH:mm:ss")) {
132
+ const units = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }
133
+ return Date.UTC(1970, 0, 1) + Number(relative[1]) * units[relative[2]!.toLowerCase() as keyof typeof units]
134
+ }
135
+ if (!/^after\s+/i.test(value)) return parseDate(value, format, lineNumber, sourceLine)
136
+ const dependencies = value
137
+ .replace(/^after\s+/i, "")
138
+ .trim()
139
+ .split(/\s+/)
140
+ const tasks = dependencies.map((id) => tasksById.get(id))
141
+ const missing = dependencies.find((_, index) => !tasks[index])
142
+ if (missing) throw syntaxError(lineNumber, sourceLine, `Unknown Gantt task id "${missing}"`)
143
+ return Math.max(...tasks.map((task) => task!.end))
144
+ }
145
+
146
+ function parseEnd(value: string, start: number, format: string, lineNumber: number, sourceLine: string): number {
147
+ const duration = value.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i)
148
+ if (!duration) return parseDate(value, format, lineNumber, sourceLine)
149
+ const units = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }
150
+ return start + Number(duration[1]) * units[duration[2]!.toLowerCase() as keyof typeof units]
151
+ }
152
+
153
+ function parseDate(value: string, format: string, lineNumber: number, sourceLine: string): number {
154
+ const numeric = Number(value)
155
+ if (format === "s" && Number.isFinite(numeric)) return numeric * 1_000
156
+ if (format === "X" && Number.isFinite(numeric)) return numeric * 1_000
157
+ if (format === "x" && Number.isFinite(numeric)) return numeric
158
+
159
+ const clock = value.match(/^(\d{2}):(\d{2})(?::(\d{2}))?$/)
160
+ if (clock && ["HH:mm", "HH:mm:ss"].includes(format)) {
161
+ const hours = Number(clock[1])
162
+ const minutes = Number(clock[2])
163
+ const seconds = Number(clock[3] ?? 0)
164
+ if (hours < 24 && minutes < 60 && seconds < 60 && (format === "HH:mm:ss") === (clock[3] !== undefined)) {
165
+ return Date.UTC(1970, 0, 1, hours, minutes, seconds)
166
+ }
167
+ }
168
+
169
+ const calendar = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/)
170
+ if (calendar && ["YYYY-MM-DD", "YYYY-MM-DD HH:mm", "YYYY-MM-DD HH:mm:ss", "YYYY-MM-DDTHH:mm", "YYYY-MM-DDTHH:mm:ss"].includes(format)) {
171
+ return Date.UTC(
172
+ Number(calendar[1]),
173
+ Number(calendar[2]) - 1,
174
+ Number(calendar[3]),
175
+ Number(calendar[4] ?? 0),
176
+ Number(calendar[5] ?? 0),
177
+ Number(calendar[6] ?? 0),
178
+ )
179
+ }
180
+ throw syntaxError(lineNumber, sourceLine, `Unsupported date "${value}" for dateFormat ${format}`)
181
+ }
182
+
183
+ function taskState(flags: Set<string>): GanttTaskState {
184
+ if (flags.has("milestone")) return "milestone"
185
+ if (flags.has("crit")) return "critical"
186
+ if (flags.has("done")) return "done"
187
+ if (flags.has("active")) return "active"
188
+ return "task"
189
+ }
190
+
191
+ function stripComment(value: string): string {
192
+ const comment = value.indexOf("%%")
193
+ return (comment < 0 ? value : value.slice(0, comment)).trim()
194
+ }
195
+
196
+ function syntaxError(lineNumber: number, sourceLine: string, reason?: string): MermaidSyntaxError {
197
+ return new MermaidSyntaxError("gantt", lineNumber, sourceLine, reason)
198
+ }
@@ -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 { GanttStyleColors } from "./style.js"
5
+ import type { GanttCellStyle } from "./types.js"
6
+
7
+ export type GanttGrid = DiagramCanvas<GanttCellStyle>
8
+
9
+ export function renderGanttGridText(grid: GanttGrid): string {
10
+ return grid.toString({ trimBottom: true })
11
+ }
12
+
13
+ export function renderGanttGridStyledText(grid: GanttGrid, colors: GanttStyleColors): StyledText {
14
+ return renderDiagramGridStyledText(grid, (run) => (run.style ? colors[run.style] : undefined), undefined, {
15
+ trimBottom: true,
16
+ })
17
+ }
@@ -0,0 +1,36 @@
1
+ import { RGBA } from "@opentui/core"
2
+ import { blendColor, rgba, type DiagramRgb } from "../core/color/style.js"
3
+ import type { GanttBaseCellStyle, GanttCellStyle } from "./types.js"
4
+
5
+ const DEFAULT_THEME_RGB = {
6
+ title: [228, 239, 232],
7
+ axis: [111, 138, 126],
8
+ section: [154, 184, 169],
9
+ task: [134, 225, 200],
10
+ active: [134, 225, 200],
11
+ critical: [230, 177, 126],
12
+ done: [111, 138, 126],
13
+ milestone: [198, 160, 246],
14
+ } as const satisfies Record<GanttBaseCellStyle, DiagramRgb>
15
+
16
+ export type GanttStyleColors = Required<Record<GanttCellStyle, RGBA>>
17
+
18
+ export function resolveGanttStyleColors(
19
+ colors: Partial<Record<GanttBaseCellStyle | "background", RGBA | undefined>> = {},
20
+ ): GanttStyleColors {
21
+ const axis = colors.axis ?? rgba(DEFAULT_THEME_RGB.axis)
22
+ const background = colors.background ?? rgba([13, 17, 23])
23
+ return {
24
+ title: colors.title ?? rgba(DEFAULT_THEME_RGB.title),
25
+ axis,
26
+ section: colors.section ?? rgba(DEFAULT_THEME_RGB.section),
27
+ task: colors.task ?? rgba(DEFAULT_THEME_RGB.task),
28
+ active: colors.active ?? rgba(DEFAULT_THEME_RGB.active),
29
+ critical: colors.critical ?? rgba(DEFAULT_THEME_RGB.critical),
30
+ done: colors.done ?? rgba(DEFAULT_THEME_RGB.done),
31
+ milestone: colors.milestone ?? rgba(DEFAULT_THEME_RGB.milestone),
32
+ trackMedium: blendColor(axis, background, 0.35),
33
+ trackDim: blendColor(axis, background, 0.55),
34
+ trackFaint: blendColor(axis, background, 0.72),
35
+ }
36
+ }
@@ -0,0 +1,45 @@
1
+ export type GanttTaskState = "task" | "active" | "critical" | "done" | "milestone"
2
+
3
+ export interface GanttSection {
4
+ label: string
5
+ }
6
+
7
+ export interface GanttTask {
8
+ label: string
9
+ id?: string
10
+ section?: GanttSection
11
+ start: number
12
+ end: number
13
+ state: GanttTaskState
14
+ }
15
+
16
+ export type GanttEntry = { type: "section"; section: GanttSection } | { type: "task"; task: GanttTask }
17
+
18
+ export interface GanttDiagram {
19
+ title?: string
20
+ dateFormat: string
21
+ axisFormat: string
22
+ tickInterval?: number
23
+ tasks: GanttTask[]
24
+ entries: GanttEntry[]
25
+ }
26
+
27
+ export interface GanttDiagramRenderOptions {
28
+ layoutMaxWidth?: number
29
+ style?: GanttRenderStyle
30
+ track?: "dots" | "line"
31
+ endpoints?: "plain" | "points"
32
+ line?: GanttLineStyle
33
+ labels?: GanttLabelLayout
34
+ sections?: "compact" | "spaced"
35
+ trackTone?: GanttTrackTone
36
+ }
37
+
38
+ export type GanttRenderStyle = "rail" | "block" | "capsule" | "points" | "track"
39
+ export type GanttLineStyle = "heavy" | "thin" | "double" | "dashed"
40
+ export type GanttLabelLayout = "right" | "left" | "tree"
41
+ export type GanttTrackTone = "medium" | "dim" | "faint"
42
+
43
+ export type GanttBaseCellStyle = "title" | "axis" | "section" | GanttTaskState
44
+ export type GanttTrackCellStyle = "trackMedium" | "trackDim" | "trackFaint"
45
+ export type GanttCellStyle = GanttBaseCellStyle | GanttTrackCellStyle
@@ -0,0 +1,8 @@
1
+ import { drawGitGraphDiagramGrid } from "./drawing.js"
2
+ import { parseMermaidGitGraphDiagram } from "./parser.js"
3
+ import { renderGitGraphGridText } from "./render-grid.js"
4
+ import type { GitGraphDiagramRenderOptions } from "./types.js"
5
+
6
+ export function renderGitGraphDiagram(content: string, options: GitGraphDiagramRenderOptions = {}): string {
7
+ return renderGitGraphGridText(drawGitGraphDiagramGrid(parseMermaidGitGraphDiagram(content), options))
8
+ }