优化 markdown 解析
This commit is contained in:
parent
e1361f9219
commit
b8c464bd4c
@ -87,7 +87,7 @@ const marked = new Marked({
|
|||||||
pedantic: false
|
pedantic: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 应用配置
|
// 重写 marked 配置
|
||||||
marked.use({
|
marked.use({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
@ -118,12 +118,40 @@ const defaultOptions: RendererOptions = {
|
|||||||
padding: '1em',
|
padding: '1em',
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
margin: '10px 8px'
|
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: {
|
inline: {
|
||||||
link: { color: '#576b95' },
|
link: { color: '#576b95' },
|
||||||
codespan: { color: '#333333' },
|
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
|
color: mergedOptions.base?.themeColor
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${text}</h${depth}>`
|
const tokens = marked.Lexer.lexInline(text)
|
||||||
|
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||||
|
return `<h${depth}${styleStr ? ` style="${styleStr}"` : ''}>${content}</h${depth}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 paragraph 方法
|
// 重写 paragraph 方法
|
||||||
renderer.paragraph = function({ text }: Tokens.Paragraph) {
|
renderer.paragraph = function({ text, tokens }: Tokens.Paragraph) {
|
||||||
const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions
|
const paragraphStyle = (mergedOptions.block?.p || {}) as StyleOptions
|
||||||
const style: StyleOptions = {
|
const style: StyleOptions = {
|
||||||
...paragraphStyle,
|
...paragraphStyle,
|
||||||
@ -180,7 +210,23 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
lineHeight: mergedOptions.base?.lineHeight
|
lineHeight: mergedOptions.base?.lineHeight
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<p${styleStr ? ` style="${styleStr}"` : ''}>${text}</p>`
|
|
||||||
|
// 处理段落中的内联标记
|
||||||
|
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 `<p${styleStr ? ` style="${styleStr}"` : ''}>${content}</p>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 blockquote 方法
|
// 重写 blockquote 方法
|
||||||
@ -191,23 +237,26 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}`
|
borderLeft: `4px solid ${mergedOptions.base?.themeColor || '#1a1a1a'}`
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${text}</blockquote>`
|
const tokens = marked.Lexer.lexInline(text)
|
||||||
|
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||||
|
|
||||||
|
return `<blockquote${styleStr ? ` style="${styleStr}"` : ''}>${content}</blockquote>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 code 方法
|
// 重写 code 方法
|
||||||
renderer.code = function({ text, lang }: Tokens.Code) {
|
renderer.code = function({ text, lang }: Tokens.Code) {
|
||||||
|
|
||||||
const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions
|
const codeStyle = (mergedOptions.block?.code_pre || {}) as StyleOptions
|
||||||
const style: StyleOptions = {
|
const style: StyleOptions = {
|
||||||
...codeStyle,
|
...codeStyle,
|
||||||
...getCodeThemeStyles(mergedOptions.codeTheme)
|
...getCodeThemeStyles(mergedOptions.codeTheme)
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
|
|
||||||
// 代码高亮处理
|
// 代码高亮处理
|
||||||
let highlighted = text
|
let highlighted = text
|
||||||
if (lang && Prism.languages[lang]) {
|
if (lang && Prism.languages[lang]) {
|
||||||
// Helper function to recursively process tokens
|
// 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') {
|
if (typeof token === 'string') {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
@ -223,7 +272,14 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
try {
|
try {
|
||||||
const grammar = Prism.languages[lang]
|
const grammar = Prism.languages[lang]
|
||||||
const tokens = Prism.tokenize(text, grammar)
|
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 `<div class="code-line"><span class="line-number" style="width:${lineNumbersWidth}px;color:#999;padding-right:1em;text-align:right;display:inline-block;user-select:none;">${index + 1}</span>${processedLine}</div>`
|
||||||
|
}).join('\n')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error highlighting code: ${error}`)
|
console.error(`Error highlighting code: ${error}`)
|
||||||
}
|
}
|
||||||
@ -234,7 +290,6 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
|
|
||||||
// 重写 codespan 方法
|
// 重写 codespan 方法
|
||||||
renderer.codespan = function({ text }: Tokens.Codespan) {
|
renderer.codespan = function({ text }: Tokens.Codespan) {
|
||||||
|
|
||||||
const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions
|
const codespanStyle = (mergedOptions.inline?.codespan || {}) as StyleOptions
|
||||||
const style: StyleOptions = {
|
const style: StyleOptions = {
|
||||||
...codespanStyle
|
...codespanStyle
|
||||||
@ -251,7 +306,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<em${styleStr ? ` style="${styleStr}"` : ''}>${text}</em>`
|
const tokens = marked.Lexer.lexInline(text)
|
||||||
|
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||||
|
|
||||||
|
return `<em${styleStr ? ` style="${styleStr}"` : ''}>${content}</em>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 strong 方法
|
// 重写 strong 方法
|
||||||
@ -263,7 +321,10 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${text}</strong>`
|
const tokens = marked.Lexer.lexInline(text)
|
||||||
|
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||||
|
|
||||||
|
return `<strong${styleStr ? ` style="${styleStr}"` : ''}>${content}</strong>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 link 方法
|
// 重写 link 方法
|
||||||
@ -273,7 +334,9 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
...linkStyle
|
...linkStyle
|
||||||
}
|
}
|
||||||
const styleStr = cssPropertiesToString(style)
|
const styleStr = cssPropertiesToString(style)
|
||||||
return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${text}</a>`
|
const tokens = marked.Lexer.lexInline(text)
|
||||||
|
const content = marked.Parser.parseInline(tokens, { renderer })
|
||||||
|
return `<a href="${href}"${title ? ` title="${title}"` : ''}${styleStr ? ` style="${styleStr}"` : ''}>${content}</a>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 image 方法
|
// 重写 image 方法
|
||||||
@ -346,6 +409,48 @@ export function convertToWechat(markdown: string, options: RendererOptions = def
|
|||||||
return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
|
return `<li${styleStr ? ` style="${styleStr}"` : ''}>${content}</li>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 添加删除线支持
|
||||||
|
renderer.del = function({ text }: Tokens.Del) {
|
||||||
|
const delStyle = (mergedOptions.inline?.del || {}) as StyleOptions
|
||||||
|
const styleStr = cssPropertiesToString(delStyle)
|
||||||
|
return `<del${styleStr ? ` style="${styleStr}"` : ''}>${text}</del>`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加脚注支持
|
||||||
|
const footnotes = new Map<string, string>()
|
||||||
|
|
||||||
|
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 `<sup${styleStr ? ` style="${styleStr}"` : ''}><a href="#fn-${footnoteToken.text}">[${footnoteToken.text}]</a></sup>`
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
// Convert Markdown to HTML using the custom renderer
|
// Convert Markdown to HTML using the custom renderer
|
||||||
const html = marked.parse(markdown, { renderer }) as string
|
const html = marked.parse(markdown, { renderer }) as string
|
||||||
|
|
||||||
|
142
src/lib/types.ts
142
src/lib/types.ts
@ -1,16 +1,92 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
import type { CodeThemeId } from '@/config/code-themes'
|
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 {
|
export interface RendererOptions {
|
||||||
base?: {
|
base?: {
|
||||||
themeColor?: string
|
themeColor?: string
|
||||||
fontSize?: string
|
fontSize?: string
|
||||||
lineHeight?: string
|
lineHeight?: string
|
||||||
textAlign?: string
|
textAlign?: 'left' | 'center' | 'right'
|
||||||
fontFamily?: string
|
fontFamily?: string
|
||||||
padding?: string
|
padding?: string
|
||||||
maxWidth?: string
|
|
||||||
margin?: string
|
margin?: string
|
||||||
|
maxWidth?: string
|
||||||
wordBreak?: string
|
wordBreak?: string
|
||||||
whiteSpace?: string
|
whiteSpace?: string
|
||||||
color?: string
|
color?: string
|
||||||
@ -24,74 +100,24 @@ export interface RendererOptions {
|
|||||||
h6?: StyleOptions
|
h6?: StyleOptions
|
||||||
p?: StyleOptions
|
p?: StyleOptions
|
||||||
blockquote?: StyleOptions
|
blockquote?: StyleOptions
|
||||||
blockquote_p?: StyleOptions
|
|
||||||
code_pre?: StyleOptions
|
code_pre?: StyleOptions
|
||||||
code?: StyleOptions
|
|
||||||
image?: StyleOptions
|
image?: StyleOptions
|
||||||
ol?: StyleOptions
|
|
||||||
ul?: StyleOptions
|
ul?: StyleOptions
|
||||||
|
ol?: StyleOptions
|
||||||
|
table?: StyleOptions
|
||||||
|
th?: StyleOptions
|
||||||
|
td?: StyleOptions
|
||||||
footnotes?: StyleOptions
|
footnotes?: StyleOptions
|
||||||
figure?: StyleOptions
|
|
||||||
}
|
}
|
||||||
inline?: {
|
inline?: {
|
||||||
strong?: StyleOptions
|
strong?: StyleOptions
|
||||||
em?: StyleOptions
|
em?: StyleOptions
|
||||||
codespan?: StyleOptions
|
codespan?: StyleOptions
|
||||||
link?: StyleOptions
|
link?: StyleOptions
|
||||||
wx_link?: StyleOptions
|
|
||||||
listitem?: StyleOptions
|
listitem?: StyleOptions
|
||||||
table?: StyleOptions
|
checkbox?: StyleOptions
|
||||||
thead?: StyleOptions
|
del?: StyleOptions
|
||||||
td?: StyleOptions
|
|
||||||
figcaption?: StyleOptions
|
|
||||||
footnote?: StyleOptions
|
footnote?: StyleOptions
|
||||||
}
|
}
|
||||||
codeTheme?: CodeThemeId
|
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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user