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,59 @@
1
+ import type { DiagramPoint } from "../core/geometry.js"
2
+ import type { StateDiagramNote } from "./types.js"
3
+
4
+ interface NoteBounds {
5
+ left: number
6
+ top: number
7
+ width: number
8
+ height: number
9
+ centerY: number
10
+ note: StateDiagramNote
11
+ connector?: StateDiagramNoteConnector
12
+ }
13
+
14
+ interface TargetBounds {
15
+ left: number
16
+ top: number
17
+ width: number
18
+ height: number
19
+ centerY: number
20
+ }
21
+
22
+ export interface StateDiagramNoteConnector {
23
+ points: readonly DiagramPoint[]
24
+ connectorY: number
25
+ }
26
+
27
+ export function stateDiagramNoteConnector(
28
+ bounds: NoteBounds,
29
+ target: TargetBounds,
30
+ ): StateDiagramNoteConnector {
31
+ if (bounds.connector) return bounds.connector
32
+ const noteX = bounds.note.position === "right" ? bounds.left - 1 : bounds.left + bounds.width
33
+ const targetX = bounds.note.position === "right" ? target.left + target.width : target.left - 1
34
+ const targetBottom = target.top + target.height - 1
35
+ const noteBottom = bounds.top + bounds.height - 1
36
+ const noteAbove = noteBottom < target.top
37
+ const noteBelow = bounds.top > targetBottom
38
+
39
+ if (noteAbove || noteBelow) {
40
+ const connectorY = bounds.centerY
41
+ return {
42
+ connectorY,
43
+ points: [
44
+ { x: targetX, y: noteAbove ? target.top - 1 : targetBottom + 1 },
45
+ { x: targetX, y: connectorY },
46
+ { x: noteX, y: connectorY },
47
+ ],
48
+ }
49
+ }
50
+
51
+ const connectorY = Math.max(bounds.top + 1, Math.min(target.centerY, bounds.top + bounds.height - 2))
52
+ return {
53
+ connectorY,
54
+ points: [
55
+ { x: targetX, y: connectorY },
56
+ { x: noteX, y: connectorY },
57
+ ],
58
+ }
59
+ }
@@ -0,0 +1,11 @@
1
+ import type { BorderStyle } from "@opentui/core"
2
+ import type { StateDiagramArrowHeadStyle } from "./types.js"
3
+
4
+ export const DEFAULT_STATE_DIAGRAM_MIN_STATE_GAP = 5
5
+ export const DEFAULT_STATE_BORDER_STYLE = "rounded" satisfies BorderStyle
6
+ export const DEFAULT_STATE_ARROW_HEAD_STYLE = "filled" satisfies StateDiagramArrowHeadStyle
7
+
8
+ export function normalizeStateMinStateGap(value: number | undefined): number {
9
+ if (value === undefined || !Number.isFinite(value)) return DEFAULT_STATE_DIAGRAM_MIN_STATE_GAP
10
+ return Math.max(1, Math.trunc(value))
11
+ }
@@ -0,0 +1,245 @@
1
+ import { decodeMermaidText, firstMeaningfulMermaidLine, numberedMermaidLines } from "../core/mermaid.js"
2
+ import { splitDiagramLines } from "../core/text-lines.js"
3
+ import { MermaidSyntaxError } from "../diagnostics.js"
4
+ import { normalizeStateDiagramEndpoint, stateDiagramEndMarkerId, stateDiagramStartMarkerId } from "./endpoint.js"
5
+ import type {
6
+ StateDiagram,
7
+ StateDiagramCompositeState,
8
+ StateDiagramDirection,
9
+ StateDiagramNote,
10
+ StateDiagramState,
11
+ StateDiagramTransition,
12
+ } from "./types.js"
13
+
14
+ const DEFAULT_DIRECTION = "LR" satisfies StateDiagramDirection
15
+ const STATE_RE = /^state\s+"([^"]+)"\s+as\s+(\S+)$/i
16
+ const STATE_DECLARE_RE = /^state\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i
17
+ const STATE_DESCRIPTION_RE = /^([A-Za-z_][A-Za-z0-9_.-]*)\s*:\s*(.+)$/
18
+ const COMPOSITE_STATE_RE = /^state\s+(?:"([^"]+)"\s+as\s+)?(\S+)\s*\{$/i
19
+ const CHOICE_STATE_RE = /^state\s+(\S+)\s+<<choice>>$/i
20
+ const PARALLEL_STATE_RE = /^state\s+(\S+)\s+<<(fork|join)>>$/i
21
+ const TRANSITION_RE = /^(\[\*\]|[^\s:]+)\s*-->\s*(\[\*\]|[^\s:]+)(?:\s*:\s*(.*))?$/
22
+ const DIRECTION_RE = /^direction\s+(TB|TD|BT|LR|RL)$/i
23
+ const NOTE_INLINE_RE = /^note\s+(left|right)\s+of\s+(\S+)\s*:\s*(.*)$/i
24
+ const NOTE_START_RE = /^note\s+(left|right)\s+of\s+(\S+)\s*$/i
25
+ const NOTE_END_RE = /^end\s+note$/i
26
+
27
+ function normalizeDirection(value?: string): StateDiagramDirection {
28
+ const upper = value?.toUpperCase()
29
+ if (upper === "TB" || upper === "TD" || upper === "BT" || upper === "LR" || upper === "RL") return upper
30
+ return DEFAULT_DIRECTION
31
+ }
32
+
33
+ function isMermaidHeader(line: string): boolean {
34
+ return line.toLowerCase() === "statediagram-v2" || line.toLowerCase() === "statediagram"
35
+ }
36
+
37
+ function ensureState(
38
+ states: Map<string, StateDiagramState>,
39
+ id: string,
40
+ label = id,
41
+ kind: StateDiagramState["kind"] = "state",
42
+ parentId?: string,
43
+ ): void {
44
+ const existing = states.get(id)
45
+ if (existing) {
46
+ if (existing.label === existing.id && label !== id) existing.label = label
47
+ if (parentId && !existing.parentId) existing.parentId = parentId
48
+ if (kind !== "state") {
49
+ existing.kind = kind
50
+ existing.label = label
51
+ }
52
+ return
53
+ }
54
+ states.set(id, parentId ? { id, label, kind, parentId } : { id, label, kind })
55
+ }
56
+
57
+ function resolveCompositeTransitionEndpoint(
58
+ id: string,
59
+ markerId: (scope?: string) => string,
60
+ compositeIds: ReadonlySet<string>,
61
+ states: Map<string, StateDiagramState>,
62
+ ): string {
63
+ if (!compositeIds.has(id)) return id
64
+ const marker = markerId(id)
65
+ return states.has(marker) ? marker : id
66
+ }
67
+
68
+ function resolveCompositeTransitions(
69
+ transitions: readonly StateDiagramTransition[],
70
+ compositeIds: ReadonlySet<string>,
71
+ states: Map<string, StateDiagramState>,
72
+ ): StateDiagramTransition[] {
73
+ return transitions.map((transition) => ({
74
+ from: resolveCompositeTransitionEndpoint(transition.from, stateDiagramEndMarkerId, compositeIds, states),
75
+ to: resolveCompositeTransitionEndpoint(transition.to, stateDiagramStartMarkerId, compositeIds, states),
76
+ label: transition.label,
77
+ }))
78
+ }
79
+
80
+ export function isMermaidStateDiagram(content: string): boolean {
81
+ return isMermaidHeader(firstMeaningfulMermaidLine(content) ?? "")
82
+ }
83
+
84
+ export function parseMermaidStateDiagram(content: string): StateDiagram {
85
+ const states = new Map<string, StateDiagramState>()
86
+ const transitions: StateDiagramTransition[] = []
87
+ const composites: StateDiagramCompositeState[] = []
88
+ const notes: StateDiagramNote[] = []
89
+ const parentStack: Array<{ id: string; lineNumber: number; sourceLine: string }> = []
90
+ let pendingNote:
91
+ | { target: string; position: "left" | "right"; lines: string[]; lineNumber: number; sourceLine: string }
92
+ | undefined
93
+ let direction: StateDiagramDirection = DEFAULT_DIRECTION
94
+
95
+ for (const source of numberedMermaidLines(content)) {
96
+ const line = source.text
97
+ if (pendingNote) {
98
+ if (NOTE_END_RE.test(line)) {
99
+ notes.push({
100
+ target: pendingNote.target,
101
+ position: pendingNote.position,
102
+ lines: pendingNote.lines.map(decodeMermaidText),
103
+ })
104
+ pendingNote = undefined
105
+ } else if (line || pendingNote.lines.length > 0) {
106
+ pendingNote.lines.push(line)
107
+ }
108
+ continue
109
+ }
110
+
111
+ if (!line || line.startsWith("%%") || isMermaidHeader(line)) continue
112
+
113
+ if (line === "}") {
114
+ if (parentStack.length === 0) {
115
+ throw new MermaidSyntaxError("state", source.lineNumber, line, 'Unexpected "}" without an open composite state')
116
+ }
117
+ parentStack.pop()
118
+ continue
119
+ }
120
+
121
+ const parentId = parentStack[parentStack.length - 1]?.id
122
+
123
+ const directionMatch = line.match(DIRECTION_RE)
124
+ if (directionMatch) {
125
+ if (parentStack.length > 0) {
126
+ throw new MermaidSyntaxError("state", source.lineNumber, line, "Composite-local direction is not supported")
127
+ }
128
+ direction = normalizeDirection(directionMatch[1])
129
+ continue
130
+ }
131
+
132
+ const inlineNoteMatch = line.match(NOTE_INLINE_RE)
133
+ if (inlineNoteMatch) {
134
+ notes.push({
135
+ position: inlineNoteMatch[1]!.toLowerCase() as "left" | "right",
136
+ target: inlineNoteMatch[2]!,
137
+ lines: splitDiagramLines(decodeMermaidText(inlineNoteMatch[3]!.trim())),
138
+ })
139
+ continue
140
+ }
141
+
142
+ const noteMatch = line.match(NOTE_START_RE)
143
+ if (noteMatch) {
144
+ pendingNote = {
145
+ position: noteMatch[1]!.toLowerCase() as "left" | "right",
146
+ target: noteMatch[2]!,
147
+ lines: [],
148
+ lineNumber: source.lineNumber,
149
+ sourceLine: line,
150
+ }
151
+ continue
152
+ }
153
+
154
+ const compositeMatch = line.match(COMPOSITE_STATE_RE)
155
+ if (compositeMatch) {
156
+ const id = compositeMatch[2]!
157
+ composites.push({
158
+ id,
159
+ label: decodeMermaidText(compositeMatch[1] ?? id),
160
+ ...(parentId ? { parentId } : {}),
161
+ })
162
+ parentStack.push({ id, lineNumber: source.lineNumber, sourceLine: line })
163
+ continue
164
+ }
165
+
166
+ const stateMatch = line.match(STATE_RE)
167
+ if (stateMatch) {
168
+ ensureState(states, stateMatch[2]!, decodeMermaidText(stateMatch[1]!), "state", parentId)
169
+ continue
170
+ }
171
+
172
+ const declaration = line.match(STATE_DECLARE_RE)
173
+ if (declaration) {
174
+ ensureState(states, declaration[1]!, declaration[1]!, "state", parentId)
175
+ continue
176
+ }
177
+
178
+ const description = line.match(STATE_DESCRIPTION_RE)
179
+ if (description) {
180
+ ensureState(states, description[1]!, decodeMermaidText(description[2]!), "state", parentId)
181
+ continue
182
+ }
183
+
184
+ const choiceMatch = line.match(CHOICE_STATE_RE)
185
+ if (choiceMatch) {
186
+ ensureState(states, choiceMatch[1]!, "", "choice", parentId)
187
+ continue
188
+ }
189
+
190
+ const parallelMatch = line.match(PARALLEL_STATE_RE)
191
+ if (parallelMatch) {
192
+ ensureState(states, parallelMatch[1]!, "", parallelMatch[2]!.toLowerCase() as "fork" | "join", parentId)
193
+ continue
194
+ }
195
+
196
+ const transitionMatch = line.match(TRANSITION_RE)
197
+ if (transitionMatch) {
198
+ const rawFrom = transitionMatch[1]!
199
+ const rawTo = transitionMatch[2]!
200
+ if (rawFrom.includes("-->") || rawTo.includes("-->")) {
201
+ throw new MermaidSyntaxError("state", source.lineNumber, line)
202
+ }
203
+ const from = normalizeStateDiagramEndpoint(rawFrom, "from", parentId)
204
+ const to = normalizeStateDiagramEndpoint(rawTo, "to", parentId)
205
+ ensureState(states, from, rawFrom === "[*]" ? "●" : from, rawFrom === "[*]" ? "start" : "state", parentId)
206
+ ensureState(states, to, rawTo === "[*]" ? "◎" : to, rawTo === "[*]" ? "end" : "state", parentId)
207
+ transitions.push({ from, to, label: decodeMermaidText(transitionMatch[3]?.trim() ?? "") })
208
+ continue
209
+ }
210
+
211
+ throw new MermaidSyntaxError("state", source.lineNumber, line)
212
+ }
213
+
214
+ if (pendingNote) {
215
+ throw new MermaidSyntaxError(
216
+ "state",
217
+ pendingNote.lineNumber,
218
+ pendingNote.sourceLine,
219
+ 'Unclosed note; expected "end note"',
220
+ )
221
+ }
222
+
223
+ const unclosedComposite = parentStack[parentStack.length - 1]
224
+ if (unclosedComposite) {
225
+ throw new MermaidSyntaxError(
226
+ "state",
227
+ unclosedComposite.lineNumber,
228
+ unclosedComposite.sourceLine,
229
+ 'Unclosed composite state; expected "}"',
230
+ )
231
+ }
232
+
233
+ if (composites.length === 0) {
234
+ return { direction, states: [...states.values()], transitions, composites, notes }
235
+ }
236
+
237
+ const compositeIds = new Set(composites.map((composite) => composite.id))
238
+ return {
239
+ direction,
240
+ states: [...states.values()].filter((state) => !compositeIds.has(state.id)),
241
+ transitions: resolveCompositeTransitions(transitions, compositeIds, states),
242
+ composites,
243
+ notes,
244
+ }
245
+ }
@@ -0,0 +1,22 @@
1
+ import type { RGBA, StyledText } from "@opentui/core"
2
+ import type { DiagramCanvas } from "../core/canvas.js"
3
+ import { renderDiagramGridStyledTextByStyle } from "../core/render-grid.js"
4
+ import type { StateStyleColors } from "./style.js"
5
+ import type { StateCellStyle } from "./types.js"
6
+
7
+ export type StateGrid = DiagramCanvas<StateCellStyle>
8
+
9
+ export function renderStateGridText(grid: StateGrid): string {
10
+ return grid.toString({ trimTop: true, trimBottom: true })
11
+ }
12
+
13
+ export function renderStateGridStyledText(
14
+ grid: StateGrid,
15
+ colors: StateStyleColors,
16
+ backgrounds?: Partial<Record<StateCellStyle, RGBA>>,
17
+ ): StyledText {
18
+ return renderDiagramGridStyledTextByStyle(grid, colors, backgrounds, {
19
+ trimTop: true,
20
+ trimBottom: true,
21
+ })
22
+ }