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
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "opentui-markdown-render",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "OpenTUI Markdown renderer with optional native LaTeX math",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "otmd": "bin/otmd.tsx"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./core": "./src/core/index.ts"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "bin",
17
+ "native/Cargo.toml",
18
+ "native/Cargo.lock",
19
+ "native/build.rs",
20
+ "native/src",
21
+ "prebuilds",
22
+ "themes",
23
+ "README.md",
24
+ "LICENSE",
25
+ "NOTICE"
26
+ ],
27
+ "scripts": {
28
+ "build:native": "bun run scripts/build-native.ts",
29
+ "generate:examples": "bun run scripts/generate-example-assets.ts",
30
+ "demo": "bun --preload @opentui/solid/preload examples/app.tsx",
31
+ "demo:plain": "bun --preload @opentui/solid/preload examples/app.tsx plain.md",
32
+ "demo:tables": "bun --preload @opentui/solid/preload examples/app.tsx tables.md",
33
+ "demo:mermaid": "bun --preload @opentui/solid/preload examples/app.tsx mermaid.md",
34
+ "demo:merman-ext": "bun --preload @opentui/solid/preload examples/app.tsx merman-ext.md",
35
+ "demo:math": "bun --preload @opentui/solid/preload examples/app.tsx math.md",
36
+ "demo:images": "bun --preload @opentui/solid/preload examples/app.tsx images.md",
37
+ "demo:mixed": "bun --preload @opentui/solid/preload examples/app.tsx rendered.md",
38
+ "demo:frame": "bun --preload @opentui/solid/preload examples/app.tsx frame.md",
39
+ "test": "bun test --preload @opentui/solid/preload --timeout 30000",
40
+ "typecheck": "tsgo --noEmit",
41
+ "verify": "bun run build:native && bun run typecheck && bun run test && bun run verify:native",
42
+ "verify:native": "cargo test --manifest-path native/Cargo.toml",
43
+ "prepack": "bun run build:native",
44
+ "demo:all": "bun --preload @opentui/solid/preload examples/app.tsx all.md"
45
+ },
46
+ "peerDependencies": {
47
+ "@opentui/core": ">=0.5.12",
48
+ "@opentui/solid": ">=0.5.12",
49
+ "solid-js": ">=1.9.0"
50
+ },
51
+ "dependencies": {
52
+ "@dagrejs/dagre": "3.1.3",
53
+ "entities": "7.0.1",
54
+ "gifuct-js": "2.1.2",
55
+ "marked": "17.0.1",
56
+ "string-width": "7.2.0"
57
+ },
58
+ "devDependencies": {
59
+ "@opentui/core": "0.5.12",
60
+ "@opentui/solid": "0.5.12",
61
+ "@tsconfig/bun": "1.0.9",
62
+ "@types/bun": "1.4.0",
63
+ "@typescript/native-preview": "7.0.0-dev.20251207.1",
64
+ "solid-js": "1.9.15"
65
+ },
66
+ "engines": {
67
+ "bun": ">=1.4.0"
68
+ }
69
+ }
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/prebuilds/linux-x64-gnu/opentui_markdown_render.node
3
+ size: 61904384 bytes
4
+ sha256: cf4087e529aed7ecccc19760625ac4f3da122f9cff616d46c821a98bab8780f4
package/src/app.tsx ADDED
@@ -0,0 +1,180 @@
1
+ import type { CliRenderer } from "@opentui/core"
2
+ import { createMemo, createSignal } from "solid-js"
3
+ import { useCanvasGraphics } from "./graphics"
4
+ import { resolveHeadingStyles } from "./headings"
5
+ import { rgbaToHex } from "./math"
6
+ import { loadRendererConfig, resolveConfiguredTheme, resolveConfiguredLayout } from "./config"
7
+ import type { ImagesConfig, MarkdownConfig, ThemeModeSetting } from "./config"
8
+ import { parseDocument, type Document, type FrameConfig, type TablesConfig } from "./core"
9
+ import { MarkdownFrame } from "./view/frame"
10
+ import { DocumentView, type RenderUnit } from "./view/document"
11
+ import type { Theme } from "./theme/types"
12
+
13
+ export type { ThemeModeSetting } from "./config"
14
+ export { loadRendererConfig } from "./config"
15
+
16
+ // A diagnostic is a warning or a failure the host may want to show in its own
17
+ // UI instead of the terminal's stderr.
18
+ export type MarkdownDiagnostic = {
19
+ readonly level: "warning" | "error"
20
+ readonly message: string
21
+ }
22
+
23
+ export type MarkdownRendererCommonProps = {
24
+ readonly renderer: CliRenderer
25
+ readonly columns?: () => number
26
+ readonly assetBase?: () => string | undefined
27
+ // A host that already resolved a theme wins over the config file and default.
28
+ readonly theme?: Theme | (() => Theme)
29
+ // A ready-made configuration. It replaces the file lookup entirely.
30
+ readonly config?: MarkdownConfig | (() => MarkdownConfig)
31
+ // An explicit JSON/JSONC config path. Without one the package walks up from
32
+ // `cwd` looking for `opentui-markdown-render.json[c]`.
33
+ readonly configPath?: string
34
+ readonly themeMode?: ThemeModeSetting | (() => ThemeModeSetting | null | undefined)
35
+ readonly cwd?: string
36
+ // The viewer window. `enabled: false` keeps the document in the whole area.
37
+ readonly frame?: FrameConfig | (() => FrameConfig)
38
+ // Table sizing and overflow. Defaults to content-sized, horizontally
39
+ // scrollable tables.
40
+ readonly tables?: TablesConfig | (() => TablesConfig)
41
+ // An embedded host passes `false` so the renderer never walks the file system
42
+ // for a config that belongs to a standalone viewer.
43
+ readonly discoverConfig?: boolean
44
+ // Warnings and errors are reported here instead of stderr, which lets a host
45
+ // put them in its own UI.
46
+ readonly onDiagnostic?: (diagnostic: MarkdownDiagnostic) => void
47
+ // The host may wrap every top-level block and every item of a top-level list.
48
+ readonly renderUnit?: RenderUnit
49
+ }
50
+
51
+ // The renderer takes either Markdown text or a document the host already parsed.
52
+ // A ready document skips the parser entirely.
53
+ export type MarkdownRendererProps = MarkdownRendererCommonProps & (
54
+ | { readonly content: () => string; readonly document?: never }
55
+ | { readonly document: () => Document; readonly content?: never }
56
+ )
57
+
58
+ // Keep the public host helper, but use the same config/runtime resolution as
59
+ // the component. Loading the file remains an optional concern of this adapter.
60
+ // The renderer argument stays for existing callers, but its terminal theme
61
+ // does not determine the default palette.
62
+ export function resolveRendererTheme(
63
+ renderer: CliRenderer,
64
+ options: {
65
+ theme?: Theme | (() => Theme)
66
+ config?: MarkdownConfig
67
+ configPath?: string
68
+ themeMode?: ThemeModeSetting | null | undefined
69
+ cwd?: string
70
+ } = {},
71
+ ): Theme {
72
+ const host = typeof options.theme === "function" ? options.theme() : options.theme
73
+ if (host) return host
74
+ const config = options.config ?? loadRendererConfig(options.configPath, options.cwd)
75
+ return resolveConfiguredTheme(config, {
76
+ themeMode: options.themeMode,
77
+ })
78
+ }
79
+
80
+ export function MarkdownRenderer(props: MarkdownRendererProps) {
81
+ const renderer = props.renderer
82
+ const graphics = useCanvasGraphics(renderer)
83
+ const surface = () => rgbaToHex(theme().background.base)
84
+ // The document is laid out for the width of the area it is rendered in, not
85
+ // for the width of the terminal: a host that insets the document (a scroll
86
+ // box with padding, a sidebar, a border) leaves less room, and a line that
87
+ // reaches past it is cut off by the terminal. The root box measures itself,
88
+ // so the layout follows the host. An explicit `columns` wins over it.
89
+ const [measured, setMeasured] = createSignal(0)
90
+ // The area the host gives the viewer: the terminal is only the fallback for
91
+ // the first frame, before the root box has been measured.
92
+ const viewportWidth = () => {
93
+ const explicit = props.columns?.()
94
+ if (explicit !== undefined && explicit > 0) return Math.max(8, explicit)
95
+ const width = measured()
96
+ return Math.max(8, width > 0 ? width : renderer.terminalWidth)
97
+ }
98
+
99
+ const themeModeSetting = () => (typeof props.themeMode === "function" ? props.themeMode() : props.themeMode)
100
+ const config = createMemo<MarkdownConfig>(() => {
101
+ const provided = typeof props.config === "function" ? props.config() : props.config
102
+ if (provided) return provided
103
+ return loadRendererConfig(props.configPath, props.cwd, {
104
+ discover: props.discoverConfig,
105
+ report: (message) => {
106
+ const handler = props.onDiagnostic
107
+ if (handler) {
108
+ handler({ level: "warning", message })
109
+ return
110
+ }
111
+ console.warn(`[opentui-markdown-render] ${message}`)
112
+ },
113
+ })
114
+ })
115
+ const theme = createMemo<Theme>(() =>
116
+ resolveRendererTheme(renderer, {
117
+ theme: props.theme,
118
+ config: config(),
119
+ themeMode: themeModeSetting(),
120
+ }),
121
+ )
122
+
123
+ // The frame decides how much of the measured area the document gets. Every
124
+ // block engine reads the same `contentWidth`, so text, tables and formulas
125
+ // cannot disagree about where the right edge is.
126
+ const frameSetting = createMemo<FrameConfig | undefined>(() => {
127
+ const explicit = typeof props.frame === "function" ? props.frame() : props.frame
128
+ return explicit ?? config().frame
129
+ })
130
+ const tableSetting = createMemo<TablesConfig | undefined>(() => {
131
+ const explicit = typeof props.tables === "function" ? props.tables() : props.tables
132
+ return explicit ?? config().tables
133
+ })
134
+ const layout = createMemo(() =>
135
+ resolveConfiguredLayout(config(), { viewport: viewportWidth(), frame: frameSetting(), tables: tableSetting() }),
136
+ )
137
+ const frame = () => layout().frame
138
+ const contentWidth = () => layout().contentWidth
139
+ const tableOptions = () => layout().tables
140
+
141
+ const document = createMemo<Document>(() => {
142
+ const ready = props.document
143
+ if (ready) return ready()
144
+ const content = props.content
145
+ if (!content) throw new Error("MarkdownRenderer requires either a content or a document accessor")
146
+ return parseDocument(content())
147
+ })
148
+ // The look of the heading levels is a configuration of its own: the colours
149
+ // come from the theme, the attributes and the spacing from the config file.
150
+ const headings = createMemo(() => resolveHeadingStyles(config().headings))
151
+ return (
152
+ <box
153
+ width="100%"
154
+ flexDirection="column"
155
+ flexShrink={0}
156
+ backgroundColor={theme().background.base}
157
+ onSizeChange={function () {
158
+ const width = this.width
159
+ if (width !== viewportWidth()) queueMicrotask(() => setMeasured(width))
160
+ }}
161
+ >
162
+ <MarkdownFrame frame={frame()} theme={theme()}>
163
+ <DocumentView
164
+ document={document()}
165
+ columns={contentWidth()}
166
+ theme={theme()}
167
+ headings={headings()}
168
+ graphics={graphics}
169
+ surface={surface}
170
+ assetBase={() => props.assetBase?.()}
171
+ imageLimits={config().images}
172
+ tableOptions={tableOptions()}
173
+ mermaidOptions={config().mermaid}
174
+ onDiagnostic={props.onDiagnostic}
175
+ renderUnit={props.renderUnit}
176
+ />
177
+ </MarkdownFrame>
178
+ </box>
179
+ )
180
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,63 @@
1
+ export type CliCommand =
2
+ | { readonly kind: "help" }
3
+ | { readonly kind: "version" }
4
+ | {
5
+ readonly kind: "run"
6
+ readonly file?: string
7
+ readonly configPath?: string
8
+ readonly discoverConfig: boolean
9
+ }
10
+
11
+ export const cliUsage = `Usage: otmd [options] [file]
12
+
13
+ Render a Markdown file in the terminal. Use - to read Markdown from stdin.
14
+
15
+ Options:
16
+ -c, --config <path> Load a specific JSON/JSONC config file
17
+ --no-config Disable automatic config discovery
18
+ -h, --help Show this help
19
+ -v, --version Show the package version
20
+ `
21
+
22
+ export function parseCliArgs(args: readonly string[]): CliCommand {
23
+ let configPath: string | undefined
24
+ let discoverConfig = true
25
+ let file: string | undefined
26
+
27
+ for (let index = 0; index < args.length; index++) {
28
+ const argument = args[index]!
29
+ if (argument === "--") {
30
+ if (file !== undefined || index + 1 >= args.length) throw new Error("Expected one Markdown file after --")
31
+ file = args[++index]
32
+ continue
33
+ }
34
+ if (argument === "-h" || argument === "--help") return { kind: "help" }
35
+ if (argument === "-v" || argument === "--version") return { kind: "version" }
36
+ if (argument === "--no-config") {
37
+ discoverConfig = false
38
+ continue
39
+ }
40
+ if (argument === "-") {
41
+ if (file !== undefined) throw new Error("Only one Markdown file can be rendered")
42
+ file = argument
43
+ continue
44
+ }
45
+ if (argument === "-c" || argument === "--config") {
46
+ const value = args[++index]
47
+ if (!value) throw new Error(`${argument} requires a path`)
48
+ configPath = value
49
+ continue
50
+ }
51
+ if (argument.startsWith("--config=")) {
52
+ const value = argument.slice("--config=".length)
53
+ if (!value) throw new Error("--config requires a path")
54
+ configPath = value
55
+ continue
56
+ }
57
+ if (argument.startsWith("-")) throw new Error(`Unknown option: ${argument}`)
58
+ if (file !== undefined) throw new Error("Only one Markdown file can be rendered")
59
+ file = argument
60
+ }
61
+
62
+ return { kind: "run", file, configPath, discoverConfig }
63
+ }
@@ -0,0 +1,46 @@
1
+ export { loadConfig, loadRendererConfig, discoverConfig, parseConfig, configFilenames, xdgConfigFilenames, xdgConfigDirectory, type ConfigEnvironment, type ConfigReporter } from "./load"
2
+ export {
3
+ resolveConfiguredTheme,
4
+ resolveConfiguredLayout,
5
+ type ThemeModeSetting,
6
+ type ThemeRuntime,
7
+ type LayoutRuntime,
8
+ } from "./resolve"
9
+ export {
10
+ statusLineFields,
11
+ isStatusLineField,
12
+ statusLineStyles,
13
+ isStatusLineStyle,
14
+ headingLevelKeys,
15
+ isHeadingLevelKey,
16
+ } from "./types"
17
+ export { isFrameAlign, isFrameStyle, frameAligns, frameStyles } from "../core/frame"
18
+ export {
19
+ isTableWidthMode,
20
+ isTableOverflow,
21
+ isTableColumnFitter,
22
+ tableWidthModes,
23
+ tableOverflows,
24
+ tableColumnFitters,
25
+ } from "../core/tables"
26
+ export type {
27
+ MarkdownConfig,
28
+ StatusLineConfig,
29
+ StatusLineField,
30
+ StatusLineBorder,
31
+ StatusLineStyle,
32
+ ImagesConfig,
33
+ MermaidConfig,
34
+ ThemeConfig,
35
+ HeadingsConfig,
36
+ HeadingLevelConfig,
37
+ HeadingLevelKey,
38
+ } from "./types"
39
+ export type { FrameConfig, FrameAlign, FrameStyle, FrameInsets, ResolvedFrame } from "../core/frame"
40
+ export type {
41
+ TablesConfig,
42
+ TableWidthMode,
43
+ TableOverflow,
44
+ TableColumnFitter,
45
+ ResolvedTableOptions,
46
+ } from "../core/tables"