Files
yn/src/renderer/plugins/markdown-render-vnode.ts
T
2021-12-20 23:15:52 +08:00

278 lines
7.5 KiB
TypeScript

import { createVNode, Fragment, Comment, Text, VNode } from 'vue'
import Token from 'markdown-it/lib/token'
import Renderer from 'markdown-it/lib/renderer'
import { escapeHtml, unescapeAll } from 'markdown-it/lib/common/utils'
import { Plugin } from '@fe/context'
const attrNameReg = /^[a-zA-Z_:][a-zA-Z0-9:._-]*$/
const defaultRules = {} as any
function getLine (token: Token, env?: Record<string, any>) {
const [lineStart, lineEnd] = token.map || [0, 1]
// macro, calc line offset, see `markdown-macro` plugin.
let sOffset = 0
if (env?.macroLines && env.bMarks && env.eMarks) {
const sPos = env.bMarks[lineStart]
for (let i = 0; i < env.macroLines.length; i++) {
const { matchPos, lineOffset, posOffset, currentPosOffset } = env.macroLines[i]
if (sPos + posOffset > matchPos && sPos + posOffset - currentPosOffset > matchPos) {
sOffset = lineOffset
} else {
break
}
}
}
return [lineStart + sOffset, lineEnd + sOffset]
}
export function setSourceLine (token: Token, env?: Record<string, any>) {
if (!token.meta) {
token.meta = {}
}
if (token.block) {
const [lineStart, lineEnd] = getLine(token, env)
if (token.map) {
token.attrJoin('class', 'source-line')
token.attrSet('data-source-line', String(lineStart + 1))
token.attrSet('data-source-line-end', String(lineEnd + 1))
if (!token.meta.attrs) {
token.meta.attrs = {}
}
token.attrs?.forEach(([name, val]) => {
token.meta.attrs[name] = val
})
}
}
}
defaultRules.code_inline = function (tokens: Token[], idx: number, _: any, __: any, slf: Renderer) {
const token = tokens[idx]
return createVNode('code', slf.renderAttrs(token) as any, token.content)
}
defaultRules.code_block = function (tokens: Token[], idx: number, _: any, __: any, slf: Renderer) {
const token = tokens[idx]
const attrs: any = slf.renderAttrs(token)
return createVNode(
'pre',
{},
[createVNode('code', attrs, [createVNode(Text, {}, token.content)])]
)
}
defaultRules.fence = function (tokens: Token[], idx: number, options: any, _: any, slf: Renderer) {
const token = tokens[idx]
const info = token.info ? unescapeAll(token.info).trim() : ''
token.attrSet('data-export-text', token.content)
let langName = ''
let langAttrs = ''
let highlighted: any; let i; let arr; let tmpAttrs; let tmpToken
if (info) {
arr = info.split(/(\s+)/g)
langName = arr[0]
langAttrs = arr.slice(2).join('')
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content)
} else {
highlighted = escapeHtml(token.content)
}
if (highlighted.indexOf('<pre') === 0) {
return highlighted + '\n'
}
const buildVNode = (attrs: any) => createVNode(
'pre',
{},
[createVNode('code', { key: highlighted, ...attrs, innerHTML: highlighted }, [])]
)
// If language exists, inject class gently, without modifying original token.
// May be, one day we will add .deepClone() for token and simplify this part, but
// now we prefer to keep things local.
if (info) {
i = token.attrIndex('class')
tmpAttrs = token.attrs ? token.attrs.slice() : []
if (i < 0) {
tmpAttrs.push(['class', options.langPrefix + langName])
} else {
tmpAttrs[i] = tmpAttrs[i].slice() as any
tmpAttrs[i][1] += ' ' + options.langPrefix + langName
}
// Fake token just to render attributes
tmpToken = {
attrs: tmpAttrs
}
return buildVNode(slf.renderAttrs(tmpToken as any))
}
return buildVNode(slf.renderAttrs(token))
}
defaultRules.image = function (tokens: Token[], idx: number, options: any, env: any, slf: Renderer) {
const token = tokens[idx]
return createVNode('img', {
...slf.renderAttrs(token) as any,
alt: slf.renderInlineAsText(token.children || [], options, env)
}, [])
}
defaultRules.hardbreak = function () {
return createVNode('br')
}
defaultRules.softbreak = function (_: Token[], __: number, options: any) {
return options.breaks ? createVNode('br') : null
}
defaultRules.text = function (tokens: Token[], idx: number) {
return createVNode(Text, {}, tokens[idx].content)
}
defaultRules.html_block = function (tokens: Token[], idx: number) {
return createHtmlVNode(tokens[idx].content)
}
defaultRules.html_inline = function (tokens: Token[], idx: number) {
return createHtmlVNode(tokens[idx].content)
}
function createHtmlVNode (html: string) {
const div = document.createElement('div')
div.innerHTML = html
const children = []
for (let i = 0; i < div.children.length; i++) {
const element = div.children[i]
const tagName = element.tagName.toLowerCase()
const attrs: Record<string, any> = {
key: element.outerHTML
}
for (let j = 0; j < element.attributes.length; j++) {
const attr = element.attributes[j]
attrs[attr.name] = attr.value
}
attrs.innerHTML = element.innerHTML
attrs.key = element.innerHTML
children.push(createVNode(tagName, attrs, []))
}
return createVNode(Fragment, {}, children)
}
function renderToken (this: Renderer, tokens: Token[], idx: number): any {
const token = tokens[idx]
if (token.nesting === -1) {
return null
}
// Tight list paragraphs
if (token.hidden) {
return createVNode(Fragment, {}, [])
}
if (token.tag === '--') {
return createVNode(Comment)
}
return createVNode(token.tag, this.renderAttrs(token) as any, [])
}
function renderAttrs (this: Renderer, token: Token) {
if (!token.attrs) {
return {}
}
const result: any = {}
token.attrs.forEach(token => {
if (attrNameReg.test(token[0])) {
result[token[0]] = token[1]
}
})
return result
}
function render (this: Renderer, tokens: Token[], options: any, env: any) {
const rules: any = this.rules
const vNodeParents: VNode[] = []
return tokens.map((token, i) => {
setSourceLine(token, env)
const type = token.type
let vnode: VNode | null = null
let parent: VNode | null = null
if (type === 'inline') {
vnode = createVNode(Fragment, {}, this.render(token.children || [], options, env))
} else if (rules[type]) {
const result = rules[type](tokens, i, options, env, this)
if (typeof result === 'string') {
vnode = createHtmlVNode(result)
} else if (result && result.node && result.parent) {
parent = result.parent
vnode = result.node
} else {
vnode = result
}
} else {
vnode = this.renderToken(tokens, i, options) as any
}
let isChild = false
const parentNode = vNodeParents.length > 0 ? vNodeParents[vNodeParents.length - 1] : null
if (vnode && parentNode) {
if (typeof parentNode.type === 'string' || parentNode.type === Fragment) {
const children = Array.isArray(parentNode.children) ? parentNode.children : []
parentNode.children = children.concat([vnode])
}
isChild = true
}
if (token.nesting === 1) {
if (parent) {
vNodeParents.push(parent)
} else if (vnode) {
vNodeParents.push(vnode)
}
}
if (token.nesting === -1) {
vNodeParents.pop()
}
return isChild ? null : vnode
}).filter(node => !!node) as any
}
export default {
name: 'markdown-render-vnode',
register: ctx => {
ctx.markdown.registerPlugin(md => {
md.renderer.rules = { ...md.renderer.rules, ...defaultRules }
md.renderer.render = render
md.renderer.renderInline = render
md.renderer.renderAttrs = renderAttrs
md.renderer.renderToken = renderToken
})
}
} as Plugin