优化 markdown 解析
This commit is contained in:
		
							parent
							
								
									e1361f9219
								
							
						
					
					
						commit
						b8c464bd4c
					
				| @ -87,7 +87,7 @@ const marked = new Marked({ | ||||
|   pedantic: false | ||||
| }) | ||||
| 
 | ||||
| // 应用配置
 | ||||
| // 重写 marked 配置
 | ||||
| marked.use({ | ||||
|   breaks: true, | ||||
|   gfm: true, | ||||
| @ -118,12 +118,40 @@ const defaultOptions: RendererOptions = { | ||||
|       padding: '1em', | ||||
|       lineHeight: '1.5', | ||||
|       margin: '10px 8px' | ||||
|     }, | ||||
|     table: { | ||||
|       width: '100%', | ||||
|       marginBottom: '1em', | ||||
|       borderCollapse: 'collapse', | ||||
|       fontSize: '14px' | ||||
|     }, | ||||
|     th: { | ||||
|       padding: '0.5em 1em', | ||||
|       borderBottom: '2px solid var(--theme-color)', | ||||
|       textAlign: 'left', | ||||
|       fontWeight: 'bold' | ||||
|     }, | ||||
|     td: { | ||||
|       padding: '0.5em 1em', | ||||
|       borderBottom: '1px solid #eee' | ||||
|     }, | ||||
|     footnotes: { | ||||
|       marginTop: '2em', | ||||
|       paddingTop: '1em', | ||||
|       borderTop: '1px solid #eee', | ||||
|       fontSize: '0.9em', | ||||
|       color: '#666' | ||||
|     } | ||||
|   }, | ||||
|   inline: { | ||||
|     link: { color: '#576b95' }, | ||||
|     codespan: { color: '#333333' }, | ||||
|     em: { color: '#666666' } | ||||
|     em: { color: '#666666' }, | ||||
|     del: { color: '#999999', textDecoration: 'line-through' }, | ||||
|     checkbox: { | ||||
|       marginRight: '0.5em', | ||||
|       verticalAlign: 'middle' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -168,11 +196,13 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       color: mergedOptions.base?.themeColor | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${text}</h${depth}>` | ||||
|     const tokens = marked.Lexer.lexInline(text) | ||||
|     const content = marked.Parser.parseInline(tokens, { renderer }) | ||||
|     return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${content}</h${depth}>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 paragraph 方法
 | ||||
|   renderer.paragraph = function({ text }: Tokens.Paragraph) { | ||||
|   renderer.paragraph = function({ text, tokens }: Tokens.Paragraph) { | ||||
|     const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions | ||||
|     const style: StyleOptions = { | ||||
|       ...paragraphStyle, | ||||
| @ -180,7 +210,23 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       lineHeight: mergedOptions.base?.lineHeight | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<p${styleStr ? ` style="${styleStr}"` : ''}>${text}</p>` | ||||
| 
 | ||||
|     // 处理段落中的内联标记
 | ||||
|     let content = text | ||||
|     if (tokens) { | ||||
|       content = tokens.map(token => { | ||||
|         if (token.type === 'text') { | ||||
|           const inlineTokens = marked.Lexer.lexInline(token.text) | ||||
|           return marked.Parser.parseInline(inlineTokens, { renderer }) | ||||
|         } | ||||
|         return marked.Parser.parseInline([token], { renderer }) | ||||
|       }).join('') | ||||
|     } else { | ||||
|       const inlineTokens = marked.Lexer.lexInline(text) | ||||
|       content = marked.Parser.parseInline(inlineTokens, { renderer }) | ||||
|     } | ||||
| 
 | ||||
|     return `<p${styleStr ? ` style="${styleStr}"` : ''}>${content}</p>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 blockquote 方法
 | ||||
| @ -191,23 +237,26 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}` | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>` | ||||
|     const tokens = marked.Lexer.lexInline(text) | ||||
|     const content = marked.Parser.parseInline(tokens, { renderer }) | ||||
|      | ||||
|     return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${content}</blockquote>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 code 方法
 | ||||
|   renderer.code = function({ text, lang }: Tokens.Code) {   | ||||
| 
 | ||||
|     const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions | ||||
|     const style: StyleOptions = { | ||||
|       ...codeStyle, | ||||
|       ...getCodeThemeStyles(mergedOptions.codeTheme) | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|      | ||||
|     // 代码高亮处理
 | ||||
|     let highlighted = text | ||||
|     if (lang && Prism.languages[lang]) { | ||||
|       // Helper function to recursively process tokens
 | ||||
|       const processToken = (token: string | Prism.Token): string => { | ||||
|       const processToken = (token: string | Prism.Token, lineNumber?: number): string => { | ||||
|         if (typeof token === 'string') { | ||||
|           return token | ||||
|         } | ||||
| @ -223,7 +272,14 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       try { | ||||
|         const grammar = Prism.languages[lang] | ||||
|         const tokens = Prism.tokenize(text, grammar) | ||||
|         highlighted = tokens.map(processToken).join('') | ||||
|         const lines = text.split('\n') | ||||
|         const lineNumbersWidth = lines.length.toString().length * 8 + 20 | ||||
|          | ||||
|         highlighted = lines.map((line, index) => { | ||||
|           const lineTokens = Prism.tokenize(line, grammar) | ||||
|           const processedLine = lineTokens.map(t => processToken(t, index + 1)).join('') | ||||
|           return `<div class="code-line"><span class="line-number" style="width:${lineNumbersWidth}px;color:#999;padding-right:1em;text-align:right;display:inline-block;user-select:none;">${index + 1}</span>${processedLine}</div>` | ||||
|         }).join('\n') | ||||
|       } catch (error) { | ||||
|         console.error(`Error highlighting code: ${error}`) | ||||
|       } | ||||
| @ -234,7 +290,6 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
| 
 | ||||
|   // 重写 codespan 方法
 | ||||
|   renderer.codespan = function({ text }: Tokens.Codespan) {   | ||||
| 
 | ||||
|     const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions | ||||
|     const style: StyleOptions = { | ||||
|       ...codespanStyle | ||||
| @ -251,7 +306,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       fontStyle: 'italic' | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>` | ||||
|     const tokens = marked.Lexer.lexInline(text) | ||||
|     const content = marked.Parser.parseInline(tokens, { renderer }) | ||||
|      | ||||
|     return `<em${styleStr ? ` style="${styleStr}"` : ''}>${content}</em>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 strong 方法
 | ||||
| @ -263,7 +321,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       fontWeight: 'bold' | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>` | ||||
|     const tokens = marked.Lexer.lexInline(text) | ||||
|     const content = marked.Parser.parseInline(tokens, { renderer }) | ||||
|      | ||||
|     return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${content}</strong>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 link 方法
 | ||||
| @ -273,7 +334,9 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|       ...linkStyle | ||||
|     } | ||||
|     const styleStr = cssPropertiesToString(style) | ||||
|     return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>` | ||||
|     const tokens = marked.Lexer.lexInline(text) | ||||
|     const content = marked.Parser.parseInline(tokens, { renderer }) | ||||
|     return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${content}</a>` | ||||
|   } | ||||
| 
 | ||||
|   // 重写 image 方法
 | ||||
| @ -346,6 +409,48 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | ||||
|     return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>` | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   // 添加删除线支持
 | ||||
|   renderer.del = function({ text }: Tokens.Del) { | ||||
|     const delStyle = (mergedOptions.inline?.del || {}) as StyleOptions | ||||
|     const styleStr = cssPropertiesToString(delStyle) | ||||
|     return `<del${styleStr ? ` style="${styleStr}"` : ''}>${text}</del>` | ||||
|   } | ||||
| 
 | ||||
|   // 添加脚注支持
 | ||||
|   const footnotes = new Map<string, string>() | ||||
| 
 | ||||
|   marked.use({ | ||||
|     extensions: [{ | ||||
|       name: 'footnote', | ||||
|       level: 'inline', | ||||
|       start(src: string) {  | ||||
|         const match = src.match(/^\[\^([^\]]+)\]/) | ||||
|         return match ? match.index : undefined  | ||||
|       }, | ||||
|       tokenizer(src: string) { | ||||
|         const match = /^\[\^([^\]]+)\]/.exec(src) | ||||
|         if (match) { | ||||
|           const token = { | ||||
|             type: 'footnote', | ||||
|             raw: match[0], | ||||
|             text: match[1], | ||||
|             tokens: [] | ||||
|           } | ||||
|           return token as unknown as Tokens.Generic | ||||
|         } | ||||
|         return undefined | ||||
|       }, | ||||
|       renderer(token: unknown) { | ||||
|         const footnoteToken = token as { text: string } | ||||
|         const footnoteStyle = (mergedOptions.inline?.footnote || {}) as StyleOptions | ||||
|         const styleStr = cssPropertiesToString(footnoteStyle) | ||||
|         return `<sup${styleStr ? ` style="${styleStr}"` : ''}><a href="#fn-${footnoteToken.text}">[${footnoteToken.text}]</a></sup>` | ||||
|       } | ||||
|     }] | ||||
|   }) | ||||
| 
 | ||||
|   // Convert Markdown to HTML using the custom renderer
 | ||||
|   const html = marked.parse(markdown, { renderer }) as string | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										142
									
								
								src/lib/types.ts
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								src/lib/types.ts
									
									
									
									
									
								
							| @ -1,16 +1,92 @@ | ||||
| import type { CSSProperties } from 'react' | ||||
| import type { CodeThemeId } from '@/config/code-themes' | ||||
| 
 | ||||
| export interface StyleOptions { | ||||
|   // Layout
 | ||||
|   display?: string | ||||
|   position?: string | ||||
|   top?: string | number | ||||
|   left?: string | number | ||||
|   right?: string | number | ||||
|   bottom?: string | number | ||||
|   width?: string | number | ||||
|   height?: string | number | ||||
|   margin?: string | number | ||||
|   marginTop?: string | number | ||||
|   marginRight?: string | number | ||||
|   marginBottom?: string | number | ||||
|   marginLeft?: string | number | ||||
|   padding?: string | number | ||||
|   paddingTop?: string | number | ||||
|   paddingRight?: string | number | ||||
|   paddingBottom?: string | number | ||||
|   paddingLeft?: string | number | ||||
|   maxWidth?: string | number | ||||
|    | ||||
|   // Typography
 | ||||
|   color?: string | ||||
|   fontSize?: string | number | ||||
|   fontFamily?: string | ||||
|   fontWeight?: string | number | ||||
|   fontStyle?: string | ||||
|   lineHeight?: string | number | ||||
|   textAlign?: 'left' | 'center' | 'right' | ||||
|   textDecoration?: string | ||||
|   textIndent?: string | number | ||||
|   letterSpacing?: string | number | ||||
|   whiteSpace?: string | ||||
|   wordBreak?: string | ||||
|    | ||||
|   // Border & Background
 | ||||
|   border?: string | ||||
|   borderTop?: string | ||||
|   borderRight?: string | ||||
|   borderBottom?: string | ||||
|   borderLeft?: string | ||||
|   borderRadius?: string | ||||
|   borderCollapse?: string | ||||
|   background?: string | ||||
|   backgroundColor?: string | ||||
|   backgroundImage?: string | ||||
|    | ||||
|   // List
 | ||||
|   listStyle?: string | ||||
|   listStyleType?: string | ||||
|   listStylePosition?: string | ||||
|    | ||||
|   // Flexbox
 | ||||
|   alignItems?: string | ||||
|   justifyContent?: string | ||||
|   flexDirection?: string | ||||
|   flexWrap?: string | ||||
|   gap?: string | ||||
|    | ||||
|   // Other
 | ||||
|   opacity?: number | ||||
|   overflow?: string | ||||
|   overflowX?: string | ||||
|   overflowY?: string | ||||
|   verticalAlign?: string | ||||
|   userSelect?: string | ||||
|   cursor?: string | ||||
|   zIndex?: number | ||||
|   boxShadow?: string | ||||
|   transition?: string | ||||
|   transform?: string | ||||
|   WebkitBackgroundClip?: string | ||||
|   WebkitTextFillColor?: string | ||||
| } | ||||
| 
 | ||||
| export interface RendererOptions { | ||||
|   base?: { | ||||
|     themeColor?: string | ||||
|     fontSize?: string | ||||
|     lineHeight?: string | ||||
|     textAlign?: string | ||||
|     textAlign?: 'left' | 'center' | 'right' | ||||
|     fontFamily?: string | ||||
|     padding?: string | ||||
|     maxWidth?: string | ||||
|     margin?: string | ||||
|     maxWidth?: string | ||||
|     wordBreak?: string | ||||
|     whiteSpace?: string | ||||
|     color?: string | ||||
| @ -24,74 +100,24 @@ export interface RendererOptions { | ||||
|     h6?: StyleOptions | ||||
|     p?: StyleOptions | ||||
|     blockquote?: StyleOptions | ||||
|     blockquote_p?: StyleOptions | ||||
|     code_pre?: StyleOptions | ||||
|     code?: StyleOptions | ||||
|     image?: StyleOptions | ||||
|     ol?: StyleOptions | ||||
|     ul?: StyleOptions | ||||
|     ol?: StyleOptions | ||||
|     table?: StyleOptions | ||||
|     th?: StyleOptions | ||||
|     td?: StyleOptions | ||||
|     footnotes?: StyleOptions | ||||
|     figure?: StyleOptions | ||||
|   } | ||||
|   inline?: { | ||||
|     strong?: StyleOptions | ||||
|     em?: StyleOptions | ||||
|     codespan?: StyleOptions | ||||
|     link?: StyleOptions | ||||
|     wx_link?: StyleOptions | ||||
|     listitem?: StyleOptions | ||||
|     table?: StyleOptions | ||||
|     thead?: StyleOptions | ||||
|     td?: StyleOptions | ||||
|     figcaption?: StyleOptions | ||||
|     checkbox?: StyleOptions | ||||
|     del?: StyleOptions | ||||
|     footnote?: StyleOptions | ||||
|   } | ||||
|   codeTheme?: CodeThemeId | ||||
| } | ||||
| 
 | ||||
| export interface StyleOptions { | ||||
|   padding?: string | ||||
|   maxWidth?: string | ||||
|   margin?: string | ||||
|   wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word' | ||||
|   whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces' | ||||
|   color?: string | ||||
|   display?: string | ||||
|   fontSize?: string | ||||
|   fontWeight?: string | ||||
|   textAlign?: string | ||||
|   paddingLeft?: string | ||||
|   marginLeft?: string | ||||
|   borderLeft?: string | ||||
|   lineHeight?: string | number | ||||
|   letterSpacing?: string | ||||
|   fontStyle?: string | ||||
|   borderRadius?: string | ||||
|   background?: string | ||||
|   marginBottom?: string | ||||
|   alignItems?: 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch' | ||||
|   gap?: string | ||||
|   overflowX?: string | ||||
|   fontFamily?: string | ||||
|   width?: string | ||||
|   listStyle?: string | ||||
|   borderStyle?: string | ||||
|   borderWidth?: string | ||||
|   borderColor?: string | ||||
|   WebkitTransformOrigin?: string | ||||
|   transformOrigin?: string | ||||
|   transform?: string | ||||
|   height?: string | ||||
|   textIndent?: string | number | ||||
|   borderCollapse?: 'collapse' | 'separate' | 'initial' | 'inherit' | ||||
|   border?: string | ||||
|   textDecoration?: string | ||||
|   borderBottom?: string | ||||
|   WebkitBackgroundClip?: string | ||||
|   WebkitTextFillColor?: string | ||||
|   '@media (max-width: 768px)'?: { | ||||
|     margin?: string | ||||
|     padding?: string | ||||
|     fontSize?: string | ||||
|   } | ||||
| }  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 tianyaxiang
						tianyaxiang