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 ``
- }
-
- 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}${tag}>`
+ }
+
+ 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 ``
+ },
+
+ 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,
+ }
+}