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.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}
` } // 使用自定义渲染器转换 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 } }