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