import { Marked } from 'marked' import type { CSSProperties } from 'react' import type { Tokens } from 'marked' // 将 React CSSProperties 转换为 CSS 字符串 function cssPropertiesToString(style: React.CSSProperties = {}): string { return Object.entries(style) .map(([key, value]) => { // 转换驼峰命名为连字符命名 const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase() // 处理对象类型的值 if (value && typeof value === 'object') { if ('toString' in value) { return `${cssKey}: ${value.toString()}` } return `${cssKey}: ${JSON.stringify(value)}` } return `${cssKey}: ${value}` }) .filter(Boolean) .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}`) } return styles.join(';') } // 预处理函数 function preprocessMarkdown(markdown: string): string { // 处理 ** 语法,但排除已经是 HTML 的部分 return markdown.replace(/(?]*)\*\*([^*]+)\*\*(?![^<]*>)/g, '$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 }) export function convertToWechat(markdown: string, options: RendererOptions = {}): string { // 创建渲染器 const customRenderer = new marked.Renderer() // 继承基础渲染器 Object.setPrototypeOf(customRenderer, baseRenderer) customRenderer.heading = function({ text, depth }: Tokens.Heading) { const style = options.block?.[`h${depth}` as keyof typeof options.block] const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.paragraph = function({ text }: Tokens.Paragraph) { const style = options.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 = options.block?.blockquote const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.code = function({ text, lang }: Tokens.Code) { const style = options.block?.code_pre const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.codespan = function({ text }: Tokens.Codespan) { const style = options.inline?.codespan const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.em = function({ text }: Tokens.Em) { const style = options.inline?.em const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.strong = function({ text }: Tokens.Strong) { const style = options.inline?.strong const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.link = function({ href, title, text }: Tokens.Link) { const style = options.inline?.link const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.image = function({ href, title, text }: Tokens.Image) { const style = options.block?.image const styleStr = cssPropertiesToString(style) return `${text}` } customRenderer.list = function(body: Tokens.List) { const ordered = body.ordered const tag = ordered ? 'ol' : 'ul' const style = options.block?.[ordered ? 'ol' : 'ul'] const styleStr = cssPropertiesToString(style) const tokens = marked.Lexer.lexInline(body.raw) const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) return `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${content}` } customRenderer.listitem = function(item: Tokens.ListItem) { const style = options.inline?.listitem const styleStr = cssPropertiesToString(style) const tokens = marked.Lexer.lexInline(item.text) const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) return `${content}` } // Convert Markdown to HTML using the custom renderer const html = marked.parse(markdown, { renderer: customRenderer }) as string // Apply base styles const baseStyles = baseStylesToString(options.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 } type RendererOptions = { base?: { primaryColor?: string textAlign?: string lineHeight?: string | number } block?: { h1?: CSSProperties h2?: CSSProperties h3?: CSSProperties h4?: CSSProperties h5?: CSSProperties h6?: CSSProperties p?: CSSProperties blockquote?: CSSProperties code_pre?: CSSProperties image?: CSSProperties ul?: CSSProperties ol?: CSSProperties } inline?: { strong?: CSSProperties em?: CSSProperties codespan?: CSSProperties link?: CSSProperties listitem?: CSSProperties } } export type { RendererOptions }