mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 23:30:04 +08:00
fix some performance problem, add partial render and undo depth (#222)
* fix some performance problem, add partial render and undo depth * optimization of performance: rewrite getBlock, getBlocks ... * optimization of performance: cache the result of tokenization * optimization of performance: render code block only needed
This commit is contained in:
parent
d19dc9f4b8
commit
f569d2e9d9
2
.github/TODOLIST.md
vendored
2
.github/TODOLIST.md
vendored
@ -68,6 +68,8 @@
|
||||
|
||||
- [ ] Refactor data structure of contentState, add copyBlock...
|
||||
|
||||
- [ ] Refactor the History data structure
|
||||
|
||||
##### Documents
|
||||
|
||||
- [ ] Manual
|
||||
|
@ -3,60 +3,7 @@ import { isCursorAtFirstLine, isCursorAtLastLine, isCursorAtBegin, isCursorAtEnd
|
||||
import { findNearestParagraph } from '../utils/domManipulate'
|
||||
import selection from '../selection'
|
||||
|
||||
const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
|
||||
|
||||
const arrowCtrl = ContentState => {
|
||||
ContentState.prototype.firstInDescendant = function (block) {
|
||||
const children = block.children
|
||||
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
|
||||
return block
|
||||
} else if (children.length) {
|
||||
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
|
||||
return this.firstInDescendant(children[1])
|
||||
} else {
|
||||
return this.firstInDescendant(children[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.lastInDescendant = function (block) {
|
||||
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
|
||||
return block
|
||||
} else if (block.children.length) {
|
||||
const children = block.children
|
||||
let lastChild = children[children.length - 1]
|
||||
while (lastChild.editable === false) {
|
||||
lastChild = this.getPreSibling(lastChild)
|
||||
}
|
||||
return this.lastInDescendant(lastChild)
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.findPreBlockInLocation = function (block) {
|
||||
const parent = this.getParent(block)
|
||||
const preBlock = this.getPreSibling(block)
|
||||
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
|
||||
return this.lastInDescendant(preBlock)
|
||||
} else if (parent) {
|
||||
return this.findPreBlockInLocation(parent)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.findNextBlockInLocation = function (block) {
|
||||
const parent = this.getParent(block)
|
||||
const nextBlock = this.getNextSibling(block)
|
||||
|
||||
if (block.nextSibling && nextBlock.editable !== false) {
|
||||
return this.firstInDescendant(nextBlock)
|
||||
} else if (parent) {
|
||||
return this.findNextBlockInLocation(parent)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.findNextRowCell = function (cell) {
|
||||
if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`)
|
||||
const row = this.getParent(cell)
|
||||
@ -126,6 +73,7 @@ const arrowCtrl = ContentState => {
|
||||
|
||||
if (show && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
switch (event.key) {
|
||||
case EVENT_KEYS.ArrowDown:
|
||||
if (index < list.length - 1) {
|
||||
@ -147,6 +95,7 @@ const arrowCtrl = ContentState => {
|
||||
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block
|
||||
let activeBlock
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
switch (event.key) {
|
||||
case EVENT_KEYS.ArrowLeft: // fallthrough
|
||||
case EVENT_KEYS.ArrowUp:
|
||||
@ -236,6 +185,7 @@ const arrowCtrl = ContentState => {
|
||||
|
||||
if (activeBlock) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const offset = activeBlock.type === 'p' ? 0 : (event.key === EVENT_KEYS.ArrowUp ? activeBlock.text.length : 0)
|
||||
const key = activeBlock.type === 'p' ? activeBlock.children[0].key : activeBlock.key
|
||||
this.cursor = {
|
||||
@ -258,6 +208,7 @@ const arrowCtrl = ContentState => {
|
||||
(preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowLeft && left === 0)
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const key = preBlock.key
|
||||
const offset = 0
|
||||
this.cursor = {
|
||||
@ -274,6 +225,7 @@ const arrowCtrl = ContentState => {
|
||||
(nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowRight && right === 0)
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const key = nextBlock.key
|
||||
const offset = 0
|
||||
this.cursor = {
|
||||
@ -289,6 +241,7 @@ const arrowCtrl = ContentState => {
|
||||
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!preBlock) return
|
||||
const key = preBlock.key
|
||||
const offset = preBlock.text.length
|
||||
@ -302,6 +255,7 @@ const arrowCtrl = ContentState => {
|
||||
(event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length)
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
let key
|
||||
if (nextBlock) {
|
||||
key = nextBlock.key
|
||||
|
@ -2,7 +2,7 @@ import createDOMPurify from 'dompurify'
|
||||
import codeMirror, { setMode, setCursorAtLastLine } from '../codeMirror'
|
||||
import { createInputInCodeBlock } from '../utils/domManipulate'
|
||||
import { escapeInBlockHtml } from '../utils'
|
||||
import { codeMirrorConfig, CLASS_OR_ID, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config'
|
||||
import { codeMirrorConfig, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config'
|
||||
|
||||
const DOMPurify = createDOMPurify(window)
|
||||
|
||||
@ -72,9 +72,10 @@ const codeBlockCtrl = ContentState => {
|
||||
return !!match
|
||||
}
|
||||
|
||||
ContentState.prototype.pre2CodeMirror = function (isRenderCursor) {
|
||||
ContentState.prototype.pre2CodeMirror = function (isRenderCursor, emptyPres) {
|
||||
if (!emptyPres.length) return
|
||||
const { eventCenter } = this
|
||||
const selector = `pre.${CLASS_OR_ID['AG_CODE_BLOCK']}, pre.${CLASS_OR_ID['AG_HTML_BLOCK']}`
|
||||
const selector = emptyPres.map(key => `pre#${key}`).join(', ')
|
||||
const pres = document.querySelectorAll(selector)
|
||||
Array.from(pres).forEach(pre => {
|
||||
const id = pre.id
|
||||
|
@ -48,8 +48,13 @@ export class History {
|
||||
}
|
||||
}
|
||||
push (state) {
|
||||
const UNDO_DEPTH = 20
|
||||
this.stack.splice(this.index + 1)
|
||||
this.stack.push(deepCopy(state))
|
||||
if (this.stack.length > UNDO_DEPTH) {
|
||||
this.stack.shift()
|
||||
this.index = this.index - 1
|
||||
}
|
||||
this.index = this.index + 1
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { setCursorAtLastLine } from '../codeMirror'
|
||||
import { getUniqueId } from '../utils'
|
||||
import selection from '../selection'
|
||||
import StateRender from '../parser/StateRender'
|
||||
@ -42,17 +43,7 @@ const prototypes = [
|
||||
importMarkdown
|
||||
]
|
||||
|
||||
// deep first search
|
||||
const convertBlocksToArray = blocks => {
|
||||
const result = []
|
||||
blocks.forEach(block => {
|
||||
result.push(block)
|
||||
if (block.children.length) {
|
||||
result.push(...convertBlocksToArray(block.children))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
|
||||
|
||||
// use to cache the keys which you don't want to remove.
|
||||
const exemption = new Set()
|
||||
@ -91,7 +82,20 @@ class ContentState {
|
||||
}
|
||||
|
||||
setCursor () {
|
||||
selection.setCursorRange(this.cursor)
|
||||
const { start: { key } } = this.cursor
|
||||
const block = this.getBlock(key)
|
||||
if (block.type !== 'pre') {
|
||||
selection.setCursorRange(this.cursor)
|
||||
} else {
|
||||
const cm = this.codeBlocks.get(key)
|
||||
const { pos } = block
|
||||
if (pos) {
|
||||
cm.focus()
|
||||
cm.setCursor(pos)
|
||||
} else {
|
||||
setCursorAtLastLine(cm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render (isRenderCursor = true) {
|
||||
@ -100,12 +104,19 @@ class ContentState {
|
||||
matches.forEach((m, i) => {
|
||||
m.active = i === index
|
||||
})
|
||||
this.stateRender.render(blocks, cursor, activeBlocks, matches)
|
||||
const emptyPres = this.stateRender.render(blocks, cursor, activeBlocks, matches)
|
||||
this.pre2CodeMirror(isRenderCursor, emptyPres)
|
||||
if (isRenderCursor) this.setCursor()
|
||||
this.pre2CodeMirror(isRenderCursor)
|
||||
this.renderMath()
|
||||
}
|
||||
|
||||
partialRender (block) {
|
||||
const { cursor } = this
|
||||
this.stateRender.partialRender(block, cursor)
|
||||
this.setCursor()
|
||||
this.renderMath(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph.
|
||||
* a line block must in a `p block` and `p block`'s children must be line blocks.
|
||||
@ -137,11 +148,11 @@ class ContentState {
|
||||
|
||||
// getBlocks
|
||||
getBlocks () {
|
||||
for (const [ key, cm ] of this.codeBlocks.entries()) {
|
||||
const value = cm.getValue()
|
||||
const block = this.getBlock(key)
|
||||
if (block) block.text = value
|
||||
}
|
||||
// for (const [ key, cm ] of this.codeBlocks.entries()) {
|
||||
// const value = cm.getValue()
|
||||
// const block = this.getBlock(key)
|
||||
// if (block) block.text = value
|
||||
// }
|
||||
return this.blocks
|
||||
}
|
||||
|
||||
@ -149,12 +160,22 @@ class ContentState {
|
||||
return this.cursor
|
||||
}
|
||||
|
||||
getArrayBlocks () {
|
||||
return convertBlocksToArray(this.blocks)
|
||||
}
|
||||
|
||||
getBlock (key) {
|
||||
return this.getArrayBlocks().filter(block => block.key === key)[0]
|
||||
let result = null
|
||||
const travel = blocks => {
|
||||
for (const block of blocks) {
|
||||
if (block.key === key) {
|
||||
result = block
|
||||
return
|
||||
}
|
||||
const { children } = block
|
||||
if (children.length) {
|
||||
travel(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
travel(this.blocks)
|
||||
return result
|
||||
}
|
||||
|
||||
getParent (block) {
|
||||
@ -183,15 +204,6 @@ class ContentState {
|
||||
return block.nextSibling ? this.getBlock(block.nextSibling) : null
|
||||
}
|
||||
|
||||
getFirstBlock () {
|
||||
const arrayBlocks = this.getArrayBlocks()
|
||||
if (arrayBlocks.length) {
|
||||
return arrayBlocks[0]
|
||||
} else {
|
||||
throw new Error('article need at least has one paragraph')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if target is descendant of parent return true, else return false
|
||||
* @param {[type]} parent [description]
|
||||
@ -459,39 +471,76 @@ class ContentState {
|
||||
return null
|
||||
}
|
||||
|
||||
getLastBlock () {
|
||||
const arrayBlocks = this.getArrayBlocks()
|
||||
const len = arrayBlocks.length
|
||||
if (len) {
|
||||
return arrayBlocks[len - 1]
|
||||
} else {
|
||||
throw new Error('article need at least has one paragraph')
|
||||
firstInDescendant (block) {
|
||||
const children = block.children
|
||||
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
|
||||
return block
|
||||
} else if (children.length) {
|
||||
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
|
||||
return this.firstInDescendant(children[1])
|
||||
} else {
|
||||
return this.firstInDescendant(children[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wordCount () {
|
||||
const blocks = this.getBlocks()
|
||||
let paragraph = blocks.length
|
||||
lastInDescendant (block) {
|
||||
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
|
||||
return block
|
||||
} else if (block.children.length) {
|
||||
const children = block.children
|
||||
let lastChild = children[children.length - 1]
|
||||
while (lastChild.editable === false) {
|
||||
lastChild = this.getPreSibling(lastChild)
|
||||
}
|
||||
return this.lastInDescendant(lastChild)
|
||||
}
|
||||
}
|
||||
|
||||
findPreBlockInLocation (block) {
|
||||
const parent = this.getParent(block)
|
||||
const preBlock = this.getPreSibling(block)
|
||||
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
|
||||
return this.lastInDescendant(preBlock)
|
||||
} else if (parent) {
|
||||
return this.findPreBlockInLocation(parent)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
findNextBlockInLocation (block) {
|
||||
const parent = this.getParent(block)
|
||||
const nextBlock = this.getNextSibling(block)
|
||||
|
||||
if (block.nextSibling && nextBlock.editable !== false) {
|
||||
return this.firstInDescendant(nextBlock)
|
||||
} else if (parent) {
|
||||
return this.findNextBlockInLocation(parent)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getLastBlock () {
|
||||
const { blocks } = this
|
||||
const len = blocks.length
|
||||
return this.lastInDescendant(blocks[len - 1])
|
||||
}
|
||||
|
||||
wordCount (markdown) {
|
||||
let paragraph = this.blocks.length
|
||||
let word = 0
|
||||
let character = 0
|
||||
let all = 0
|
||||
|
||||
const travel = block => {
|
||||
if (block.text) {
|
||||
const text = block.text
|
||||
const removedChinese = text.replace(/[\u4e00-\u9fa5]/g, '')
|
||||
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
|
||||
const chineseWordLength = text.length - removedChinese.length
|
||||
word += chineseWordLength + tokens.length
|
||||
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
|
||||
all += text.length
|
||||
}
|
||||
if (block.children.length) {
|
||||
block.children.forEach(child => travel(child))
|
||||
}
|
||||
}
|
||||
const removedChinese = markdown.replace(/[\u4e00-\u9fa5]/g, '')
|
||||
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
|
||||
const chineseWordLength = markdown.length - removedChinese.length
|
||||
word += chineseWordLength + tokens.length
|
||||
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
|
||||
all += markdown.length
|
||||
|
||||
blocks.forEach(block => travel(block))
|
||||
return { word, paragraph, character, all }
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,9 @@ import { CLASS_OR_ID } from '../config'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
const mathCtrl = ContentState => {
|
||||
ContentState.prototype.renderMath = function () {
|
||||
const mathEles = document.querySelectorAll(`.${CLASS_OR_ID['AG_MATH_RENDER']}`)
|
||||
ContentState.prototype.renderMath = function (block) {
|
||||
const selector = block ? `#${block.key} .${CLASS_OR_ID['AG_MATH_RENDER']}` : `.${CLASS_OR_ID['AG_MATH_RENDER']}`
|
||||
const mathEles = document.querySelectorAll(selector)
|
||||
const { loadMathMap } = this
|
||||
for (const math of mathEles) {
|
||||
const content = math.getAttribute('data-math')
|
||||
|
@ -1,8 +1,8 @@
|
||||
import selection from '../selection'
|
||||
import { tokenizer } from '../parser/parse'
|
||||
import { conflict } from '../utils'
|
||||
import { conflict, isMetaKey } from '../utils'
|
||||
import { getTextContent } from '../utils/domManipulate'
|
||||
import { CLASS_OR_ID } from '../config'
|
||||
import { CLASS_OR_ID, EVENT_KEYS } from '../config'
|
||||
|
||||
const INLINE_UPDATE_FREGMENTS = [
|
||||
'^([*+-]\\s)', // Bullet list
|
||||
@ -64,8 +64,7 @@ const updateCtrl = ContentState => {
|
||||
|
||||
switch (true) {
|
||||
case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
|
||||
this.updateHr(block, hr)
|
||||
return true
|
||||
return this.updateHr(block, hr)
|
||||
|
||||
case !!bullet:
|
||||
this.updateList(block, 'bullet', bullet)
|
||||
@ -81,8 +80,7 @@ const updateCtrl = ContentState => {
|
||||
return true
|
||||
|
||||
case !!header:
|
||||
this.updateHeader(block, header, text)
|
||||
return true
|
||||
return this.updateHeader(block, header, text)
|
||||
|
||||
case !!blockquote:
|
||||
this.updateBlockQuote(block)
|
||||
@ -96,11 +94,15 @@ const updateCtrl = ContentState => {
|
||||
|
||||
// thematic break
|
||||
ContentState.prototype.updateHr = function (block, marker) {
|
||||
block.type = 'hr'
|
||||
block.text = marker
|
||||
block.children.length = 0
|
||||
const { key } = block
|
||||
this.cursor.start.key = this.cursor.end.key = key
|
||||
if (block.type !== 'hr') {
|
||||
block.type = 'hr'
|
||||
block.text = marker
|
||||
block.children.length = 0
|
||||
const { key } = block
|
||||
this.cursor.start.key = this.cursor.end.key = key
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
ContentState.prototype.updateList = function (block, type, marker = '') {
|
||||
@ -230,8 +232,10 @@ const updateCtrl = ContentState => {
|
||||
block.type = newType
|
||||
block.text = text
|
||||
block.children.length = 0
|
||||
this.cursor.start.key = this.cursor.end.key = block.key
|
||||
return true
|
||||
}
|
||||
this.cursor.start.key = this.cursor.end.key = block.key
|
||||
return false
|
||||
}
|
||||
|
||||
ContentState.prototype.updateBlockQuote = function (block) {
|
||||
@ -272,12 +276,16 @@ const updateCtrl = ContentState => {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
const key = start.key
|
||||
const block = this.getBlock(key)
|
||||
|
||||
// bugfix: #67 problem 1
|
||||
if (block && block.icon) return event.preventDefault()
|
||||
|
||||
if (isMetaKey(event)) {
|
||||
return
|
||||
}
|
||||
const { start: oldStart, end: oldEnd } = this.cursor
|
||||
|
||||
if (event.type === 'keyup' && (event.key === 'ArrowUp' || event.key === 'ArrowDown') && floatBox.show) {
|
||||
if (event.type === 'keyup' && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) && floatBox.show) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -322,7 +330,6 @@ const updateCtrl = ContentState => {
|
||||
end.offset !== oldEnd.offset
|
||||
) {
|
||||
this.cursor = { start, end }
|
||||
// this.render()
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -331,6 +338,7 @@ const updateCtrl = ContentState => {
|
||||
const paragraph = document.querySelector(`#${key}`)
|
||||
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
|
||||
let needRender = false
|
||||
let isPartialRender = false
|
||||
if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
|
||||
// first cell in thead
|
||||
const cursorBlock = block.children[1].children[0].children[0].children[0]
|
||||
@ -342,6 +350,7 @@ const updateCtrl = ContentState => {
|
||||
}
|
||||
return this.render()
|
||||
}
|
||||
|
||||
// remove temp block which generated by operation on code block
|
||||
if (block && block.key !== oldKey) {
|
||||
let oldBlock = this.getBlock(oldKey)
|
||||
@ -362,7 +371,7 @@ const updateCtrl = ContentState => {
|
||||
if (block && block.type === 'pre') {
|
||||
if (block.key !== oldKey) {
|
||||
this.cursor = lastCursor = { start, end }
|
||||
this.render()
|
||||
if (event.type === 'click') this.render()
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -405,12 +414,16 @@ const updateCtrl = ContentState => {
|
||||
needRender = true
|
||||
}
|
||||
|
||||
if (key === oldKey) {
|
||||
isPartialRender = true
|
||||
}
|
||||
|
||||
this.cursor = lastCursor = { start, end }
|
||||
const checkMarkedUpdate = this.checkNeedRender(block)
|
||||
const checkInlineUpdate = this.isCollapse() && this.checkInlineUpdate(block)
|
||||
|
||||
if (checkMarkedUpdate || checkInlineUpdate || needRender) {
|
||||
this.render()
|
||||
isPartialRender && !checkInlineUpdate ? this.partialRender(block) : this.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.ag-float-box {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: -10000px;
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
opacity: 0;
|
||||
max-height: 168px;
|
||||
min-width: 130px;
|
||||
@ -12,7 +12,7 @@
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
|
||||
list-style: none;
|
||||
transition: opacity .4s ease-in;
|
||||
transition: opacity .15s ease-in;
|
||||
overflow: auto;
|
||||
background: #fff;
|
||||
z-index: 10000;
|
||||
|
@ -110,8 +110,8 @@ class Aganippe {
|
||||
|
||||
dispatchChange () {
|
||||
const { eventCenter } = this
|
||||
const markdown = this.getMarkdown()
|
||||
const wordCount = this.getWordCount()
|
||||
const markdown = this.markdown = this.getMarkdown()
|
||||
const wordCount = this.getWordCount(markdown)
|
||||
const cursor = this.getCursor()
|
||||
eventCenter.dispatch('change', markdown, wordCount, cursor)
|
||||
}
|
||||
@ -435,13 +435,12 @@ class Aganippe {
|
||||
}
|
||||
|
||||
exportUnstylishHtml () {
|
||||
const blocks = this.contentState.getBlocks()
|
||||
const markdown = new ExportMarkdown(blocks).generate()
|
||||
const { markdown } = this
|
||||
return exportHtml(markdown)
|
||||
}
|
||||
|
||||
getWordCount () {
|
||||
return this.contentState.wordCount()
|
||||
getWordCount (markdown) {
|
||||
return this.contentState.wordCount(markdown)
|
||||
}
|
||||
|
||||
getCursor () {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import virtualize from 'snabbdom-virtualize/strings'
|
||||
import { LOWERCASE_TAGS, CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
|
||||
import { CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
|
||||
import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils'
|
||||
import { insertAfter, operateClassName } from '../utils/domManipulate'
|
||||
import { tokenizer } from './parse'
|
||||
@ -19,6 +19,7 @@ class StateRender {
|
||||
constructor (eventCenter) {
|
||||
this.eventCenter = eventCenter
|
||||
this.loadImageMap = new Map()
|
||||
this.tokenCache = new Map()
|
||||
this.container = null
|
||||
}
|
||||
|
||||
@ -52,11 +53,16 @@ class StateRender {
|
||||
}
|
||||
|
||||
/**
|
||||
* [render]: 2 steps:
|
||||
* render vdom
|
||||
* [render All blocks]
|
||||
* @param {[Array]} blocks [description]
|
||||
* @param {[Object]} cursor [description]
|
||||
* @param {[Object]} activeBlocks [description]
|
||||
* @param {[Array]} matches [description]
|
||||
* @return {[undefined]} [description]
|
||||
*/
|
||||
render (blocks, cursor, activeBlocks, matches) {
|
||||
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||
const emptyPres = []
|
||||
|
||||
const renderBlock = block => {
|
||||
const type = block.type === 'hr' ? 'p' : block.type
|
||||
@ -145,7 +151,14 @@ class StateRender {
|
||||
const { text } = block
|
||||
let children = ''
|
||||
if (text) {
|
||||
children = tokenizer(text, highlights).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
|
||||
let tokens = null
|
||||
if (highlights.length === 0 && this.tokenCache.has(text)) {
|
||||
tokens = this.tokenCache.get(text)
|
||||
} else {
|
||||
tokens = tokenizer(text, highlights)
|
||||
this.tokenCache.set(text, tokens)
|
||||
}
|
||||
children = tokens.reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
|
||||
}
|
||||
|
||||
if (/th|td/.test(block.type)) {
|
||||
@ -179,12 +192,6 @@ class StateRender {
|
||||
Object.assign(data.dataset, { role: block.type })
|
||||
}
|
||||
|
||||
if (block.type === 'pre') {
|
||||
if (block.lang) Object.assign(data.dataset, { lang: block.lang })
|
||||
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
|
||||
children = ''
|
||||
}
|
||||
|
||||
if (block.type === 'input') {
|
||||
const { checked, type, key } = block
|
||||
Object.assign(data.attrs, { type: 'checkbox' })
|
||||
@ -200,6 +207,19 @@ class StateRender {
|
||||
blockSelector += `.${CLASS_OR_ID['AG_TEMP']}`
|
||||
}
|
||||
|
||||
if (block.type === 'pre') {
|
||||
const { key, lang } = block
|
||||
const pre = document.querySelector(`pre#${key}`)
|
||||
if (pre) {
|
||||
operateClassName(pre, isActive ? 'add' : 'remove', CLASS_OR_ID['AG_ACTIVE'])
|
||||
return toVNode(pre)
|
||||
}
|
||||
if (lang) Object.assign(data.dataset, { lang })
|
||||
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
|
||||
children = ''
|
||||
emptyPres.push(key)
|
||||
}
|
||||
|
||||
return h(blockSelector, data, children)
|
||||
}
|
||||
}
|
||||
@ -213,6 +233,48 @@ class StateRender {
|
||||
const oldVdom = toVNode(rootDom)
|
||||
|
||||
patch(oldVdom, newVdom)
|
||||
return emptyPres
|
||||
}
|
||||
|
||||
partialRender (block, cursor) {
|
||||
const { key, text } = block
|
||||
const type = block.type === 'hr' ? 'p' : block.type
|
||||
let selector = `${type}#${key}`
|
||||
const oldDom = document.querySelector(selector)
|
||||
const oldVdom = toVNode(oldDom)
|
||||
|
||||
selector += `.${CLASS_OR_ID['AG_PARAGRAPH']}.${CLASS_OR_ID['AG_ACTIVE']}`
|
||||
if (type === 'span') {
|
||||
selector += `.${CLASS_OR_ID['AG_LINE']}`
|
||||
}
|
||||
|
||||
const data = {
|
||||
attrs: {},
|
||||
dataset: {}
|
||||
}
|
||||
|
||||
let children = ''
|
||||
if (text) {
|
||||
children = tokenizer(text, []).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
|
||||
}
|
||||
|
||||
if (/th|td/.test(type)) {
|
||||
const { align } = block
|
||||
if (align) {
|
||||
Object.assign(data.attrs, { style: `text-align:${align}` })
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
const newVdom = h(selector, data, children)
|
||||
patch(oldVdom, newVdom)
|
||||
}
|
||||
|
||||
hr (h, cursor, block, token, outerClass) {
|
||||
|
@ -335,22 +335,27 @@ const importRegister = ContentState => {
|
||||
|
||||
ContentState.prototype.importCursor = function (cursor) {
|
||||
// set cursor
|
||||
if (cursor) {
|
||||
const blocks = this.getArrayBlocks()
|
||||
const travel = blocks => {
|
||||
for (const block of blocks) {
|
||||
const { text, key } = block
|
||||
const { key, text, children } = block
|
||||
if (text) {
|
||||
const offset = block.text.indexOf(CURSOR_DNA)
|
||||
const offset = text.indexOf(CURSOR_DNA)
|
||||
if (offset > -1) {
|
||||
// remove the CURSOR_DNA in the block text
|
||||
block.text = text.substring(0, offset) + text.substring(offset + CURSOR_DNA.length)
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (children.length) {
|
||||
travel(children)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cursor) {
|
||||
travel(this.blocks)
|
||||
} else {
|
||||
const lastBlock = this.getLastBlock()
|
||||
const key = lastBlock.key
|
||||
|
@ -43,7 +43,7 @@
|
||||
|
||||
created () {
|
||||
this.$nextTick(() => {
|
||||
const { markdown = '', theme } = this
|
||||
const { markdown = '', theme, cursor } = this
|
||||
const container = this.$refs.sourceCode
|
||||
const codeMirrorConfig = {
|
||||
value: markdown,
|
||||
@ -60,11 +60,16 @@
|
||||
}
|
||||
}
|
||||
if (theme === 'dark') codeMirrorConfig.theme = 'railscasts'
|
||||
this.editor = codeMirror(container, codeMirrorConfig)
|
||||
const editor = this.editor = codeMirror(container, codeMirrorConfig)
|
||||
bus.$on('file-loaded', this.setMarkdown)
|
||||
bus.$on('dotu-select', this.handleSelectDoutu)
|
||||
|
||||
this.listenChange()
|
||||
if (cursor) {
|
||||
editor.setCursor(cursor)
|
||||
} else {
|
||||
setCursorAtLastLine(editor)
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeDestroy () {
|
||||
|
Loading…
Reference in New Issue
Block a user