mkdnsite 0.0.1
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/LICENSE +21 -0
- package/README.md +211 -0
- package/package.json +72 -0
- package/src/adapters/cloudflare.ts +85 -0
- package/src/adapters/fly.ts +22 -0
- package/src/adapters/local.ts +153 -0
- package/src/adapters/netlify.ts +17 -0
- package/src/adapters/types.ts +54 -0
- package/src/adapters/vercel.ts +48 -0
- package/src/cli.ts +140 -0
- package/src/client/scripts.ts +106 -0
- package/src/config/defaults.ts +68 -0
- package/src/config/schema.ts +192 -0
- package/src/content/filesystem.ts +210 -0
- package/src/content/frontmatter.ts +66 -0
- package/src/content/github.ts +211 -0
- package/src/content/types.ts +86 -0
- package/src/discovery/llmstxt.ts +70 -0
- package/src/handler.ts +188 -0
- package/src/index.ts +57 -0
- package/src/negotiate/accept.ts +72 -0
- package/src/negotiate/headers.ts +56 -0
- package/src/render/bun-native.ts +54 -0
- package/src/render/components/index.ts +149 -0
- package/src/render/page-shell.ts +121 -0
- package/src/render/portable.ts +222 -0
- package/src/render/types.ts +74 -0
- package/src/theme/prose-css.ts +377 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { MkdnSiteConfig } from '../config/schema.ts'
|
|
2
|
+
import type { MarkdownMeta, NavNode } from '../content/types.ts'
|
|
3
|
+
import { THEME_CSS } from '../theme/prose-css.ts'
|
|
4
|
+
import { CLIENT_SCRIPTS } from '../client/scripts.ts'
|
|
5
|
+
|
|
6
|
+
interface PageShellProps {
|
|
7
|
+
renderedContent: string
|
|
8
|
+
meta: MarkdownMeta
|
|
9
|
+
config: MkdnSiteConfig
|
|
10
|
+
nav?: NavNode
|
|
11
|
+
currentSlug: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a full HTML page wrapping the markdown content.
|
|
16
|
+
* This is pure SSR — no client-side React hydration required.
|
|
17
|
+
*/
|
|
18
|
+
export function renderPage (props: PageShellProps): string {
|
|
19
|
+
const { renderedContent, meta, config, nav, currentSlug } = props
|
|
20
|
+
|
|
21
|
+
const title = meta.title != null
|
|
22
|
+
? `${meta.title} — ${config.site.title}`
|
|
23
|
+
: config.site.title
|
|
24
|
+
const description = meta.description ?? config.site.description ?? ''
|
|
25
|
+
const lang = config.site.lang ?? 'en'
|
|
26
|
+
|
|
27
|
+
const navHtml = (config.theme.showNav && nav != null)
|
|
28
|
+
? renderNav(nav, currentSlug)
|
|
29
|
+
: ''
|
|
30
|
+
|
|
31
|
+
const clientScripts = config.client.enabled
|
|
32
|
+
? CLIENT_SCRIPTS(config.client)
|
|
33
|
+
: ''
|
|
34
|
+
|
|
35
|
+
const themeToggleEnabled = config.client.enabled && config.client.themeToggle
|
|
36
|
+
const colorScheme = config.theme.colorScheme ?? 'system'
|
|
37
|
+
|
|
38
|
+
// Blocking script to set data-theme before paint (prevents FOUC)
|
|
39
|
+
const themeInitScript = colorScheme !== 'system'
|
|
40
|
+
// Fixed color scheme: always use the configured value
|
|
41
|
+
? `<script>document.documentElement.setAttribute("data-theme","${colorScheme}")</script>`
|
|
42
|
+
: themeToggleEnabled
|
|
43
|
+
// System default + toggle: check localStorage first, then system preference
|
|
44
|
+
? '<script>(function(){var t=localStorage.getItem("mkdn-theme");if(t==="dark"||t==="light"){document.documentElement.setAttribute("data-theme",t)}else{document.documentElement.setAttribute("data-theme",window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light")}})()</script>'
|
|
45
|
+
// System default, no toggle: just respect system preference
|
|
46
|
+
: '<script>(function(){document.documentElement.setAttribute("data-theme",window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light")})()</script>'
|
|
47
|
+
|
|
48
|
+
const themeToggleHtml = themeToggleEnabled
|
|
49
|
+
? `<button class="mkdn-theme-toggle" type="button" aria-label="Toggle theme">
|
|
50
|
+
<svg class="mkdn-icon-sun" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
51
|
+
<svg class="mkdn-icon-moon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
|
52
|
+
</button>`
|
|
53
|
+
: ''
|
|
54
|
+
|
|
55
|
+
return `<!DOCTYPE html>
|
|
56
|
+
<html lang="${esc(lang)}">
|
|
57
|
+
<head>
|
|
58
|
+
<meta charset="utf-8">
|
|
59
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
60
|
+
<title>${esc(title)}</title>
|
|
61
|
+
${description !== '' ? `<meta name="description" content="${esc(description)}">` : ''}
|
|
62
|
+
<meta name="generator" content="mkdnsite">
|
|
63
|
+
${config.client.math ? '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css" crossorigin="anonymous">' : ''}
|
|
64
|
+
<style>${THEME_CSS}</style>
|
|
65
|
+
${themeInitScript}
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
${themeToggleHtml}
|
|
69
|
+
<div class="mkdn-layout">
|
|
70
|
+
${navHtml !== '' ? `<nav class="mkdn-nav" aria-label="Site navigation">${navHtml}</nav>` : ''}
|
|
71
|
+
<main class="mkdn-main">
|
|
72
|
+
<article class="mkdn-article mkdn-prose">
|
|
73
|
+
${renderedContent}
|
|
74
|
+
</article>
|
|
75
|
+
<footer class="mkdn-footer">
|
|
76
|
+
<p>Powered by <a href="https://mkdn.site">mkdnsite</a></p>
|
|
77
|
+
</footer>
|
|
78
|
+
</main>
|
|
79
|
+
</div>
|
|
80
|
+
${clientScripts}
|
|
81
|
+
</body>
|
|
82
|
+
</html>`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function render404 (config: MkdnSiteConfig): string {
|
|
86
|
+
return renderPage({
|
|
87
|
+
renderedContent: '<h1>404 — Page Not Found</h1><p>The page you\'re looking for doesn\'t exist.</p><p><a href="/">← Back to home</a></p>',
|
|
88
|
+
meta: { title: 'Not Found' },
|
|
89
|
+
config,
|
|
90
|
+
currentSlug: ''
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderNav (node: NavNode, currentSlug: string, depth = 0): string {
|
|
95
|
+
if (depth === 0) {
|
|
96
|
+
const items = node.children.map(c => renderNav(c, currentSlug, 1)).join('\n')
|
|
97
|
+
return `<div class="mkdn-nav-inner"><ul class="mkdn-nav-list">${items}</ul></div>`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isActive = currentSlug === node.slug
|
|
101
|
+
|
|
102
|
+
if (node.isSection && node.children.length > 0) {
|
|
103
|
+
const childItems = node.children
|
|
104
|
+
.map(c => renderNav(c, currentSlug, depth + 1))
|
|
105
|
+
.join('\n')
|
|
106
|
+
return `<li class="mkdn-nav-section">
|
|
107
|
+
<span class="mkdn-nav-section-title">${esc(node.title)}</span>
|
|
108
|
+
<ul>${childItems}</ul>
|
|
109
|
+
</li>`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `<li${isActive ? ' class="active"' : ''}><a href="${node.slug}"${isActive ? ' aria-current="page"' : ''}>${esc(node.title)}</a></li>`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function esc (str: string): string {
|
|
116
|
+
return str
|
|
117
|
+
.replace(/&/g, '&')
|
|
118
|
+
.replace(/</g, '<')
|
|
119
|
+
.replace(/>/g, '>')
|
|
120
|
+
.replace(/"/g, '"')
|
|
121
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import Markdown from 'react-markdown'
|
|
4
|
+
import remarkGfm from 'remark-gfm'
|
|
5
|
+
import remarkMath from 'remark-math'
|
|
6
|
+
import { remarkAlert } from 'remark-github-blockquote-alert'
|
|
7
|
+
import rehypeSlug from 'rehype-slug'
|
|
8
|
+
import rehypeKatex from 'rehype-katex'
|
|
9
|
+
import rehypeRaw from 'rehype-raw'
|
|
10
|
+
import rehypeSanitize from 'rehype-sanitize'
|
|
11
|
+
import { defaultSchema } from 'hast-util-sanitize'
|
|
12
|
+
import type { ReactElement } from 'react'
|
|
13
|
+
import type { Highlighter } from 'shiki'
|
|
14
|
+
import type { ComponentOverrides } from '../config/schema.ts'
|
|
15
|
+
import type { MarkdownRenderer } from './types.ts'
|
|
16
|
+
import { buildComponents } from './components/index.ts'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extended sanitization schema.
|
|
20
|
+
* Starts from the GitHub-style default and adds support for:
|
|
21
|
+
* - className on div, span, p, svg, path (alerts, KaTeX)
|
|
22
|
+
* - SVG elements and attributes (alert icons)
|
|
23
|
+
* - KaTeX attributes (style, aria)
|
|
24
|
+
*/
|
|
25
|
+
const sanitizeSchema = {
|
|
26
|
+
...defaultSchema,
|
|
27
|
+
tagNames: [
|
|
28
|
+
...(defaultSchema.tagNames ?? []),
|
|
29
|
+
'svg', 'path', 'span'
|
|
30
|
+
],
|
|
31
|
+
attributes: {
|
|
32
|
+
...defaultSchema.attributes,
|
|
33
|
+
div: [...(defaultSchema.attributes?.div ?? []), 'className', 'dir'],
|
|
34
|
+
span: [...(defaultSchema.attributes?.span ?? []), 'className', 'style'],
|
|
35
|
+
p: [...(defaultSchema.attributes?.p ?? []), 'className'],
|
|
36
|
+
svg: ['viewBox', 'width', 'height', 'ariaHidden', 'fill', 'xmlns', 'className'],
|
|
37
|
+
path: ['d', 'fill', 'fillRule']
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Portable markdown renderer using react-markdown.
|
|
43
|
+
*
|
|
44
|
+
* Works in any JavaScript runtime (Bun, Node, CF Workers, Deno).
|
|
45
|
+
* Uses remark-gfm for GitHub-Flavored Markdown support and
|
|
46
|
+
* rehype plugins for heading IDs and anchor links.
|
|
47
|
+
*/
|
|
48
|
+
export class PortableRenderer implements MarkdownRenderer {
|
|
49
|
+
readonly engine = 'portable' as const
|
|
50
|
+
private readonly highlighter?: Highlighter
|
|
51
|
+
private readonly syntaxTheme?: string
|
|
52
|
+
private readonly syntaxThemeDark?: string
|
|
53
|
+
private readonly math: boolean
|
|
54
|
+
|
|
55
|
+
constructor (highlighter?: Highlighter, syntaxTheme?: string, syntaxThemeDark?: string, math?: boolean) {
|
|
56
|
+
this.highlighter = highlighter
|
|
57
|
+
this.syntaxTheme = syntaxTheme
|
|
58
|
+
this.syntaxThemeDark = syntaxThemeDark
|
|
59
|
+
this.math = math !== false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
renderToElement (markdown: string, overrides?: ComponentOverrides): ReactElement {
|
|
63
|
+
const components = buildComponents(overrides)
|
|
64
|
+
|
|
65
|
+
// Map our ComponentOverrides to react-markdown's expected component shape
|
|
66
|
+
const rmComponents = mapToReactMarkdownComponents(
|
|
67
|
+
components,
|
|
68
|
+
this.highlighter,
|
|
69
|
+
this.syntaxTheme,
|
|
70
|
+
this.syntaxThemeDark
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const remarkPlugins: any[] = [remarkGfm, remarkAlert]
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
const rehypePlugins: any[] = [rehypeSlug, rehypeRaw, [rehypeSanitize, sanitizeSchema]]
|
|
77
|
+
|
|
78
|
+
if (this.math) {
|
|
79
|
+
remarkPlugins.push(remarkMath)
|
|
80
|
+
// rehype-katex must run before rehype-raw
|
|
81
|
+
rehypePlugins.splice(1, 0, rehypeKatex)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return React.createElement(Markdown, {
|
|
85
|
+
remarkPlugins,
|
|
86
|
+
rehypePlugins,
|
|
87
|
+
components: rmComponents
|
|
88
|
+
}, markdown)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
renderToHtml (markdown: string, overrides?: ComponentOverrides): string {
|
|
92
|
+
const element = this.renderToElement(markdown, overrides)
|
|
93
|
+
return renderToString(element)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract raw text from React children recursively.
|
|
99
|
+
* Needed to get the source code string from react-markdown's code element.
|
|
100
|
+
*/
|
|
101
|
+
function extractText (children: unknown): string {
|
|
102
|
+
if (typeof children === 'string') return children
|
|
103
|
+
if (typeof children === 'number') return String(children)
|
|
104
|
+
if (children == null || typeof children === 'boolean') return ''
|
|
105
|
+
if (Array.isArray(children)) return children.map(extractText).join('')
|
|
106
|
+
if (typeof children === 'object' && 'props' in (children as Record<string, unknown>)) {
|
|
107
|
+
const props = (children as Record<string, unknown>).props as Record<string, unknown>
|
|
108
|
+
return extractText(props.children)
|
|
109
|
+
}
|
|
110
|
+
return ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Map our ComponentOverrides type to react-markdown's Components type.
|
|
115
|
+
* react-markdown passes slightly different props, so we adapt here.
|
|
116
|
+
*/
|
|
117
|
+
function mapToReactMarkdownComponents (
|
|
118
|
+
overrides: ComponentOverrides,
|
|
119
|
+
highlighter?: Highlighter,
|
|
120
|
+
syntaxTheme?: string,
|
|
121
|
+
syntaxThemeDark?: string
|
|
122
|
+
): Record<string, React.ComponentType<Record<string, unknown>>> {
|
|
123
|
+
const mapped: Record<string, React.ComponentType<Record<string, unknown>>> = {}
|
|
124
|
+
|
|
125
|
+
if (overrides.h1 != null) {
|
|
126
|
+
const H1 = overrides.h1
|
|
127
|
+
mapped.h1 = (props: Record<string, unknown>) =>
|
|
128
|
+
React.createElement(H1, {
|
|
129
|
+
id: props.id as string | undefined,
|
|
130
|
+
level: 1
|
|
131
|
+
}, props.children as React.ReactNode)
|
|
132
|
+
}
|
|
133
|
+
if (overrides.h2 != null) {
|
|
134
|
+
const H2 = overrides.h2
|
|
135
|
+
mapped.h2 = (props: Record<string, unknown>) =>
|
|
136
|
+
React.createElement(H2, {
|
|
137
|
+
id: props.id as string | undefined,
|
|
138
|
+
level: 2
|
|
139
|
+
}, props.children as React.ReactNode)
|
|
140
|
+
}
|
|
141
|
+
if (overrides.h3 != null) {
|
|
142
|
+
const H3 = overrides.h3
|
|
143
|
+
mapped.h3 = (props: Record<string, unknown>) =>
|
|
144
|
+
React.createElement(H3, {
|
|
145
|
+
id: props.id as string | undefined,
|
|
146
|
+
level: 3
|
|
147
|
+
}, props.children as React.ReactNode)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Direct pass-through for components with matching prop shapes
|
|
151
|
+
const directMappings: Array<keyof ComponentOverrides> = [
|
|
152
|
+
'p', 'a', 'img', 'blockquote', 'table', 'thead', 'tbody',
|
|
153
|
+
'tr', 'th', 'td', 'ul', 'ol', 'li', 'hr', 'code'
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
for (const key of directMappings) {
|
|
157
|
+
if (overrides[key] != null) {
|
|
158
|
+
mapped[key] = overrides[key] as React.ComponentType<Record<string, unknown>>
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Code block handling: use shiki when available, otherwise fall back to default
|
|
163
|
+
if (highlighter != null && syntaxTheme != null) {
|
|
164
|
+
const loadedLangs = new Set(highlighter.getLoadedLanguages())
|
|
165
|
+
const darkTheme = syntaxThemeDark
|
|
166
|
+
const lightTheme = syntaxTheme
|
|
167
|
+
|
|
168
|
+
mapped.pre = (props: Record<string, unknown>) => {
|
|
169
|
+
const children = props.children as React.ReactElement
|
|
170
|
+
const codeProps = children?.props as Record<string, unknown> | undefined
|
|
171
|
+
const className = (codeProps?.className as string) ?? ''
|
|
172
|
+
const langMatch = className.match(/language-(\S+)/)
|
|
173
|
+
const language = langMatch?.[1]
|
|
174
|
+
|
|
175
|
+
// Extract raw code text from React children
|
|
176
|
+
const raw = extractText(codeProps?.children).replace(/\n$/, '')
|
|
177
|
+
|
|
178
|
+
// Only use shiki if the language is loaded
|
|
179
|
+
if (language != null && loadedLangs.has(language)) {
|
|
180
|
+
const themeOpts = darkTheme != null
|
|
181
|
+
? { themes: { light: lightTheme, dark: darkTheme }, defaultColor: false as const }
|
|
182
|
+
: { theme: lightTheme }
|
|
183
|
+
|
|
184
|
+
const html = highlighter.codeToHtml(raw, { lang: language, ...themeOpts })
|
|
185
|
+
|
|
186
|
+
return React.createElement('div', {
|
|
187
|
+
className: 'mkdn-code-block',
|
|
188
|
+
'data-language': language,
|
|
189
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fall back to default pre/code rendering
|
|
194
|
+
if (overrides.pre != null) {
|
|
195
|
+
const Pre = overrides.pre
|
|
196
|
+
return React.createElement(Pre, {
|
|
197
|
+
language: language != null && language !== '' ? language : undefined
|
|
198
|
+
}, codeProps?.children as React.ReactNode)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return React.createElement('pre', null,
|
|
202
|
+
React.createElement('code', {
|
|
203
|
+
className: language != null ? `language-${language}` : undefined
|
|
204
|
+
}, codeProps?.children as React.ReactNode)
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
} else if (overrides.pre != null) {
|
|
208
|
+
const Pre = overrides.pre
|
|
209
|
+
mapped.pre = (props: Record<string, unknown>) => {
|
|
210
|
+
const children = props.children as React.ReactElement
|
|
211
|
+
const codeProps = children?.props as Record<string, unknown> | undefined
|
|
212
|
+
const className = (codeProps?.className as string) ?? ''
|
|
213
|
+
const language = className.replace('language-', '')
|
|
214
|
+
|
|
215
|
+
return React.createElement(Pre, {
|
|
216
|
+
language: language !== '' ? language : undefined
|
|
217
|
+
}, codeProps?.children as React.ReactNode)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return mapped
|
|
222
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
2
|
+
import type { Highlighter } from 'shiki'
|
|
3
|
+
import type { ComponentOverrides, RendererEngine } from '../config/schema.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Markdown renderer interface.
|
|
7
|
+
*
|
|
8
|
+
* Implementations convert markdown strings into React elements
|
|
9
|
+
* that can be rendered to HTML via renderToString().
|
|
10
|
+
*
|
|
11
|
+
* The component overrides allow users to customize how each
|
|
12
|
+
* markdown element is rendered (headings, links, code blocks, etc.).
|
|
13
|
+
*/
|
|
14
|
+
export interface MarkdownRenderer {
|
|
15
|
+
/** Which engine is actually in use */
|
|
16
|
+
readonly engine: RendererEngine
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render a markdown string to a React element tree.
|
|
20
|
+
* The returned element can be passed to renderToString().
|
|
21
|
+
*/
|
|
22
|
+
renderToElement: (markdown: string, overrides?: ComponentOverrides) => ReactElement
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a markdown string directly to an HTML string.
|
|
26
|
+
* Convenience method that calls renderToElement + renderToString.
|
|
27
|
+
*/
|
|
28
|
+
renderToHtml: (markdown: string, overrides?: ComponentOverrides) => string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RendererOptions {
|
|
32
|
+
engine?: RendererEngine
|
|
33
|
+
syntaxTheme?: string
|
|
34
|
+
syntaxThemeDark?: string
|
|
35
|
+
math?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create the appropriate renderer for the current runtime and config.
|
|
40
|
+
*/
|
|
41
|
+
export async function createRenderer (engineOrOpts: RendererEngine | RendererOptions = 'portable'): Promise<MarkdownRenderer> {
|
|
42
|
+
const opts: RendererOptions = typeof engineOrOpts === 'string'
|
|
43
|
+
? { engine: engineOrOpts }
|
|
44
|
+
: engineOrOpts
|
|
45
|
+
const engine = opts.engine ?? 'portable'
|
|
46
|
+
|
|
47
|
+
// Create shiki highlighter if syntax themes are provided
|
|
48
|
+
let highlighter: Highlighter | undefined
|
|
49
|
+
if (opts.syntaxTheme != null) {
|
|
50
|
+
const { createHighlighter } = await import('shiki')
|
|
51
|
+
const themes = [opts.syntaxTheme]
|
|
52
|
+
if (opts.syntaxThemeDark != null) themes.push(opts.syntaxThemeDark)
|
|
53
|
+
const langs = [
|
|
54
|
+
'javascript', 'typescript', 'jsx', 'tsx',
|
|
55
|
+
'html', 'css', 'json', 'jsonc',
|
|
56
|
+
'bash', 'shell', 'sh', 'zsh',
|
|
57
|
+
'python', 'ruby', 'go', 'rust', 'java', 'c', 'cpp',
|
|
58
|
+
'yaml', 'toml', 'markdown', 'sql', 'graphql',
|
|
59
|
+
'diff', 'xml', 'docker', 'nginx', 'ini'
|
|
60
|
+
]
|
|
61
|
+
highlighter = await createHighlighter({ themes, langs })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (engine === 'bun-native') {
|
|
65
|
+
if (typeof Bun !== 'undefined' && Bun.markdown != null) {
|
|
66
|
+
const { BunNativeRenderer } = await import('./bun-native.ts')
|
|
67
|
+
return new BunNativeRenderer()
|
|
68
|
+
}
|
|
69
|
+
console.warn('mkdnsite: bun-native renderer requested but Bun.markdown not available. Falling back to portable.')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { PortableRenderer } = await import('./portable.ts')
|
|
73
|
+
return new PortableRenderer(highlighter, opts.syntaxTheme, opts.syntaxThemeDark, opts.math)
|
|
74
|
+
}
|