import { Marked } from 'marked' import type { CSSProperties } from 'react' import type { Tokens } from 'marked' // 将样式对象转换为 CSS 字符串 function cssPropertiesToString(style: StyleOptions = {}): string { return Object.entries(style) .filter(([_, value]) => value !== undefined) .map(([key, value]) => { // 转换驼峰命名为连字符命名 const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase() return `${cssKey}: ${value}` }) .join(';') } // 将基础样式选项转换为 CSS 字符串 function baseStylesToString(base: RendererOptions['base'] = {}): string { const styles: string[] = [] if (base.primaryColor) { styles.push(`--md-primary-color: ${base.primaryColor}`) } if (base.textAlign) { styles.push(`text-align: ${base.textAlign}`) } if (base.lineHeight) { styles.push(`line-height: ${base.lineHeight}`) } if (base.fontSize) { styles.push(`font-size: ${base.fontSize}`) } return styles.join(';') } // 预处理函数 function preprocessMarkdown(markdown: string): string { return markdown // 处理 ** 语法,但排除已经是 HTML 的部分 .replace(/(?]*)\*\*([^*]+)\*\*(?![^<]*>)/g, '$1') // 处理无序列表的 - 标记,但排除代码块内的部分 .replace(/^(?!\s*```)([ \t]*)-\s+/gm, '$1• ') } // Initialize marked instance const marked = new Marked() // 创建基础渲染器 const baseRenderer = new marked.Renderer() // 重写 strong 渲染器 baseRenderer.strong = function(text) { return `${text}` } // 应用配置和渲染器 marked.setOptions({ gfm: true, breaks: true, async: false, pedantic: false, renderer: baseRenderer }) const defaultOptions: RendererOptions = { base: { primaryColor: '#333333', textAlign: 'left', lineHeight: '1.75', fontSize: '15px', themeColor: '#1a1a1a' }, block: { h1: { fontSize: '24px' }, h2: { fontSize: '20px' }, h3: { fontSize: '18px' }, p: { fontSize: '15px', color: '#333333' }, code_pre: { fontSize: '14px', color: '#333333' }, blockquote: { fontSize: '15px', color: '#666666' } }, inline: { link: { color: '#576b95' }, codespan: { color: '#333333' }, em: { color: '#666666' } } } export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string { // 创建渲染器 const customRenderer = new marked.Renderer() // 继承基础渲染器 Object.setPrototypeOf(customRenderer, baseRenderer) // 合并选项 const mergedOptions = { base: { ...defaultOptions.base, ...options.base }, block: { ...defaultOptions.block, ...options.block }, inline: { ...defaultOptions.inline, ...options.inline } } customRenderer.heading = function({ text, depth }: Tokens.Heading) { const style = { ...mergedOptions.block?.[`h${depth}`], color: mergedOptions.base?.themeColor // 使用主题颜色 } const styleStr = cssPropertiesToString(style) const tokens = marked.Lexer.lexInline(text) const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) return `${content}` } customRenderer.paragraph = function({ text }: Tokens.Paragraph) { const style = mergedOptions.block?.p const styleStr = cssPropertiesToString(style) const tokens = marked.Lexer.lexInline(text) const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) return `${content}

` } customRenderer.blockquote = function({ text }: Tokens.Blockquote) { const style = mergedOptions.block?.blockquote const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.code = function({ text, lang }: Tokens.Code) { const style = mergedOptions.block?.code_pre const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.codespan = function({ text }: Tokens.Codespan) { const style = mergedOptions.inline?.codespan const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.em = function({ text }: Tokens.Em) { const style = mergedOptions.inline?.em const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.strong = function({ text }: Tokens.Strong) { const style = mergedOptions.inline?.strong const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.link = function({ href, title, text }: Tokens.Link) { const style = mergedOptions.inline?.link const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.image = function({ href, title, text }: Tokens.Image) { const style = mergedOptions.block?.image const styleStr = cssPropertiesToString(style) return `${text}` } // 重写 list 方法 customRenderer.list = function(token: Tokens.List): string { const tag = token.ordered ? 'ol' : 'ul' try { const style = mergedOptions.block?.[token.ordered ? 'ol' : 'ul'] const styleStr = cssPropertiesToString(style) const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : '' return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${token.items.map(item => customRenderer.listitem(item)).join('')}` } catch (error) { console.error(`Error rendering list: ${error}`) return `<${tag}>${token.items.map(item => customRenderer.listitem(item)).join('')}` } } // 重写 listitem 方法 customRenderer.listitem = function(item: Tokens.ListItem) { try { const style = mergedOptions.inline?.listitem const styleStr = cssPropertiesToString(style) // 移除列表项开头的破折号和空格 let itemText = item.text.replace(/^- /, '') // 处理任务列表项 if (item.task) { const checkbox = ` ` itemText = checkbox + itemText } // 使用 Lexer 和 Parser 处理剩余的内联标记 const tokens = marked.Lexer.lexInline(itemText) const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) return `${content}` } catch (error) { console.error(`Error rendering list item: ${error}`) return `
  • ${item.text}
  • ` } } // Convert Markdown to HTML using the custom renderer const html = marked.parse(markdown, { renderer: customRenderer }) as string // Apply base styles const baseStyles = baseStylesToString(mergedOptions.base) return baseStyles ? `
    ${html}
    ` : html } export function convertToXiaohongshu(markdown: string): string { // 预处理 markdown markdown = preprocessMarkdown(markdown) const renderer = new marked.Renderer() // 自定义渲染规则 renderer.heading = function({ text, depth }: Tokens.Heading) { const fontSize = { [1]: '20px', [2]: '18px', [3]: '16px', [4]: '15px', [5]: '14px', [6]: '14px' }[depth] || '14px' return `${text}` } renderer.paragraph = function({ text }: Tokens.Paragraph) { return `

    ${text}

    ` } // 使用自定义渲染器转换 Markdown return marked.parse(markdown, { renderer }) as string } export interface StyleOptions { fontSize?: string color?: string margin?: string padding?: string border?: string borderLeft?: string borderBottom?: string borderRadius?: string background?: string fontWeight?: string fontStyle?: string textDecoration?: string display?: string lineHeight?: string | number textAlign?: string paddingLeft?: string overflowX?: string width?: string letterSpacing?: string fontFamily?: string WebkitBackgroundClip?: string WebkitTextFillColor?: string } export interface RendererOptions { base?: { primaryColor?: string textAlign?: string lineHeight?: string | number fontSize?: string themeColor?: string } block?: { [key: string]: StyleOptions } inline?: { [key: string]: StyleOptions } }