import type { Renderer, RendererObject, Tokens } from 'marked' import { marked } from 'marked' import hljs from 'highlight.js' import mermaid from 'mermaid' import { toMerged } from 'es-toolkit' import type { PropertiesHyphen } from 'csstype' import { MDKatex } from './MDKatex' import type { ExtendedProperties, IOpts, ThemeStyles } from '@/types' marked.use(MDKatex({ nonStandard: true })) function buildTheme({ theme, fonts, size }: IOpts): ThemeStyles { const base = toMerged(theme.base, { 'font-family': fonts, 'font-size': size, }) const mergeStyles = (styles: Record): Record => Object.fromEntries( Object.entries(styles).map(([ele, style]) => [ele, toMerged(base, style)]), ) return { ...mergeStyles(theme.inline), ...mergeStyles(theme.block), } as ThemeStyles } function buildAddition(): string { return ` ` } function getStyles(styleMapping: ThemeStyles, tokenName: string, addition: string = ``): string { const dict = styleMapping[tokenName as keyof ThemeStyles] if (!dict) { return `` } const styles = Object.entries(dict) .map(([key, value]) => `${key}:${value}`) .join(`;`) return `style="${styles}${addition}"` } function buildFootnoteArray(footnotes: [number, string, string][]): string { return footnotes .map(([index, title, link]) => link === title ? `[${index}]: ${title}
` : `[${index}] ${title}: ${link}
`, ) .join(`\n`) } function transform(legend: string, text: string | null, title: string | null): string { const options = legend.split(`-`) for (const option of options) { if (option === `alt` && text) { return text } if (option === `title` && title) { return title } } return `` } const macCodeSvg = ` `.trim() export function initRenderer(opts: IOpts) { const footnotes: [number, string, string][] = [] let footnoteIndex: number = 0 let styleMapping: ThemeStyles = buildTheme(opts) let codeIndex: number = 0 let listIndex: number = 0 let isOrdered: boolean = false function styles(tag: string, addition: string = ``): string { return getStyles(styleMapping, tag, addition) } function styledContent(styleLabel: string, content: string, tagName?: string): string { const tag = tagName ?? styleLabel return `<${tag} ${styles(styleLabel)}>${content}` } function addFootnote(title: string, link: string): number { footnotes.push([++footnoteIndex, title, link]) return footnoteIndex } function reset(newOpts: Partial): void { footnotes.length = 0 footnoteIndex = 0 setOptions(newOpts) } function setOptions(newOpts: Partial): void { opts = { ...opts, ...newOpts } styleMapping = buildTheme(opts) } const buildFootnotes = () => { if (!footnotes.length) { return `` } return ( styledContent(`h4`, `引用链接`) + styledContent(`footnotes`, buildFootnoteArray(footnotes), `p`) ) } const renderer: RendererObject = { heading({ tokens, depth }: Tokens.Heading) { const text = this.parser.parseInline(tokens) const tag = `h${depth}` return styledContent(tag, text) }, paragraph({ tokens }: Tokens.Paragraph): string { const text = this.parser.parseInline(tokens) const isFigureImage = text.includes(`/g, `

`) return styledContent(`blockquote`, text) }, code({ text, lang = `` }: Tokens.Code): string { if (lang.startsWith(`mermaid`)) { clearTimeout(codeIndex) codeIndex = setTimeout(() => { mermaid.run() }, 0) as any as number return `

${text}
` } const langText = lang.split(` `)[0] const language = hljs.getLanguage(langText) ? langText : `plaintext` let highlighted = hljs.highlight(text, { language }).value highlighted = highlighted .replace(/\r\n/g, `
`) .replace(/\n/g, `
`) .replace(/(>[^<]+)|(^[^<]+)/g, str => str.replace(/\s/g, ` `)) const span = `` const code = `${highlighted}` return `
${span}${code}
` }, codespan({ text }: Tokens.Codespan): string { return styledContent(`codespan`, text, `code`) }, listitem(item: Tokens.ListItem): string { const prefix = isOrdered ? `${listIndex + 1}. ` : `• ` const content = item.tokens.map(t => (this[t.type as keyof Renderer] as (token: T) => string)(t)).join(``) return styledContent(`listitem`, `${prefix}${content}`, `li`) }, list({ ordered, items }: Tokens.List): string { const listItems = [] for (let i = 0; i < items.length; i++) { isOrdered = ordered listIndex = i const item = items[i] listItems.push(this.listitem(item)) } const label = ordered ? `ol` : `ul` return styledContent(label, listItems.join(``)) }, image({ href, title, text }: Tokens.Image): string { const subText = styledContent(`figcaption`, transform(opts.legend!, text, title)) const figureStyles = styles(`figure`) const imgStyles = styles(`image`) return `
${text}${subText}
` }, link({ href, title, text }: Tokens.Link): string { if (href.startsWith(`https://mp.weixin.qq.com`)) { return `${text}` } if (href === text) { return text } if (opts.status) { const ref = addFootnote(title || text, href) return `${text}[${ref}]` } return styledContent(`link`, text, `span`) }, strong({ tokens }: Tokens.Strong): string { return styledContent(`strong`, this.parser.parseInline(tokens)) }, em({ tokens }: Tokens.Em): string { return styledContent(`em`, this.parser.parseInline(tokens), `span`) }, table({ header, rows }: Tokens.Table): string { const headerRow = header .map(cell => this.tablecell(cell)) .join(``) const body = rows .map((row) => { const rowContent = row .map(cell => this.tablecell(cell)) .join(``) return styledContent(`tr`, rowContent) }) .join(``) return `
${headerRow}${body}
` }, tablecell(token: Tokens.TableCell): string { const text = this.parser.parseInline(token.tokens) return styledContent(`td`, text) }, hr(_: Tokens.Hr): string { return styledContent(`hr`, ``) }, } marked.use({ renderer }) return { buildAddition, buildFootnotes, setOptions, reset, } }