优化 markdown 解析
This commit is contained in:
		
							parent
							
								
									e1361f9219
								
							
						
					
					
						commit
						b8c464bd4c
					
				| @ -87,7 +87,7 @@ const marked = new Marked({ | |||||||
|   pedantic: false |   pedantic: false | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| // 应用配置
 | // 重写 marked 配置
 | ||||||
| marked.use({ | marked.use({ | ||||||
|   breaks: true, |   breaks: true, | ||||||
|   gfm: true, |   gfm: true, | ||||||
| @ -118,12 +118,40 @@ const defaultOptions: RendererOptions = { | |||||||
|       padding: '1em', |       padding: '1em', | ||||||
|       lineHeight: '1.5', |       lineHeight: '1.5', | ||||||
|       margin: '10px 8px' |       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: { |   inline: { | ||||||
|     link: { color: '#576b95' }, |     link: { color: '#576b95' }, | ||||||
|     codespan: { color: '#333333' }, |     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 |       color: mergedOptions.base?.themeColor | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 paragraph 方法
 | ||||||
|   renderer.paragraph = function({ text }: Tokens.Paragraph) { |   renderer.paragraph = function({ text, tokens }: Tokens.Paragraph) { | ||||||
|     const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions |     const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions | ||||||
|     const style: StyleOptions = { |     const style: StyleOptions = { | ||||||
|       ...paragraphStyle, |       ...paragraphStyle, | ||||||
| @ -180,7 +210,23 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       lineHeight: mergedOptions.base?.lineHeight |       lineHeight: mergedOptions.base?.lineHeight | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 blockquote 方法
 | ||||||
| @ -191,23 +237,26 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}` |       borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}` | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 code 方法
 | ||||||
|   renderer.code = function({ text, lang }: Tokens.Code) {   |   renderer.code = function({ text, lang }: Tokens.Code) {   | ||||||
| 
 |  | ||||||
|     const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions |     const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions | ||||||
|     const style: StyleOptions = { |     const style: StyleOptions = { | ||||||
|       ...codeStyle, |       ...codeStyle, | ||||||
|       ...getCodeThemeStyles(mergedOptions.codeTheme) |       ...getCodeThemeStyles(mergedOptions.codeTheme) | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     const styleStr = cssPropertiesToString(style) | ||||||
|  |      | ||||||
|     // 代码高亮处理
 |     // 代码高亮处理
 | ||||||
|     let highlighted = text |     let highlighted = text | ||||||
|     if (lang && Prism.languages[lang]) { |     if (lang && Prism.languages[lang]) { | ||||||
|       // Helper function to recursively process tokens
 |       // 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') { |         if (typeof token === 'string') { | ||||||
|           return token |           return token | ||||||
|         } |         } | ||||||
| @ -223,7 +272,14 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       try { |       try { | ||||||
|         const grammar = Prism.languages[lang] |         const grammar = Prism.languages[lang] | ||||||
|         const tokens = Prism.tokenize(text, grammar) |         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) { |       } catch (error) { | ||||||
|         console.error(`Error highlighting code: ${error}`) |         console.error(`Error highlighting code: ${error}`) | ||||||
|       } |       } | ||||||
| @ -234,7 +290,6 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
| 
 | 
 | ||||||
|   // 重写 codespan 方法
 |   // 重写 codespan 方法
 | ||||||
|   renderer.codespan = function({ text }: Tokens.Codespan) {   |   renderer.codespan = function({ text }: Tokens.Codespan) {   | ||||||
| 
 |  | ||||||
|     const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions |     const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions | ||||||
|     const style: StyleOptions = { |     const style: StyleOptions = { | ||||||
|       ...codespanStyle |       ...codespanStyle | ||||||
| @ -251,7 +306,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       fontStyle: 'italic' |       fontStyle: 'italic' | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 strong 方法
 | ||||||
| @ -263,7 +321,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       fontWeight: 'bold' |       fontWeight: 'bold' | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 link 方法
 | ||||||
| @ -273,7 +334,9 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|       ...linkStyle |       ...linkStyle | ||||||
|     } |     } | ||||||
|     const styleStr = cssPropertiesToString(style) |     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 方法
 |   // 重写 image 方法
 | ||||||
| @ -346,6 +409,48 @@ export function convertToWechat(markdown: string, options: RendererOptions = def | |||||||
|     return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>` |     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
 |   // Convert Markdown to HTML using the custom renderer
 | ||||||
|   const html = marked.parse(markdown, { renderer }) as string |   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 { CSSProperties } from 'react' | ||||||
| import type { CodeThemeId } from '@/config/code-themes' | 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 { | export interface RendererOptions { | ||||||
|   base?: { |   base?: { | ||||||
|     themeColor?: string |     themeColor?: string | ||||||
|     fontSize?: string |     fontSize?: string | ||||||
|     lineHeight?: string |     lineHeight?: string | ||||||
|     textAlign?: string |     textAlign?: 'left' | 'center' | 'right' | ||||||
|     fontFamily?: string |     fontFamily?: string | ||||||
|     padding?: string |     padding?: string | ||||||
|     maxWidth?: string |  | ||||||
|     margin?: string |     margin?: string | ||||||
|  |     maxWidth?: string | ||||||
|     wordBreak?: string |     wordBreak?: string | ||||||
|     whiteSpace?: string |     whiteSpace?: string | ||||||
|     color?: string |     color?: string | ||||||
| @ -24,74 +100,24 @@ export interface RendererOptions { | |||||||
|     h6?: StyleOptions |     h6?: StyleOptions | ||||||
|     p?: StyleOptions |     p?: StyleOptions | ||||||
|     blockquote?: StyleOptions |     blockquote?: StyleOptions | ||||||
|     blockquote_p?: StyleOptions |  | ||||||
|     code_pre?: StyleOptions |     code_pre?: StyleOptions | ||||||
|     code?: StyleOptions |  | ||||||
|     image?: StyleOptions |     image?: StyleOptions | ||||||
|     ol?: StyleOptions |  | ||||||
|     ul?: StyleOptions |     ul?: StyleOptions | ||||||
|  |     ol?: StyleOptions | ||||||
|  |     table?: StyleOptions | ||||||
|  |     th?: StyleOptions | ||||||
|  |     td?: StyleOptions | ||||||
|     footnotes?: StyleOptions |     footnotes?: StyleOptions | ||||||
|     figure?: StyleOptions |  | ||||||
|   } |   } | ||||||
|   inline?: { |   inline?: { | ||||||
|     strong?: StyleOptions |     strong?: StyleOptions | ||||||
|     em?: StyleOptions |     em?: StyleOptions | ||||||
|     codespan?: StyleOptions |     codespan?: StyleOptions | ||||||
|     link?: StyleOptions |     link?: StyleOptions | ||||||
|     wx_link?: StyleOptions |  | ||||||
|     listitem?: StyleOptions |     listitem?: StyleOptions | ||||||
|     table?: StyleOptions |     checkbox?: StyleOptions | ||||||
|     thead?: StyleOptions |     del?: StyleOptions | ||||||
|     td?: StyleOptions |  | ||||||
|     figcaption?: StyleOptions |  | ||||||
|     footnote?: StyleOptions |     footnote?: StyleOptions | ||||||
|   } |   } | ||||||
|   codeTheme?: CodeThemeId |   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