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
	 tianyaxiang
						tianyaxiang