opentui-markdown-render 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentui-markdown-render",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenTUI Markdown renderer with optional native LaTeX math",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,12 +56,12 @@
56
56
  "string-width": "7.2.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@opentui/core": "0.5.12",
60
- "@opentui/solid": "0.5.12",
59
+ "@opentui/core": "0.8.2",
60
+ "@opentui/solid": "0.8.2",
61
61
  "@tsconfig/bun": "1.0.9",
62
62
  "@types/bun": "1.4.0",
63
63
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
64
- "solid-js": "1.9.15"
64
+ "solid-js": "1.9.18"
65
65
  },
66
66
  "engines": {
67
67
  "bun": ">=1.4.0"
package/src/app.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import type { CliRenderer } from "@opentui/core"
2
- import { createMemo, createSignal } from "solid-js"
1
+ import type { CliRenderer, SyntaxStyle } from "@opentui/core"
2
+ import { createMemo, createSignal, onCleanup } from "solid-js"
3
3
  import { useCanvasGraphics } from "./graphics"
4
4
  import { resolveHeadingStyles } from "./headings"
5
5
  import { rgbaToHex } from "./math"
@@ -9,6 +9,7 @@ import { parseDocument, type Document, type FrameConfig, type TablesConfig } fro
9
9
  import { MarkdownFrame } from "./view/frame"
10
10
  import { DocumentView, type RenderUnit } from "./view/document"
11
11
  import type { Theme } from "./theme/types"
12
+ import { generateSyntax } from "./theme/syntax"
12
13
 
13
14
  export type { ThemeModeSetting } from "./config"
14
15
  export { loadRendererConfig } from "./config"
@@ -32,6 +33,8 @@ export type MarkdownRendererCommonProps = {
32
33
  // `cwd` looking for `opentui-markdown-render.json[c]`.
33
34
  readonly configPath?: string
34
35
  readonly themeMode?: ThemeModeSetting | (() => ThemeModeSetting | null | undefined)
36
+ // Host-controlled display-math enlargement. Inline formulas keep text size.
37
+ readonly mathScale?: number | (() => number)
35
38
  readonly cwd?: string
36
39
  // The viewer window. `enabled: false` keeps the document in the whole area.
37
40
  readonly frame?: FrameConfig | (() => FrameConfig)
@@ -97,6 +100,10 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
97
100
  }
98
101
 
99
102
  const themeModeSetting = () => (typeof props.themeMode === "function" ? props.themeMode() : props.themeMode)
103
+ const mathScale = () => {
104
+ const value = typeof props.mathScale === "function" ? props.mathScale() : props.mathScale
105
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : 1
106
+ }
100
107
  const config = createMemo<MarkdownConfig>(() => {
101
108
  const provided = typeof props.config === "function" ? props.config() : props.config
102
109
  if (provided) return provided
@@ -148,6 +155,14 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
148
155
  // The look of the heading levels is a configuration of its own: the colours
149
156
  // come from the theme, the attributes and the spacing from the config file.
150
157
  const headings = createMemo(() => resolveHeadingStyles(config().headings))
158
+ let currentSyntax: SyntaxStyle | undefined
159
+ const syntaxStyle = createMemo(() => {
160
+ currentSyntax?.destroy()
161
+ const nativeScene = (renderer as CliRenderer & { readonly nativeScene?: object }).nativeScene
162
+ currentSyntax = generateSyntax(theme(), headings(), theme().markdown.codeBlock, nativeScene)
163
+ return currentSyntax
164
+ })
165
+ onCleanup(() => currentSyntax?.destroy())
151
166
  return (
152
167
  <box
153
168
  width="100%"
@@ -171,6 +186,8 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
171
186
  imageLimits={config().images}
172
187
  tableOptions={tableOptions()}
173
188
  mermaidOptions={config().mermaid}
189
+ mathScale={mathScale()}
190
+ syntaxStyle={syntaxStyle()}
174
191
  onDiagnostic={props.onDiagnostic}
175
192
  renderUnit={props.renderUnit}
176
193
  />
package/src/index.ts CHANGED
@@ -89,6 +89,7 @@ export {
89
89
  bundledThemeNames,
90
90
  themeFileNames,
91
91
  generateSyntax,
92
+ type SyntaxResourceOwner,
92
93
  resolveTheme,
93
94
  mergeTheme,
94
95
  deriveStatusLineTheme,
@@ -30,10 +30,19 @@ export function loadNativeMath() {
30
30
 
31
31
  async function importNativeMath(): Promise<NativeModule | undefined> {
32
32
  if (process.platform !== "linux" || process.arch !== "x64") return undefined
33
- try {
34
- const file = path.join(import.meta.dir, "..", "..", "prebuilds", "linux-x64-gnu", "opentui_markdown_render.node")
35
- return createRequire(import.meta.url)(file) as NativeModule
36
- } catch {
37
- return undefined
33
+ const relative = path.join("prebuilds", "linux-x64-gnu", "opentui_markdown_render.node")
34
+ // The first location is the published package layout. The second supports a
35
+ // host bundling the TypeScript implementation into its own package while
36
+ // copying `prebuilds` beside the generated entrypoint.
37
+ const candidates = [
38
+ path.join(import.meta.dir, "..", "..", relative),
39
+ path.join(import.meta.dir, relative),
40
+ path.join(import.meta.dir, "node_modules", "opentui-markdown-render", relative),
41
+ ]
42
+ for (const file of candidates) {
43
+ try {
44
+ return createRequire(import.meta.url)(file) as NativeModule
45
+ } catch {}
38
46
  }
47
+ return undefined
39
48
  }
@@ -1,7 +1,7 @@
1
1
  export { defaultTheme } from "./defaults"
2
2
  export { loadTheme, bundledThemeNames, themeFileNames } from "./load"
3
3
  export type { ThemeEnvironment } from "./load"
4
- export { generateSyntax } from "./syntax"
4
+ export { generateSyntax, type SyntaxResourceOwner } from "./syntax"
5
5
  export { resolveTheme, mergeTheme, deriveStatusLineTheme } from "./resolve"
6
6
  export { mixColors } from "./colors"
7
7
  export type { Theme, ThemeMode, ThemePreset, ThemeOverride, StatusLineTheme, StatusLineThemeOverride, FrameTheme, FrameThemeOverride, MarkdownThemeOverride, HeadingLevel } from "./types"
@@ -2,7 +2,14 @@ import { SyntaxStyle, type RGBA, type ThemeTokenStyle } from "@opentui/core"
2
2
  import { defaultHeadingStyles, headingColor, headingLevels, type HeadingStyles } from "../headings"
3
3
  import type { HeadingLevel, Theme } from "./types"
4
4
 
5
- export function generateSyntax(theme: Theme, headings: HeadingStyles = defaultHeadingStyles, defaultColor: RGBA = theme.text.base) {
5
+ export type SyntaxResourceOwner = object
6
+
7
+ export function generateSyntax(
8
+ theme: Theme,
9
+ headings: HeadingStyles = defaultHeadingStyles,
10
+ defaultColor: RGBA = theme.text.base,
11
+ owner?: SyntaxResourceOwner,
12
+ ) {
6
13
  const { markdown, syntax, text } = theme
7
14
  const feedback = text.feedback
8
15
 
@@ -22,7 +29,7 @@ export function generateSyntax(theme: Theme, headings: HeadingStyles = defaultHe
22
29
  rule([`markup.heading.${level}`], headingColor(theme, level), headingStyle(level)),
23
30
  )
24
31
 
25
- return SyntaxStyle.fromTheme([
32
+ const rules = [
26
33
  rule(["default"], defaultColor),
27
34
  rule(["prompt"], theme.accent),
28
35
  rule(["comment", "comment.documentation"], syntax.comment, { italic: true }),
@@ -84,7 +91,11 @@ export function generateSyntax(theme: Theme, headings: HeadingStyles = defaultHe
84
91
  rule(["error"], feedback.error, { bold: true }),
85
92
  rule(["warning"], feedback.warning, { bold: true }),
86
93
  rule(["info"], feedback.info),
87
- ])
94
+ ]
95
+ // OpenTUI 0.8 makes native resource ownership explicit. Passing the owner is
96
+ // harmless on older supported releases, whose factory ignores extra args.
97
+ const fromTheme = SyntaxStyle.fromTheme as (rules: ThemeTokenStyle[], owner?: SyntaxResourceOwner) => SyntaxStyle
98
+ return fromTheme(rules, owner)
88
99
  }
89
100
 
90
101
  function rule(
package/src/view/code.tsx CHANGED
@@ -1,4 +1,6 @@
1
- import { infoStringToFiletype } from "@opentui/core"
1
+ import { infoStringToFiletype, type SyntaxStyle } from "@opentui/core"
2
+ import { useRenderer } from "@opentui/solid"
3
+ import { onCleanup } from "solid-js"
2
4
  import { generateSyntax } from "../theme/syntax"
3
5
  import type { Theme } from "../theme/types"
4
6
  import type { HeadingStyles } from "../headings"
@@ -8,14 +10,19 @@ export type CodeBlockProps = {
8
10
  language: string | null
9
11
  theme: Theme
10
12
  headings: HeadingStyles
13
+ syntaxStyle?: SyntaxStyle
11
14
  }
12
15
 
13
16
  export function CodeBlock(props: CodeBlockProps) {
17
+ const renderer = useRenderer()
18
+ const nativeScene = (renderer as typeof renderer & { readonly nativeScene?: object }).nativeScene
19
+ const owned = props.syntaxStyle ? undefined : generateSyntax(props.theme, props.headings, props.theme.markdown.codeBlock, nativeScene)
20
+ onCleanup(() => owned?.destroy())
14
21
  return (
15
22
  <code
16
23
  content={props.content}
17
24
  filetype={infoStringToFiletype(props.language ?? "")}
18
- syntaxStyle={generateSyntax(props.theme, props.headings, props.theme.markdown.codeBlock)}
25
+ syntaxStyle={props.syntaxStyle ?? owned!}
19
26
  fg={props.theme.markdown.codeBlock}
20
27
  bg={props.theme.background.base}
21
28
  conceal={false}
@@ -1,6 +1,6 @@
1
1
  import { createMemo, For, type Accessor, type JSX } from "solid-js"
2
2
  import stringWidth from "string-width"
3
- import { type TextChunk } from "@opentui/core"
3
+ import { type SyntaxStyle, type TextChunk } from "@opentui/core"
4
4
  import { assertNever, type Block, type Document, type ListItem, type UnitInfo } from "../core/document"
5
5
  import type { ResolvedTableOptions } from "../core/tables"
6
6
  import type { ImagesConfig, MermaidConfig } from "../config/types"
@@ -30,6 +30,8 @@ export type DocumentViewProps = {
30
30
  imageLimits?: ImagesConfig
31
31
  tableOptions: ResolvedTableOptions
32
32
  mermaidOptions?: MermaidConfig
33
+ mathScale?: number
34
+ syntaxStyle?: SyntaxStyle
33
35
  onDiagnostic?: (diagnostic: { level: "warning" | "error"; message: string }) => void
34
36
  renderUnit?: RenderUnit
35
37
  }
@@ -88,16 +90,19 @@ export function DocumentView(props: DocumentViewProps) {
88
90
  labelColor={() => props.theme.markdown.image} textColor={() => props.theme.markdown.imageText}
89
91
  limits={props.imageLimits} />
90
92
  case "display-math": return <DisplayMath content={block.content} open={block.open} close={block.close}
91
- maxWidth={() => columns} theme={() => props.theme} graphics={props.graphics} surface={props.surface} />
93
+ maxWidth={() => columns} theme={() => props.theme} graphics={props.graphics} surface={props.surface}
94
+ scale={() => props.mathScale ?? 1} />
92
95
  case "quote": return <box border={columns > 2 ? ["left"] : []} borderColor={props.theme.markdown.blockQuote}
93
96
  paddingLeft={columns > 2 ? 1 : 0} width="100%" flexDirection="column">
94
97
  <For each={block.blocks}>{(child) => <BlockView block={() => child}
95
98
  columns={columns > 2 ? columns - 2 : columns} quote />}</For>
96
99
  </box>
97
100
  case "list": return <ListBlockView block={() => block} columns={columns} quote={input.quote} wrapItems={false} />
98
- case "code": return <CodeBlock content={block.content} language={block.language} theme={props.theme} headings={props.headings} />
101
+ case "code": return <CodeBlock content={block.content} language={block.language} theme={props.theme}
102
+ headings={props.headings} syntaxStyle={props.syntaxStyle} />
99
103
  case "mermaid": return <MermaidDiagramView source={block.content} theme={props.theme} columns={columns}
100
- mermaidOptions={props.mermaidOptions} headings={props.headings} onDiagnostic={props.onDiagnostic} />
104
+ mermaidOptions={props.mermaidOptions} headings={props.headings} syntaxStyle={props.syntaxStyle}
105
+ onDiagnostic={props.onDiagnostic} />
101
106
  case "table": return <TableBlock block={block} options={props.tableOptions} theme={props.theme} headings={props.headings}
102
107
  hyperlinks={props.graphics.hyperlinks?.() ?? true} />
103
108
  case "literal": return <text fg={props.theme.markdown.text} wrapMode="word" content={block.text} />
package/src/view/math.tsx CHANGED
@@ -12,6 +12,7 @@ export function DisplayMath(props: {
12
12
  surface: () => string
13
13
  theme: () => Theme
14
14
  graphics: CanvasGraphics
15
+ scale: () => number
15
16
  }) {
16
17
  const [failed, setFailed] = createSignal(false)
17
18
  const color = () => rgbaToHex(props.theme().text.base)
@@ -27,11 +28,12 @@ export function DisplayMath(props: {
27
28
  color: color(),
28
29
  background: props.surface(),
29
30
  cell: props.graphics.cell(),
31
+ scale: props.scale(),
30
32
  }),
31
33
  async (key): Promise<{ raster: MathImage } | { error: string }> => {
32
- const input = JSON.parse(key) as { content: string; color: string; background: string; cell: CellSize | null }
34
+ const input = JSON.parse(key) as { content: string; color: string; background: string; cell: CellSize | null; scale: number }
33
35
  try {
34
- const raster = await renderMathImage(input.content, true, input.color, input.background, input.cell)
36
+ const raster = await renderMathImage(input.content, true, input.color, input.background, input.cell, input.scale)
35
37
  return raster ? { raster } : { error: "Renderer returned no image" }
36
38
  } catch (error) {
37
39
  return { error: error instanceof Error ? error.message : String(error) }
@@ -69,7 +71,7 @@ export function DisplayMath(props: {
69
71
  <Match when={raster()}>
70
72
  <box width="100%" height={box().rows} flexShrink={0} alignItems="center" justifyContent="center">
71
73
  <image
72
- id={`opentui-markdown-math-${mathImageID(props.content, true, color(), props.graphics.cell())}-${props.surface()}`}
74
+ id={`opentui-markdown-math-${mathImageID(props.content, true, color(), props.graphics.cell(), props.scale())}-${props.surface()}`}
73
75
  source={raster()!.png}
74
76
  fit="fit"
75
77
  protocol="auto"
@@ -1,5 +1,5 @@
1
1
  import { createEffect, createMemo, Show, type Accessor } from "solid-js"
2
- import { type MouseEvent, type TextRenderable } from "@opentui/core"
2
+ import { type MouseEvent, type SyntaxStyle, type TextRenderable } from "@opentui/core"
3
3
  import { measureMermaidDiagram, type PreparedDiagram } from "../merman/diagram"
4
4
  import { detectMermaidDiagram } from "../merman/detect"
5
5
  import { MermaidSyntaxError } from "../merman/diagnostics"
@@ -16,6 +16,7 @@ export type MermaidDiagramViewProps = {
16
16
  columns: number
17
17
  mermaidOptions?: MermaidConfig
18
18
  headings?: HeadingStyles
19
+ syntaxStyle?: SyntaxStyle
19
20
  onDiagnostic?: (diagnostic: { level: "error"; message: string }) => void
20
21
  }
21
22
 
@@ -121,7 +122,7 @@ export function MermaidDiagramView(props: MermaidDiagramViewProps) {
121
122
  })
122
123
  return <box width="100%" flexDirection="column" flexShrink={0}>
123
124
  <Show when={prepared()} fallback={<CodeBlock content={props.source} language="mermaid"
124
- theme={props.theme} headings={props.headings ?? defaultHeadingStyles} />}>
125
+ theme={props.theme} headings={props.headings ?? defaultHeadingStyles} syntaxStyle={props.syntaxStyle} />}>
125
126
  {(value) => <DiagramText prepared={value} />}
126
127
  </Show>
127
128
  </box>