用 Markdoc 而非 Markdown 渲染博客,以在未来支持自定义元素
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-03-27 17:06:03 +08:00
parent bc5247ddc8
commit 4c5abe8b01
10 changed files with 1582 additions and 32 deletions

View File

@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true
[*.{js,ts,astro,json,yml}]
[*.{js,ts,astro,json,yml,vue,svelte}]
indent_style = space
indent_size = 2

View File

@ -5,11 +5,15 @@ import node from '@astrojs/node'
import svelte from '@astrojs/svelte'
import vue from '@astrojs/vue';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
adapter: node({
mode: 'standalone',
}),
integrations: [svelte()],
})
integrations: [svelte(), vue(), react()],
})

1441
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,15 +13,23 @@
},
"dependencies": {
"@astrojs/node": "^10.0.4",
"@astrojs/react": "^5.0.2",
"@astrojs/svelte": "^8.0.4",
"@astrojs/vue": "^6.0.1",
"@markdoc/markdoc": "^0.5.7",
"@traptitech/markdown-it-katex": "^3.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"astro": "^6.1.0",
"axios": "^1.13.6",
"katex": "^0.16.43",
"markdown-it": "^14.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"shiki": "^4.0.2",
"svelte": "^5.55.0",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vue": "^3.5.31"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",

View File

@ -0,0 +1,35 @@
import React, { useEffect, useState } from 'react'
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
import { toMarkdocTree } from '../lib/markdoc'
import { CodeBlock } from './markdoc/CodeBlock.tsx'
export const MarkdocContent: React.FC<{
content: string
}> = ({ content }) => {
/* 注意一下这个元件是动态的 */
const [tree, setTree] = useState<any>(null)
useEffect(() => {
let active = true
toMarkdocTree(content).then((result) => {
if (active) setTree(result)
})
return () => {
active = false
}
}, [content])
if (!tree) return <div className="loading">... (̀́)و</div>
return <MarkdocTreeRender tree={tree} />
}
export const MarkdocTreeRender: React.FC<{
tree: RenderableTreeNode
}> = ({ tree }) => {
return (
<div className="markdoc-container">
{Markdoc.renderers.react(tree, React, { components: { CodeBlock } })}
</div>
)
}

View File

@ -0,0 +1,5 @@
import React from 'react'
export const CodeBlock: React.FC<{ highlightedHtml: string }> = (props) => {
return <div dangerouslySetInnerHTML={{ __html: props.highlightedHtml }}></div>
}

36
src/lib/markdoc.ts Normal file
View File

@ -0,0 +1,36 @@
import Markdoc, { type Config } from '@markdoc/markdoc'
import { ensureShikiEngine, shikiRender } from './markdown'
const { nodes, Tag } = Markdoc
export const markdocConfig: Config = {
nodes: {
fence: {
render: 'CodeBlock',
attributes: {
...nodes.fence.attributes,
highlightedHtml: { type: String },
},
transform(node, config) {
const attributes = node.transformAttributes(config)
const highlightedHtml = shikiRender(
node.attributes.content,
node.attributes.language,
)
return new Tag(
'CodeBlock',
{ ...attributes, highlightedHtml },
node.transformChildren(config),
)
},
},
},
}
export const toMarkdocTree = async (content: string) => {
const ast = Markdoc.parse(content)
await ensureShikiEngine()
return Markdoc.transform(ast, markdocConfig)
}

View File

@ -1,10 +1,14 @@
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import { createHighlighter, type Highlighter } from 'shiki'
import {
createHighlighter,
type BundledLanguage,
type Highlighter,
} from 'shiki'
let highlighter: Highlighter | null = null
const allowed_langs = [
const allowed_langs: BundledLanguage[] = [
'python',
'javascript',
'typescript',
@ -27,8 +31,31 @@ const allowed_langs = [
'md',
'sh',
'mermaid',
'cpp',
]
export async function ensureShikiEngine() {
if (highlighter === null) {
highlighter = await createHighlighter({
themes: ['one-light', 'one-dark-pro'],
/* 额...要我自己定义需要的所有语言吗 */
langs: allowed_langs,
})
}
return highlighter
}
export function shikiRender(code: string, lang: string) {
return highlighter!.codeToHtml(code, {
lang: (allowed_langs as string[]).indexOf(lang) != -1 ? lang : 'text',
themes: {
light: 'one-light',
dark: 'one-dark-pro',
},
defaultColor: false,
})
}
/**
* KaTeX 渲染结果修改。
*
@ -64,27 +91,12 @@ function setupKatexCopy(md: MarkdownIt) {
* 为了让客户端和服务端表现相同,所以单独拿出来了一个 Markdown 渲染器。
*/
export async function renderMarkdown(content: string): Promise<string> {
if (highlighter === null) {
highlighter = await createHighlighter({
themes: ['one-light', 'one-dark-pro'],
/* 额...要我自己定义需要的所有语言吗 */
langs: allowed_langs,
})
}
await ensureShikiEngine()
const md = new MarkdownIt({
html: true,
linkify: true,
highlight: (code, lang) => {
return highlighter!.codeToHtml(code, {
lang: allowed_langs.indexOf(lang) != -1 ? lang : 'text',
themes: {
light: 'one-light',
dark: 'one-dark-pro',
},
defaultColor: false,
})
},
highlight: shikiRender,
})
md.use(mdKatex)

View File

@ -1,11 +1,12 @@
---
import { getBlog } from '../../lib/apis/legacy/blog'
import BaseLayout from '../../layout/BaseLayout.astro'
import { renderMarkdown } from '../../lib/markdown'
import { MarkdocTreeRender } from '../../components/MarkdocRenderer.tsx'
import { toMarkdocTree } from '../../lib/markdoc'
export const prerender = false
const { blog_id } = Astro.params
const { blog_id = '' } = Astro.params
const blog_id_num = parseInt(blog_id)
if (isNaN(blog_id_num) || blog_id_num < 0) {
@ -17,7 +18,10 @@ if (blogData === null) {
return Astro.redirect('/404')
}
const blogRendered = await renderMarkdown(blogData.content)
const tree = await toMarkdocTree(blogData.content)
---
<BaseLayout set:html={blogRendered} />
<!-- <BaseLayout set:html={blogRendered} /> -->
<BaseLayout>
<MarkdocTreeRender tree={tree} client:load />
</BaseLayout>

View File

@ -1,5 +1,14 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}