diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
index c1cbd13..4570037 100644
--- a/src/lib/markdown.ts
+++ b/src/lib/markdown.ts
@@ -87,7 +87,7 @@ const marked = new Marked({
pedantic: false
})
-// 应用配置
+// 重写 marked 配置
marked.use({
breaks: true,
gfm: true,
@@ -118,12 +118,40 @@ const defaultOptions: RendererOptions = {
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' }
+ em: { color: '#666666' },
+ del: { color: '#999999', textDecoration: 'line-through' },
+ checkbox: {
+ marginRight: '0.5em',
+ verticalAlign: 'middle'
+ }
}
}
@@ -168,11 +196,13 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
color: mergedOptions.base?.themeColor
}
const styleStr = cssPropertiesToString(style)
- return `${text}`
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer })
+ return `${content}`
}
// 重写 paragraph 方法
- renderer.paragraph = function({ text }: Tokens.Paragraph) {
+ renderer.paragraph = function({ text, tokens }: Tokens.Paragraph) {
const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions
const style: StyleOptions = {
...paragraphStyle,
@@ -180,7 +210,23 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
lineHeight: mergedOptions.base?.lineHeight
}
const styleStr = cssPropertiesToString(style)
- return `
${text}
`
+
+ // 处理段落中的内联标记
+ 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 方法
@@ -191,23 +237,26 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}`
}
const styleStr = cssPropertiesToString(style)
- return `${text}
`
+ 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): string => {
+ const processToken = (token: string | Prism.Token, lineNumber?: number): string => {
if (typeof token === 'string') {
return token
}
@@ -223,7 +272,14 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
try {
const grammar = Prism.languages[lang]
const tokens = Prism.tokenize(text, grammar)
- highlighted = tokens.map(processToken).join('')
+ 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}`)
}
@@ -234,7 +290,6 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
// 重写 codespan 方法
renderer.codespan = function({ text }: Tokens.Codespan) {
-
const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions
const style: StyleOptions = {
...codespanStyle
@@ -251,7 +306,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
fontStyle: 'italic'
}
const styleStr = cssPropertiesToString(style)
- return `${text}`
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer })
+
+ return `${content}`
}
// 重写 strong 方法
@@ -263,7 +321,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
fontWeight: 'bold'
}
const styleStr = cssPropertiesToString(style)
- return `${text}`
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer })
+
+ return `${content}`
}
// 重写 link 方法
@@ -273,7 +334,9 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
...linkStyle
}
const styleStr = cssPropertiesToString(style)
- return `${text}`
+ const tokens = marked.Lexer.lexInline(text)
+ const content = marked.Parser.parseInline(tokens, { renderer })
+ return `${content}`
}
// 重写 image 方法
@@ -346,6 +409,48 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
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
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 659674b..e01f235 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,16 +1,92 @@
import type { CSSProperties } from 'react'
import type { CodeThemeId } from '@/config/code-themes'
+export interface StyleOptions {
+ // Layout
+ display?: string
+ position?: string
+ top?: string | number
+ left?: string | number
+ right?: string | number
+ bottom?: string | number
+ width?: string | number
+ height?: string | number
+ margin?: string | number
+ marginTop?: string | number
+ marginRight?: string | number
+ marginBottom?: string | number
+ marginLeft?: string | number
+ padding?: string | number
+ paddingTop?: string | number
+ paddingRight?: string | number
+ paddingBottom?: string | number
+ paddingLeft?: string | number
+ maxWidth?: string | number
+
+ // Typography
+ color?: string
+ fontSize?: string | number
+ fontFamily?: string
+ fontWeight?: string | number
+ fontStyle?: string
+ lineHeight?: string | number
+ textAlign?: 'left' | 'center' | 'right'
+ textDecoration?: string
+ textIndent?: string | number
+ letterSpacing?: string | number
+ whiteSpace?: string
+ wordBreak?: string
+
+ // Border & Background
+ border?: string
+ borderTop?: string
+ borderRight?: string
+ borderBottom?: string
+ borderLeft?: string
+ borderRadius?: string
+ borderCollapse?: string
+ background?: string
+ backgroundColor?: string
+ backgroundImage?: string
+
+ // List
+ listStyle?: string
+ listStyleType?: string
+ listStylePosition?: string
+
+ // Flexbox
+ alignItems?: string
+ justifyContent?: string
+ flexDirection?: string
+ flexWrap?: string
+ gap?: string
+
+ // Other
+ opacity?: number
+ overflow?: string
+ overflowX?: string
+ overflowY?: string
+ verticalAlign?: string
+ userSelect?: string
+ cursor?: string
+ zIndex?: number
+ boxShadow?: string
+ transition?: string
+ transform?: string
+ WebkitBackgroundClip?: string
+ WebkitTextFillColor?: string
+}
+
export interface RendererOptions {
base?: {
themeColor?: string
fontSize?: string
lineHeight?: string
- textAlign?: string
+ textAlign?: 'left' | 'center' | 'right'
fontFamily?: string
padding?: string
- maxWidth?: string
margin?: string
+ maxWidth?: string
wordBreak?: string
whiteSpace?: string
color?: string
@@ -24,74 +100,24 @@ export interface RendererOptions {
h6?: StyleOptions
p?: StyleOptions
blockquote?: StyleOptions
- blockquote_p?: StyleOptions
code_pre?: StyleOptions
- code?: StyleOptions
image?: StyleOptions
- ol?: StyleOptions
ul?: StyleOptions
+ ol?: StyleOptions
+ table?: StyleOptions
+ th?: StyleOptions
+ td?: StyleOptions
footnotes?: StyleOptions
- figure?: StyleOptions
}
inline?: {
strong?: StyleOptions
em?: StyleOptions
codespan?: StyleOptions
link?: StyleOptions
- wx_link?: StyleOptions
listitem?: StyleOptions
- table?: StyleOptions
- thead?: StyleOptions
- td?: StyleOptions
- figcaption?: StyleOptions
+ checkbox?: StyleOptions
+ del?: StyleOptions
footnote?: StyleOptions
}
codeTheme?: CodeThemeId
-}
-
-export interface StyleOptions {
- padding?: string
- maxWidth?: string
- margin?: string
- wordBreak?: 'normal' | 'break-all' | 'keep-all' | 'break-word'
- whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces'
- color?: string
- display?: string
- fontSize?: string
- fontWeight?: string
- textAlign?: string
- paddingLeft?: string
- marginLeft?: string
- borderLeft?: string
- lineHeight?: string | number
- letterSpacing?: string
- fontStyle?: string
- borderRadius?: string
- background?: string
- marginBottom?: string
- alignItems?: 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch'
- gap?: string
- overflowX?: string
- fontFamily?: string
- width?: string
- listStyle?: string
- borderStyle?: string
- borderWidth?: string
- borderColor?: string
- WebkitTransformOrigin?: string
- transformOrigin?: string
- transform?: string
- height?: string
- textIndent?: string | number
- borderCollapse?: 'collapse' | 'separate' | 'initial' | 'inherit'
- border?: string
- textDecoration?: string
- borderBottom?: string
- WebkitBackgroundClip?: string
- WebkitTextFillColor?: string
- '@media (max-width: 768px)'?: {
- margin?: string
- padding?: string
- fontSize?: string
- }
}
\ No newline at end of file