refactor: update renderer (#346)

This commit is contained in:
Libin YANG 2024-08-22 19:22:25 +08:00 committed by GitHub
parent b94750384a
commit a65c86e2ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 222 additions and 250 deletions

View File

@ -3,23 +3,14 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta <meta name="keywords" content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs" />
name="keywords" <meta name="description" content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器" />
content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs"
/>
<meta
name="description"
content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器"
/>
<meta <meta
name="viewport" name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/> />
<title>微信 Markdown 编辑器 | Doocs 开源社区</title> <title>微信 Markdown 编辑器 | Doocs 开源社区</title>
<link <link rel="shortcut icon" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/favicon.png" />
rel="shortcut icon"
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/favicon.png"
/>
<link <link
rel="apple-touch-icon-precomposed" 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" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"

View File

@ -574,8 +574,8 @@ function uploadImage(params) {
:deep(.CodeMirror) { :deep(.CodeMirror) {
border: 1px solid #eee; border: 1px solid #eee;
height: 300px !important; height: 300px !important;
font-family: 'Fira Mono', 'DejaVu Sans Mono', Menlo, Consolas, font-family: 'Fira Mono', 'DejaVu Sans Mono', Menlo, Consolas, 'Liberation Mono', Monaco, 'Lucida Console',
'Liberation Mono', Monaco, 'Lucida Console', monospace !important; monospace !important;
line-height: 20px; line-height: 20px;
.CodeMirror-scroll { .CodeMirror-scroll {

View File

@ -87,8 +87,9 @@ export const useStore = defineStore(`store`, () => {
// 更新编辑器 // 更新编辑器
const editorRefresh = () => { const editorRefresh = () => {
codeThemeChange() codeThemeChange()
const renderer = wxRenderer
const renderer = wxRenderer.getRenderer(isCiteStatus.value) renderer.reset()
renderer.setOptions({ status: isCiteStatus.value, legend: legend.value })
marked.setOptions({ renderer }) marked.setOptions({ renderer })
let outputTemp = marked.parse(editor.value.getValue(0)) let outputTemp = marked.parse(editor.value.getValue(0))

View File

@ -2,77 +2,85 @@ import { Renderer, marked } from 'marked'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import markedKatex from 'marked-katex-extension' import markedKatex from 'marked-katex-extension'
marked.use(markedKatex({ marked.use(
markedKatex({
throwOnError: false, throwOnError: false,
output: `html`, output: `html`,
nonStandard: true, nonStandard: true,
})) }),
)
class WxRenderer { class WxRenderer extends Renderer {
constructor(opts) { constructor(opts) {
super()
this.opts = opts this.opts = opts
let footnotes = [] this.footnotes = []
let footnoteIndex = 0 this.footnoteIndex = 0
let styleMapping = new Map() this.styleMapping = this.buildTheme(opts.theme)
}
const merge = (base, extend) => Object.assign({}, base, extend) reset = () => {
this.footnotes = []
this.footnoteIndex = 0
}
this.buildTheme = (themeTpl) => { merge = (base, extend) => ({ ...base, ...extend })
const mapping = {}
const base = merge(themeTpl.BASE, { buildTheme = (themeTpl) => {
const base = this.merge(themeTpl.BASE, {
'font-family': this.opts.fonts, 'font-family': this.opts.fonts,
'font-size': this.opts.size, 'font-size': this.opts.size,
}) })
for (const ele in themeTpl.inline) {
if (Object.prototype.hasOwnProperty.call(themeTpl.inline, ele)) { const mapping = {
const style = themeTpl.inline[ele] ...Object.fromEntries(
mapping[ele] = merge(themeTpl.BASE, style) Object.entries(themeTpl.inline).map(([ele, style]) => [
} ele,
this.merge(base, style),
]),
),
...Object.fromEntries(
Object.entries(themeTpl.block).map(([ele, style]) => [
ele,
this.merge(base, style),
]),
),
} }
const base_block = merge(base, {})
for (const ele in themeTpl.block) {
if (Object.prototype.hasOwnProperty.call(themeTpl.block, ele)) {
const style = themeTpl.block[ele]
mapping[ele] = merge(base_block, style)
}
}
return mapping return mapping
} }
const getStyles = (tokenName, addition) => { getStyles = (tokenName, addition = ``) => {
const arr = [] const dict = this.styleMapping[tokenName]
const dict = styleMapping[tokenName]
if (!dict) if (!dict)
return `` return ``
for (const key in dict) { const styles = Object.entries(dict)
arr.push(`${key}:${dict[key]}`) .map(([key, value]) => `${key}:${value}`)
} .join(`;`)
return `style="${arr.join(`;`) + (addition || ``)}"` return `style="${styles}${addition}"`
} }
const addFootnote = (title, link) => { addFootnote = (title, link) => {
footnotes.push([++footnoteIndex, title, link]) this.footnotes.push([++this.footnoteIndex, title, link])
return footnoteIndex return this.footnoteIndex
} }
this.buildFootnotes = () => { buildFootnotes = () => {
const footnoteArray = footnotes.map((x) => { if (!this.footnotes.length)
if (x[1] === x[2]) {
return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code>: <i>${x[1]}</i><br/>`
}
return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code> ${x[1]}: <i>${x[2]}</i><br/>`
})
if (!footnoteArray.length) {
return `` return ``
} const footnoteArray = this.footnotes
return `<h4 ${getStyles(`h4`)}>引用链接</h4><p ${getStyles( .map(([index, title, link]) =>
link === title
? `<code style="font-size: 90%; opacity: 0.6;">[${index}]</code>: <i>${title}</i><br/>`
: `<code style="font-size: 90%; opacity: 0.6;">[${index}]</code> ${title}: <i>${link}</i><br/>`,
)
.join(`\n`)
return `<h4 ${this.getStyles(`h4`)}>引用链接</h4><p ${this.getStyles(
`footnotes`, `footnotes`,
)}>${footnoteArray.join(`\n`)}</p>` )}>${footnoteArray}</p>`
} }
this.buildAddition = () => { buildAddition = () => `
return `
<style> <style>
.preview-wrapper pre::before { .preview-wrapper pre::before {
position: absolute; position: absolute;
@ -88,56 +96,38 @@ class WxRenderer {
} }
</style> </style>
` `
setOptions = (newOpts) => {
this.opts = this.merge(this.opts, newOpts)
this.styleMapping = this.buildTheme(this.opts.theme)
} }
this.setOptions = (newOpts) => { heading = (text, level) => {
this.opts = merge(this.opts, newOpts) const tag = `h${level}`
return `<${tag} ${this.getStyles(tag)}>${text}</${tag}>`
} }
this.hasFootnotes = () => footnotes.length !== 0 paragraph = (text) => {
const isFigureImage = text.includes(`<figure`) && text.includes(`<img`)
this.getRenderer = (status) => { const isEmpty = text.trim() === ``
footnotes = [] return isFigureImage ? text : isEmpty ? `` : `<p ${this.getStyles(`p`)}>${text}</p>`
footnoteIndex = 0
styleMapping = this.buildTheme(this.opts.theme)
const renderer = new Renderer()
renderer.heading = (text, level) => {
switch (level) {
case 1:
return `<h1 ${getStyles(`h1`)}>${text}</h1>`
case 2:
return `<h2 ${getStyles(`h2`)}>${text}</h2>`
case 3:
return `<h3 ${getStyles(`h3`)}>${text}</h3>`
default:
return `<h4 ${getStyles(`h4`)}>${text}</h4>`
}
}
renderer.paragraph = (text) => {
if (text.includes(`<figure`) && text.includes(`<img`)) {
return text
}
return text.replace(/ /g, ``) === ``
? ``
: `<p ${getStyles(`p`)}>${text}</p>`
} }
renderer.blockquote = (text) => { blockquote = (text) => {
text = text.replace(/<p.*?>/g, `<p ${getStyles(`blockquote_p`)}>`) text = text.replace(/<p.*?>/g, `<p ${this.getStyles(`blockquote_p`)}>`)
return `<blockquote ${getStyles(`blockquote`)}>${text}</blockquote>` return `<blockquote ${this.getStyles(`blockquote`)}>${text}</blockquote>`
} }
renderer.code = (text, lang = ``) => {
code = (text, lang = ``) => {
if (lang.startsWith(`mermaid`)) { if (lang.startsWith(`mermaid`)) {
setTimeout(() => { setTimeout(() => {
window.mermaid?.run() window.mermaid?.run()
}, 0) }, 0)
return `<center><pre class="mermaid">${text}</pre></center>` return `<center><pre class="mermaid">${text}</pre></center>`
} }
lang = lang.split(` `)[0] const langText = lang.split(` `)[0]
lang = hljs.getLanguage(lang) ? lang : `plaintext` const language = hljs.getLanguage(langText) ? langText : `plaintext`
text = hljs.highlight(text, { language: lang }).value text = hljs.highlight(text, { language }).value
text = text text = text
.replace(/\r\n/g, `<br/>`) .replace(/\r\n/g, `<br/>`)
.replace(/\n/g, `<br/>`) .replace(/\n/g, `<br/>`)
@ -145,87 +135,78 @@ class WxRenderer {
return str.replace(/\s/g, `&nbsp;`) return str.replace(/\s/g, `&nbsp;`)
}) })
return `<pre class="hljs code__pre" ${getStyles( return `<pre class="hljs code__pre" ${this.getStyles(
`code_pre`, `code_pre`,
)}><code class="language-${lang}" ${getStyles( )}><code class="language-${lang}" ${this.getStyles(
`code`, `code`,
)}>${text}</code></pre>` )}>${text}</code></pre>`
} }
renderer.codespan = (text, _) =>
`<code ${getStyles(`codespan`)}>${text}</code>`
renderer.listitem = text =>
`<li ${getStyles(`listitem`)}><span><%s/></span>${text}</li>`
renderer.list = (text, ordered, _) => { codespan = text => `<code ${this.getStyles(`codespan`)}>${text}</code>`
text = text.replace(/<\/*p .*?>/g, ``).replace(/<\/*p>/g, ``)
listitem = text => `<li ${this.getStyles(`listitem`)}><span><%s/></span>${text}</li>`
list = (text, ordered) => {
text = text.replace(/<\/*p.*?>/g, ``).replace(/<\/*p>/g, ``)
const segments = text.split(`<%s/>`) const segments = text.split(`<%s/>`)
if (!ordered) { if (!ordered) {
text = segments.join(``) return `<ul ${this.getStyles(`ul`)}>${segments.join(``)}</ul>`
return `<ul ${getStyles(`ul`)}>${text}</ul>`
}
text = segments[0]
for (let i = 1; i < segments.length; i++) {
text = `${text + i}. ${segments[i]}`
}
return `<ol ${getStyles(`ol`)}>${text}</ol>`
}
renderer.image = (href, title, text) => {
const createSubText = (s) => {
if (!s) {
return ``
} }
return `<figcaption ${getStyles(`figcaption`)}>${s}</figcaption>` const orderedText = segments.map((segment, i) => (i > 0 ? `${i}. ` : ``) + segment).join(``)
return `<ol ${this.getStyles(`ol`)}>${orderedText}</ol>`
} }
const transform = (title, alt) => {
const legend = localStorage.getItem(`legend`) image = (href, title, text) => {
switch (legend) { const createSubText = s => s ? `<figcaption ${this.getStyles(`figcaption`)}>${s}</figcaption>` : ``
case `alt`: const transform = {
return alt 'alt': () => text,
case `title`: 'title': () => title,
return title 'alt-title': () => text || title,
case `alt-title`: 'title-alt': () => title || text,
return alt || title }[this.opts.legend] || (() => ``)
case `title-alt`:
return title || alt const subText = createSubText(transform())
default: const figureStyles = this.getStyles(`figure`)
return `` const imgStyles = this.getStyles(`image`)
}
}
const subText = createSubText(transform(title, text))
const figureStyles = getStyles(`figure`)
const imgStyles = getStyles(`image`)
return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>` return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`
} }
renderer.link = (href, title, text) => {
link = (href, title, text) => {
if (href.startsWith(`https://mp.weixin.qq.com`)) { if (href.startsWith(`https://mp.weixin.qq.com`)) {
return `<a href="${href}" title="${title || text}" ${getStyles( return `<a href="${href}" title="${title || text}" ${this.getStyles(
`wx_link`, `wx_link`,
)}>${text}</a>` )}>${text}</a>`
} }
if (href === text) { if (href === text)
return text return text
if (this.opts.status) {
const ref = this.addFootnote(title || text, href)
return `<span ${this.getStyles(
`link`,
)}>${text}<sup>[${ref}]</sup></span>`
} }
if (status) { return `<span ${this.getStyles(`link`)}>${text}</span>`
const ref = addFootnote(title || text, href)
return `<span ${getStyles(`link`)}>${text}<sup>[${ref}]</sup></span>`
}
return `<span ${getStyles(`link`)}>${text}</span>`
}
renderer.strong = text =>
`<strong ${getStyles(`strong`)}>${text}</strong>`
renderer.em = text =>
`<span style="font-style: italic;">${text}</span>`
renderer.table = (header, body) =>
`<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles(
`thead`,
)}>${header}</thead><tbody>${body}</tbody></table></section>`
renderer.tablecell = (text, _) =>
`<td ${getStyles(`td`)}>${text}</td>`
renderer.hr = () => `<hr ${getStyles(`hr`)}>`
return renderer
}
} }
strong = text => `<strong ${this.getStyles(`strong`)}>${text}</strong>`
em = text => `<span style="font-style: italic;">${text}</span>`
table = (header, body) => `
<section style="padding:0 8px;">
<table class="preview-table">
<thead ${this.getStyles(`thead`)}>${header}</thead>
<tbody>${body}</tbody>
</table>
</section>`
tablecell = text => `<td ${this.getStyles(`td`)}>${text}</td>`
hr = () => `<hr ${this.getStyles(`hr`)}/>`
} }
export default WxRenderer export default WxRenderer

View File

@ -170,7 +170,6 @@ function beforeUpload(file) {
// //
function uploaded(imageUrl) { function uploaded(imageUrl) {
console.log(`图片上传之后: `, imageUrl)
if (!imageUrl) { if (!imageUrl) {
ElMessage.error(`上传图片未知异常`) ElMessage.error(`上传图片未知异常`)
return return
@ -182,27 +181,27 @@ function uploaded(imageUrl) {
// Markdown URL // Markdown URL
toRaw(store.editor).replaceSelection(`\n${markdownImage}\n`, cursor) toRaw(store.editor).replaceSelection(`\n${markdownImage}\n`, cursor)
ElMessage.success(`图片上传成功`) ElMessage.success(`图片上传成功`)
// formatContent()
// onEditorRefresh()
} }
function uploadImage(file, cb) { function uploadImage(file, cb) {
isImgLoading.value = true isImgLoading.value = true
toBase64(file) toBase64(file)
.then((base64Content) => { .then(base64Content => fileApi.fileUpload(base64Content, file))
fileApi
.fileUpload(base64Content, file)
.then((url) => { .then((url) => {
console.log(url) console.log(url)
cb ? cb(url) : uploaded(url) if (cb) {
}) cb(url)
.catch((err) => { }
ElMessage.error(err.message) else {
}) uploaded(url)
}
}) })
.catch((err) => { .catch((err) => {
ElMessage.error(err.message) ElMessage.error(err.message)
}) })
.finally(() => {
isImgLoading.value = false isImgLoading.value = false
})
} }
const changeTimer = ref(0) const changeTimer = ref(0)