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 +4 -4
- package/src/app.tsx +19 -2
- package/src/index.ts +1 -0
- package/src/math/native.ts +14 -5
- package/src/theme/index.ts +1 -1
- package/src/theme/syntax.ts +14 -3
- package/src/view/code.tsx +9 -2
- package/src/view/document.tsx +9 -4
- package/src/view/math.tsx +5 -3
- package/src/view/mermaid.tsx +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentui-markdown-render",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
60
|
-
"@opentui/solid": "0.
|
|
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.
|
|
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
package/src/math/native.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
}
|
package/src/theme/index.ts
CHANGED
|
@@ -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"
|
package/src/theme/syntax.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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={
|
|
25
|
+
syntaxStyle={props.syntaxStyle ?? owned!}
|
|
19
26
|
fg={props.theme.markdown.codeBlock}
|
|
20
27
|
bg={props.theme.background.base}
|
|
21
28
|
conceal={false}
|
package/src/view/document.tsx
CHANGED
|
@@ -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}
|
|
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}
|
|
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"
|
package/src/view/mermaid.tsx
CHANGED
|
@@ -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>
|