From 2d40869fc2a33865f07be0850b263e407d0890d3 Mon Sep 17 00:00:00 2001 From: YangFong Date: Wed, 4 Sep 2024 13:00:42 +0800 Subject: [PATCH] refactor: renderer upgrade to ts (#379) * refactor: renderer upgrade to ts * fix: options of katex * fix: render of katex * chore: update marked-katex-extension to version 5.1.2 * chore: build theme * chore: update renderer * feat: move build addition func * chore: styled content * chore: update renderer * chore: update packages * chore: update renderer * fix: logic of merged * chore: update renderer * chore: update * feat: add token type * feat: add styles func * chore: update renderer * fix: parse table cell * feat: horizontal table scrolling is supported * fix: strong tag * feat: parse strong and em label * fix: parse list * chore: update example docs * chore: update --------- Co-authored-by: Libin YANG --- README.md | 5 +- index.html | 10 +- package-lock.json | 376 +++++++++++++++++---------------- package.json | 2 +- src/assets/example/markdown.md | 39 ++-- src/config/theme.ts | 16 +- src/stores/index.js | 23 +- src/types/index.ts | 21 +- src/utils/renderer.js | 251 ---------------------- src/utils/renderer.ts | 268 +++++++++++++++++++++++ 10 files changed, 529 insertions(+), 482 deletions(-) delete mode 100644 src/utils/renderer.js create mode 100644 src/utils/renderer.ts diff --git a/README.md b/README.md index 258fb8e..7e83ae2 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@
-[![status](https://img.shields.io/github/actions/workflow/status/doocs/md/build.yml?style=flat-square&labelColor=564341&color=42cc23 -)](https://github.com/doocs/md/actions) [![node](https://img.shields.io/badge/node-%3E%3D20-42cc23?style=flat-square&labelColor=564341)](https://nodejs.org/en/about/previous-releases) [![release](https://img.shields.io/github/v/release/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md/releases) [![license](https://img.shields.io/github/license/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](./LICENSE) [![pr](https://img.shields.io/badge/prs-welcome-42cc23?style=flat-square&labelColor=564341)](https://github.com/doocs/md/pulls)
[![stars](https://img.shields.io/github/stars/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md/stargazers) [![forks](https://img.shields.io/github/forks/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md) +[![status](https://img.shields.io/github/actions/workflow/status/doocs/md/build.yml?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md/actions) [![node](https://img.shields.io/badge/node-%3E%3D20-42cc23?style=flat-square&labelColor=564341)](https://nodejs.org/en/about/previous-releases) [![release](https://img.shields.io/github/v/release/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md/releases) [![license](https://img.shields.io/github/license/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](./LICENSE) [![pr](https://img.shields.io/badge/prs-welcome-42cc23?style=flat-square&labelColor=564341)](https://github.com/doocs/md/pulls)
[![stars](https://img.shields.io/github/stars/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md/stargazers) [![forks](https://img.shields.io/github/forks/doocs/md?style=flat-square&labelColor=564341&color=42cc23)](https://github.com/doocs/md)
@@ -29,7 +28,7 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章 欢迎各位朋友随时提交 PR,让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 [Discussions 讨论区](https://github.com/doocs/md/discussions)反馈。 -> 我们项目最新版本基于 Vue3 开发,基于 Vue2 的旧版本已经不再维护,如果你需要 Vue2 版本,请切换到 [1.x 分支](https://github.com/doocs/md/tree/1.x)。 +注:我们项目最新版本基于 Vue3 开发,基于 Vue2 的旧版本已经不再维护,如果你需要 Vue2 版本,请切换到 [1.x](https://github.com/doocs/md/tree/1.x) 分支。 ## 功能特性 diff --git a/index.html b/index.html index 2cd0a60..33a66b4 100644 --- a/index.html +++ b/index.html @@ -15,15 +15,10 @@ rel="apple-touch-icon-precomposed" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png" /> - - - ` - - setOptions = (newOpts) => { - this.opts = this.merge(this.opts, newOpts) - this.styleMapping = this.buildTheme(this.opts.theme) - } - - heading({ tokens, depth }) { - const text = this.parser.parseInline(tokens) - const tag = `h${depth}` - return this.styledContent(tag, text) - } - - paragraph({ tokens }) { - const text = this.parser.parseInline(tokens) - const isFigureImage = text.includes(`/g, `

`) - return this.styledContent(`blockquote`, text) - } - - codeIndex = 0 - code({ text, lang = `` }) { - if (lang.startsWith(`mermaid`)) { - clearTimeout(this.codeIndex) - this.codeIndex = setTimeout(() => { - mermaid.run() - }, 0) - 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, ` `)) - - return `
${highlighted}
` - } - - codespan({ text }) { - return this.styledContent(`codespan`, text, `code`) - } - - listitem(tokens, prefix) { - return `
  • ${prefix}${this.parser.parseInline(tokens)}
  • ` - } - - list({ ordered, items }) { - const listItems = [] - for (let i = 0; i < items.length; i++) { - const { tokens } = items[i] - const prefix = ordered ? `${i + 1}. ` : `• ` - for (const token of tokens) { - if (token.type === `list`) { - listItems.push(this.list(token)) - } - else if (token.type !== `space`) { - listItems.push(this.listitem([token], prefix)) - } - } - } - const label = ordered ? `ol` : `ul` - return this.styledContent(label, listItems.join(``)) - } - - image({ href, title, text }) { - const createSubText = s => - s ? `
    ${s}
    ` : `` - const transform - = { - 'alt': () => text, - 'title': () => title, - 'alt-title': () => text || title, - 'title-alt': () => title || text, - }[this.opts.legend] || (() => ``) - - const subText = createSubText(transform()) - const figureStyles = this.getStyles(`figure`) - const imgStyles = this.getStyles(`image`) - return `
    ${text}${subText}
    ` - } - - link({ href, title, text }) { - if (href.startsWith(`https://mp.weixin.qq.com`)) { - return `${text}` - } - if (href === text) { - return text - } - if (this.opts.status) { - const ref = this.addFootnote(title || text, href) - return `${text}[${ref}]` - } - return this.styledContent(`link`, text, `span`) - } - - strong({ text }) { - return this.styledContent(`strong`, text) - } - - em({ text }) { - return `${text}` - } - - table({ header, rows }) { - const headerRow = header.map(cell => this.styledContent(`td`, cell.text)).join(``) - const body = rows.map((row) => { - const rowContent = row.map(cell => this.styledContent(`td`, cell.text)).join(``) - return this.styledContent(`tr`, rowContent) - }).join(``) - return ` -
    - - ${headerRow} - ${body} -
    -
    ` - } - - tablecell({ text }) { - return this.styledContent(`td`, text) - } - - hr(_) { - return this.styledContent(`hr`, ``) - } -} - -export default WxRenderer diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts new file mode 100644 index 0000000..6222d63 --- /dev/null +++ b/src/utils/renderer.ts @@ -0,0 +1,268 @@ +import type { Renderer, RendererObject, Tokens } from 'marked' +import { marked } from 'marked' +import hljs from 'highlight.js' +import markedKatex from 'marked-katex-extension' +import mermaid from 'mermaid' +import { toMerged } from 'es-toolkit' + +import type { PropertiesHyphen } from 'csstype' +import type { ExtendedProperties, IOpts, ThemeStyles } from '@/types' + +marked.use( + markedKatex({ + throwOnError: false, + output: `html`, + 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 `` +} + +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 { + if (!content) { + return `` + } + 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: IOpts): void { + footnotes.length = 0 + footnoteIndex = 0 + setOptions(newOpts) + } + + function setOptions(newOpts: IOpts): 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, ` `)) + + return `
    ${highlighted}
    ` + }, + + 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(): string { + return styledContent(`hr`, ``) + }, + } + + marked.use({ renderer }) + + return { + buildAddition, + buildFootnotes, + setOptions, + reset, + } +}