diff --git a/src/components/editor/WechatEditor.tsx b/src/components/editor/WechatEditor.tsx
index 06e1582..9581978 100644
--- a/src/components/editor/WechatEditor.tsx
+++ b/src/components/editor/WechatEditor.tsx
@@ -5,8 +5,7 @@ import { templates } from '@/config/wechat-templates'
import { cn } from '@/lib/utils'
import { useToast } from '@/components/ui/use-toast'
import { ToastAction } from '@/components/ui/toast'
-import { convertToWechat, getCodeThemeStyles } from '@/lib/markdown'
-import { type RendererOptions } from '@/lib/types'
+import { convertToWechat, getCodeThemeStyles, type RendererOptions } from '@/lib/markdown'
import { useEditorSync } from './hooks/useEditorSync'
import { useAutoSave } from './hooks/useAutoSave'
import { EditorToolbar } from './components/EditorToolbar'
diff --git a/src/components/editor/components/EditorToolbar.tsx b/src/components/editor/components/EditorToolbar.tsx
index e245fe7..05aacb4 100644
--- a/src/components/editor/components/EditorToolbar.tsx
+++ b/src/components/editor/components/EditorToolbar.tsx
@@ -8,7 +8,7 @@ import { TemplateManager } from '../../template/TemplateManager'
import { StyleConfigDialog } from '../StyleConfigDialog'
import { ArticleList } from '../ArticleList'
import { type Article } from '../constants'
-import { type RendererOptions } from '@/lib/types'
+import { type RendererOptions } from '@/lib/markdown'
import { ThemeToggle } from '@/components/theme/ThemeToggle'
import { Logo } from '@/components/icons/Logo'
import Link from 'next/link'
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
deleted file mode 100644
index dca5e7c..0000000
--- a/src/lib/markdown.ts
+++ /dev/null
@@ -1,458 +0,0 @@
-import { Marked } from 'marked'
-import type { CSSProperties } from 'react'
-import type { Tokens } from 'marked'
-import { codeThemes, type CodeThemeId } from '@/config/code-themes'
-import Prism from 'prismjs'
-import 'prismjs/components/prism-javascript'
-import 'prismjs/components/prism-typescript'
-import 'prismjs/components/prism-jsx'
-import 'prismjs/components/prism-tsx'
-import 'prismjs/components/prism-css'
-import 'prismjs/components/prism-scss'
-import 'prismjs/components/prism-json'
-import 'prismjs/components/prism-yaml'
-import 'prismjs/components/prism-markdown'
-import 'prismjs/components/prism-bash'
-import 'prismjs/components/prism-python'
-import 'prismjs/components/prism-java'
-import 'prismjs/components/prism-go'
-import 'prismjs/components/prism-rust'
-import 'prismjs/components/prism-sql'
-import 'prismjs/components/prism-docker'
-import 'prismjs/components/prism-nginx'
-import { StyleOptions, type RendererOptions } from '@/lib/types'
-
-// 将样式对象转换为 CSS 字符串
-function cssPropertiesToString(style: StyleOptions = {}): string {
- if (!style) return ''
-
- return Object.entries(style)
- .filter(([_, value]) => value !== undefined && value !== null)
- .map(([key, value]) => {
- // 处理媒体查询
- if (key === '@media (max-width: 768px)') {
- return '' // 我们不在内联样式中包含媒体查询
- }
-
- // 转换驼峰命名为连字符命名
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
-
- // 处理数字值
- if (typeof value === 'number' && !cssKey.includes('line-height')) {
- value = `${value}px`
- }
-
- return `${cssKey}: ${value}`
- })
- .filter(Boolean) // 移除空字符串
- .join(';')
-}
-
-// 将基础样式选项转换为 CSS 字符串
-function baseStylesToString(base: RendererOptions['base'] = {}): string {
- if (!base) return ''
-
- const styles: string[] = []
-
- if (base.lineHeight) {
- styles.push(`line-height: ${base.lineHeight}`)
- }
- if (base.fontSize) {
- styles.push(`font-size: ${base.fontSize}`)
- }
- if (base.textAlign) {
- styles.push(`text-align: ${base.textAlign}`)
- }
- if (base.themeColor) {
- styles.push(`--theme-color: ${base.themeColor}`)
- }
-
- 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({
- gfm: true,
- breaks: true,
- async: false,
- pedantic: false
-})
-
-// 重写 marked 配置
-marked.use({
- breaks: true,
- gfm: true,
- walkTokens(token: Tokens.Generic) {
- // 确保列表项被正确处理
- if (token.type === 'list') {
- (token as Tokens.List).items.forEach(item => {
- if (item.task) {
- item.checked = !!item.checked
- }
- })
- }
- }
-})
-
-const defaultOptions: RendererOptions = {
- base: {
- themeColor: '#1a1a1a',
- fontSize: '15px',
- lineHeight: '1.75',
- textAlign: 'left'
- },
- block: {
- code_pre: {
- fontSize: '14px',
- overflowX: 'auto',
- borderRadius: '8px',
- padding: '1em',
- lineHeight: '1.5',
- margin: '10px 8px'
- },
- table: {
- width: '100%',
- marginBottom: '1em',
- borderCollapse: 'collapse',
- fontSize: '14px'
- },
- th: {
- padding: '0.5em 1em',
- borderBottom: '2px solid var(--theme-color)',
- textAlign: 'left',
- fontWeight: 'bold'
- },
- td: {
- padding: '0.5em 1em',
- borderBottom: '1px solid #eee'
- },
- footnotes: {
- marginTop: '2em',
- paddingTop: '1em',
- borderTop: '1px solid #eee',
- fontSize: '0.9em',
- color: '#666'
- }
- },
- inline: {
- link: { color: '#576b95' },
- codespan: { color: '#333333' },
- em: { color: '#666666' },
- del: { color: '#999999', textDecoration: 'line-through' },
- checkbox: {
- marginRight: '0.5em',
- verticalAlign: 'middle'
- }
- }
-}
-
-// 获取代码主题的样式
-export function getCodeThemeStyles(theme: CodeThemeId): StyleOptions {
- const themeConfig = codeThemes.find(t => t.id === theme)
- if (!themeConfig) return {}
-
- return {
- background: themeConfig.theme.background,
- color: themeConfig.theme.text,
- }
-}
-
-// 获取代码token的样式
-function getTokenStyles(theme: CodeThemeId, tokenType: string): string {
- const themeConfig = codeThemes.find(t => t.id === theme)
- if (!themeConfig) return ''
-
- const tokenColor = themeConfig.theme[tokenType as keyof typeof themeConfig.theme]
- if (!tokenColor) return ''
- return `color: ${tokenColor};`
-}
-
-export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string {
- const renderer = new marked.Renderer()
-
- // 合并选项
- const mergedOptions = {
- base: { ...defaultOptions.base, ...options.base },
- block: { ...defaultOptions.block, ...options.block },
- inline: { ...defaultOptions.inline, ...options.inline },
- codeTheme: options.codeTheme || codeThemes[0].id
- }
-
- // 重写 heading 方法
- renderer.heading = function({ text, depth }: Tokens.Heading) {
- const headingKey = `h${depth}` as keyof RendererOptions['block']
- const headingStyle = (mergedOptions.block?.[headingKey] || {}) as StyleOptions
- const style: StyleOptions = {
- ...headingStyle,
- color: mergedOptions.base?.themeColor
- }
- const styleStr = cssPropertiesToString(style)
- const tokens = marked.Lexer.lexInline(text)
- const content = marked.Parser.parseInline(tokens, { renderer })
- return `${content}`
- }
-
- // 重写 paragraph 方法
- renderer.paragraph = function({ text, tokens }: Tokens.Paragraph) {
- const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions
- const style: StyleOptions = {
- ...paragraphStyle,
- fontSize: mergedOptions.base?.fontSize,
- lineHeight: mergedOptions.base?.lineHeight
- }
- const styleStr = cssPropertiesToString(style)
-
- // 处理段落中的内联标记
- let content = text
- if (tokens) {
- content = tokens.map(token => {
- if (token.type === 'text') {
- const inlineTokens = marked.Lexer.lexInline(token.text)
- return marked.Parser.parseInline(inlineTokens, { renderer })
- }
- return marked.Parser.parseInline([token], { renderer })
- }).join('')
- } else {
- const inlineTokens = marked.Lexer.lexInline(text)
- content = marked.Parser.parseInline(inlineTokens, { renderer })
- }
-
- return `
${content}
`
- }
-
- // 重写 blockquote 方法
- renderer.blockquote = function({ text }: Tokens.Blockquote) {
- const blockquoteStyle = (mergedOptions.block?.blockquote || {}) as StyleOptions
- const style: StyleOptions = {
- ...blockquoteStyle,
- borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}`
- }
- const styleStr = cssPropertiesToString(style)
- const tokens = marked.Lexer.lexInline(text)
- const content = marked.Parser.parseInline(tokens, { renderer })
-
- return `${content}
`
- }
-
- // 重写 code 方法
- renderer.code = function({ text, lang }: Tokens.Code) {
- const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions
- const style: StyleOptions = {
- ...codeStyle,
- ...getCodeThemeStyles(mergedOptions.codeTheme)
- }
- const styleStr = cssPropertiesToString(style)
-
- // 代码高亮处理
- let highlighted = text
- if (lang && Prism.languages[lang]) {
- // Helper function to recursively process tokens
- const processToken = (token: string | Prism.Token, lineNumber?: number): string => {
- if (typeof token === 'string') {
- return token
- }
-
- const tokenStyle = getTokenStyles(mergedOptions.codeTheme, token.type)
- const content = Array.isArray(token.content)
- ? token.content.map(t => processToken(t)).join('')
- : processToken(token.content)
-
- return `${content}`
- }
-
- try {
- const grammar = Prism.languages[lang]
- const tokens = Prism.tokenize(text, grammar)
- const lines = text.split('\n')
- const lineNumbersWidth = lines.length.toString().length * 8 + 20
-
- highlighted = lines.map((line, index) => {
- const lineTokens = Prism.tokenize(line, grammar)
- const processedLine = lineTokens.map(t => processToken(t, index + 1)).join('')
- return `${index + 1}${processedLine}
`
- }).join('\n')
- } catch (error) {
- console.error(`Error highlighting code: ${error}`)
- }
- }
-
- return `${highlighted}
`
- }
-
- // 重写 codespan 方法
- renderer.codespan = function({ text }: Tokens.Codespan) {
- const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions
- const style: StyleOptions = {
- ...codespanStyle
- }
- const styleStr = cssPropertiesToString(style)
- return `${text}
`
- }
-
- // 重写 em 方法
- renderer.em = function({ text }: Tokens.Em) {
- const emStyle = (mergedOptions.inline?.em || {}) as StyleOptions
- const style: StyleOptions = {
- ...emStyle,
- fontStyle: 'italic'
- }
- const styleStr = cssPropertiesToString(style)
- const tokens = marked.Lexer.lexInline(text)
- const content = marked.Parser.parseInline(tokens, { renderer })
-
- return `${content}`
- }
-
- // 重写 strong 方法
- renderer.strong = function({ text }: Tokens.Strong) {
- const strongStyle = (mergedOptions.inline?.strong || {}) as StyleOptions
- const style: StyleOptions = {
- ...strongStyle,
- color: mergedOptions.base?.themeColor,
- fontWeight: 'bold'
- }
- const styleStr = cssPropertiesToString(style)
- const tokens = marked.Lexer.lexInline(text)
- const content = marked.Parser.parseInline(tokens, { renderer })
-
- return `${content}`
- }
-
- // 重写 link 方法
- renderer.link = function({ href, title, text }: Tokens.Link) {
- const linkStyle = (mergedOptions.inline?.link || {}) as StyleOptions
- const style: StyleOptions = {
- ...linkStyle
- }
- const styleStr = cssPropertiesToString(style)
- return `${text}`
- }
-
- // 重写 image 方法
- renderer.image = function({ href, title, text }: Tokens.Image) {
- const imageStyle = (mergedOptions.block?.image || {}) as StyleOptions
- const style: StyleOptions = {
- ...imageStyle,
- maxWidth: '100%',
- display: 'block',
- margin: '0.5em auto'
- }
- const styleStr = cssPropertiesToString(style)
- return `
`
- }
-
- // 重写 list 方法
- renderer.list = function(token: Tokens.List) {
- const tag = token.ordered ? 'ol' : 'ul'
- const listStyle = (mergedOptions.block?.[tag] || {}) as StyleOptions
- const style: StyleOptions = {
- ...listStyle,
- listStyle: token.ordered ? 'decimal' : 'disc',
- paddingLeft: '2em',
- marginBottom: '16px'
- }
- const styleStr = cssPropertiesToString(style)
- const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''
-
- const items = token.items.map(item => {
- let itemText = item.text
- if (item.task) {
- const checkbox = ` `
- itemText = checkbox + itemText
- }
- return renderer.listitem({ ...item, text: itemText })
- }).join('')
-
- return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${items}${tag}>`
- }
-
- // 重写 listitem 方法
- renderer.listitem = function(item: Tokens.ListItem) {
- const listitemStyle = (mergedOptions.inline?.listitem || {}) as StyleOptions
- const style: StyleOptions = {
- ...listitemStyle,
- marginBottom: '8px',
- display: 'list-item'
- }
- const styleStr = cssPropertiesToString(style)
-
- // 处理嵌套列表
- let content = item.text
- if (item.tokens) {
- content = item.tokens.map(token => {
- if (token.type === 'list') {
- // 递归处理嵌套列表
- return renderer.list(token as Tokens.List)
- } else {
- // 处理其他类型的 token
- const tokens = marked.Lexer.lexInline(token.raw)
- return marked.Parser.parseInline(tokens, { renderer })
- }
- }).join('')
- } else {
- // 如果没有 tokens,则按普通文本处理
- const tokens = marked.Lexer.lexInline(content)
- content = marked.Parser.parseInline(tokens, { renderer })
- }
-
- return `${content}`
- }
-
- // 添加删除线支持
- renderer.del = function({ text }: Tokens.Del) {
- const delStyle = (mergedOptions.inline?.del || {}) as StyleOptions
- const styleStr = cssPropertiesToString(delStyle)
- return `${text}`
- }
-
- // 添加脚注支持
- const footnotes = new Map()
-
- marked.use({
- extensions: [{
- name: 'footnote',
- level: 'inline',
- start(src: string) {
- const match = src.match(/^\[\^([^\]]+)\]/)
- return match ? match.index : undefined
- },
- tokenizer(src: string) {
- const match = /^\[\^([^\]]+)\]/.exec(src)
- if (match) {
- const token = {
- type: 'footnote',
- raw: match[0],
- text: match[1],
- tokens: []
- }
- return token as unknown as Tokens.Generic
- }
- return undefined
- },
- renderer(token: unknown) {
- const footnoteToken = token as { text: string }
- const footnoteStyle = (mergedOptions.inline?.footnote || {}) as StyleOptions
- const styleStr = cssPropertiesToString(footnoteStyle)
- return `[${footnoteToken.text}]`
- }
- }]
- })
-
- // Convert Markdown to HTML using the custom renderer
- const html = marked.parse(markdown, { renderer }) as string
-
- // Apply base styles
- const baseStyles = baseStylesToString(mergedOptions.base)
- return baseStyles ? `` : html
-}
-
-export type { RendererOptions }
\ No newline at end of file
diff --git a/src/lib/markdown/code-highlight.ts b/src/lib/markdown/code-highlight.ts
new file mode 100644
index 0000000..a447998
--- /dev/null
+++ b/src/lib/markdown/code-highlight.ts
@@ -0,0 +1,55 @@
+import Prism from 'prismjs'
+import 'prismjs/components/prism-javascript'
+import 'prismjs/components/prism-typescript'
+import 'prismjs/components/prism-jsx'
+import 'prismjs/components/prism-tsx'
+import 'prismjs/components/prism-css'
+import 'prismjs/components/prism-scss'
+import 'prismjs/components/prism-json'
+import 'prismjs/components/prism-yaml'
+import 'prismjs/components/prism-markdown'
+import 'prismjs/components/prism-bash'
+import 'prismjs/components/prism-python'
+import 'prismjs/components/prism-java'
+import 'prismjs/components/prism-go'
+import 'prismjs/components/prism-rust'
+import 'prismjs/components/prism-sql'
+import 'prismjs/components/prism-docker'
+import 'prismjs/components/prism-nginx'
+import type { CodeThemeId } from '@/config/code-themes'
+import { getTokenStyles } from './styles'
+
+// Helper function to recursively process tokens
+function processToken(token: string | Prism.Token, codeTheme: CodeThemeId): string {
+ if (typeof token === 'string') {
+ return token
+ }
+
+ const tokenStyle = getTokenStyles(codeTheme, token.type)
+ const content = Array.isArray(token.content)
+ ? token.content.map(t => processToken(t, codeTheme)).join('')
+ : processToken(token.content, codeTheme)
+
+ return `${content}`
+}
+
+export function highlightCode(text: string, lang: string, codeTheme: CodeThemeId): string {
+ if (!lang || !Prism.languages[lang]) {
+ return text
+ }
+
+ try {
+ const grammar = Prism.languages[lang]
+ const lines = text.split('\n')
+ const lineNumbersWidth = lines.length.toString().length * 8 + 20
+
+ return lines.map((line, index) => {
+ const lineTokens = Prism.tokenize(line, grammar)
+ const processedLine = lineTokens.map(t => processToken(t, codeTheme)).join('')
+ return `${index + 1}${processedLine}
`
+ }).join('\n')
+ } catch (error) {
+ console.error(`Error highlighting code: ${error}`)
+ return text
+ }
+}
\ No newline at end of file
diff --git a/src/lib/markdown/index.ts b/src/lib/markdown/index.ts
new file mode 100644
index 0000000..5fe2a8f
--- /dev/null
+++ b/src/lib/markdown/index.ts
@@ -0,0 +1,80 @@
+import type { RendererOptions } from './types'
+import { MarkdownParser } from './parser'
+import { getCodeThemeStyles } from './styles'
+
+// 预处理函数
+function preprocessMarkdown(markdown: string): string {
+ return markdown
+ // 处理 ** 语法,但排除已经是 HTML 的部分
+ .replace(/(?]*)\*\*([^*]+)\*\*(?![^<]*>)/g, '$1')
+ // 处理无序列表的 - 标记,但排除代码块内的部分
+ .replace(/^(?!\s*```)([ \t]*)-\s+/gm, '$1• ')
+}
+
+export const defaultOptions: RendererOptions = {
+ base: {
+ themeColor: '#1a1a1a',
+ fontSize: '15px',
+ lineHeight: '1.75',
+ textAlign: 'left'
+ },
+ block: {
+ code_pre: {
+ fontSize: '14px',
+ overflowX: 'auto',
+ borderRadius: '8px',
+ padding: '1em',
+ lineHeight: '1.5',
+ margin: '10px 8px'
+ },
+ table: {
+ width: '100%',
+ marginBottom: '1em',
+ borderCollapse: 'collapse',
+ fontSize: '14px'
+ },
+ th: {
+ padding: '0.5em 1em',
+ borderBottom: '2px solid var(--theme-color)',
+ textAlign: 'left',
+ fontWeight: 'bold'
+ },
+ td: {
+ padding: '0.5em 1em',
+ borderBottom: '1px solid #eee'
+ },
+ footnotes: {
+ marginTop: '2em',
+ paddingTop: '1em',
+ borderTop: '1px solid #eee',
+ fontSize: '0.9em',
+ color: '#666'
+ }
+ },
+ inline: {
+ link: { color: '#576b95' },
+ codespan: { color: '#333333' },
+ em: { color: '#666666' },
+ del: { color: '#999999', textDecoration: 'line-through' },
+ checkbox: {
+ marginRight: '0.5em',
+ verticalAlign: 'middle'
+ }
+ }
+}
+
+export function convertToWechat(markdown: string, options: RendererOptions = defaultOptions): string {
+ const mergedOptions = {
+ base: { ...defaultOptions.base, ...options.base },
+ block: { ...defaultOptions.block, ...options.block },
+ inline: { ...defaultOptions.inline, ...options.inline },
+ codeTheme: options.codeTheme || 'github'
+ }
+
+ const parser = new MarkdownParser(mergedOptions)
+ return parser.parse(markdown)
+}
+
+export { getCodeThemeStyles }
+export type { RendererOptions }
+export * from './types'
\ No newline at end of file
diff --git a/src/lib/markdown/parser.ts b/src/lib/markdown/parser.ts
new file mode 100644
index 0000000..81ad418
--- /dev/null
+++ b/src/lib/markdown/parser.ts
@@ -0,0 +1,88 @@
+import { marked } from 'marked'
+import type { RendererOptions } from './types'
+import { MarkdownRenderer } from './renderer'
+import { baseStylesToString } from './styles'
+
+export class MarkdownParser {
+ private options: RendererOptions
+ private renderer: MarkdownRenderer
+
+ constructor(options: RendererOptions) {
+ this.options = options
+ this.renderer = new MarkdownRenderer(options)
+ this.initializeMarked()
+ }
+
+ private initializeMarked() {
+ marked.use({
+ gfm: true,
+ breaks: true,
+ async: false,
+ pedantic: false
+ })
+
+ marked.use({
+ breaks: true,
+ gfm: true,
+ walkTokens(token) {
+ // 确保列表项被正确处理
+ if (token.type === 'list') {
+ (token as any).items.forEach((item: any) => {
+ if (item.task) {
+ item.checked = !!item.checked
+ }
+ })
+ }
+ }
+ })
+
+ // 添加脚注支持
+ const options = this.options // 在闭包中保存 options 引用
+ marked.use({
+ extensions: [{
+ name: 'footnote',
+ level: 'inline',
+ start(src: string) {
+ const match = src.match(/^\[\^([^\]]+)\]/)
+ return match ? match.index : undefined
+ },
+ tokenizer(src: string) {
+ const match = /^\[\^([^\]]+)\]/.exec(src)
+ if (match) {
+ const token = {
+ type: 'footnote',
+ raw: match[0],
+ text: match[1],
+ tokens: []
+ }
+ return token as any
+ }
+ return undefined
+ },
+ renderer(token: any) {
+ const footnoteStyle = (options?.inline?.footnote || {})
+ const styleStr = Object.entries(footnoteStyle)
+ .map(([key, value]) => `${key}:${value}`)
+ .join(';')
+ return `[${token.text}]`
+ }
+ }]
+ })
+ }
+
+ public parse(markdown: string): string {
+ const preprocessed = this.preprocessMarkdown(markdown)
+ const html = marked.parse(preprocessed, { renderer: this.renderer.getRenderer() }) as string
+ const baseStyles = baseStylesToString(this.options.base)
+ return baseStyles ? `` : html
+ }
+
+ // 预处理 markdown 文本
+ private preprocessMarkdown(markdown: string): string {
+ return markdown
+ // 处理 ** 语法,但排除已经是 HTML 的部分
+ .replace(/(?]*)\*\*([^*]+)\*\*(?![^<]*>)/g, '$1')
+ // 处理无序列表的 - 标记,但排除代码块内的部分
+ .replace(/^(?!\s*```)([ \t]*)-\s+/gm, '$1• ')
+ }
+}
\ No newline at end of file
diff --git a/src/lib/markdown/renderer.ts b/src/lib/markdown/renderer.ts
new file mode 100644
index 0000000..dc61cbf
--- /dev/null
+++ b/src/lib/markdown/renderer.ts
@@ -0,0 +1,211 @@
+import { marked } from 'marked'
+import type { Tokens } from 'marked'
+import type { RendererOptions } from './types'
+import { cssPropertiesToString } from './styles'
+import { highlightCode } from './code-highlight'
+
+export class MarkdownRenderer {
+ private renderer: typeof marked.Renderer.prototype
+ private options: RendererOptions
+
+ constructor(options: RendererOptions) {
+ this.options = options
+ this.renderer = new marked.Renderer()
+ this.initializeRenderer()
+ }
+
+ private initializeRenderer() {
+ // 重写 heading 方法
+ this.renderer.heading = ({ text, depth }: Tokens.Heading) => {
+ const headingKey = `h${depth}` as keyof RendererOptions['block']
+ const headingStyle = (this.options.block?.[headingKey] || {})
+ const style = {
+ ...headingStyle,
+ color: this.options.base?.themeColor
+ }
+ const styleStr = cssPropertiesToString(style)
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer: this.renderer })
+ return `${content}`
+ }
+
+ // 重写 paragraph 方法
+ this.renderer.paragraph = ({ text, tokens }: Tokens.Paragraph) => {
+ const paragraphStyle = (this.options.block?.p || {})
+ const style = {
+ ...paragraphStyle,
+ fontSize: this.options.base?.fontSize,
+ lineHeight: this.options.base?.lineHeight
+ }
+ const styleStr = cssPropertiesToString(style)
+
+ // 处理段落中的内联标记
+ let content = text
+ if (tokens) {
+ content = tokens.map(token => {
+ if (token.type === 'text') {
+ const inlineTokens = marked.Lexer.lexInline(token.text)
+ return marked.Parser.parseInline(inlineTokens, { renderer: this.renderer })
+ }
+ return marked.Parser.parseInline([token], { renderer: this.renderer })
+ }).join('')
+ } else {
+ const inlineTokens = marked.Lexer.lexInline(text)
+ content = marked.Parser.parseInline(inlineTokens, { renderer: this.renderer })
+ }
+
+ return `${content}
`
+ }
+
+ // 重写 blockquote 方法
+ this.renderer.blockquote = ({ text }: Tokens.Blockquote) => {
+ const blockquoteStyle = (this.options.block?.blockquote || {})
+ const style = {
+ ...blockquoteStyle,
+ borderLeft: `4px solid ${this.options.base?.themeColor || '#1a1a1a'}`
+ }
+ const styleStr = cssPropertiesToString(style)
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer: this.renderer })
+
+ return `${content}
`
+ }
+
+ // 重写 code 方法
+ this.renderer.code = ({ text, lang }: Tokens.Code) => {
+ const codeStyle = (this.options.block?.code_pre || {})
+ const style = {
+ ...codeStyle
+ }
+ const styleStr = cssPropertiesToString(style)
+
+ const highlighted = highlightCode(text, lang || '', this.options.codeTheme || 'github')
+
+ return `${highlighted}
`
+ }
+
+ // 重写 codespan 方法
+ this.renderer.codespan = ({ text }: Tokens.Codespan) => {
+ const codespanStyle = (this.options.inline?.codespan || {})
+ const styleStr = cssPropertiesToString(codespanStyle)
+ return `${text}
`
+ }
+
+ // 重写 em 方法
+ this.renderer.em = ({ text }: Tokens.Em) => {
+ const emStyle = (this.options.inline?.em || {})
+ const style = {
+ ...emStyle,
+ fontStyle: 'italic'
+ }
+ const styleStr = cssPropertiesToString(style)
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer: this.renderer })
+
+ return `${content}`
+ }
+
+ // 重写 strong 方法
+ this.renderer.strong = ({ text }: Tokens.Strong) => {
+ const strongStyle = (this.options.inline?.strong || {})
+ const style = {
+ ...strongStyle,
+ color: this.options.base?.themeColor,
+ fontWeight: 'bold'
+ }
+ const styleStr = cssPropertiesToString(style)
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer: this.renderer })
+
+ return `${content}`
+ }
+
+ // 重写 link 方法
+ this.renderer.link = ({ href, title, text }: Tokens.Link) => {
+ const linkStyle = (this.options.inline?.link || {})
+ const styleStr = cssPropertiesToString(linkStyle)
+ return `${text}`
+ }
+
+ // 重写 image 方法
+ this.renderer.image = ({ href, title, text }: Tokens.Image) => {
+ const imageStyle = (this.options.block?.image || {})
+ const style = {
+ ...imageStyle,
+ maxWidth: '100%',
+ display: 'block',
+ margin: '0.5em auto'
+ }
+ const styleStr = cssPropertiesToString(style)
+ return `
`
+ }
+
+ // 重写 list 方法
+ this.renderer.list = (token: Tokens.List) => {
+ const tag = token.ordered ? 'ol' : 'ul'
+ const listStyle = (this.options.block?.[tag] || {})
+ const style = {
+ ...listStyle,
+ listStyle: token.ordered ? 'decimal' : 'disc',
+ paddingLeft: '2em',
+ marginBottom: '16px'
+ }
+ const styleStr = cssPropertiesToString(style)
+ const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : ''
+
+ const items = token.items.map(item => {
+ let itemText = item.text
+ if (item.task) {
+ const checkbox = ` `
+ itemText = checkbox + itemText
+ }
+ return this.renderer.listitem({ ...item, text: itemText })
+ }).join('')
+
+ return `<${tag}${startAttr}${styleStr ? ` style="${styleStr}"` : ''}>${items}${tag}>`
+ }
+
+ // 重写 listitem 方法
+ this.renderer.listitem = (item: Tokens.ListItem) => {
+ const listitemStyle = (this.options.inline?.listitem || {})
+ const style = {
+ ...listitemStyle,
+ marginBottom: '8px',
+ display: 'list-item'
+ }
+ const styleStr = cssPropertiesToString(style)
+
+ // 处理嵌套列表
+ let content = item.text
+ if (item.tokens) {
+ content = item.tokens.map(token => {
+ if (token.type === 'list') {
+ // 递归处理嵌套列表
+ return this.renderer.list(token as Tokens.List)
+ } else {
+ // 处理其他类型的 token
+ const tokens = marked.Lexer.lexInline(token.raw)
+ return marked.Parser.parseInline(tokens, { renderer: this.renderer })
+ }
+ }).join('')
+ } else {
+ // 如果没有 tokens,则按普通文本处理
+ const tokens = marked.Lexer.lexInline(content)
+ content = marked.Parser.parseInline(tokens, { renderer: this.renderer })
+ }
+
+ return `${content}`
+ }
+
+ // 添加删除线支持
+ this.renderer.del = ({ text }: Tokens.Del) => {
+ const delStyle = (this.options.inline?.del || {})
+ const styleStr = cssPropertiesToString(delStyle)
+ return `${text}`
+ }
+ }
+
+ public getRenderer(): typeof marked.Renderer.prototype {
+ return this.renderer
+ }
+}
\ No newline at end of file
diff --git a/src/lib/markdown/styles.ts b/src/lib/markdown/styles.ts
new file mode 100644
index 0000000..9b93b7a
--- /dev/null
+++ b/src/lib/markdown/styles.ts
@@ -0,0 +1,71 @@
+import type { StyleOptions, RendererOptions } from './types'
+import { codeThemes, type CodeThemeId } from '@/config/code-themes'
+
+// 将样式对象转换为 CSS 字符串
+export function cssPropertiesToString(style: StyleOptions = {}): string {
+ if (!style) return ''
+
+ return Object.entries(style)
+ .filter(([_, value]) => value !== undefined && value !== null)
+ .map(([key, value]) => {
+ // 处理媒体查询
+ if (key === '@media (max-width: 768px)') {
+ return '' // 我们不在内联样式中包含媒体查询
+ }
+
+ // 转换驼峰命名为连字符命名
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
+
+ // 处理数字值
+ if (typeof value === 'number' && !cssKey.includes('line-height')) {
+ value = `${value}px`
+ }
+
+ return `${cssKey}: ${value}`
+ })
+ .filter(Boolean) // 移除空字符串
+ .join(';')
+}
+
+// 将基础样式选项转换为 CSS 字符串
+export function baseStylesToString(base: RendererOptions['base'] = {}): string {
+ if (!base) return ''
+
+ const styles: string[] = []
+
+ if (base.lineHeight) {
+ styles.push(`line-height: ${base.lineHeight}`)
+ }
+ if (base.fontSize) {
+ styles.push(`font-size: ${base.fontSize}`)
+ }
+ if (base.textAlign) {
+ styles.push(`text-align: ${base.textAlign}`)
+ }
+ if (base.themeColor) {
+ styles.push(`--theme-color: ${base.themeColor}`)
+ }
+
+ return styles.join(';')
+}
+
+// 获取代码主题的样式
+export function getCodeThemeStyles(theme: CodeThemeId): StyleOptions {
+ const themeConfig = codeThemes.find(t => t.id === theme)
+ if (!themeConfig) return {}
+
+ return {
+ background: themeConfig.theme.background,
+ color: themeConfig.theme.text,
+ }
+}
+
+// 获取代码token的样式
+export function getTokenStyles(theme: CodeThemeId, tokenType: string): string {
+ const themeConfig = codeThemes.find(t => t.id === theme)
+ if (!themeConfig) return ''
+
+ const tokenColor = themeConfig.theme[tokenType as keyof typeof themeConfig.theme]
+ if (!tokenColor) return ''
+ return `color: ${tokenColor};`
+}
\ No newline at end of file
diff --git a/src/lib/types.ts b/src/lib/markdown/types.ts
similarity index 100%
rename from src/lib/types.ts
rename to src/lib/markdown/types.ts