add LaTeX 公式的支持
This commit is contained in:
parent
7ae14c5599
commit
1a02300026
@ -26,10 +26,12 @@
|
||||
"@tiptap/pm": "^2.2.4",
|
||||
"@tiptap/react": "^2.2.4",
|
||||
"@tiptap/starter-kit": "^2.2.4",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"katex": "^0.16.21",
|
||||
"lucide-react": "^0.474.0",
|
||||
"marked": "^15.0.6",
|
||||
"next": "14.1.0",
|
||||
|
@ -59,6 +59,9 @@ importers:
|
||||
'@tiptap/starter-kit':
|
||||
specifier: ^2.2.4
|
||||
version: 2.11.3
|
||||
'@types/katex':
|
||||
specifier: ^0.16.7
|
||||
version: 0.16.7
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
@ -71,6 +74,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
katex:
|
||||
specifier: ^0.16.21
|
||||
version: 0.16.21
|
||||
lucide-react:
|
||||
specifier: ^0.474.0
|
||||
version: 0.474.0(react@18.3.1)
|
||||
@ -819,6 +825,9 @@ packages:
|
||||
'@tiptap/starter-kit@2.11.3':
|
||||
resolution: {integrity: sha512-UGKS6+TA/7yMGqHBK5S/Kxis6iy3Tw0gvVg1EkYHUmkApLJypE87wUMkIeLeD9dd5+2WkxWcYMhC9R3ByjulBg==}
|
||||
|
||||
'@types/katex@0.16.7':
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
@ -976,6 +985,10 @@ packages:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@8.3.0:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
@ -1165,6 +1178,10 @@ packages:
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
katex@0.16.21:
|
||||
resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
|
||||
hasBin: true
|
||||
|
||||
kleur@3.0.3:
|
||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2437,6 +2454,8 @@ snapshots:
|
||||
'@tiptap/extension-text-style': 2.11.3(@tiptap/core@2.11.3(@tiptap/pm@2.11.3))
|
||||
'@tiptap/pm': 2.11.3
|
||||
|
||||
'@types/katex@0.16.7': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
@ -2584,6 +2603,8 @@ snapshots:
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
commander@8.3.0: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
@ -2754,6 +2775,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
katex@0.16.21:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
||||
kleur@3.0.3: {}
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import '@/styles/code-themes.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { ThemeProvider } from '@/components/theme/ThemeProvider'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Toaster } from '@/components/ui/toaster'
|
||||
|
@ -58,11 +58,18 @@ const cheatSheet = [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '数学公式',
|
||||
items: [
|
||||
{ label: '行内公式', syntax: '$E = mc^2$' },
|
||||
{ label: '行间公式', syntax: '$$\\frac{n!}{k!(n-k)!} = \\binom{n}{k}$$' },
|
||||
{ label: '带反引号的行间公式', syntax: '$$`\\sum_{i=1}^n a_i = \\int_0^{\\pi} \\sin(x) dx`$$' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '其他',
|
||||
items: [
|
||||
{ label: '水平分割线', syntax: '---' },
|
||||
{ label: '数学公式', syntax: '$E = mc^2$' },
|
||||
{ label: '注脚', syntax: '这里是文字[^1]\n\n[^1]: 这里是注脚' },
|
||||
]
|
||||
},
|
||||
|
@ -49,6 +49,11 @@ export const defaultOptions: RendererOptions = {
|
||||
borderTop: '1px solid #eee',
|
||||
fontSize: '0.9em',
|
||||
color: '#666'
|
||||
},
|
||||
latex: {
|
||||
margin: '1em 0',
|
||||
fontSize: '1.1em',
|
||||
textAlign: 'center'
|
||||
}
|
||||
},
|
||||
inline: {
|
||||
@ -59,6 +64,9 @@ export const defaultOptions: RendererOptions = {
|
||||
checkbox: {
|
||||
marginRight: '0.5em',
|
||||
verticalAlign: 'middle'
|
||||
},
|
||||
latex: {
|
||||
fontSize: '1.1em'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,16 @@
|
||||
import { marked } from 'marked'
|
||||
import type { Tokens } from 'marked'
|
||||
import type { Tokens, TokenizerAndRendererExtension } from 'marked'
|
||||
import type { RendererOptions } from './types'
|
||||
import { cssPropertiesToString } from './styles'
|
||||
import { highlightCode } from './code-highlight'
|
||||
import katex from 'katex'
|
||||
|
||||
// 自定义 LaTeX 块的 Token 类型
|
||||
interface LatexBlockToken extends Tokens.Generic {
|
||||
type: 'latexBlock'
|
||||
raw: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export class MarkdownRenderer {
|
||||
private renderer: typeof marked.Renderer.prototype
|
||||
@ -12,9 +20,74 @@ export class MarkdownRenderer {
|
||||
this.options = options
|
||||
this.renderer = new marked.Renderer()
|
||||
this.initializeRenderer()
|
||||
this.initializeLatexExtension()
|
||||
}
|
||||
|
||||
private initializeLatexExtension() {
|
||||
// 添加 LaTeX 块的 tokenizer
|
||||
const latexBlockTokenizer: TokenizerAndRendererExtension = {
|
||||
name: 'latexBlock',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.match(/^\$\$\n/)?.index
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const rule = /^\$\$\n([\s\S]*?)\n\$\$/
|
||||
const match = rule.exec(src)
|
||||
if (match) {
|
||||
return {
|
||||
type: 'latexBlock',
|
||||
raw: match[0],
|
||||
tokens: [],
|
||||
text: match[1].trim()
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer: (token) => {
|
||||
try {
|
||||
const latexStyle = (this.options.block?.latex || {})
|
||||
const style = {
|
||||
...latexStyle,
|
||||
display: 'block',
|
||||
margin: '1em 0',
|
||||
textAlign: 'center'
|
||||
}
|
||||
const styleStr = cssPropertiesToString(style)
|
||||
const rendered = katex.renderToString(token.text, {
|
||||
displayMode: true,
|
||||
throwOnError: false
|
||||
})
|
||||
return `<div${styleStr ? ` style="${styleStr}"` : ''}>${rendered}</div>`
|
||||
} catch (error) {
|
||||
console.error('LaTeX rendering error:', error)
|
||||
return token.raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册扩展
|
||||
marked.use({ extensions: [latexBlockTokenizer] })
|
||||
}
|
||||
|
||||
private initializeRenderer() {
|
||||
// 重写 text 方法来处理行内 LaTeX 公式
|
||||
this.renderer.text = (token: Tokens.Text | Tokens.Escape) => {
|
||||
// 处理行内公式 $...$ 和行间公式 $$`...`$$
|
||||
return token.text.replace(/\$\$`([^`]+)`\$\$|\$([^$\n]+?)\$/g, (match, backtick, inline) => {
|
||||
try {
|
||||
const formula = backtick || inline
|
||||
const isDisplayMode = !!backtick
|
||||
return katex.renderToString(formula.trim(), {
|
||||
displayMode: isDisplayMode,
|
||||
throwOnError: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('LaTeX rendering error:', error)
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重写 heading 方法
|
||||
this.renderer.heading = ({ text, depth }: Tokens.Heading) => {
|
||||
const headingKey = `h${depth}` as keyof RendererOptions['block']
|
||||
@ -208,4 +281,4 @@ export class MarkdownRenderer {
|
||||
public getRenderer(): typeof marked.Renderer.prototype {
|
||||
return this.renderer
|
||||
}
|
||||
}
|
||||
}
|
@ -110,6 +110,7 @@ export interface RendererOptions {
|
||||
td?: StyleOptions
|
||||
thead?: StyleOptions
|
||||
footnotes?: StyleOptions
|
||||
latex?: StyleOptions
|
||||
}
|
||||
inline?: {
|
||||
strong?: StyleOptions
|
||||
@ -120,6 +121,7 @@ export interface RendererOptions {
|
||||
checkbox?: StyleOptions
|
||||
del?: StyleOptions
|
||||
footnote?: StyleOptions
|
||||
latex?: StyleOptions
|
||||
}
|
||||
codeTheme?: CodeThemeId
|
||||
}
|
Loading…
Reference in New Issue
Block a user