opentui-markdown-render 0.1.0 → 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.
- package/README.md +50 -0
- package/bin/otmd.tsx +99 -0
- package/package.json +6 -1
- package/src/cli.ts +63 -0
- package/src/config/index.ts +2 -1
- package/src/config/load.ts +39 -10
- package/src/config/resolve.ts +3 -4
- package/src/config/theme.ts +25 -3
- package/src/config/types.ts +6 -3
- package/src/index.ts +9 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/load.ts +39 -0
- package/src/theme/resolve.ts +3 -2
- package/src/theme/types.ts +3 -2
- package/themes/dark.json +32 -0
- package/themes/example.jsonc +98 -0
- package/themes/light.json +32 -0
- package/themes/solarized-dark.json +32 -0
- package/themes/solarized-light.json +32 -0
package/README.md
CHANGED
|
@@ -77,6 +77,25 @@ Press `c` to open the configuration panel. Use Up/Down (or `j`/`k`) to select an
|
|
|
77
77
|
|
|
78
78
|
See [Examples and demo fixtures](examples/README.md) for the complete fixture matrix and [Getting started](docs/getting-started.md) for a step-by-step walkthrough.
|
|
79
79
|
|
|
80
|
+
### Install the `otmd` CLI
|
|
81
|
+
|
|
82
|
+
The package also provides `otmd`, a terminal Markdown viewer. npm can install the
|
|
83
|
+
package globally, but Bun is still required at runtime because the CLI executes
|
|
84
|
+
TypeScript and uses the Bun native addon loader:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm install -g opentui-markdown-render
|
|
88
|
+
# or: bun add -g opentui-markdown-render
|
|
89
|
+
|
|
90
|
+
otmd --version
|
|
91
|
+
otmd README.md
|
|
92
|
+
cat README.md | otmd -
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use `otmd --help` for options such as `--config` and `--no-config`. The current
|
|
96
|
+
native math prebuild targets Linux x64 with glibc; other platforms keep readable
|
|
97
|
+
math source when the addon is unavailable.
|
|
98
|
+
|
|
80
99
|
### Install in an OpenTUI application
|
|
81
100
|
|
|
82
101
|
```sh
|
|
@@ -85,6 +104,37 @@ bun add opentui-markdown-render @opentui/core @opentui/solid solid-js
|
|
|
85
104
|
|
|
86
105
|
`@opentui/core`, `@opentui/solid`, and `solid-js` are peer dependencies. The package currently targets Bun 1.4+, OpenTUI 0.5.12+, and Solid 1.9+. Native prebuild coverage is currently Linux x64 with glibc.
|
|
87
106
|
|
|
107
|
+
### Configuration discovery
|
|
108
|
+
|
|
109
|
+
With automatic discovery enabled, the nearest project configuration wins. The
|
|
110
|
+
loader checks `opentui-markdown-render.jsonc` and then
|
|
111
|
+
`opentui-markdown-render.json` while walking from the current working directory
|
|
112
|
+
up through its parents. If no project file exists, it checks the XDG user
|
|
113
|
+
configuration directory:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
$XDG_CONFIG_HOME/otmd/config.jsonc # or ~/.config when XDG_CONFIG_HOME is unset
|
|
117
|
+
$XDG_CONFIG_HOME/otmd/config.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Pass `--config PATH` to `otmd` or `configPath` to `MarkdownRenderer` to select a
|
|
121
|
+
file explicitly. Embedded hosts can pass `discoverConfig={false}` when they own
|
|
122
|
+
configuration themselves.
|
|
123
|
+
|
|
124
|
+
Themes are complete JSON/JSONC files. Select a bundled theme or a user theme
|
|
125
|
+
from `$XDG_CONFIG_HOME/otmd/themes` with:
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
{
|
|
129
|
+
"theme": { "name": "dark" }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The repository includes [`themes/example.jsonc`](themes/example.jsonc), a fully
|
|
134
|
+
commented template showing every theme setting. Copy it to
|
|
135
|
+
`~/.config/otmd/themes/my-theme.jsonc` and select it with
|
|
136
|
+
`{ "theme": { "name": "my-theme" } }`.
|
|
137
|
+
|
|
88
138
|
The smallest useful component looks like this:
|
|
89
139
|
|
|
90
140
|
```tsx
|
package/bin/otmd.tsx
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createCliRenderer } from "@opentui/core"
|
|
3
|
+
import { render } from "@opentui/solid"
|
|
4
|
+
import { createSignal, onCleanup, Show } from "solid-js"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import {
|
|
7
|
+
MarkdownRenderer,
|
|
8
|
+
StatusLine,
|
|
9
|
+
cliUsage,
|
|
10
|
+
loadRendererConfig,
|
|
11
|
+
parseCliArgs,
|
|
12
|
+
resolveRendererTheme,
|
|
13
|
+
useCanvasGraphics,
|
|
14
|
+
} from "../src"
|
|
15
|
+
|
|
16
|
+
const manifest = await Bun.file(new URL("../package.json", import.meta.url)).json() as { version: string }
|
|
17
|
+
|
|
18
|
+
function fail(message: string): never {
|
|
19
|
+
console.error(`otmd: ${message}`)
|
|
20
|
+
console.error("Try 'otmd --help' for usage.")
|
|
21
|
+
process.exit(2)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let command
|
|
25
|
+
try {
|
|
26
|
+
command = parseCliArgs(process.argv.slice(2))
|
|
27
|
+
} catch (error) {
|
|
28
|
+
fail(error instanceof Error ? error.message : String(error))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (command.kind === "help") {
|
|
32
|
+
process.stdout.write(cliUsage)
|
|
33
|
+
process.exit(0)
|
|
34
|
+
}
|
|
35
|
+
if (command.kind === "version") {
|
|
36
|
+
process.stdout.write(`${manifest.version}\n`)
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const file = command.file
|
|
41
|
+
const filePath = file && file !== "-" ? path.resolve(file) : undefined
|
|
42
|
+
const content = file === "-"
|
|
43
|
+
? await new Response(Bun.stdin).text()
|
|
44
|
+
: filePath
|
|
45
|
+
? await readFile(filePath)
|
|
46
|
+
: fail("a Markdown file is required (use - to read stdin)")
|
|
47
|
+
const assetBase = filePath ? path.dirname(filePath) : process.cwd()
|
|
48
|
+
const config = loadRendererConfig(command.configPath, process.cwd(), { discover: command.discoverConfig })
|
|
49
|
+
const renderer = await createCliRenderer({})
|
|
50
|
+
|
|
51
|
+
function Otmd() {
|
|
52
|
+
const graphics = useCanvasGraphics(renderer)
|
|
53
|
+
const theme = () => resolveRendererTheme(renderer, { config, cwd: process.cwd() })
|
|
54
|
+
const [position] = createSignal("1/1")
|
|
55
|
+
const onKeypress = (key: { name?: string }) => {
|
|
56
|
+
if (key.name === "q" || key.name === "escape") {
|
|
57
|
+
renderer.destroy()
|
|
58
|
+
process.exit(0)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
renderer.keyInput.on("keypress", onKeypress)
|
|
62
|
+
onCleanup(() => renderer.keyInput.off("keypress", onKeypress))
|
|
63
|
+
const state = () => ({
|
|
64
|
+
title: filePath ? path.basename(filePath) : "stdin",
|
|
65
|
+
protocol: graphics.protocol(),
|
|
66
|
+
cell: graphics.cell() ? `${graphics.cell()!.width}x${graphics.cell()!.height}` : undefined,
|
|
67
|
+
position: position(),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<box width="100%" height="100%" flexDirection="column" backgroundColor={theme().background.base}>
|
|
72
|
+
<scrollbox width="100%" flexGrow={1} contentOptions={{ paddingLeft: 1, paddingRight: 1, paddingTop: 1, paddingBottom: 1 }}>
|
|
73
|
+
<MarkdownRenderer
|
|
74
|
+
content={() => content}
|
|
75
|
+
renderer={renderer}
|
|
76
|
+
assetBase={() => assetBase}
|
|
77
|
+
config={() => config}
|
|
78
|
+
cwd={process.cwd()}
|
|
79
|
+
discoverConfig={false}
|
|
80
|
+
/>
|
|
81
|
+
</scrollbox>
|
|
82
|
+
<Show when={config.statusline?.enabled !== false}>
|
|
83
|
+
<StatusLine theme={theme()} state={state} fields={config.statusline?.fields} separator={config.statusline?.separator} border={config.statusline?.border} style={config.statusline?.style} highlight={config.statusline?.highlight} />
|
|
84
|
+
</Show>
|
|
85
|
+
</box>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await render(() => <Otmd />, renderer)
|
|
90
|
+
|
|
91
|
+
async function readFile(filePath: string) {
|
|
92
|
+
const file = Bun.file(filePath)
|
|
93
|
+
if (!(await file.exists())) fail(`file not found: ${filePath}`)
|
|
94
|
+
try {
|
|
95
|
+
return await file.text()
|
|
96
|
+
} catch (error) {
|
|
97
|
+
fail(`unable to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opentui-markdown-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenTUI Markdown renderer with optional native LaTeX math",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"otmd": "bin/otmd.tsx"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./src/index.ts",
|
|
9
12
|
"./core": "./src/core/index.ts"
|
|
10
13
|
},
|
|
11
14
|
"files": [
|
|
12
15
|
"src",
|
|
16
|
+
"bin",
|
|
13
17
|
"native/Cargo.toml",
|
|
14
18
|
"native/Cargo.lock",
|
|
15
19
|
"native/build.rs",
|
|
16
20
|
"native/src",
|
|
17
21
|
"prebuilds",
|
|
22
|
+
"themes",
|
|
18
23
|
"README.md",
|
|
19
24
|
"LICENSE",
|
|
20
25
|
"NOTICE"
|
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
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { loadConfig, loadRendererConfig, discoverConfig, parseConfig, configFilenames, type ConfigReporter } from "./load"
|
|
1
|
+
export { loadConfig, loadRendererConfig, discoverConfig, parseConfig, configFilenames, xdgConfigFilenames, xdgConfigDirectory, type ConfigEnvironment, type ConfigReporter } from "./load"
|
|
2
2
|
export {
|
|
3
3
|
resolveConfiguredTheme,
|
|
4
4
|
resolveConfiguredLayout,
|
|
@@ -31,6 +31,7 @@ export type {
|
|
|
31
31
|
StatusLineStyle,
|
|
32
32
|
ImagesConfig,
|
|
33
33
|
MermaidConfig,
|
|
34
|
+
ThemeConfig,
|
|
34
35
|
HeadingsConfig,
|
|
35
36
|
HeadingLevelConfig,
|
|
36
37
|
HeadingLevelKey,
|
package/src/config/load.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
2
3
|
import path from "node:path"
|
|
3
4
|
import { isFrameAlign, isFrameStyle } from "../core/frame"
|
|
4
5
|
import { isTableColumnFitter, isTableOverflow, isTableWidthMode } from "../core/tables"
|
|
5
6
|
import { isHeadingLevelKey, isStatusLineField, isStatusLineStyle, type HeadingLevelConfig, type HeadingsConfig, type MarkdownConfig, type StatusLineField } from "./types"
|
|
6
|
-
import {
|
|
7
|
+
import { validateThemeConfig } from "./theme"
|
|
7
8
|
|
|
8
9
|
export const configFilenames = ["opentui-markdown-render.jsonc", "opentui-markdown-render.json"]
|
|
10
|
+
export const xdgConfigFilenames = ["config.jsonc", "config.json"]
|
|
11
|
+
export const xdgConfigDirectory = "otmd"
|
|
9
12
|
|
|
10
13
|
// Config warnings normally go to stderr. A host that embeds the renderer can
|
|
11
14
|
// collect them instead and show them in its own UI.
|
|
@@ -46,7 +49,15 @@ export function loadRendererConfig(
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
export
|
|
52
|
+
export type ConfigEnvironment = {
|
|
53
|
+
readonly XDG_CONFIG_HOME?: string
|
|
54
|
+
readonly HOME?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function discoverConfig(
|
|
58
|
+
start: string,
|
|
59
|
+
environment: ConfigEnvironment = { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, HOME: process.env.HOME },
|
|
60
|
+
): string | undefined {
|
|
50
61
|
let directory = path.resolve(start)
|
|
51
62
|
while (true) {
|
|
52
63
|
for (const name of configFilenames) {
|
|
@@ -54,24 +65,42 @@ export function discoverConfig(start: string): string | undefined {
|
|
|
54
65
|
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate
|
|
55
66
|
}
|
|
56
67
|
const parent = path.dirname(directory)
|
|
57
|
-
if (parent === directory)
|
|
68
|
+
if (parent === directory) break
|
|
58
69
|
directory = parent
|
|
59
70
|
}
|
|
71
|
+
|
|
72
|
+
return discoverXdgConfig(environment)
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
function discoverXdgConfig(environment: ConfigEnvironment): string | undefined {
|
|
76
|
+
const configured = environment.XDG_CONFIG_HOME
|
|
77
|
+
const configHome = configured && path.isAbsolute(configured)
|
|
78
|
+
? configured
|
|
79
|
+
: path.join(environment.HOME || os.homedir(), ".config")
|
|
80
|
+
const directory = path.join(configHome, xdgConfigDirectory)
|
|
81
|
+
for (const name of xdgConfigFilenames) {
|
|
82
|
+
const candidate = path.join(directory, name)
|
|
83
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate
|
|
68
84
|
}
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseConfig(source: string, file = "config", report?: ConfigReporter): MarkdownConfig {
|
|
89
|
+
const parsed = parseJsonc(source, file)
|
|
69
90
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
70
91
|
throw new Error(`Invalid config ${file}: expected a JSON object`)
|
|
71
92
|
}
|
|
72
93
|
return validateConfig(parsed as Record<string, unknown>, file, report)
|
|
73
94
|
}
|
|
74
95
|
|
|
96
|
+
export function parseJsonc(source: string, file = "config"): unknown {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(stripJsonComments(source))
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`Invalid config ${file}: ${error instanceof Error ? error.message : String(error)}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
const sections = ["theme", "statusline", "images", "mermaid", "headings", "frame", "tables"] as const
|
|
76
105
|
|
|
77
106
|
function validateConfig(value: Record<string, unknown>, file: string, report?: ConfigReporter): MarkdownConfig {
|
|
@@ -82,7 +111,7 @@ function validateConfig(value: Record<string, unknown>, file: string, report?: C
|
|
|
82
111
|
}
|
|
83
112
|
}
|
|
84
113
|
return {
|
|
85
|
-
theme: isRecord(value.theme) ?
|
|
114
|
+
theme: isRecord(value.theme) ? validateThemeConfig(value.theme, file, report) : undefined,
|
|
86
115
|
statusline: isRecord(value.statusline) ? validateStatusLine(value.statusline, file, report) : undefined,
|
|
87
116
|
images: isRecord(value.images) ? validateImages(value.images) : undefined,
|
|
88
117
|
mermaid: isRecord(value.mermaid) ? validateMermaid(value.mermaid, file, report) : undefined,
|
package/src/config/resolve.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Theme, ThemePreset } from "../theme/types"
|
|
2
2
|
import { resolveTheme } from "../theme/resolve"
|
|
3
|
+
import { loadTheme } from "../theme/load"
|
|
3
4
|
import { createDocumentLayout, type DocumentLayout, type FrameConfig, type TablesConfig } from "../core"
|
|
4
5
|
import type { MarkdownConfig } from "./types"
|
|
5
6
|
|
|
@@ -15,10 +16,8 @@ export type ThemeRuntime = {
|
|
|
15
16
|
export function resolveConfiguredTheme(config: MarkdownConfig, runtime: ThemeRuntime = {}): Theme {
|
|
16
17
|
if (runtime.theme) return runtime.theme
|
|
17
18
|
const explicit = runtime.themeMode ?? undefined
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
// host setting must win over the mode in the file, including for defaults.
|
|
21
|
-
const override = explicit ? { ...config.theme, mode } : config.theme
|
|
19
|
+
const override = loadTheme(explicit ?? config.theme?.name ?? "light")
|
|
20
|
+
const mode = override?.mode ?? "light"
|
|
22
21
|
return resolveTheme({ mode, override })
|
|
23
22
|
}
|
|
24
23
|
|
package/src/config/theme.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThemeOverride } from "../theme/types"
|
|
2
|
+
import type { ThemeConfig } from "./types"
|
|
2
3
|
import type { ConfigReporter } from "./load"
|
|
3
4
|
|
|
4
5
|
// A JSON file only carries strings, not RGBA instances. OpenTUI accepts these
|
|
@@ -11,14 +12,15 @@ const colorNames = new Set([
|
|
|
11
12
|
"brightblue", "brightyellow", "brightcyan", "brightmagenta", "brightwhite",
|
|
12
13
|
])
|
|
13
14
|
|
|
14
|
-
type Field = "color" | "mode" | "colors" | { readonly [key: string]: Field }
|
|
15
|
+
type Field = "name" | "color" | "mode" | "colors" | { readonly [key: string]: Field }
|
|
15
16
|
const colors = <K extends string>(keys: readonly K[]): Record<K, Field> =>
|
|
16
17
|
Object.fromEntries(keys.map((key) => [key, "color"])) as Record<K, Field>
|
|
17
18
|
|
|
18
19
|
// The allowlist follows ThemeOverride, not the resolved Theme: headingLevels
|
|
19
20
|
// is an internal six-colour array, while heading1..heading6 are file options.
|
|
20
|
-
const
|
|
21
|
+
const themeFileFields: Record<string, Field> = {
|
|
21
22
|
mode: "mode",
|
|
23
|
+
palette: "colors",
|
|
22
24
|
accent: "color",
|
|
23
25
|
categorical: "colors",
|
|
24
26
|
text: {
|
|
@@ -84,5 +86,25 @@ export function validateTheme(value: Record<string, unknown>, file: string, repo
|
|
|
84
86
|
}
|
|
85
87
|
return result
|
|
86
88
|
}
|
|
87
|
-
return validate(value,
|
|
89
|
+
return validate(value, themeFileFields, "theme") as ThemeOverride
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function validateThemeConfig(value: Record<string, unknown>, file: string, report?: ConfigReporter): ThemeConfig {
|
|
93
|
+
const warn = (message: string) => {
|
|
94
|
+
if (report) report(message)
|
|
95
|
+
else console.warn(`[opentui-markdown-render] ${message}`)
|
|
96
|
+
}
|
|
97
|
+
const result: { name?: string } = {}
|
|
98
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
99
|
+
if (key !== "name") {
|
|
100
|
+
warn(`Unknown theme option "theme.${key}" in ${file}`)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (typeof entry !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(entry)) {
|
|
104
|
+
warn(`Invalid theme option "theme.name" in ${file}`)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
result.name = entry
|
|
108
|
+
}
|
|
109
|
+
return result
|
|
88
110
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { FrameConfig } from "../core/frame"
|
|
2
2
|
import type { TablesConfig } from "../core/tables"
|
|
3
|
-
import type { ThemeOverride } from "../theme/types"
|
|
4
3
|
|
|
5
4
|
// The configuration file is a plain JSON/JSONC object. Every section is
|
|
6
5
|
// optional; a missing section keeps the bundled default.
|
|
7
6
|
export type MarkdownConfig = {
|
|
8
|
-
//
|
|
9
|
-
readonly theme?:
|
|
7
|
+
// Selects a complete theme file from the bundled or XDG theme directory.
|
|
8
|
+
readonly theme?: ThemeConfig
|
|
10
9
|
readonly statusline?: StatusLineConfig
|
|
11
10
|
readonly images?: ImagesConfig
|
|
12
11
|
readonly mermaid?: MermaidConfig
|
|
@@ -17,6 +16,10 @@ export type MarkdownConfig = {
|
|
|
17
16
|
readonly tables?: TablesConfig
|
|
18
17
|
}
|
|
19
18
|
|
|
19
|
+
export type ThemeConfig = {
|
|
20
|
+
readonly name?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
20
23
|
// The status line is a single row of labelled fields. `fields` selects and
|
|
21
24
|
// orders them; a field without a value is skipped.
|
|
22
25
|
export type StatusLineConfig = {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { MarkdownRenderer, resolveRendererTheme, loadRendererConfig, type MarkdownRendererProps, type MarkdownRendererCommonProps, type MarkdownDiagnostic, type ThemeModeSetting } from "./app"
|
|
2
|
+
export { cliUsage, parseCliArgs, type CliCommand } from "./cli"
|
|
2
3
|
export type { RenderUnit } from "./view/document"
|
|
3
4
|
export { parseDocument, type Document, type Block, type Inline, type ListItem, type TableCell, type SourceRange, type UnitInfo } from "./core"
|
|
4
5
|
export {
|
|
@@ -58,6 +59,8 @@ export {
|
|
|
58
59
|
discoverConfig,
|
|
59
60
|
parseConfig,
|
|
60
61
|
configFilenames,
|
|
62
|
+
xdgConfigFilenames,
|
|
63
|
+
xdgConfigDirectory,
|
|
61
64
|
resolveConfiguredTheme,
|
|
62
65
|
resolveConfiguredLayout,
|
|
63
66
|
statusLineFields,
|
|
@@ -71,15 +74,20 @@ export {
|
|
|
71
74
|
type StatusLineStyle,
|
|
72
75
|
type ImagesConfig,
|
|
73
76
|
type MermaidConfig,
|
|
77
|
+
type ThemeConfig,
|
|
74
78
|
type HeadingsConfig,
|
|
75
79
|
type HeadingLevelConfig,
|
|
76
80
|
type HeadingLevelKey,
|
|
77
81
|
type ConfigReporter,
|
|
82
|
+
type ConfigEnvironment,
|
|
78
83
|
type ThemeRuntime,
|
|
79
84
|
type LayoutRuntime,
|
|
80
85
|
} from "./config"
|
|
81
86
|
export {
|
|
82
87
|
defaultTheme,
|
|
88
|
+
loadTheme,
|
|
89
|
+
bundledThemeNames,
|
|
90
|
+
themeFileNames,
|
|
83
91
|
generateSyntax,
|
|
84
92
|
resolveTheme,
|
|
85
93
|
mergeTheme,
|
|
@@ -89,6 +97,7 @@ export {
|
|
|
89
97
|
type ThemeMode,
|
|
90
98
|
type ThemePreset,
|
|
91
99
|
type ThemeOverride,
|
|
100
|
+
type ThemeEnvironment,
|
|
92
101
|
type StatusLineTheme,
|
|
93
102
|
type StatusLineThemeOverride,
|
|
94
103
|
type MarkdownThemeOverride,
|
package/src/theme/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { defaultTheme } from "./defaults"
|
|
2
|
+
export { loadTheme, bundledThemeNames, themeFileNames } from "./load"
|
|
3
|
+
export type { ThemeEnvironment } from "./load"
|
|
2
4
|
export { generateSyntax } from "./syntax"
|
|
3
5
|
export { resolveTheme, mergeTheme, deriveStatusLineTheme } from "./resolve"
|
|
4
6
|
export { mixColors } from "./colors"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { parseJsonc } from "../config/load"
|
|
5
|
+
import { validateTheme } from "../config/theme"
|
|
6
|
+
import type { ThemeOverride } from "./types"
|
|
7
|
+
|
|
8
|
+
export const bundledThemeNames = ["light", "dark", "solarized-light", "solarized-dark"] as const
|
|
9
|
+
export const themeFileNames = [".jsonc", ".json"] as const
|
|
10
|
+
|
|
11
|
+
export type ThemeEnvironment = {
|
|
12
|
+
readonly XDG_CONFIG_HOME?: string
|
|
13
|
+
readonly HOME?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function loadTheme(
|
|
17
|
+
name: string,
|
|
18
|
+
environment: ThemeEnvironment = { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, HOME: process.env.HOME },
|
|
19
|
+
): ThemeOverride | undefined {
|
|
20
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) return undefined
|
|
21
|
+
const configured = environment.XDG_CONFIG_HOME
|
|
22
|
+
const configHome = configured && path.isAbsolute(configured)
|
|
23
|
+
? configured
|
|
24
|
+
: path.join(environment.HOME || os.homedir(), ".config")
|
|
25
|
+
const directories = [
|
|
26
|
+
path.join(configHome, "otmd", "themes"),
|
|
27
|
+
path.resolve(import.meta.dir, "..", "..", "themes"),
|
|
28
|
+
]
|
|
29
|
+
for (const directory of directories) {
|
|
30
|
+
for (const suffix of themeFileNames) {
|
|
31
|
+
const file = path.join(directory, `${name}${suffix}`)
|
|
32
|
+
if (!existsSync(file) || !statSync(file).isFile()) continue
|
|
33
|
+
const parsed = parseJsonc(readFileSync(file, "utf8"), file)
|
|
34
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined
|
|
35
|
+
return validateTheme(parsed as Record<string, unknown>, file)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
package/src/theme/resolve.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function mergeTheme(base: Theme, override: ThemeOverride | undefined): Th
|
|
|
37
37
|
if (!override) return base
|
|
38
38
|
const color = (value: ColorInput | undefined, fallback: RGBA) => (value === undefined ? fallback : parseColor(value))
|
|
39
39
|
const accent = color(override.accent, base.accent)
|
|
40
|
+
const palette = override.palette ? override.palette.map((entry) => parseColor(entry)) : base.palette
|
|
40
41
|
const background = { base: color(override.background?.base, base.background.base) }
|
|
41
42
|
const text = {
|
|
42
43
|
base: color(override.text?.base, base.text.base),
|
|
@@ -56,10 +57,10 @@ export function mergeTheme(base: Theme, override: ThemeOverride | undefined): Th
|
|
|
56
57
|
),
|
|
57
58
|
}
|
|
58
59
|
const mode = override.mode ? (override.mode.endsWith("light") ? "light" : "dark") : base.mode
|
|
59
|
-
const derived = deriveStatusLineTheme({ mode, accent, background, text, palette
|
|
60
|
+
const derived = deriveStatusLineTheme({ mode, accent, background, text, palette })
|
|
60
61
|
return {
|
|
61
62
|
mode,
|
|
62
|
-
palette
|
|
63
|
+
palette,
|
|
63
64
|
accent,
|
|
64
65
|
categorical: override.categorical ? override.categorical.map((entry) => parseColor(entry)) : base.categorical,
|
|
65
66
|
text: {
|
package/src/theme/types.ts
CHANGED
|
@@ -120,10 +120,11 @@ export type MarkdownThemeOverride = {
|
|
|
120
120
|
readonly heading6?: ColorInput
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// Host
|
|
124
|
-
//
|
|
123
|
+
// Host-side theme APIs accept a partial color-input override. JSON/JSONC
|
|
124
|
+
// application config selects a complete theme file by name instead.
|
|
125
125
|
export type ThemeOverride = {
|
|
126
126
|
readonly mode?: ThemePreset
|
|
127
|
+
readonly palette?: readonly ColorInput[]
|
|
127
128
|
readonly accent?: ColorInput
|
|
128
129
|
readonly categorical?: readonly ColorInput[]
|
|
129
130
|
readonly text?: {
|
package/themes/dark.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mode": "dark",
|
|
3
|
+
"accent": "#58a6ff",
|
|
4
|
+
"categorical": ["#58a6ff", "#d2a8ff", "#3fb950", "#d29922", "#f85149", "#ffa657"],
|
|
5
|
+
"text": {
|
|
6
|
+
"base": "#e6edf3",
|
|
7
|
+
"muted": "#8b949e",
|
|
8
|
+
"feedback": { "error": "#f85149", "warning": "#d29922", "success": "#3fb950", "info": "#58a6ff" },
|
|
9
|
+
"action": { "primary": { "focused": "#58a6ff" } }
|
|
10
|
+
},
|
|
11
|
+
"background": { "base": "#0d1117" },
|
|
12
|
+
"border": { "base": "#30363d" },
|
|
13
|
+
"frame": { "border": "#30363d", "title": "#8b949e" },
|
|
14
|
+
"markdown": {
|
|
15
|
+
"text": "#e6edf3", "heading": "#d2a8ff",
|
|
16
|
+
"heading1": "#58a6ff", "heading2": "#d2a8ff", "heading3": "#d2a8ff",
|
|
17
|
+
"heading4": "#8b949e", "heading5": "#8b949e", "heading6": "#8b949e",
|
|
18
|
+
"link": "#58a6ff", "linkText": "#58a6ff", "code": "#ffa657",
|
|
19
|
+
"blockQuote": "#8b949e", "emphasis": "#e6edf3", "strong": "#e6edf3",
|
|
20
|
+
"horizontalRule": "#30363d", "listItem": "#e6edf3", "listEnumeration": "#e6edf3",
|
|
21
|
+
"image": "#58a6ff", "imageText": "#58a6ff", "codeBlock": "#e6edf3"
|
|
22
|
+
},
|
|
23
|
+
"syntax": {
|
|
24
|
+
"comment": "#8b949e", "keyword": "#ff7b72", "function": "#d2a8ff",
|
|
25
|
+
"variable": "#ffa657", "string": "#a5d6ff", "number": "#79c0ff",
|
|
26
|
+
"type": "#ffa657", "operator": "#ff7b72", "punctuation": "#c9d1d9"
|
|
27
|
+
},
|
|
28
|
+
"diff": {
|
|
29
|
+
"text": { "added": "#3fb950", "removed": "#f85149", "context": "#8b949e" },
|
|
30
|
+
"background": { "added": "#12261e", "removed": "#25171c", "context": "#161b22" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
// The semantic mode controls contrast-sensitive derived colours.
|
|
3
|
+
"mode": "dark",
|
|
4
|
+
|
|
5
|
+
// Optional fixed Mermaid diagram palette. Omit it to derive diagram colours.
|
|
6
|
+
"palette": [
|
|
7
|
+
"#58a6ff", "#d2a8ff", "#3fb950", "#d29922", "#f85149", "#ffa657"
|
|
8
|
+
],
|
|
9
|
+
|
|
10
|
+
// Main accent and the categorical colours used by diagrams and UI elements.
|
|
11
|
+
"accent": "#58a6ff",
|
|
12
|
+
"categorical": ["#58a6ff", "#d2a8ff", "#3fb950", "#d29922", "#f85149", "#ffa657"],
|
|
13
|
+
|
|
14
|
+
"text": {
|
|
15
|
+
"base": "#e6edf3",
|
|
16
|
+
"muted": "#8b949e",
|
|
17
|
+
"feedback": {
|
|
18
|
+
"error": "#f85149",
|
|
19
|
+
"warning": "#d29922",
|
|
20
|
+
"success": "#3fb950",
|
|
21
|
+
"info": "#58a6ff"
|
|
22
|
+
},
|
|
23
|
+
"action": {
|
|
24
|
+
"primary": {
|
|
25
|
+
"focused": "#58a6ff"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
"background": {
|
|
31
|
+
"base": "#0d1117"
|
|
32
|
+
},
|
|
33
|
+
"border": {
|
|
34
|
+
"base": "#30363d"
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Frame and statusline are optional in the resolved API; these examples show
|
|
38
|
+
// the complete file shape and can be adjusted independently.
|
|
39
|
+
"frame": {
|
|
40
|
+
"border": "#30363d",
|
|
41
|
+
"title": "#8b949e"
|
|
42
|
+
},
|
|
43
|
+
"statusline": {
|
|
44
|
+
"background": "#161b22",
|
|
45
|
+
"foreground": "#e6edf3",
|
|
46
|
+
"title": "#58a6ff",
|
|
47
|
+
"muted": "#8b949e",
|
|
48
|
+
"border": "#58a6ff"
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"markdown": {
|
|
52
|
+
"text": "#e6edf3",
|
|
53
|
+
"heading": "#d2a8ff",
|
|
54
|
+
"heading1": "#58a6ff",
|
|
55
|
+
"heading2": "#d2a8ff",
|
|
56
|
+
"heading3": "#d2a8ff",
|
|
57
|
+
"heading4": "#8b949e",
|
|
58
|
+
"heading5": "#8b949e",
|
|
59
|
+
"heading6": "#8b949e",
|
|
60
|
+
"link": "#58a6ff",
|
|
61
|
+
"linkText": "#58a6ff",
|
|
62
|
+
"code": "#ffa657",
|
|
63
|
+
"blockQuote": "#8b949e",
|
|
64
|
+
"emphasis": "#e6edf3",
|
|
65
|
+
"strong": "#e6edf3",
|
|
66
|
+
"horizontalRule": "#30363d",
|
|
67
|
+
"listItem": "#e6edf3",
|
|
68
|
+
"listEnumeration": "#e6edf3",
|
|
69
|
+
"image": "#58a6ff",
|
|
70
|
+
"imageText": "#58a6ff",
|
|
71
|
+
"codeBlock": "#e6edf3"
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"syntax": {
|
|
75
|
+
"comment": "#8b949e",
|
|
76
|
+
"keyword": "#ff7b72",
|
|
77
|
+
"function": "#d2a8ff",
|
|
78
|
+
"variable": "#ffa657",
|
|
79
|
+
"string": "#a5d6ff",
|
|
80
|
+
"number": "#79c0ff",
|
|
81
|
+
"type": "#ffa657",
|
|
82
|
+
"operator": "#ff7b72",
|
|
83
|
+
"punctuation": "#c9d1d9"
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"diff": {
|
|
87
|
+
"text": {
|
|
88
|
+
"added": "#3fb950",
|
|
89
|
+
"removed": "#f85149",
|
|
90
|
+
"context": "#8b949e"
|
|
91
|
+
},
|
|
92
|
+
"background": {
|
|
93
|
+
"added": "#12261e",
|
|
94
|
+
"removed": "#25171c",
|
|
95
|
+
"context": "#161b22"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mode": "light",
|
|
3
|
+
"accent": "#0969da",
|
|
4
|
+
"categorical": ["#0969da", "#8250df", "#1a7f37", "#9a6700", "#cf222e", "#bc4c00"],
|
|
5
|
+
"text": {
|
|
6
|
+
"base": "#1f2328",
|
|
7
|
+
"muted": "#59636e",
|
|
8
|
+
"feedback": { "error": "#cf222e", "warning": "#9a6700", "success": "#1a7f37", "info": "#0969da" },
|
|
9
|
+
"action": { "primary": { "focused": "#0969da" } }
|
|
10
|
+
},
|
|
11
|
+
"background": { "base": "#ffffff" },
|
|
12
|
+
"border": { "base": "#8c959f" },
|
|
13
|
+
"frame": { "border": "#8c959f", "title": "#59636e" },
|
|
14
|
+
"markdown": {
|
|
15
|
+
"text": "#1f2328", "heading": "#8250df",
|
|
16
|
+
"heading1": "#0969da", "heading2": "#8250df", "heading3": "#8250df",
|
|
17
|
+
"heading4": "#6f42c1", "heading5": "#3d444d", "heading6": "#3d444d",
|
|
18
|
+
"link": "#0969da", "linkText": "#0969da", "code": "#953800",
|
|
19
|
+
"blockQuote": "#59636e", "emphasis": "#1f2328", "strong": "#1f2328",
|
|
20
|
+
"horizontalRule": "#d1d9e0", "listItem": "#1f2328", "listEnumeration": "#1f2328",
|
|
21
|
+
"image": "#0969da", "imageText": "#0969da", "codeBlock": "#1f2328"
|
|
22
|
+
},
|
|
23
|
+
"syntax": {
|
|
24
|
+
"comment": "#59636e", "keyword": "#cf222e", "function": "#8250df",
|
|
25
|
+
"variable": "#1f2328", "string": "#0a3069", "number": "#0550ae",
|
|
26
|
+
"type": "#0550ae", "operator": "#0550ae", "punctuation": "#1f2328"
|
|
27
|
+
},
|
|
28
|
+
"diff": {
|
|
29
|
+
"text": { "added": "#1a7f37", "removed": "#cf222e", "context": "#59636e" },
|
|
30
|
+
"background": { "added": "#dafbe1", "removed": "#ffebe9", "context": "#f6f8fa" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mode": "dark",
|
|
3
|
+
"palette": ["#002b36", "#073642", "#586e75", "#657b83", "#839496", "#93a1a1", "#eee8d5", "#fdf6e3", "#b58900", "#cb4b16", "#dc322f", "#d33682", "#6c71c4", "#268bd2", "#2aa198", "#859900"],
|
|
4
|
+
"accent": "#268bd2",
|
|
5
|
+
"categorical": ["#268bd2", "#6c71c4", "#859900", "#b58900", "#dc322f", "#cb4b16"],
|
|
6
|
+
"text": {
|
|
7
|
+
"base": "#93a1a1", "muted": "#839496",
|
|
8
|
+
"feedback": { "error": "#dc322f", "warning": "#b58900", "success": "#859900", "info": "#268bd2" },
|
|
9
|
+
"action": { "primary": { "focused": "#268bd2" } }
|
|
10
|
+
},
|
|
11
|
+
"background": { "base": "#002b36" },
|
|
12
|
+
"border": { "base": "#586e75" },
|
|
13
|
+
"frame": { "border": "#586e75", "title": "#93a1a1" },
|
|
14
|
+
"markdown": {
|
|
15
|
+
"text": "#93a1a1", "heading": "#268bd2",
|
|
16
|
+
"heading1": "#268bd2", "heading2": "#268bd2", "heading3": "#268bd2",
|
|
17
|
+
"heading4": "#268bd2", "heading5": "#268bd2", "heading6": "#268bd2",
|
|
18
|
+
"link": "#268bd2", "linkText": "#268bd2", "code": "#cb4b16",
|
|
19
|
+
"blockQuote": "#839496", "emphasis": "#93a1a1", "strong": "#93a1a1",
|
|
20
|
+
"horizontalRule": "#586e75", "listItem": "#93a1a1", "listEnumeration": "#93a1a1",
|
|
21
|
+
"image": "#268bd2", "imageText": "#268bd2", "codeBlock": "#93a1a1"
|
|
22
|
+
},
|
|
23
|
+
"syntax": {
|
|
24
|
+
"comment": "#839496", "keyword": "#dc322f", "function": "#6c71c4",
|
|
25
|
+
"variable": "#93a1a1", "string": "#2aa198", "number": "#d33682",
|
|
26
|
+
"type": "#b58900", "operator": "#268bd2", "punctuation": "#93a1a1"
|
|
27
|
+
},
|
|
28
|
+
"diff": {
|
|
29
|
+
"text": { "added": "#859900", "removed": "#dc322f", "context": "#839496" },
|
|
30
|
+
"background": { "added": "#073642", "removed": "#073642", "context": "#073642" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mode": "light",
|
|
3
|
+
"palette": ["#002b36", "#073642", "#586e75", "#657b83", "#839496", "#93a1a1", "#eee8d5", "#fdf6e3", "#b58900", "#cb4b16", "#dc322f", "#d33682", "#6c71c4", "#268bd2", "#2aa198", "#859900"],
|
|
4
|
+
"accent": "#268bd2",
|
|
5
|
+
"categorical": ["#268bd2", "#6c71c4", "#859900", "#b58900", "#dc322f", "#cb4b16"],
|
|
6
|
+
"text": {
|
|
7
|
+
"base": "#586e75", "muted": "#657b83",
|
|
8
|
+
"feedback": { "error": "#dc322f", "warning": "#b58900", "success": "#859900", "info": "#268bd2" },
|
|
9
|
+
"action": { "primary": { "focused": "#268bd2" } }
|
|
10
|
+
},
|
|
11
|
+
"background": { "base": "#fdf6e3" },
|
|
12
|
+
"border": { "base": "#657b83" },
|
|
13
|
+
"frame": { "border": "#657b83", "title": "#586e75" },
|
|
14
|
+
"markdown": {
|
|
15
|
+
"text": "#586e75", "heading": "#268bd2",
|
|
16
|
+
"heading1": "#268bd2", "heading2": "#268bd2", "heading3": "#268bd2",
|
|
17
|
+
"heading4": "#268bd2", "heading5": "#268bd2", "heading6": "#268bd2",
|
|
18
|
+
"link": "#268bd2", "linkText": "#268bd2", "code": "#cb4b16",
|
|
19
|
+
"blockQuote": "#657b83", "emphasis": "#586e75", "strong": "#586e75",
|
|
20
|
+
"horizontalRule": "#657b83", "listItem": "#586e75", "listEnumeration": "#586e75",
|
|
21
|
+
"image": "#268bd2", "imageText": "#268bd2", "codeBlock": "#586e75"
|
|
22
|
+
},
|
|
23
|
+
"syntax": {
|
|
24
|
+
"comment": "#657b83", "keyword": "#dc322f", "function": "#6c71c4",
|
|
25
|
+
"variable": "#586e75", "string": "#2aa198", "number": "#d33682",
|
|
26
|
+
"type": "#b58900", "operator": "#268bd2", "punctuation": "#586e75"
|
|
27
|
+
},
|
|
28
|
+
"diff": {
|
|
29
|
+
"text": { "added": "#859900", "removed": "#dc322f", "context": "#657b83" },
|
|
30
|
+
"background": { "added": "#eee8d5", "removed": "#eee8d5", "context": "#eee8d5" }
|
|
31
|
+
}
|
|
32
|
+
}
|