用 Markdoc 而非 Markdown 渲染博客,以在未来支持自定义元素
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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
1441
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -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",
|
||||
|
||||
35
src/components/MarkdocRenderer.tsx
Normal file
35
src/components/MarkdocRenderer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
src/components/markdoc/CodeBlock.tsx
Normal file
5
src/components/markdoc/CodeBlock.tsx
Normal 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
36
src/lib/markdoc.ts
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user