add LaTeX 公式的支持
This commit is contained in:
parent
7ae14c5599
commit
1a02300026
@ -26,10 +26,12 @@
|
|||||||
"@tiptap/pm": "^2.2.4",
|
"@tiptap/pm": "^2.2.4",
|
||||||
"@tiptap/react": "^2.2.4",
|
"@tiptap/react": "^2.2.4",
|
||||||
"@tiptap/starter-kit": "^2.2.4",
|
"@tiptap/starter-kit": "^2.2.4",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"@types/marked": "^6.0.0",
|
"@types/marked": "^6.0.0",
|
||||||
"@types/prismjs": "^1.26.5",
|
"@types/prismjs": "^1.26.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"katex": "^0.16.21",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
"marked": "^15.0.6",
|
"marked": "^15.0.6",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
|
@ -59,6 +59,9 @@ importers:
|
|||||||
'@tiptap/starter-kit':
|
'@tiptap/starter-kit':
|
||||||
specifier: ^2.2.4
|
specifier: ^2.2.4
|
||||||
version: 2.11.3
|
version: 2.11.3
|
||||||
|
'@types/katex':
|
||||||
|
specifier: ^0.16.7
|
||||||
|
version: 0.16.7
|
||||||
'@types/marked':
|
'@types/marked':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@ -71,6 +74,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
katex:
|
||||||
|
specifier: ^0.16.21
|
||||||
|
version: 0.16.21
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.474.0
|
specifier: ^0.474.0
|
||||||
version: 0.474.0(react@18.3.1)
|
version: 0.474.0(react@18.3.1)
|
||||||
@ -819,6 +825,9 @@ packages:
|
|||||||
'@tiptap/starter-kit@2.11.3':
|
'@tiptap/starter-kit@2.11.3':
|
||||||
resolution: {integrity: sha512-UGKS6+TA/7yMGqHBK5S/Kxis6iy3Tw0gvVg1EkYHUmkApLJypE87wUMkIeLeD9dd5+2WkxWcYMhC9R3ByjulBg==}
|
resolution: {integrity: sha512-UGKS6+TA/7yMGqHBK5S/Kxis6iy3Tw0gvVg1EkYHUmkApLJypE87wUMkIeLeD9dd5+2WkxWcYMhC9R3ByjulBg==}
|
||||||
|
|
||||||
|
'@types/katex@0.16.7':
|
||||||
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
|
|
||||||
'@types/linkify-it@5.0.0':
|
'@types/linkify-it@5.0.0':
|
||||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
|
|
||||||
@ -976,6 +985,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
commander@8.3.0:
|
||||||
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
@ -1165,6 +1178,10 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
katex@0.16.21:
|
||||||
|
resolution: {integrity: sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
kleur@3.0.3:
|
kleur@3.0.3:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
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/extension-text-style': 2.11.3(@tiptap/core@2.11.3(@tiptap/pm@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/linkify-it@5.0.0': {}
|
||||||
|
|
||||||
'@types/markdown-it@14.1.2':
|
'@types/markdown-it@14.1.2':
|
||||||
@ -2584,6 +2603,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
|
commander@8.3.0: {}
|
||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
@ -2754,6 +2775,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
katex@0.16.21:
|
||||||
|
dependencies:
|
||||||
|
commander: 8.3.0
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
|||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import '@/styles/code-themes.css'
|
import '@/styles/code-themes.css'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
import { ThemeProvider } from '@/components/theme/ThemeProvider'
|
import { ThemeProvider } from '@/components/theme/ThemeProvider'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
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: '其他',
|
title: '其他',
|
||||||
items: [
|
items: [
|
||||||
{ label: '水平分割线', syntax: '---' },
|
{ label: '水平分割线', syntax: '---' },
|
||||||
{ label: '数学公式', syntax: '$E = mc^2$' },
|
|
||||||
{ label: '注脚', syntax: '这里是文字[^1]\n\n[^1]: 这里是注脚' },
|
{ label: '注脚', syntax: '这里是文字[^1]\n\n[^1]: 这里是注脚' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -49,6 +49,11 @@ export const defaultOptions: RendererOptions = {
|
|||||||
borderTop: '1px solid #eee',
|
borderTop: '1px solid #eee',
|
||||||
fontSize: '0.9em',
|
fontSize: '0.9em',
|
||||||
color: '#666'
|
color: '#666'
|
||||||
|
},
|
||||||
|
latex: {
|
||||||
|
margin: '1em 0',
|
||||||
|
fontSize: '1.1em',
|
||||||
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inline: {
|
inline: {
|
||||||
@ -59,6 +64,9 @@ export const defaultOptions: RendererOptions = {
|
|||||||
checkbox: {
|
checkbox: {
|
||||||
marginRight: '0.5em',
|
marginRight: '0.5em',
|
||||||
verticalAlign: 'middle'
|
verticalAlign: 'middle'
|
||||||
|
},
|
||||||
|
latex: {
|
||||||
|
fontSize: '1.1em'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import type { Tokens } from 'marked'
|
import type { Tokens, TokenizerAndRendererExtension } from 'marked'
|
||||||
import type { RendererOptions } from './types'
|
import type { RendererOptions } from './types'
|
||||||
import { cssPropertiesToString } from './styles'
|
import { cssPropertiesToString } from './styles'
|
||||||
import { highlightCode } from './code-highlight'
|
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 {
|
export class MarkdownRenderer {
|
||||||
private renderer: typeof marked.Renderer.prototype
|
private renderer: typeof marked.Renderer.prototype
|
||||||
@ -12,9 +20,74 @@ export class MarkdownRenderer {
|
|||||||
this.options = options
|
this.options = options
|
||||||
this.renderer = new marked.Renderer()
|
this.renderer = new marked.Renderer()
|
||||||
this.initializeRenderer()
|
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() {
|
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 方法
|
// 重写 heading 方法
|
||||||
this.renderer.heading = ({ text, depth }: Tokens.Heading) => {
|
this.renderer.heading = ({ text, depth }: Tokens.Heading) => {
|
||||||
const headingKey = `h${depth}` as keyof RendererOptions['block']
|
const headingKey = `h${depth}` as keyof RendererOptions['block']
|
||||||
|
@ -110,6 +110,7 @@ export interface RendererOptions {
|
|||||||
td?: StyleOptions
|
td?: StyleOptions
|
||||||
thead?: StyleOptions
|
thead?: StyleOptions
|
||||||
footnotes?: StyleOptions
|
footnotes?: StyleOptions
|
||||||
|
latex?: StyleOptions
|
||||||
}
|
}
|
||||||
inline?: {
|
inline?: {
|
||||||
strong?: StyleOptions
|
strong?: StyleOptions
|
||||||
@ -120,6 +121,7 @@ export interface RendererOptions {
|
|||||||
checkbox?: StyleOptions
|
checkbox?: StyleOptions
|
||||||
del?: StyleOptions
|
del?: StyleOptions
|
||||||
footnote?: StyleOptions
|
footnote?: StyleOptions
|
||||||
|
latex?: StyleOptions
|
||||||
}
|
}
|
||||||
codeTheme?: CodeThemeId
|
codeTheme?: CodeThemeId
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user