From dd2753a65c5814319aa3418edc90e086921a926a Mon Sep 17 00:00:00 2001 From: tianyaxiang Date: Sat, 1 Feb 2025 14:10:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A8=A1=E7=89=88=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/editor/WechatEditor.tsx | 115 ++++++++----------------- src/config/wechat-templates.ts | 1 + src/lib/markdown.ts | 56 ++++++------ 3 files changed, 62 insertions(+), 110 deletions(-) diff --git a/src/components/editor/WechatEditor.tsx b/src/components/editor/WechatEditor.tsx index 4fb26fc..d7a2f69 100644 --- a/src/components/editor/WechatEditor.tsx +++ b/src/components/editor/WechatEditor.tsx @@ -106,18 +106,33 @@ export default function WechatEditor() { p: { ...(template?.options?.block?.p || {}), ...(styleOptions.block?.p || {}), - fontSize: styleOptions.block?.p?.fontSize || template?.options?.block?.p?.fontSize || '15px', - lineHeight: styleOptions.block?.p?.lineHeight || template?.options?.block?.p?.lineHeight || 2, - letterSpacing: styleOptions.block?.p?.letterSpacing || template?.options?.block?.p?.letterSpacing || '0.1em' + fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px', + lineHeight: styleOptions.base?.lineHeight || template?.options?.base?.lineHeight || 2 + }, + ol: { + ...(template?.options?.block?.ol || {}), + ...(styleOptions.block?.ol || {}), + fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px', + }, + ul: { + ...(template?.options?.block?.ul || {}), + ...(styleOptions.block?.ul || {}), + fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px', } }, inline: { ...(template?.options?.inline || {}), - ...(styleOptions.inline || {}) + ...(styleOptions.inline || {}), + listitem: { + ...(template?.options?.inline?.listitem || {}), + ...(styleOptions.inline?.listitem || {}), + fontSize: styleOptions.base?.fontSize || template?.options?.base?.fontSize || '15px', + } } } const html = convertToWechat(value, mergedOptions) + if (!template?.transform) return html try { @@ -202,84 +217,28 @@ export default function WechatEditor() { } }, [toast]) + // 渲染预览内容 + const renderPreview = useCallback(() => { + const content = getPreviewContent() + return ( +
+ ) + }, [getPreviewContent]) + const handleCopy = useCallback(async () => { - const previewContent = previewRef.current?.querySelector('.preview-content') as HTMLElement | null - if (!previewContent) { - toast({ - variant: "destructive", - title: "复制失败", - description: "未找到预览内容", - duration: 2000 - }) - return - } - try { - // 创建临时容器来处理样式 + const htmlContent = getPreviewContent() const tempDiv = document.createElement('div') - tempDiv.innerHTML = previewContent.innerHTML - // 获取当前模板 - const template = templates.find(t => t.id === selectedTemplate) - - // 处理 CSS 变量 - const cssVariables = { - '--foreground': styleOptions.base?.themeColor || template?.options.base?.themeColor || '#1a1a1a', - '--background': '#ffffff', - '--muted': '#f1f5f9', - '--muted-foreground': '#64748b', - '--blockquote-background': 'rgba(0, 0, 0, 0.05)' - } + tempDiv.innerHTML = htmlContent + const plainText = tempDiv.textContent || tempDiv.innerText - // 替换所有元素中的 CSS 变量 - const replaceVariables = (element: HTMLElement) => { - const style = window.getComputedStyle(element) - const properties = ['color', 'background-color', 'border-color', 'border-left-color', 'font-size'] - - // 获取元素的计算样式 - const computedStyle = window.getComputedStyle(element) - // 保留原始样式 - const originalStyle = element.getAttribute('style') || '' - - // 如果是段落元素,确保应用字体大小 - if (element.tagName.toLowerCase() === 'p') { - // alert(element.style.fontSize ) - element.style.fontSize = '15px' - } - - properties.forEach(prop => { - const value = style.getPropertyValue(prop) - if (value.includes('var(')) { - let finalValue = value - Object.entries(cssVariables).forEach(([variable, replacement]) => { - finalValue = finalValue.replace(`var(${variable})`, replacement) - }) - element.style[prop as any] = finalValue - } else if (prop === 'font-size' && !element.style.fontSize) { - // 如果没有字体大小,从计算样式中获取 - element.style.fontSize = computedStyle.fontSize - } - }) - - // 递归处理子元素 - Array.from(element.children).forEach(child => { - if (child instanceof HTMLElement) { - replaceVariables(child) - } - }) - } - - // 处理所有元素 - Array.from(tempDiv.children).forEach(child => { - if (child instanceof HTMLElement) { - replaceVariables(child) - } - }) - - // 创建并写入剪贴板 await navigator.clipboard.write([ new ClipboardItem({ - 'text/html': new Blob([previewContent.innerHTML], { type: 'text/html' }), - 'text/plain': new Blob([previewContent.innerText], { type: 'text/plain' }) + 'text/html': new Blob([htmlContent], { type: 'text/html' }), + 'text/plain': new Blob([plainText], { type: 'text/plain' }) }) ]) @@ -291,7 +250,7 @@ export default function WechatEditor() { } catch (err) { console.error('Copy error:', err) try { - await navigator.clipboard.writeText(previewContent.innerText) + await navigator.clipboard.writeText(previewContent) toast({ title: "复制成功", description: "已复制预览内容(仅文本)", @@ -306,7 +265,7 @@ export default function WechatEditor() { }) } } - }, [previewRef, selectedTemplate, styleOptions, toast]) + }, [previewRef, toast, getPreviewContent]) // 检测是否为移动设备 const isMobile = useCallback(() => { diff --git a/src/config/wechat-templates.ts b/src/config/wechat-templates.ts index e4d69c6..e0eb09e 100644 --- a/src/config/wechat-templates.ts +++ b/src/config/wechat-templates.ts @@ -89,6 +89,7 @@ export const templates: Template[] = [ // 段落 p: { + 'fontSize': `var(--fontSize)`, 'margin': `1.5em 8px`, 'letterSpacing': `0.1em`, 'color': `hsl(var(--foreground))`, diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index ca427aa..c101a02 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -17,10 +17,7 @@ function cssPropertiesToString(style: StyleOptions = {}): string { // 将基础样式选项转换为 CSS 字符串 function baseStylesToString(base: RendererOptions['base'] = {}): string { const styles: string[] = [] - - if (base.primaryColor) { - styles.push(`--md-primary-color: ${base.primaryColor}`) - } + if (base.lineHeight) { styles.push(`line-height: ${base.lineHeight}`) } @@ -84,11 +81,10 @@ const defaultOptions: RendererOptions = { } export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string { - // 创建渲染器 - const customRenderer = new marked.Renderer() - + const renderer = new marked.Renderer() + // 继承基础渲染器 - Object.setPrototypeOf(customRenderer, baseRenderer) + Object.setPrototypeOf(renderer, baseRenderer) // 合并选项 const mergedOptions = { @@ -97,7 +93,7 @@ export function convertToWechat(markdown: string, options: RendererOptions = def inline: { ...defaultOptions.inline, ...options.inline } } - customRenderer.heading = function({ text, depth }: Tokens.Heading) { + renderer.heading = function({ text, depth }: Tokens.Heading) { const style = { ...mergedOptions.block?.[`h${depth}`], color: mergedOptions.base?.themeColor, // 使用主题颜色 @@ -105,78 +101,77 @@ export function convertToWechat(markdown: string, options: RendererOptions = def } const styleStr = cssPropertiesToString(style) const tokens = marked.Lexer.lexInline(text) - const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) + const content = marked.Parser.parseInline(tokens, { renderer }) return `${content}` } - customRenderer.paragraph = function({ text }: Tokens.Paragraph) { - const style = mergedOptions.block?.p - + renderer.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}

` + const content = marked.Parser.parseInline(tokens, { renderer }) + return `

${content}

` } - customRenderer.blockquote = function({ text }: Tokens.Blockquote) { + renderer.blockquote = function({ text }: Tokens.Blockquote) { const style = mergedOptions.block?.blockquote const styleStr = cssPropertiesToString(style) return `${text}` } - customRenderer.code = function({ text, lang }: Tokens.Code) { + renderer.code = function({ text, lang }: Tokens.Code) { const style = mergedOptions.block?.code_pre const styleStr = cssPropertiesToString(style) return `${text}` } - customRenderer.codespan = function({ text }: Tokens.Codespan) { + renderer.codespan = function({ text }: Tokens.Codespan) { const style = mergedOptions.inline?.codespan const styleStr = cssPropertiesToString(style) return `${text}` } - customRenderer.em = function({ text }: Tokens.Em) { + renderer.em = function({ text }: Tokens.Em) { const style = mergedOptions.inline?.em const styleStr = cssPropertiesToString(style) return `${text}` } - customRenderer.strong = function({ text }: Tokens.Strong) { + renderer.strong = function({ text }: Tokens.Strong) { const style = mergedOptions.inline?.strong const styleStr = cssPropertiesToString(style) return `${text}` } - customRenderer.link = function({ href, title, text }: Tokens.Link) { + renderer.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) { + renderer.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 { +renderer.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('')}` + return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${token.items.map(item => renderer.listitem(item)).join('')}` } catch (error) { console.error(`Error rendering list: ${error}`) - return `<${tag}>${token.items.map(item => customRenderer.listitem(item)).join('')}` + return `<${tag}>${token.items.map(item => renderer.listitem(item)).join('')}` } } // 重写 listitem 方法 -customRenderer.listitem = function(item: Tokens.ListItem) { +renderer.listitem = function(item: Tokens.ListItem) { try { const style = mergedOptions.inline?.listitem const styleStr = cssPropertiesToString(style) @@ -192,7 +187,7 @@ customRenderer.listitem = function(item: Tokens.ListItem) { // 使用 Lexer 和 Parser 处理剩余的内联标记 const tokens = marked.Lexer.lexInline(itemText) - const content = marked.Parser.parseInline(tokens, { renderer: customRenderer }) + const content = marked.Parser.parseInline(tokens, { renderer }) return `${content}` } catch (error) { @@ -201,15 +196,12 @@ customRenderer.listitem = function(item: Tokens.ListItem) { } } - - - // Convert Markdown to HTML using the custom renderer - const html = marked.parse(markdown, { renderer: customRenderer }) as string + const html = marked.parse(markdown, { renderer }) as string // Apply base styles const baseStyles = baseStylesToString(mergedOptions.base) - return baseStyles ? `
${html}
` : html + return baseStyles ? `
${html}
` : html } export function convertToXiaohongshu(markdown: string): string {