mirror of
https://github.com/marktext/marktext.git
synced 2025-05-11 22:41:05 +08:00
369 lines
12 KiB
JavaScript
369 lines
12 KiB
JavaScript
import { LOWERCASE_TAGS, CLASS_OR_ID } from '../config'
|
|
import { conflict, isLengthEven, isEven, getIdWithoutSet, loadImage } from '../utils'
|
|
import { insertAfter, operateClassName } from '../utils/domManipulate.js'
|
|
import selection from '../selection'
|
|
import { tokenizer } from './parse'
|
|
import { validEmoji } from '../emojis'
|
|
|
|
const snabbdom = require('snabbdom')
|
|
const patch = snabbdom.init([ // Init patch function with chosen modules
|
|
require('snabbdom/modules/class').default, // makes it easy to toggle classes
|
|
require('snabbdom/modules/props').default, // for setting properties on DOM elements
|
|
require('snabbdom/modules/dataset').default
|
|
])
|
|
const h = require('snabbdom/h').default // helper function for creating vnodes
|
|
const toVNode = require('snabbdom/tovnode').default
|
|
|
|
class StateRender {
|
|
constructor () {
|
|
this.container = null
|
|
this.vdom = null
|
|
}
|
|
|
|
setContainer (container) {
|
|
this.container = container
|
|
}
|
|
|
|
checkConflicted (block, token, cursor) {
|
|
const key = block.key
|
|
const cursorKey = cursor.key
|
|
if (key !== cursorKey) {
|
|
return false
|
|
} else {
|
|
const { start, end } = token.range
|
|
const { start: cStart, end: cEnd } = cursor.range
|
|
return conflict([start, end], [cStart, cEnd])
|
|
}
|
|
}
|
|
|
|
getClassName (outerClass, block, token, cursor) {
|
|
return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE'])
|
|
}
|
|
/**
|
|
* [render]: 2 steps:
|
|
* render vdom
|
|
* return set cursor method
|
|
*/
|
|
render (blocks, cursor, activeBlockKey) {
|
|
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
|
|
|
const renderBlock = block => {
|
|
const type = block.type === 'hr' ? 'p' : block.type
|
|
let blockSelector = block.key === activeBlockKey || block.key === cursor.key
|
|
? `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}.${CLASS_OR_ID['AG_ACTIVE']}`
|
|
: `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}`
|
|
|
|
if (block.children.length) {
|
|
return h(blockSelector, block.children.map(child => renderBlock(child)))
|
|
} else {
|
|
let children = block.text
|
|
? tokenizer(block.text).reduce((acc, token) => {
|
|
const chunk = this[token.type](h, cursor, block, token)
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, [])
|
|
: [ h(LOWERCASE_TAGS.br) ]
|
|
const data = {
|
|
dataset: {}
|
|
}
|
|
if (/^h\d$/.test(block.type)) {
|
|
Object.assign(data.dataset, { head: block.type })
|
|
}
|
|
if (/^h/.test(block.type)) { // h\d or hr
|
|
Object.assign(data.dataset, { role: block.type })
|
|
}
|
|
if (block.type === 'pre') {
|
|
if (block.lang) Object.assign(data.dataset, { lang: block.lang })
|
|
blockSelector += `.${CLASS_OR_ID['AG_CODE_BLOCK']}`
|
|
children = ''
|
|
}
|
|
|
|
if (block.temp) {
|
|
blockSelector += `.${CLASS_OR_ID['AG_TEMP']}`
|
|
}
|
|
|
|
return h(blockSelector, data, children)
|
|
}
|
|
}
|
|
|
|
const children = blocks.map(block => {
|
|
return renderBlock(block)
|
|
})
|
|
|
|
const newVdom = h(selector, children)
|
|
const root = document.querySelector(selector) || this.container
|
|
const oldVdom = toVNode(root)
|
|
|
|
patch(oldVdom, newVdom)
|
|
|
|
this.vdom = newVdom
|
|
if (cursor && cursor.range) {
|
|
const cursorEle = document.querySelector(`#${cursor.key}`)
|
|
selection.importSelection(cursor.range, cursorEle)
|
|
}
|
|
}
|
|
|
|
hr (h, cursor, block, token, outerClass) {
|
|
const className = CLASS_OR_ID['AG_GRAY']
|
|
return [
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker)
|
|
]
|
|
}
|
|
|
|
header (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
return [
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker)
|
|
]
|
|
}
|
|
|
|
['code_fense'] (h, cursor, block, token, outerClass) {
|
|
return [
|
|
h(`a.${CLASS_OR_ID['AG_GRAY']}`, {
|
|
props: { href: '#' }
|
|
}, token.marker),
|
|
h(`a.${CLASS_OR_ID['AG_LANGUAGE']}`, {
|
|
props: { href: '#' }
|
|
}, token.content)
|
|
]
|
|
}
|
|
|
|
backlash (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
return [
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker)
|
|
]
|
|
}
|
|
|
|
['inline_code'] (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
return [
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker),
|
|
h('code', token.content),
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker)
|
|
]
|
|
}
|
|
|
|
text (h, cursor, block, token) {
|
|
return token.content
|
|
}
|
|
|
|
emoji (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
const validation = validEmoji(token.content)
|
|
const finalClass = validation ? className : CLASS_OR_ID['AG_WARN']
|
|
const emojiVdom = validation
|
|
? h(`a.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, { dataset: { emoji: validation.emoji } }, token.content)
|
|
: h(`a.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`, token.content)
|
|
return [
|
|
h(`a.${finalClass}`, { props: { href: '#' } }, token.marker),
|
|
emojiVdom,
|
|
h(`a.${finalClass}`, { props: { href: '#' } }, token.marker)
|
|
]
|
|
}
|
|
|
|
// render factory of `del`,`em`,`strong`
|
|
delEmStrongFac (type, h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
if (isLengthEven(token.backlash)) {
|
|
return [
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker),
|
|
h(type, [
|
|
...token.children.reduce((acc, to) => {
|
|
const chunk = this[to.type](h, cursor, block, to, className)
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, []),
|
|
...this.backlashInToken(token.backlash, className)
|
|
]),
|
|
h(`a.${className}`, {
|
|
props: { href: '#' }
|
|
}, token.marker)
|
|
]
|
|
} else {
|
|
return [
|
|
token.marker,
|
|
...token.children.reduce((acc, to) => {
|
|
const chunk = this[to.type](h, cursor, block, to, className)
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, []),
|
|
...this.backlashInToken(token.backlash, className),
|
|
token.marker
|
|
]
|
|
}
|
|
}
|
|
|
|
backlashInToken (backlashes, outerClass) {
|
|
const chunks = backlashes.split('')
|
|
const len = chunks.length
|
|
const result = []
|
|
let i
|
|
|
|
for (i = 0; i < len; i++) {
|
|
if (isEven(i)) {
|
|
result.push(
|
|
h(`a.${outerClass}`, {
|
|
props: {
|
|
href: '#'
|
|
}
|
|
}, chunks[i])
|
|
)
|
|
} else {
|
|
result.push(
|
|
h(`a.${CLASS_OR_ID['AG_BACKLASH']}`, {
|
|
props: {
|
|
href: '#'
|
|
}
|
|
}, chunks[i])
|
|
)
|
|
}
|
|
}
|
|
|
|
result.push(
|
|
h(`a.${CLASS_OR_ID['AG_BUG']}`) // the extral a tag for fix bug
|
|
)
|
|
|
|
return result
|
|
}
|
|
// I dont want operate dom directly, is there any better method? need help!
|
|
image (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']
|
|
|
|
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
|
const id = getIdWithoutSet()
|
|
loadImage(token.src + encodeURI(token.backlash.second))
|
|
.then(url => {
|
|
const imageWrapper = document.querySelector(`#${id}`)
|
|
const img = document.createElement('img')
|
|
img.src = url
|
|
img.alt = token.title + encodeURI(token.backlash.first)
|
|
if (imageWrapper) {
|
|
insertAfter(img, imageWrapper)
|
|
operateClassName(imageWrapper, 'add', className)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
const imageWrapper = document.querySelector(`#${id}`)
|
|
if (imageWrapper) {
|
|
operateClassName(imageWrapper, 'add', CLASS_OR_ID['AG_IMAGE_FAIL'])
|
|
}
|
|
})
|
|
|
|
return [
|
|
h(`a#${id}.${imageClass}`, { props: { href: '#' } }, [
|
|
`,
|
|
')'
|
|
])
|
|
]
|
|
} else {
|
|
return [
|
|
'
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, []),
|
|
...this.backlashInToken(token.backlash.first, className),
|
|
'](',
|
|
token.src,
|
|
...this.backlashInToken(token.backlash.second, className),
|
|
')'
|
|
]
|
|
}
|
|
}
|
|
|
|
['auto_link'] (h, cursor, block, token, outerClass) {
|
|
return [
|
|
h('a', {
|
|
porps: {
|
|
href: token.href
|
|
}
|
|
}, token.href)
|
|
]
|
|
}
|
|
|
|
// 'link': /^(\[)((?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*?)(\\*)\]\((.*?)(\\*)\)/, // can nest
|
|
link (h, cursor, block, token, outerClass) {
|
|
const className = this.getClassName(outerClass, block, token, cursor)
|
|
const linkClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_LINK_IN_BRACKET']
|
|
if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
|
|
if (!token.children.length && !token.backlash.first) { // no-text-link
|
|
return [
|
|
h(`a.${CLASS_OR_ID['AG_GRAY']}`, { props: { href: '#' } }, '[]('),
|
|
h('span', {
|
|
dataset: {
|
|
href: token.href + encodeURI(token.backlash.second),
|
|
role: 'link'
|
|
}
|
|
}, [
|
|
token.href,
|
|
...this.backlashInToken(token.backlash.second, className)
|
|
]),
|
|
h(`a.${CLASS_OR_ID['AG_GRAY']}`, { props: { href: '#' } }, ')')
|
|
]
|
|
} else { // has children
|
|
return [
|
|
h(`a.${className}`, { props: { href: '#' } }, '['),
|
|
h('span', {
|
|
dataset: {
|
|
href: token.href + encodeURI(token.backlash.second),
|
|
role: 'link'
|
|
}
|
|
}, [
|
|
...token.children.reduce((acc, to) => {
|
|
const chunk = this[to.type](h, cursor, block, to, className)
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, []),
|
|
...this.backlashInToken(token.backlash.first, className)
|
|
]),
|
|
h(`a.${className}`, { props: { href: '#' } }, ']('),
|
|
h(`span.${linkClassName}`, [
|
|
token.href,
|
|
...this.backlashInToken(token.backlash.second, className)
|
|
]),
|
|
h(`a.${className}`, { props: { href: '#' } }, ')')
|
|
]
|
|
}
|
|
} else {
|
|
return [
|
|
'[',
|
|
...token.children.reduce((acc, to) => {
|
|
const chunk = this[to.type](h, cursor, block, to, className)
|
|
return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
|
|
}, []),
|
|
...this.backlashInToken(token.backlash.first, className),
|
|
`](${token.href}`,
|
|
...this.backlashInToken(token.backlash.second, className),
|
|
')'
|
|
]
|
|
}
|
|
}
|
|
|
|
del (h, cursor, block, token, outerClass) {
|
|
return this.delEmStrongFac('del', h, cursor, block, token, outerClass)
|
|
}
|
|
|
|
em (h, cursor, block, token, outerClass) {
|
|
return this.delEmStrongFac('em', h, cursor, block, token, outerClass)
|
|
}
|
|
|
|
strong (h, cursor, block, token, outerClass) {
|
|
return this.delEmStrongFac('strong', h, cursor, block, token, outerClass)
|
|
}
|
|
}
|
|
|
|
export default StateRender
|