diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 6330b11..415ba69 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -1,11 +1,6 @@ -import { marked, type Tokens } from 'marked' +import { Marked } from 'marked' import type { CSSProperties } from 'react' - -// 配置 marked 选项 -marked.setOptions({ - gfm: true, - breaks: true -}) +import type { Tokens } from 'marked' // 将 React CSSProperties 转换为 CSS 字符串 function cssPropertiesToString(style: React.CSSProperties = {}): string { @@ -43,129 +38,144 @@ function baseStylesToString(base: RendererOptions['base'] = {}): string { 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 renderer = new marked.Renderer() - // 标题渲染 - renderer.heading = ({ text, depth }: Tokens.Heading) => { + // 预处理 markdown + markdown = preprocessMarkdown(markdown) + + // 创建渲染器 + 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}` } - // 段落渲染 - renderer.paragraph = (text) => { + customRenderer.paragraph = function({ text }: Tokens.Paragraph) { const style = options.block?.p const styleStr = cssPropertiesToString(style) - const content = typeof text === 'object' ? (text.text || text.toString()) : text - - return `${content}

` + return `${text}

` } - // 引用渲染 - renderer.blockquote = (quote) => { + customRenderer.blockquote = function({ text }: Tokens.Blockquote) { const style = options.block?.blockquote const styleStr = cssPropertiesToString(style) - const content = typeof quote === 'object' ? (quote.text || quote.toString()) : quote - - return `${content}` + return `${text}` } - // 代码块渲染 - renderer.code = ({ text, lang = '' }: { text: string; lang?: string }) => { + customRenderer.code = function({ text, lang }: Tokens.Code) { const style = options.block?.code_pre const styleStr = cssPropertiesToString(style) - return `${text}` + return `${text}` } - // 行内代码渲染 - renderer.codespan = (code) => { + customRenderer.codespan = function({ text }: Tokens.Codespan) { const style = options.inline?.codespan const styleStr = cssPropertiesToString(style) - return `${code}` + return `${text}` } - // 强调(斜体)渲染 - renderer.em = (text) => { + customRenderer.em = function({ text }: Tokens.Em) { const style = options.inline?.em const styleStr = cssPropertiesToString(style) return `${text}` } - // 加粗渲染 - renderer.strong = (text) => { + customRenderer.strong = function({ text }: Tokens.Strong) { const style = options.inline?.strong const styleStr = cssPropertiesToString(style) return `${text}` } - // 链接渲染 - renderer.link = ({ href, title, tokens }: Tokens.Link) => { + customRenderer.link = function({ href, title, text }: Tokens.Link) { const style = options.inline?.link const styleStr = cssPropertiesToString(style) - const text = tokens?.map(t => 'text' in t ? t.text : '').join('') || '' return `${text}` } - // 图片渲染 - renderer.image = ({ href, title, text }: Tokens.Image) => { + customRenderer.image = function({ href, title, text }: Tokens.Image) { const style = options.block?.image const styleStr = cssPropertiesToString(style) return `${text}` } - // 列表渲染 - renderer.list = (token: Tokens.List) => { - const tag = token.ordered ? 'ol' : 'ul' - const style = options.block?.[token.ordered ? 'ol' : 'ul'] + 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) - return `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${token.items.map(item => item.text).join('')}` + return `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${body.raw}` } - // 列表项渲染 - renderer.listitem = (text) => { + customRenderer.listitem = function(item: Tokens.ListItem) { const style = options.inline?.listitem const styleStr = cssPropertiesToString(style) - return `${text}` + return `${item.text}` } - marked.use({ renderer }) + // Convert Markdown to HTML using the custom renderer + const html = marked.parse(markdown, { renderer: customRenderer }) as string - // 转换 Markdown 为 HTML - const html = marked.parse(markdown, { async: false }) as string - - // 应用基础样式 + // Apply base styles const baseStyles = baseStylesToString(options.base) return baseStyles ? `
${html}
` : html } -// 转换为小红书格式 export function convertToXiaohongshu(markdown: string): string { - // 配置小红书特定的样式 - const xiaohongshuRenderer = new marked.Renderer() + // 预处理 markdown + markdown = preprocessMarkdown(markdown) + + const renderer = new marked.Renderer() - xiaohongshuRenderer.heading = ({ text, depth }: Tokens.Heading) => { + // 自定义渲染规则 + renderer.heading = function({ text, depth }: Tokens.Heading) { const fontSize = { - 1: '20px', - 2: '18px', - 3: '16px', - 4: '15px', - 5: '14px', - 6: '14px' - }[depth] + [1]: '20px', + [2]: '18px', + [3]: '16px', + [4]: '15px', + [5]: '14px', + [6]: '14px' + }[depth] || '14px' return `${text}` } - xiaohongshuRenderer.paragraph = (text) => { + renderer.paragraph = function({ text }: Tokens.Paragraph) { return `

${text}

` } - marked.setOptions({ renderer: xiaohongshuRenderer }) - - let html = marked(markdown) - html = `
${html}
` - - return html + // 使用自定义渲染器转换 Markdown + return marked.parse(markdown, { renderer }) as string } type RendererOptions = {