forked from mxx1111/mdlook
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender-article.ts
More file actions
166 lines (143 loc) · 5.14 KB
/
Copy pathrender-article.ts
File metadata and controls
166 lines (143 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import type { ThemeName } from '@md/shared/configs'
import type { HeadingStyles } from '@md/shared/configs/style'
import type { HeadingStylesInput, LegendValue } from './config-options'
import fs from 'node:fs'
import path from 'node:path'
import { initRenderer } from '@md/core/renderer'
import { processCSS } from '@md/core/theme/cssProcessor'
import { generateCSSVariables, generateHeadingStyles } from '@md/core/theme/cssVariables'
import { postProcessHtml, renderMarkdown } from '@md/core/utils'
import {
assertAllowedCodeBlockThemeUrl,
defaultRenderOptions,
} from './config-options'
const CODE_BLOCK_FETCH_TIMEOUT_MS = 10_000
export interface RenderMarkdownInput {
markdown: string
theme?: ThemeName
primaryColor?: string
fontFamily?: string
fontSize?: string
legend?: LegendValue
isMacCodeBlock?: boolean
isShowLineNumber?: boolean
citeStatus?: boolean
countStatus?: boolean
themeMode?: 'light' | 'dark'
isUseIndent?: boolean
isUseJustify?: boolean
headingStyles?: HeadingStylesInput
codeBlockTheme?: string
customCSS?: string
}
const themeDir = path.resolve(import.meta.dirname, `../../shared/src/configs/theme-css`)
function escapeStyleContent(css: string): string {
return css.replace(/<\/style/gi, `<\\/style`)
}
function loadCSSFile(filename: string): string {
try {
return fs.readFileSync(path.join(themeDir, filename), `utf-8`)
}
catch (err) {
throw new Error(
`[md-mcp-server] Failed to load CSS file: ${filename}. Ensure the monorepo source structure is intact.`,
{ cause: err },
)
}
}
const baseCSSContent = loadCSSFile(`base.css`)
const themeMap: Record<string, string> = {
default: loadCSSFile(`default.css`),
grace: loadCSSFile(`grace.css`),
simple: loadCSSFile(`simple.css`),
}
const hljsCssCache = new Map<string, string>()
export async function fetchCodeBlockCSS(url: string): Promise<string> {
assertAllowedCodeBlockThemeUrl(url)
const cached = hljsCssCache.get(url)
if (cached)
return cached
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(CODE_BLOCK_FETCH_TIMEOUT_MS),
})
if (!response.ok)
throw new Error(`Failed to fetch code block theme CSS (${response.status}): ${url}`)
const css = escapeStyleContent(await response.text())
hljsCssCache.set(url, css)
return css
}
catch (err) {
if (err instanceof Error && err.name === `TimeoutError`) {
throw new Error(
`Timed out fetching code block theme CSS after ${CODE_BLOCK_FETCH_TIMEOUT_MS}ms: ${url}`,
{ cause: err },
)
}
throw err
}
}
function normalizeHeadingStyles(input?: HeadingStylesInput): HeadingStyles | undefined {
if (!input)
return undefined
const normalized: HeadingStyles = {}
for (const [level, style] of Object.entries(input)) {
if (style && style !== `default`)
normalized[level as keyof HeadingStyles] = style
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}
export async function buildRenderedOutput(input: RenderMarkdownInput) {
const theme = input.theme ?? defaultRenderOptions.theme
const primaryColor = input.primaryColor ?? defaultRenderOptions.primaryColor
const fontFamily = input.fontFamily ?? defaultRenderOptions.fontFamily
const fontSize = input.fontSize ?? defaultRenderOptions.fontSize
const legend = input.legend ?? defaultRenderOptions.legend
const codeBlockTheme = input.codeBlockTheme ?? defaultRenderOptions.codeBlockTheme
const headingStyles = normalizeHeadingStyles(input.headingStyles)
const renderer = initRenderer({
isMacCodeBlock: input.isMacCodeBlock ?? defaultRenderOptions.isMacCodeBlock,
isShowLineNumber: input.isShowLineNumber ?? defaultRenderOptions.isShowLineNumber,
citeStatus: input.citeStatus ?? defaultRenderOptions.citeStatus,
countStatus: input.countStatus ?? defaultRenderOptions.countStatus,
themeMode: input.themeMode ?? defaultRenderOptions.themeMode,
legend,
})
const { html: baseHtml, readingTime } = renderMarkdown(input.markdown, renderer)
const processedHtml = postProcessHtml(baseHtml, readingTime, renderer)
const { yamlData } = renderer.parseFrontMatterAndContent(input.markdown)
const cssConfig = {
primaryColor,
fontFamily,
fontSize,
isUseIndent: input.isUseIndent ?? defaultRenderOptions.isUseIndent,
isUseJustify: input.isUseJustify ?? defaultRenderOptions.isUseJustify,
headingStyles,
}
const variablesCSS = generateCSSVariables(cssConfig)
const headingStylesCSS = generateHeadingStyles(cssConfig)
const themeCSS = themeMap[theme] || themeMap.default
const hljsCSS = codeBlockTheme ? await fetchCodeBlockCSS(codeBlockTheme) : ``
const customCSS = escapeStyleContent(input.customCSS?.trim() ?? ``)
let mergedCSS = [
variablesCSS,
baseCSSContent,
themeCSS,
headingStylesCSS,
hljsCSS,
customCSS,
].filter(Boolean).join(`\n\n`)
mergedCSS = processCSS(mergedCSS)
const html = `<style>\n${mergedCSS}\n</style>\n${processedHtml}`
return {
html,
frontMatter: yamlData,
readingTime: {
words: readingTime.words,
minutes: readingTime.minutes,
},
}
}
export function clearHljsCssCache() {
hljsCssCache.clear()
}