213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
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, '<strong>$1</strong>')
|
|
}
|
|
|
|
// Initialize marked instance
|
|
const marked = new Marked()
|
|
|
|
// 创建基础渲染器
|
|
const baseRenderer = new marked.Renderer()
|
|
|
|
// 重写 strong 渲染器
|
|
baseRenderer.strong = function(text) {
|
|
return `<strong>${text}</strong>`
|
|
}
|
|
|
|
// 应用配置和渲染器
|
|
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 `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${text}</h${depth}>`
|
|
}
|
|
|
|
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 `<p${styleStr ? ` style="${styleStr}"` : ''}>${content}</p>`
|
|
}
|
|
|
|
customRenderer.blockquote = function({ text }: Tokens.Blockquote) {
|
|
const style = options.block?.blockquote
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>`
|
|
}
|
|
|
|
customRenderer.code = function({ text, lang }: Tokens.Code) {
|
|
const style = options.block?.code_pre
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<pre${styleStr ? ` style="${styleStr}"` : ''}><code class="language-${lang || ''}">${text}</code></pre>`
|
|
}
|
|
|
|
customRenderer.codespan = function({ text }: Tokens.Codespan) {
|
|
const style = options.inline?.codespan
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<code${styleStr ? ` style="${styleStr}"` : ''}>${text}</code>`
|
|
}
|
|
|
|
customRenderer.em = function({ text }: Tokens.Em) {
|
|
const style = options.inline?.em
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>`
|
|
}
|
|
|
|
customRenderer.strong = function({ text }: Tokens.Strong) {
|
|
const style = options.inline?.strong
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>`
|
|
}
|
|
|
|
customRenderer.link = function({ href, title, text }: Tokens.Link) {
|
|
const style = options.inline?.link
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>`
|
|
}
|
|
|
|
customRenderer.image = function({ href, title, text }: Tokens.Image) {
|
|
const style = options.block?.image
|
|
const styleStr = cssPropertiesToString(style)
|
|
return `<img src="${href}"${title ? ` title="${title}"` : ''} alt="${text}"${styleStr ? ` style="${styleStr}"` : ''} />`
|
|
}
|
|
|
|
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}</${tag}>`
|
|
}
|
|
|
|
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 `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
|
|
}
|
|
|
|
// 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 ? `<div style="${baseStyles}">${html}</div>` : 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 `<h${depth} style="margin-top: 25px; margin-bottom: 12px; font-weight: bold; font-size: ${fontSize}; color: #222;">${text}</h${depth}>`
|
|
}
|
|
|
|
renderer.paragraph = function({ text }: Tokens.Paragraph) {
|
|
return `<p style="margin-bottom: 16px; line-height: 1.6; font-size: 15px; color: #222;">${text}</p>`
|
|
}
|
|
|
|
// 使用自定义渲染器转换 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 } |