/**
 * This file is copy from [medium-editor](https://github.com/yabwe/medium-editor)
 * and customize for specialized use.
 */
import {
  isBlockContainer,
  traverseUp,
  isAganippeEditorElement,
  getFirstSelectableLeafNode,
  isElementAtBeginningOfBlock,
  findPreviousSibling,
  getClosestBlockContainer,
  getCursorPositionWithinMarkedText
} from './utils/domManipulate'

const filterOnlyParentElements = node => {
  return isBlockContainer(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP
}

class Selection {
  constructor (doc) {
    this.doc = doc // document
  }

  findMatchingSelectionParent (testElementFunction, contentWindow) {
    const selection = contentWindow.getSelection()
    let range
    let current

    if (selection.rangeCount === 0) {
      return false
    }

    range = selection.getRangeAt(0)
    current = range.commonAncestorContainer

    return traverseUp(current, testElementFunction)
  }

  getSelectionElement (contentWindow) {
    return this.findMatchingSelectionParent(el => {
      return isAganippeEditorElement(el)
    }, contentWindow)
  }

  // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
  // Tim Down
  exportSelection (root) {
    if (!root) {
      return null
    }

    let selectionState = null
    const selection = this.doc.getSelection()

    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0)
      const preSelectionRange = range.cloneRange()

      preSelectionRange.selectNodeContents(root)
      preSelectionRange.setEnd(range.startContainer, range.startOffset)

      const start = preSelectionRange.toString().length
      selectionState = {
        start,
        end: start + range.toString().length
      }

      // Check to see if the selection starts with any images
      // if so we need to make sure the the beginning of the selection is
      // set correctly when importing selection
      if (this.doesRangeStartWithImages(range)) {
        selectionState.startsWithImage = true
      }

      // Check to see if the selection has any trailing images
      // if so, this this means we need to look for them when we import selection
      const trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset)
      if (trailingImageCount) {
        selectionState.trailingImageCount = trailingImageCount
      }

      // If start = 0 there may still be an empty paragraph before it, but we don't care.
      if (start !== 0) {
        const emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(root, range.startContainer, range.startOffset)
        if (emptyBlocksIndex !== -1) {
          selectionState.emptyBlocksIndex = emptyBlocksIndex
        }
      }
    }
    return selectionState
  }

  // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
  // Tim Down
  //
  // {object} selectionState - the selection to import
  // {DOMElement} root - the root element the selection is being restored inside of
  // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
  //      subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
  //      anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
  //      in MS IE.
  importSelection (selectionState, root, favorLaterSelectionAnchor) {
    if (!selectionState || !root) {
      throw new Error('your must provide a [selectionState] and a [root] element')
    }

    let range = this.doc.createRange()
    range.setStart(root, 0)
    range.collapse(true)

    let node = root
    const nodeStack = []
    let charIndex = 0
    let foundStart = false
    let foundEnd = false
    let trailingImageCount = 0
    let stop = false
    let nextCharIndex
    let allowRangeToStartAtEndOfNode = false
    let lastTextNode = null

    // When importing selection, the start of the selection may lie at the end of an element
    // or at the beginning of an element.  Since visually there is no difference between these 2
    // we will try to move the selection to the beginning of an element since this is generally
    // what users will expect and it's a more predictable behavior.
    //
    // However, there are some specific cases when we don't want to do this:
    //  1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]
    //  2) The selection starts with an image, which is special since an image doesn't have any 'content'
    //     as far as selection and ranges are concerned
    //  3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)
    //
    // For these cases, we want the selection to start at a very specific location, so we should NOT
    // automatically move the cursor to the beginning of the first actual chunk of text
    if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {
      allowRangeToStartAtEndOfNode = true
    }

    while (!stop && node) {
      // Only iterate over elements and text nodes
      if (node.nodeType > 3) {
        node = nodeStack.pop()
        continue
      }

      // If we hit a text node, we need to add the amount of characters to the overall count
      if (node.nodeType === 3 && !foundEnd) {
        nextCharIndex = charIndex + node.length
        // Check if we're at or beyond the start of the selection we're importing
        if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
          // NOTE: We only want to allow a selection to start at the END of an element if
          //  allowRangeToStartAtEndOfNode is true
          if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {
            range.setStart(node, selectionState.start - charIndex)
            foundStart = true
          } else {
            // We're at the end of a text node where the selection could start but we shouldn't
            // make the selection start here because allowRangeToStartAtEndOfNode is false.
            // However, we should keep a reference to this node in case there aren't any more
            // text nodes after this, so that we have somewhere to import the selection to
            lastTextNode = node
          }
        }
        // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing
        if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
          if (!selectionState.trailingImageCount) {
            range.setEnd(node, selectionState.end - charIndex)
            stop = true
          } else {
            foundEnd = true
          }
        }
        charIndex = nextCharIndex
      } else {
        if (selectionState.trailingImageCount && foundEnd) {
          if (node.nodeName.toLowerCase() === 'img') {
            trailingImageCount++
          }
          if (trailingImageCount === selectionState.trailingImageCount) {
            // Find which index the image is in its parent's children
            let endIndex = 0
            while (node.parentNode.childNodes[endIndex] !== node) {
              endIndex++
            }
            range.setEnd(node.parentNode, endIndex + 1)
            stop = true
          }
        }

        if (!stop && node.nodeType === 1) {
          // this is an element
          // add all its children to the stack
          let i = node.childNodes.length - 1
          while (i >= 0) {
            nodeStack.push(node.childNodes[i])
            i -= 1
          }
        }
      }

      if (!stop) {
        node = nodeStack.pop()
      }
    }

    // If we've gone through the entire text but didn't find the beginning of a text node
    // to make the selection start at, we should fall back to starting the selection
    // at the END of the last text node we found
    if (!foundStart && lastTextNode) {
      range.setStart(lastTextNode, lastTextNode.length)
      range.setEnd(lastTextNode, lastTextNode.length)
    }

    if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
      range = this.importSelectionMoveCursorPastBlocks(root, selectionState.emptyBlocksIndex, range)
    }

    // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.
    if (favorLaterSelectionAnchor) {
      range = this.importSelectionMoveCursorPastAnchor(selectionState, range)
    }

    this.selectRange(range)
  }

  // Utility method called from importSelection only
  importSelectionMoveCursorPastAnchor (selectionState, range) {
    const nodeInsideAnchorTagFunction = function (node) {
      return node.nodeName.toLowerCase() === 'a'
    }
    if (selectionState.start === selectionState.end &&
      range.startContainer.nodeType === 3 &&
      range.startOffset === range.startContainer.nodeValue.length &&
      traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
      let prevNode = range.startContainer
      let currentNode = range.startContainer.parentNode
      while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
        if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
          currentNode = null
        } else {
          prevNode = currentNode
          currentNode = currentNode.parentNode
        }
      }
      if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
        let currentNodeIndex = null
        for (let i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
          if (currentNode.parentNode.childNodes[i] === currentNode) {
            currentNodeIndex = i
          }
        }
        range.setStart(currentNode.parentNode, currentNodeIndex + 1)
        range.collapse(true)
      }
    }
    return range
  }

  // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks
  // to move the cursor back to the start of the correct paragraph
  importSelectionMoveCursorPastBlocks (root, index = 1, range) {
    const treeWalker = this.doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false)
    let startContainer = range.startContainer
    let startBlock
    let targetNode
    let currIndex = 0
    // If index is 0, we still want to move to the next block

    // Chrome counts newlines and spaces that separate block elements as actual elements.
    // If the selection is inside one of these text nodes, and it has a previous sibling
    // which is a block element, we want the treewalker to start at the previous sibling
    // and NOT at the parent of the textnode
    if (startContainer.nodeType === 3 && isBlockContainer(startContainer.previousSibling)) {
      startBlock = startContainer.previousSibling
    } else {
      startBlock = getClosestBlockContainer(startContainer)
    }

    // Skip over empty blocks until we hit the block we want the selection to be in
    while (treeWalker.nextNode()) {
      if (!targetNode) {
        // Loop through all blocks until we hit the starting block element
        if (startBlock === treeWalker.currentNode) {
          targetNode = treeWalker.currentNode
        }
      } else {
        targetNode = treeWalker.currentNode
        currIndex++
        // We hit the target index, bail
        if (currIndex === index) {
          break
        }
        // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
        if (targetNode.textContent.length > 0) {
          break
        }
      }
    }

    if (!targetNode) {
      targetNode = startBlock
    }

    // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
    // element at the beginning of the block
    range.setStart(getFirstSelectableLeafNode(targetNode), 0)

    return range
  }

  // Returns -1 unless the cursor is at the beginning of a paragraph/block
  // If the paragraph/block is preceeded by empty paragraphs/block (with no text)
  // it will return the number of empty paragraphs before the cursor.
  // Otherwise, it will return 0, which indicates the cursor is at the beginning
  // of a paragraph/block, and not at the end of the paragraph/block before it
  getIndexRelativeToAdjacentEmptyBlocks (root, cursorContainer, cursorOffset) {
    // If there is text in front of the cursor, that means there isn't only empty blocks before it
    if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {
      return -1
    }

    // Check if the block that contains the cursor has any other text in front of the cursor
    let node = cursorContainer
    if (node.nodeType !== 3) {
      node = cursorContainer.childNodes[cursorOffset]
    }
    if (node) {
      // The element isn't at the beginning of a block, so it has content before it
      if (!isElementAtBeginningOfBlock(node)) {
        return -1
      }

      const previousSibling = findPreviousSibling(node)
      // If there is no previous sibling, this is the first text element in the editor
      if (!previousSibling) {
        return -1
      } else if (previousSibling.nodeValue) {
        // If the previous sibling has text, then there are no empty blocks before this
        return -1
      }
    }

    // Walk over block elements, counting number of empty blocks between last piece of text
    // and the block the cursor is in
    const closestBlock = getClosestBlockContainer(cursorContainer)
    const treeWalker = this.doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false)
    let emptyBlocksCount = 0
    while (treeWalker.nextNode()) {
      const blockIsEmpty = treeWalker.currentNode.textContent === ''
      if (blockIsEmpty || emptyBlocksCount > 0) {
        emptyBlocksCount += 1
      }
      if (treeWalker.currentNode === closestBlock) {
        return emptyBlocksCount
      }
      if (!blockIsEmpty) {
        emptyBlocksCount = 0
      }
    }

    return emptyBlocksCount
  }

  // Returns true if the selection range begins with an image tag
  // Returns false if the range starts with any non empty text nodes
  doesRangeStartWithImages (range) {
    if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {
      return false
    }

    if (range.startContainer.nodeName.toLowerCase() === 'img') {
      return true
    }

    const img = range.startContainer.querySelector('img')
    if (!img) {
      return false
    }

    const treeWalker = this.doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false)
    while (treeWalker.nextNode()) {
      const next = treeWalker.currentNode
      // If we hit the image, then there isn't any text before the image so
      // the image is at the beginning of the range
      if (next === img) {
        break
      }
      // If we haven't hit the iamge, but found text that contains content
      // then the range doesn't start with an image
      if (next.nodeValue) {
        return false
      }
    }

    return true
  }

  getTrailingImageCount (root, selectionState, endContainer, endOffset) {
    // If the endOffset of a range is 0, the endContainer doesn't contain images
    // If the endContainer is a text node, there are no trailing images
    if (endOffset === 0 || endContainer.nodeType !== 1) {
      return 0
    }

    // If the endContainer isn't an image, and doesn't have an image descendants
    // there are no trailing images
    if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {
      return 0
    }

    let lastNode = endContainer.childNodes[endOffset - 1]
    while (lastNode.hasChildNodes()) {
      lastNode = lastNode.lastChild
    }

    let node = root
    const nodeStack = []
    let charIndex = 0
    let foundStart = false
    let foundEnd = false
    let stop = false
    let nextCharIndex
    let trailingImages = 0

    while (!stop && node) {
      // Only iterate over elements and text nodes
      if (node.nodeType > 3) {
        node = nodeStack.pop()
        continue
      }

      if (node.nodeType === 3 && !foundEnd) {
        trailingImages = 0
        nextCharIndex = charIndex + node.length
        if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
          foundStart = true
        }
        if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
          foundEnd = true
        }
        charIndex = nextCharIndex
      } else {
        if (node.nodeName.toLowerCase() === 'img') {
          trailingImages++
        }

        if (node === lastNode) {
          stop = true
        } else if (node.nodeType === 1) {
          // this is an element
          // add all its children to the stack
          let i = node.childNodes.length - 1
          while (i >= 0) {
            nodeStack.push(node.childNodes[i])
            i -= 1
          }
        }
      }

      if (!stop) {
        node = nodeStack.pop()
      }
    }

    return trailingImages
  }

  // determine if the current selection contains any 'content'
  // content being any non-white space text or an image
  selectionContainsContent () {
    const sel = this.doc.getSelection()

    // collapsed selection or selection withour range doesn't contain content
    if (!sel || sel.isCollapsed || !sel.rangeCount) {
      return false
    }

    // if toString() contains any text, the selection contains some content
    if (sel.toString().trim() !== '') {
      return true
    }

    // if selection contains only image(s), it will return empty for toString()
    // so check for an image manually
    const selectionNode = this.getSelectedParentElement(sel.getRangeAt(0))
    if (selectionNode) {
      if (selectionNode.nodeName.toLowerCase() === 'img' ||
        (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {
        return true
      }
    }

    return false
  }

  selectionInContentEditableFalse (contentWindow) {
    // determine if the current selection is exclusively inside
    // a contenteditable="false", though treat the case of an
    // explicit contenteditable="true" inside a "false" as false.
    let sawtrue
    const sawfalse = this.findMatchingSelectionParent(function (el) {
      const ce = el && el.getAttribute('contenteditable')
      if (ce === 'true') {
        sawtrue = true
      }
      return el.nodeName !== '#text' && ce === 'false'
    }, contentWindow)

    return !sawtrue && sawfalse
  }

  // http://stackoverflow.com/questions/4176923/html-of-selected-text
  // by Tim Down
  getSelectionHtml () {
    const sel = this.doc.getSelection()
    let i
    let html = ''
    let len
    let container
    if (sel.rangeCount) {
      container = this.doc.createElement('div')
      for (i = 0, len = sel.rangeCount; i < len; i += 1) {
        container.appendChild(sel.getRangeAt(i).cloneContents())
      }
      html = container.innerHTML
    }
    return html
  }

  chopHtmlByCursor (root) {
    const { left } = this.getCaretOffsets(root)
    const markedText = root.textContent
    const { type, info } = getCursorPositionWithinMarkedText(markedText, left)
    let pre = markedText.slice(0, left)
    let post = markedText.slice(left)
    switch (type) {
      case 'OUT':
        return {
          pre,
          post
        }
      case 'IN':
        return {
          pre: `${pre}${info}`,
          post: `${info}${post}`
        }
      case 'LEFT':
        return {
          pre: markedText.slice(0, left - info),
          post: markedText.slice(left - info)
        }
      case 'RIGHT':
        return {
          pre: markedText.slice(0, left + info),
          post: markedText.slice(left + info)
        }
    }
  }

  /**
   *  Find the caret position within an element irrespective of any inline tags it may contain.
   *
   *  @param {DOMElement} An element containing the cursor to find offsets relative to.
   *  @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
   *  @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
   */
  getCaretOffsets (element, range) {
    let preCaretRange
    let postCaretRange

    if (!range) {
      range = window.getSelection().getRangeAt(0)
    }

    preCaretRange = range.cloneRange()
    postCaretRange = range.cloneRange()

    preCaretRange.selectNodeContents(element)
    preCaretRange.setEnd(range.endContainer, range.endOffset)

    postCaretRange.selectNodeContents(element)
    postCaretRange.setStart(range.endContainer, range.endOffset)

    return {
      left: preCaretRange.toString().length,
      right: postCaretRange.toString().length
    }
  }

  // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
  rangeSelectsSingleNode (range) {
    const startNode = range.startContainer
    return startNode === range.endContainer &&
    startNode.hasChildNodes() &&
    range.endOffset === range.startOffset + 1
  }

  getSelectedParentElement (range) {
    if (!range) {
      return null
    }

    // Selection encompasses a single element
    if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
      return range.startContainer.childNodes[range.startOffset]
    }

    // Selection range starts inside a text node, so get its parent
    if (range.startContainer.nodeType === 3) {
      return range.startContainer.parentNode
    }

    // Selection starts inside an element
    return range.startContainer
  }

  getSelectedElements () {
    const selection = this.doc.getSelection()
    let range
    let toRet
    let currNode

    if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {
      return []
    }

    range = selection.getRangeAt(0)

    if (range.commonAncestorContainer.nodeType === 3) {
      toRet = []
      currNode = range.commonAncestorContainer
      while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
        toRet.push(currNode.parentNode)
        currNode = currNode.parentNode
      }

      return toRet
    }

    return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
      return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true
    })
  }

  selectNode (node) {
    const range = this.doc.createRange()
    range.selectNodeContents(node)
    this.selectRange(range)
  }

  select (startNode, startOffset, endNode, endOffset) {
    const range = this.doc.createRange()
    range.setStart(startNode, startOffset)
    if (endNode) {
      range.setEnd(endNode, endOffset)
    } else {
      range.collapse(true)
    }
    this.selectRange(range)
    return range
  }

  /**
   *  Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.
   *
   *  @param {boolean} moveCursorToStart  A boolean representing whether or not to set the caret to the beginning of the prior selection.
   */
  clearSelection (moveCursorToStart) {
    if (moveCursorToStart) {
      this.doc.getSelection().collapseToStart()
    } else {
      this.doc.getSelection().collapseToEnd()
    }
  }

  /**
   * Move cursor to the given node with the given offset.
   *
   * @param  {DomElement}  node    Element where to jump
   * @param  {integer}     offset  Where in the element should we jump, 0 by default
   */
  moveCursor (node, offset) {
    this.select(node, offset)
  }

  getSelectionRange () {
    const selection = this.doc.getSelection()
    if (selection.rangeCount === 0) {
      return null
    }
    return selection.getRangeAt(0)
  }

  selectRange (range) {
    const selection = this.doc.getSelection()

    selection.removeAllRanges()
    selection.addRange(range)
  }

  // http://stackoverflow.com/questions/1197401/
  // how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contenteditable
  // by You
  getSelectionStart () {
    const node = this.doc.getSelection().anchorNode
    const startNode = (node && node.nodeType === 3 ? node.parentNode : node)

    return startNode
  }
}

export default new Selection(document)


* [codeBlockUpdate if block updated to `pre` return true, else return false]

ContentState.prototype.codeBlockUpdate = function (block) {

const match = CODE_UPDATE_REP.exec(block.text)

ContentState.prototype.selectLanguage = function (paragraph, name) {

const block = this.getBlock(paragraph.id)

block.text = block.text.replace(/^(`+)([^`]+$)/g, `$1${name}`)


import { deepCopy } from '../utils'

export class History {
  constructor (contentState) {
    this.stack = []
    this.index = -1
    this.contentState = contentState
  }
  undo () {
    if (this.index > 0) {
      this.index = this.index - 1
      console.log(this.index)
      const state = deepCopy(this.stack[this.index])
      switch (state.type) {
        case 'normal':
          this.contentState.blocks = state.blocks
          this.contentState.cursor = state.cursor
          this.contentState.render()
          break
        case 'codeBlock':
          const id = state.id
          const codeBlock = this.contentState.codeBlocks.get(id)
          codeBlock.focus()
          codeBlock.undo()
          break
      }
    }
  }
  redo () {
    const { index, stack } = this
    const len = stack.length
    if (index < len - 1) {
      this.index = index + 1
      const state = deepCopy(stack[this.index])
      switch (state.type) {
        case 'normal':
          this.contentState.blocks = state.blocks
          this.contentState.cursor = state.cursor
          this.contentState.render()
          break
        case 'codeBlock':
          const id = state.id
          const codeBlock = this.contentState.codeBlocks.get(id)
          codeBlock.focus()
          codeBlock.redo()
          break
      }
    }
  }
  push (state) {
    this.stack.splice(this.index + 1)
    this.stack.push(deepCopy(state))
    this.index = this.index + 1
    console.log(this.index)
    console.log(this.stack)
  }
}

export default History


const mode = pre.getAttribute('data-lang')

const input = createInputInCodeBlock(pre)

import ContentState from './contentState'
import selection from './selection'
import eventCenter from './event'
import { LOWERCASE_TAGS, EVENT_KEYS, CLASS_OR_ID } from './config'
import { throttle, debounce } from './utils'
import { search } from './codeMirror'
import { checkEditLanguage, replaceLanguage } from './codeMirror/language'
import Emoji, { checkEditEmoji, setInlineEmoji } from './emojis'
import floatBox from './floatBox'
import { findNearestParagraph, operateClassName } from './utils/domManipulate'
import ExportMarkdown from './utils/exportMarkdown'

class Aganippe {
  constructor (container, options) {
    this.container = container
    this.eventCenter = eventCenter
    this.floatBox = floatBox
    this.contentState = new ContentState()
    this.emoji = new Emoji() // emoji instance: has search(text) clear() methods.

    this.init()
  }

  init () {
    this.ensureContainerDiv()
    const { container, contentState, eventCenter } = this
    contentState.stateRender.setContainer(container.children[0])
    contentState.render()

    eventCenter.subscribe('editEmoji', throttle(this.subscribeEditEmoji.bind(this), 200))
    this.dispatchEditeEmoji()
    eventCenter.subscribe('editLanguage', throttle(this.subscribeEditLanguage.bind(this)))
    this.dispatchEditLanguage()

    eventCenter.subscribe('hideFloatBox', this.subscribeHideFloatBox.bind(this))
    this.dispatchHideFloatBox()

    // if you dont click the keyboard after 1 second, the garbageCollection will run.
    eventCenter.attachDOMEvent(container, 'keydown', debounce(event => {
      this.contentState.historyHandler(event)
      this.contentState.garbageCollection()
      const markdown = this.getMarkdown()
      eventCenter.dispatch('auto-save', markdown)
    }, 1024))

    this.imageClick()
    this.dispatchArrow()
    this.dispatchBackspace()
    this.dispatchEnter()
    this.dispatchUpdateState()
  }

  /**
   * [ensureContainerDiv ensure container element is div]
   */
  ensureContainerDiv () {
    const { container } = this
    const div = document.createElement(LOWERCASE_TAGS.div)
    const rootdom = document.createElement(LOWERCASE_TAGS.div)
    const attrs = container.attributes
    const parentNode = container.parentNode
    // copy attrs from origin container to new div element
    Array.from(attrs).forEach(attr => {
      div.setAttribute(attr.name, attr.value)
    })
    div.setAttribute('contenteditable', true)
    div.classList.add('mousetrap')
    div.appendChild(rootdom)
    parentNode.insertBefore(div, container)
    parentNode.removeChild(container)
    this.container = div
  }

  /**
   * dispatchEditeEmoji
  */
  dispatchEditeEmoji () {
    const { container, eventCenter } = this
    const changeHandler = event => {
      const node = selection.getSelectionStart()
      const emojiNode = checkEditEmoji(node)
      if (emojiNode && event.key !== EVENT_KEYS.Enter) {
        eventCenter.dispatch('editEmoji', emojiNode)
      }
    }
    eventCenter.attachDOMEvent(container, 'keyup', changeHandler) // don't listen `input` event
  }
  subscribeEditEmoji (emojiNode) {
    const text = emojiNode.textContent.trim()
    if (text) {
      const list = this.emoji.search(text).map(l => {
        return Object.assign(l, { text: l.aliases[0] })
      })
      const { left, top } = emojiNode.getBoundingClientRect()
      const cb = item => {
        setInlineEmoji(emojiNode, item, selection)
        this.floatBox.hideIfNeeded()
      }
      if (list.length) {
        this.floatBox.showIfNeeded({
          left: `${left}px`, top: `${top + 25 + document.body.scrollTop}px`
        }, cb)
        this.floatBox.setOptions(list)
      } else {
        this.floatBox.hideIfNeeded()
      }
    }
  }

  dispatchHideFloatBox () {
    const { container, eventCenter } = this

    const handler = event => {
      if (event.target && event.target.classList.contains(CLASS_OR_ID['AG_LANGUAGE_INPUT'])) {
        return
      }
      if (event.type === 'click') return eventCenter.dispatch('hideFloatBox')
      const node = selection.getSelectionStart()
      const paragraph = findNearestParagraph(node)
      const selectionState = selection.exportSelection(paragraph)
      const lang = checkEditLanguage(paragraph, selectionState)
      const emojiNode = node && checkEditEmoji(node)
      if (!emojiNode && !lang) {
        eventCenter.dispatch('hideFloatBox')
      }
    }

    eventCenter.attachDOMEvent(container, 'click', handler)
    eventCenter.attachDOMEvent(container, 'keyup', handler)
  }

  subscribeHideFloatBox () {
    this.floatBox.hideIfNeeded()
  }

  /**
   * dispatchIsEditLanguage
   */
  dispatchEditLanguage () {
    const { container, eventCenter } = this
    const inputHandler = event => {
      const node = selection.getSelectionStart()
      const paragraph = findNearestParagraph(node)
      const selectionState = selection.exportSelection(paragraph)
      const lang = checkEditLanguage(paragraph, selectionState)
      if (lang) {
        eventCenter.dispatch('editLanguage', paragraph, lang)
      }
    }

    eventCenter.attachDOMEvent(container, 'input', inputHandler)
  }

  subscribeEditLanguage (paragraph, lang, cb) {
    const { left, top } = paragraph.getBoundingClientRect()
    const modes = search(lang).map(mode => {
      return Object.assign(mode, { text: mode.name })
    })

    const callback = item => {
      replaceLanguage(paragraph, item.name, selection)
      this.floatBox.hideIfNeeded()
    }
    if (modes.length) {
      this.floatBox.showIfNeeded({
        left: `${left}px`,
        top: `${top + 25 + document.body.scrollTop}px`
      }, cb || callback)
      this.floatBox.setOptions(modes)
    } else {
      this.floatBox.hideIfNeeded()
    }
  }

  dispatchBackspace () {
    const { container, eventCenter } = this

    const handler = event => {
      if (event.key === EVENT_KEYS.Backspace) {
        this.contentState.backspaceHandler(event)
      }
    }

    eventCenter.attachDOMEvent(container, 'keydown', handler)
  }

  dispatchEnter (event) {
    const { container, eventCenter } = this
    const handler = event => {
      if (event.key === EVENT_KEYS.Enter) {
        this.contentState.enterHandler(event)
      }
    }

    eventCenter.attachDOMEvent(container, 'keydown', handler)
  }

  // dispach arrow event
  dispatchArrow () {
    const { container, eventCenter } = this
    const handler = event => {
      switch (event.key) {
        case EVENT_KEYS.ArrowUp: // fallthrough
        case EVENT_KEYS.ArrowDown: // fallthrough
        case EVENT_KEYS.ArrowLeft: // fallthrough
        case EVENT_KEYS.ArrowRight: // fallthrough
          this.contentState.arrowHandler(event)
          break
      }
    }
    eventCenter.attachDOMEvent(container, 'keydown', handler)
  }

  dispatchUpdateState () {
    const { container, eventCenter } = this
    let isEditChinese = false
    const changeHandler = event => {
      if (event.type === 'compositionstart') {
        isEditChinese = true
        return false
      }
      if (event.type === 'compositionend') {
        isEditChinese = false
      }

      if (!isEditChinese) {
        this.contentState.updateState(event)
      }
    }

    eventCenter.attachDOMEvent(container, 'click', changeHandler)
    eventCenter.attachDOMEvent(container, 'compositionend', changeHandler)
    eventCenter.attachDOMEvent(container, 'compositionstart', changeHandler)
    eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
  }

  imageClick () {
    const { container, eventCenter } = this
    const handler = event => {
      const target = event.target
      const markedImageText = target.previousElementSibling
      if (markedImageText && markedImageText.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) {
        const textLen = markedImageText.textContent.length
        operateClassName(markedImageText, 'remove', CLASS_OR_ID['AG_HIDE'])
        operateClassName(markedImageText, 'add', CLASS_OR_ID['AG_GRAY'])
        selection.importSelection({
          start: textLen,
          end: textLen
        }, markedImageText)
      }
    }

    eventCenter.attachDOMEvent(container, 'click', handler)
  }

  getMarkdown () {
    const blocks = this.contentState.getBlocks()
    const markdown = new ExportMarkdown(blocks).generate()
    return markdown
  }

  setMarkdown (text) {
    this.contentState.importMarkdown(text)
  }

  on (event, listener) {
    const { eventCenter } = this
    eventCenter.subscribe(event, listener)
  }

  undo () {
    console.log('undo')
    this.contentState.history.undo()
  }

  redo () {
    this.contentState.history.redo()
  }

  destroy () {
    this.eventCenter.detachAllDomEvents()
    this.emoji.clear() // clear emoji cache for memory recycle
    this.contentState.clear()
    this.container = null
    this.contentState = null
    this.eventCenter = null
    this.emoji = null
    this.floatBox = null
  }
}

export default Aganippe


'use strict'

import fs from 'fs'
import path from 'path'
import { BrowserWindow } from 'electron'
import windowStateKeeper from 'electron-window-state'

export const windows = new Map()

const createWindow = (pathname, options = {}) => {
  const mainWindowState = windowStateKeeper({
    defaultWidth: 1200,
    defaultHeight: 800
  })

  const { x, y, width, height } = mainWindowState
  const winOpt = Object.assign({ x, y, width, height }, {
    useContentSize: true,
    show: false,
    frame: false,
    titleBarStyle: 'hidden'
  }, options)
  let win = new BrowserWindow(winOpt)

  const winURL = process.env.NODE_ENV === 'development'
    ? `http://localhost:9080`
    : `file://${__dirname}/index.html`

  win.loadURL(winURL)

  win.once('ready-to-show', () => {
    win.show()

    if (pathname) {
      const filename = path.basename(pathname)
      fs.readFile(path.resolve(pathname), 'utf-8', (err, file) => {
        if (err) return console.log(err)
        win.webContents.send('AGANI::file-loaded', {
          file,
          filename,
          pathname
        })
      })
    }
  })

  win.on('focus', () => {
    win.webContents.send('AGANI::window-active-status', { status: true })
  })

  win.on('blur', () => {
    win.webContents.send('AGANI::window-active-status', { status: false })
  })

  win.on('close', () => { // before closed
    if (windows.has(win.id)) {
      windows.delete(win.id)
    }
  })

  windows.set(win.id, win)
  return win
}

export default createWindow


const garbageCtrl = ContentState => {
  ContentState.prototype.garbageCollection = function () {
    for (let key of this.keys) {
      if (!document.querySelector(`#${key}`)) {
        this.keys.delete(key)
      }
    }
  }
}

export default garbageCtrl


import { CLASS_OR_ID } from '../config'
/**
 * check edit language
 */
export const checkEditLanguage = (paragraph, selectionState) => {
  const text = paragraph.textContent
  const { start } = selectionState
  const token = text.match(/(^`{3,})([^`]+)/)
  if (token) {
    const len = token[1].length
    const lang = token[2].trim()
    if (start < len) return false
    if (!lang) return false
    return lang
  } else {
    return false
  }
}

export const replaceLanguage = (paragraph, mode, selection) => {
  if (paragraph.tagName.toLowerCase() === 'input') {
    paragraph.value = mode
    return paragraph.focus()
  }
  paragraph.querySelector(`.${CLASS_OR_ID['AG_LANGUAGE']}`).textContent = mode
  const offset = paragraph.textContent.length
  selection.importSelection({
    start: offset,
    end: offset
  }, paragraph)
}


import floatBox from '../floatBox'
import { EVENT_KEYS } from '../config'
import {
  isCursorAtFirstLine, isCursorAtLastLine, isCursorAtBegin, search, // eslint-disable-line no-unused-vars
  isCursorAtEnd, setCursorAtFirstLine, onlyHaveOneLine, setCursorAtLastLine // eslint-disable-line no-unused-vars
} from '../codeMirror'
import { findNearestParagraph } from '../utils/domManipulate'
import selection from '../selection'

const arrowCtrl = ContentState => {
  ContentState.prototype.arrowHandler = function (event) {
    // when the float box is show, use up and down to select item.
    const { list, index, show } = floatBox
    const node = selection.getSelectionStart()
    const paragraph = findNearestParagraph(node)
    const id = paragraph.id
    const block = this.getBlock(id)
    const preBlock = this.getPreSibling(block)
    const nextBlock = this.getNextSibling(block)
    const { left, right } = selection.getCaretOffsets(paragraph)

    if (show && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown)) {
      event.preventDefault()
      switch (event.key) {
        case EVENT_KEYS.ArrowDown:
          if (index < list.length - 1) {
            floatBox.setOptions(list, index + 1)
          }
          break
        case EVENT_KEYS.ArrowUp:
          if (index > 0) {
            floatBox.setOptions(list, index - 1)
          }
          break
      }
    } else if (block.type === 'pre') {
      // handle cursor in code block. the case at firstline or lastline.
      const cm = this.codeBlocks.get(id)

      let activeBlock
      event.preventDefault()
      switch (event.key) {
        case EVENT_KEYS.ArrowLeft: // fallthrough
        case EVENT_KEYS.ArrowUp:
          if (
            (event.key === EVENT_KEYS.ArrowUp && isCursorAtFirstLine(cm) && preBlock) ||
            (event.key === EVENT_KEYS.ArrowLeft && isCursorAtBegin(cm) && preBlock)
          ) {
            activeBlock = preBlock
            if (preBlock.type === 'pre') {
              activeBlock = this.createBlock('p')
              activeBlock.temp = true
              this.insertAfter(activeBlock, preBlock)
            }
          }
          break
        case EVENT_KEYS.ArrowRight: // fallthrough
        case EVENT_KEYS.ArrowDown:
          if (
            (event.key === EVENT_KEYS.ArrowDown && isCursorAtLastLine(cm) && nextBlock) ||
            (event.key === EVENT_KEYS.ArrowRight && isCursorAtEnd(cm) && nextBlock)
          ) {
            activeBlock = nextBlock
            if (nextBlock.type === 'pre') {
              activeBlock = this.createBlock('p')
              activeBlock.temp = true
              this.insertAfter(activeBlock, block)
            }
          } else if (!nextBlock) {
            activeBlock = this.createBlock('p')
            this.insertAfter(activeBlock, block)
          }
          break
      }
      if (activeBlock) {
        const len = activeBlock.text.length
        this.cursor.key = activeBlock.key
        this.cursor.range = { start: len, end: len }
        this.render()
      }
    } else if (
      (preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowUp) ||
      (preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowLeft && left === 0)
    ) {
      event.preventDefault()
      const codeBlockId = preBlock.key
      const cm = this.codeBlocks.get(codeBlockId)
      return setCursorAtLastLine(cm)
    } else if (
      (nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowDown) ||
      (nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowRight && right === 0)
    ) {
      event.preventDefault()
      const codeBlockId = nextBlock.key
      const cm = this.codeBlocks.get(codeBlockId)
      return setCursorAtFirstLine(cm)
    }
  }
}

export default arrowCtrl


const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')

hotClient.subscribe(event => {
  /**
   * Reload browser when HTMLWebpackPlugin emits a new index.html
   *
   * Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
   * https://github.com/SimulatedGREG/electron-vue/issues/437
   * https://github.com/jantimon/html-webpack-plugin/issues/680
   */
  // if (event.action === 'reload') {
  //   window.location.reload()
  // }

  /**
   * Notify `mainWindow` when `main` process is compiling,
   * giving notice for an expected reload of the `electron` process
   */
  if (event.action === 'compiling') {
    document.body.innerHTML += `
      <style>
        #dev-client {
          background: #4fc08d;
          border-radius: 4px;
          bottom: 20px;
          box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
          color: #fff;
          font-family: 'Source Sans Pro', sans-serif;
          left: 20px;
          padding: 8px 12px;
          position: absolute;
        }
      </style>

      <div id="dev-client">
        Compiling Main Process...
      </div>
    `
  }
})


import 'codemirror/addon/edit/closebrackets'
import 'codemirror/addon/edit/closetag'
import 'codemirror/mode/meta'
import codeMirror from 'codemirror/lib/codemirror'

import loadmode from './loadmode'
import languages from './modes'
import 'codemirror/lib/codemirror.css'
import './index.css'

loadmode(codeMirror)
window.CodeMirror = codeMirror

const modes = codeMirror.modeInfo
codeMirror.modeURL = process.env.NODE_ENV !== 'production'
  ? './node_modules/codemirror/mode/%N/%N.js'
  : './codemirror/mode/%N/%N.js'

const getModeFromName = name => {
  let result = null
  const lang = languages.filter(lang => lang.name === name)[0]
  if (lang) {
    const { name, mode, mime } = lang
    const matched = modes.filter(m => {
      if (m.mime) {
        if (Array.isArray(m.mime)) {
          return m.mime.indexOf(mime) > -1 && m.mode === mode
        } else {
          return m.mime === mime && m.mode === mode
        }
      } else if (m.mimes.length > 0 && Array.isArray(m.mimes)) {
        return m.mimes.indexOf(mime) > -1 && m.mode === mode
      } else {
        return false
      }
    })
    if (matched.length && typeof matched[0] === 'object') {
      result = {
        name,
        mode: matched[0]
      }
    }
  }
  return result
}

export const search = text => {
  const matchedLangs = languages.filter(lang => {
    return new RegExp(text, 'i').test(lang.name)
  })

  return matchedLangs.map(lang => {
    return getModeFromName(lang.name)
  })
    .filter(lang => !!lang)
}

/**
 * set cursor at the end of last line.
 */
export const setCursorAtLastLine = cm => {
  const lastLine = cm.lastLine()
  const lineHandle = cm.getLineHandle(lastLine)

  cm.focus()
  cm.setCursor(lastLine, lineHandle.text.length)
}

// if cursor at firstline return true
export const isCursorAtFirstLine = cm => {
  const cursor = cm.getCursor()
  const { line, ch, outside } = cursor
  return line === 0 && ch === 0 && outside
}

export const isCursorAtLastLine = cm => {
  const lastLine = cm.lastLine()
  const cursor = cm.getCursor()
  const { line, outside, sticky } = cursor
  return line === lastLine && (outside || !sticky)
}

export const isCursorAtBegin = cm => {
  const cursor = cm.getCursor()
  const { line, ch, hitSide } = cursor
  return line === 0 && ch === 0 && hitSide
}

export const onlyHaveOneLine = cm => {
  return cm.lineCount() === 1
}

export const isCursorAtEnd = cm => {
  const lastLine = cm.lastLine()
  const lastLineHandle = cm.getLineHandle(lastLine)
  const cursor = cm.getCursor()
  const { line, ch, hitSide } = cursor
  return line === lastLine && ch === lastLineHandle.text.length && hitSide
}

export const setCursorAtFirstLine = cm => {
  cm.focus()
  cm.setCursor(0, 0)
}

export const setMode = (doc, text) => {
  const m = getModeFromName(text)

  if (!m) {
    const errMsg = !text
      ? 'You\'d better provided a language mode when you create code block'
      : `${text} is not a valid language mode!`
    return Promise.reject(errMsg) // eslint-disable-line prefer-promise-reject-errors
  }

  const { mode, mime } = m.mode
  console.log(mode)
  return new Promise(resolve => {
    codeMirror.requireMode(mode, () => {
      doc.setOption('mode', mime || mode)
      codeMirror.autoLoadMode(doc, mode)
      resolve(m)
    })
  })
}

export default codeMirror


keep keep keep keep keep replace keep keep keep

import codeMirror, {
  setMode, setCursorAtLastLine
} from '../codeMirror'

import { createInputInCodeBlock } from '../utils/domManipulate'
import { codeMirrorConfig, CLASS_OR_ID } from '../config'
import floatBox from '../floatBox'
import eventCenter from '../event'

const CODE_UPDATE_REP = /^`{3,}(.*)/

const codeBlockCtrl = ContentState => {
  /**
   * [codeBlockUpdate if block updated to `pre` return true, else return false]
   */
  ContentState.prototype.codeBlockUpdate = function (block) {
    const match = CODE_UPDATE_REP.exec(block.text)
    if (match) {
      block.type = 'pre'
      block.text = ''
      block.lang = match[1]
    }
    return !!match
  }

  ContentState.prototype.pre2CodeMirror = function () {
    const pres = document.querySelectorAll(`pre.${CLASS_OR_ID['AG_CODE_BLOCK']}`)
    Array.from(pres).forEach(pre => {
      const id = pre.id
      const block = this.getBlock(id)

      if (this.codeBlocks.has(id)) {
        const cm = this.codeBlocks.get(id)
        if (block.pos && this.cursor.key === block.key) {
          cm.focus()
          cm.setCursor(block.pos)
        }
        return
      }

      pre.innerHTML = ''
      const autofocus = id === this.cursor.key
      const config = Object.assign(codeMirrorConfig, { autofocus })
      const codeBlock = codeMirror(pre, config)
      const mode = pre.getAttribute('data-lang')
      const input = createInputInCodeBlock(pre)

      const handler = langMode => {
        const {
          mode
        } = langMode
        setMode(codeBlock, mode)
          .then(m => {
            pre.setAttribute('data-lang', m.name)
            input.value = m.name
            block.lang = m.name.toLowerCase()
            input.blur()
            if (this.cursor.key === block.key) {
              if (block.pos) {
                codeBlock.focus()
                codeBlock.setCursor(block.pos)
              } else {
                setCursorAtLastLine(codeBlock)
              }
            }
          })
          .catch(err => {
            console.warn(err)
          })
        floatBox.hideIfNeeded()
      }

      if (block.text) {
        codeBlock.setValue(block.text)
      }

      this.codeBlocks.set(id, codeBlock)

      if (mode) {
        handler({
          mode
        })
      }

      eventCenter.attachDOMEvent(input, 'keyup', () => {
        const value = input.value
        eventCenter.dispatch('editLanguage', input, value.trim(), handler)
      })

      codeBlock.on('focus', (cm, event) => {
        block.pos = cm.getCursor()
      })

      codeBlock.on('blur', (cm, event) => {
        block.pos = cm.getCursor()
      })

      let lastUndoLength = 0
      codeBlock.on('change', (cm, change) => {
        const { undo } = cm.historySize()
        if (undo > lastUndoLength) {
          this.history.push({
            type: 'codeBlock',
            id
          })
          lastUndoLength = undo
        }
      })
    })
  }
}

export default codeBlockCtrl


@import "night/mermaid.dark.css";

:root {
    --bg-color:  #363B40;
    --side-bar-bg-color: #2E3033;
    --text-color: #b8bfc6;
    --code-block-bg-color: inherit;/*#f8f8f8;*/

    --item-hover-bg-color: #0a0d16;
    --control-text-color: #b7b7b7;
    --control-text-hover-color: #eee;
    --window-border: 1px dashed #9b9b9b;

    --active-file-bg-color: rgb(34, 34, 34);
    --active-file-border-color: #8d8df0;

    --primary-color: #a3d5fe;

    --active-file-text-color: white;
    --item-hover-bg-color: #70717d;
    --item-hover-text-color: white;
    --primary-color: #6dc1e7;
}

html {
    font-size: 16px;
}

html,
body {
    -webkit-text-size-adjust: 100%;
    -ms-text-size-adjust: 100%;
    background: #363B40;
    background: var(--bg-color);
    fill: currentColor;
}

#ag-editor-id {
    max-width: 914px;
    padding-top: 20px;
}

html,
body,
button,
input,
select,
textarea,
div.code-tooltip-content {
    color: #b8bfc6;
}

div.code-tooltip,
.md-hover-tip .md-arrow:after {
    background: #4B535A;
}

.popover.bottom > .arrow:after {
    border-bottom-color: #4B535A;
}

html,
body,
button,
input,
select,
textarea {
    font-style: normal;
    line-height: 1.625rem;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}

hr {
    height: 2px;
    border: 0;
    margin: 50px 0 !important;
}

h1,
h2,
h3,
h4,
h5,
h6 {
    font-family: "Lucida Grande", "Corbal", Georgia, serif;
    font-weight: normal;
    clear: both;
    -ms-word-wrap: break-word;
    word-wrap: break-word;
    margin: 0;
    padding: 0;
    color: #DEDEDE
}

h1 {
    font-size: 2.5rem;
    /* 36px */
    line-height: 2.75rem;
    /* 40px */
    margin-bottom: 1.5rem;
    /* 24px */
    letter-spacing: -1.5px;
}

h2 {
    font-size: 1.63rem;
    /* 24px */
    line-height: 1.875rem;
    /* 30px */
    margin-bottom: 1.5rem;
    /* 24px */
    letter-spacing: -1px;
    font-weight: bold;
}

h3 {
    font-size: 1.17rem;
    /* 18px */
    line-height: 1.5rem;
    /* 24px */
    margin-bottom: 1.5rem;
    /* 24px */
    letter-spacing: -1px;
    font-weight: bold;
}

h4 {
    font-size: 1.12rem;
    /* 16px */
    line-height: 1.375rem;
    /* 22px */
    margin-bottom: 1.5rem;
    /* 24px */
    color: white;
}

h5 {
    font-size: 0.97rem;
    /* 16px */
    line-height: 1.25rem;
    /* 22px */
    margin-bottom: 1.5rem;
    /* 24px */
    font-weight: bold;
}

h6 {
    font-size: 0.93rem;
    /* 16px */
    line-height: 1rem;
    /* 16px */
    margin-bottom: 0.75rem;
    color: white;
}

@media (min-width: 980px) {
    #ag-editor-id h3:before,
    #ag-editor-id h4:before,
    #ag-editor-id h5:before,
    #ag-editor-id h6:before {
        color: #ddd;
        border: 1px solid #ddd;
        border-radius: 3px;
        position: absolute;
        left: -1.642857143rem;
        top: .357142857rem;
        float: left;
        font-size: 9px;
        padding-left: 2px;
        padding-right: 2px;
        vertical-align: bottom;
        font-weight: normal;
        line-height: normal;
    }

    #ag-editor-id h3:before {
        content: 'h3';
    }

    #ag-editor-id h4:before {
        content: 'h4';
    }

    #ag-editor-id h5:before {
        content: 'h5';
        top: 0px;
    }

    #ag-editor-id h6:before {
        content: 'h6';
        top: 0px;
    }
}

a {
    text-decoration: none;
    outline: 0;
}

a:hover {
    outline: 0;
}

a:focus {
    outline: thin dotted;
}

sup.md-footnote {
    background-color: #555;
    color: #ddd;
}

p {
    -ms-word-wrap: break-word;
    word-wrap: break-word;
}

p,
ul,
dd,
ol,
hr,
address,
pre,
table,
iframe,
.wp-caption,
.wp-audio-shortcode,
.wp-video-shortcode {
    margin-top: 0;
    margin-bottom: 1.5rem;
    /* 24px */
}

audio:not([controls]) {
    display: none;
}

[hidden] {
    display: none;
}

::-moz-selection {
    background: #4a89dc;
    color: #fff;
    text-shadow: none;
}

*.in-text-selection,
::selection {
    background: #4a89dc;
    color: #fff;
    text-shadow: none;
}

ul,
ol {
    padding: 0 0 0 1.875rem;
    /* 30px */
}

ul {
    list-style: square;
}

ol {
    list-style: decimal;
}

ul ul,
ol ol,
ul ol,
ol ul {
    margin: 0;
}

b,
th,
dt,
strong {
    font-weight: bold;
}

i,
em,
dfn,
cite {
    font-style: italic;
}

blockquote {
    padding-left: 1.875rem;
    margin: 0 0 1.875rem 1.875rem;
    border-left: solid 2px #474d54;
    padding-left: 30px;
    margin-top: 35px;
}

pre,
code,
kbd,
tt,
var {
    background: rgba(0, 0, 0, 0.05);
    font-size: 0.875rem;
    font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
}

pre.md-fences {
    padding: 10px 30px;
    margin-bottom: 20px;
    border: 1px solid;
}

.md-fences .code-tooltip {
    bottom: -3.2em;
}

code,
kbd,
tt,
var {
    padding: 2px 5px;
}

table {
    max-width: 100%;
    width: 100%;
    border-collapse: collapse;
    border-spacing: 0;
}

th,
td {
    padding: 5px 10px;
    vertical-align: top;
}

a {
    -webkit-transition: all .2s ease-in-out;
    transition: all .2s ease-in-out;
}

hr {
    background: #474d54;
    /* variable */
}

#ag-editor-id>*:first-child {
    margin-top: 40px;
}

h1 {
    margin-top: 2em;
}

a {
    color: #e0e0e0;
    text-decoration: underline;
}

a:hover {
    color: #fff;
}

.md-inline-math script {
    color: #81b1db;
}

b,
th,
dt,
strong {
    color: #DEDEDE;
    /* variable */
}

mark {
    background: #D3D40E;
}

blockquote {
    color: #9DA2A6;
}

table a {
    color: #DEDEDE;
    /* variable */
}

th,
td {
    border: solid 1px #474d54;
    /* variable */
}

.task-list {
    padding-left: 0;
}

.task-list-item {
    padding-left: 1.25rem;
}

.task-list-item input {
    top: 0.1875rem;
}

.task-list-item input:before {
    content: "";
    display: inline-block;
    width: 0.875rem;
    height: 0.875rem;
    vertical-align: middle;
    text-align: center;
    border: 1px solid #b8bfc6;
    background-color: #363B40;
    margin-top: -0.4375rem;
}

.task-list-item input:checked:before,
.task-list-item input[checked]:before {
    content: '\221A';
    /*◘*/
    font-size: 0.625rem;
    line-height: 0.625rem;
    color: #DEDEDE;
}

/** markdown source **/
.cm-s-typora-default .cm-header,
.cm-s-typora-default .cm-property
{
    color: #cebcca;
}

.CodeMirror.cm-s-typora-default div.CodeMirror-cursor{
    border-left: 3px solid #b8bfc6;
}

.cm-s-typora-default .cm-comment {
    color: #9FB1FF;
}

.cm-s-typora-default .cm-string {
    color: #A7A7D9
}

.cm-s-typora-default .cm-atom, .cm-s-typora-default .cm-number {
    color: #848695;
    font-style: italic;
}

.cm-s-typora-default .cm-link {
    color: #95B94B;
}

.cm-s-typora-default .CodeMirror-activeline-background {
    background: rgba(51, 51, 51, 0.72);
}

/** code highlight */

.cm-s-inner .cm-variable,
.cm-s-inner .cm-operator,
.cm-s-inner .cm-property {
    color: #b8bfc6;
}

.cm-s-inner .cm-keyword {
    color: #C88FD0;
}

.cm-s-inner .cm-tag {
    color: #7DF46A;
}

.cm-s-inner .cm-attribute {
    color: #7575E4;
}

.CodeMirror div.CodeMirror-cursor {
    border-left: 1px solid #b8bfc6;
    z-index: 3;
}

.cm-s-inner .cm-string {
    color: #D26B6B;
}

.cm-s-inner .cm-comment,
.cm-s-inner.cm-comment {
    color: #DA924A;
}

.cm-s-inner .cm-header,
.cm-s-inner .cm-def,
.cm-s-inner.cm-header,
.cm-s-inner.cm-def {
    color: #8d8df0;
}

.cm-s-inner .cm-quote,
.cm-s-inner.cm-quote {
    color: #57ac57;
}

.cm-s-inner .cm-hr {
    color: #d8d5d5;
}

.cm-s-inner .cm-link {
    color: #d3d3ef;
}

.cm-s-inner .cm-negative {
    color: #d95050;
}

.cm-s-inner .cm-positive {
    color: #50e650;
}

.cm-s-inner .cm-string-2 {
    color: #f50;
}

.cm-s-inner .cm-meta,
.cm-s-inner .cm-qualifier {
    color: #b7b3b3;
}

.cm-s-inner .cm-builtin {
    color: #f3b3f8;
}

.cm-s-inner .cm-bracket {
    color: #997;
}

.cm-s-inner .cm-atom,
.cm-s-inner.cm-atom {
    color: #84B6CB;
}

.cm-s-inner .cm-number {
    color: #64AB8F;
}

.cm-s-inner .cm-variable {
    color: #b8bfc6;
}

.cm-s-inner .cm-variable-2 {
    color: #9FBAD5;
}

.cm-s-inner .cm-variable-3 {
    color: #1cc685;
}

.CodeMirror-selectedtext {
    background: #4a89dc;
    color: #fff !important;
    text-shadow: none;
}

.CodeMirror-gutters {
    background: transparent;
    border-right: 1px solid transparent;
}

/** quick open **/
.auto-suggest-container {
    border: 0px;
    background-color: #525C65;
}

#typora-quick-open {
    background-color: #525C65;
}

#typora-quick-open input{
    background-color: #525C65;
    border: 0;
    border-bottom: 1px solid grey;
}

.typora-quick-open-item {
    background-color: inherit;
    color: inherit;
}

.typora-quick-open-item.active {
    background-color: #4D8BDB;
    color: white;
}

.typora-search-spinner > div {
  background-color: #fff;
}

#ag-editor-id pre.md-meta-block {
    border-bottom: 1px dashed #ccc;
    background: transparent;
    padding-bottom: 0.6em;
    line-height: 1.6em;
}

.btn,
.btn .btn-default {
    background: transparent;
    color: #b8bfc6;
}

.md-table-edit {
    border-top: 1px solid gray;
    background-color: #363B40;
}

.popover-title {
    background: transparent;
}

.md-image>.md-meta {
    color: #BBBBBB;
    background: transparent;
}

.md-expand.md-image>.md-meta {
    color: #DDD;
}

#ag-editor-id>h3:before,
#ag-editor-id>h4:before,
#ag-editor-id>h5:before,
#ag-editor-id>h6:before {
    border: none;
    border-radius: 0px;
    color: #888;
    text-decoration: underline;
    left: -1.4rem;
    top: 0.2rem;
}

#ag-editor-id>h3.md-focus:before {
    top: 2px;
}

#ag-editor-id>h4.md-focus:before {
    top: 2px;
}

.md-toc-item {
    color: #A8C2DC;
}

#ag-editor-id div.md-toc-tooltip {
    background-color: #363B40;
}

.dropdown-menu .btn:hover,
.dropdown-menu .btn:focus,
.md-toc .btn:hover,
.md-toc .btn:focus {
    color: white;
    background: black;
}

#toc-dropmenu {
    background: rgba(50, 54, 59, 0.93);
    border: 1px solid rgba(253, 253, 253, 0.15);
}

#toc-dropmenu .divider {
    background-color: #9b9b9b;
}

.outline-expander:before {
    top: 2px;
}

#typora-sidebar {
    box-shadow: none;
    border-right: 1px dashed;
    border-right: none;
}

.sidebar-tabs {
    border-bottom:0;
}

#typora-sidebar:hover .outline-title-wrapper {
    border-left: 1px dashed;
}

.outline-title-wrapper .btn {
    color: inherit;
}

.outline-item:hover {
    border-color: #363B40;
    background-color: #363B40;
    color: white;
}

h1.md-focus .md-attr,
h2.md-focus .md-attr,
h3.md-focus .md-attr,
h4.md-focus .md-attr,
h5.md-focus .md-attr,
h6.md-focus .md-attr,
.md-header-span .md-attr {
    color: #8C8E92;
    display: inline;
}

.md-comment {
    color: #5a95e3;
    opacity: 1;
}

.md-inline-math g,
.md-inline-math svg {
    stroke: #b8bfc6 !important;
    fill: #b8bfc6 !important;
}

[md-inline='inline_math'] {
    color: #9CB2E9;
}

#math-inline-preview .md-arrow:after {
    background: black;
}

.modal-content {
    background: var(--bg-color);
    border: 0;
}

.modal-title {
    font-size: 1.5em;
}

.modal-content input {
    background-color: rgba(26, 21, 21, 0.51);
    color: white;
}

.modal-content .input-group-addon {
    background-color: rgba(0, 0, 0, 0.17);
    color: white;
}

.modal-backdrop {
    background-color: rgba(174, 174, 174, 0.7);
}

.modal-content .btn-primary {
    border-color: var(--primary-color);
}

.md-table-resize-popover {
    background-color: #4B535A;
}

.form-inline .input-group .input-group-addon {
    color: white;
}

#md-searchpanel {
    border-bottom: 1px dashed grey;
}

/** UI for electron */

.context-menu {
    background-color: #42464A;
}

.context-menu.dropdown-menu .divider {
    background-color: #777777;
}

footer {
    color: inherit;
}

@media (max-width: 1000px) {
    footer {
        border-top: none;
    }
    footer:hover {
        color: inherit;
    }
}

#file-info-file-path .file-info-field-value:hover {
    background-color: #555;
    color: #dedede;
}

.megamenu-content,
.megamenu-opened header {
    background: var(--bg-color);
}

.megamenu-menu-panel h2,
.megamenu-menu-panel h1,
.long-btn {
    color: inherit;
}

.megamenu-menu-panel input[type='text'] {
    background: inherit;
    border: 0;
    border-bottom: 1px solid;
}

#recent-file-panel-action-btn {
    background: inherit;
    border: 1px grey solid;
}

.megamenu-menu-panel .btn:hover,
.megamenu-menu-panel .btn:focus,
.megamenu-menu-panel .btn.focus {
    color: white;
}

.megamenu-menu-panel .dropdown-menu > li > a {
    color: inherit;
    background-color: #2F353A;
    text-decoration: none;
}

.megamenu-menu-panel table td:nth-child(1) {
    color: inherit;
    font-weight: bold;
}

.megamenu-menu-panel tbody tr:hover td:nth-child(1) {
    color: white;
}

.modal-footer .btn-default,
.modal-footer .btn-primary,
.modal-footer .btn-default:not(:hover) {
    border: 1px solid;
    border-color: transparent;
}

.btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default {
    color: white;
    border: 1px solid #ddd;
    background-color: inherit;
}

.modal-header {
    border-bottom: 0;
}

.modal-footer {
    border-top: 0;
}

#recent-file-panel tbody tr:nth-child(2n-1) {
    background-color: transparent !important;
}

.megamenu-menu-panel tbody tr:hover td:nth-child(2) {
    color: inherit;
}

.megamenu-menu-panel .btn {
    border: 1px solid #eee;
    background: transparent;
}

.mouse-hover .toolbar-icon.btn:hover,
#w-full.mouse-hover,
#w-pin.mouse-hover {
    background-color: inherit;
}

.typora-node::-webkit-scrollbar {
    width: 5px;
}

.typora-node::-webkit-scrollbar-thumb:vertical {
    background: rgba(250, 250, 250, 0.3);
}

.typora-node::-webkit-scrollbar-thumb:vertical:active {
    background: rgba(250, 250, 250, 0.5);
}

#w-unpin {
    background-color: #4182c4;
}

#top-titlebar, #top-titlebar * {
    color: var(--item-hover-text-color);
}

.typora-sourceview-on #toggle-sourceview-btn,
#footer-word-count:hover,
.show-word-count #footer-word-count {
    background: #333333;
}

#toggle-sourceview-btn:hover {
    color: #eee;
    background: #333333;
}

/** focus mode */
.on-focus-mode .md-end-block:not(.md-focus):not(.md-focus-container) * {
    color: #686868 !important;
}

.on-focus-mode .md-end-block:not(.md-focus) img,
.on-focus-mode .task-list-item:not(.md-focus-container)>input {
    opacity: #686868 !important;
}

.on-focus-mode li[cid]:not(.md-focus-container){
    color: #686868;
}

.on-focus-mode .md-fences.md-focus .CodeMirror-code>*:not(.CodeMirror-activeline) *,
.on-focus-mode .CodeMirror.cm-s-inner:not(.CodeMirror-focused) * {
    color: #686868 !important;
}

.on-focus-mode .md-focus,
.on-focus-mode .md-focus-container {
    color: #fff;
}

.on-focus-mode #typora-source .CodeMirror-code>*:not(.CodeMirror-activeline) * {
    color: #686868 !important;
}

/*diagrams*/
#ag-editor-id .md-focus .md-diagram-panel {
    border: 1px solid #ddd;
    margin-left: -30px;
}

.md-diagram-panel-error {
    color: #f1908e;
}

.active-tab-files #info-panel-tab-file,
.active-tab-files #info-panel-tab-file:hover,
.active-tab-outline #info-panel-tab-outline,
.active-tab-outline #info-panel-tab-outline:hover {
    color: #eee;
}

.sidebar-footer-item:hover,
.footer-item:hover {
    background: inherit;
    color: white;
}

.ty-side-sort-btn.active,
.ty-side-sort-btn:hover,
.selected-folder-menu-item a:after {
    color: white;
}

#sidebar-files-menu {
    border:solid 1px;
    box-shadow: 4px 4px 20px rgba(0, 0, 0, 0.79);
    background-color: var(--bg-color);
}

.file-list-item {
    border-bottom:none;
}

.file-list-item-summary {
    opacity: 1;
}

.file-list-item.active:first-child {
    border-top: none;
}

.sidebar-footer {
    border-top: 1px solid #555;
}

.file-node-background {
    height: 32px;
}

.file-library-node.active>.file-node-content,
.file-list-item.active {
    color: white;
    color: var(--active-file-text-color);
}

.file-library-node.active>.file-node-background{
    background-color: rgb(34, 34, 34);
    background-color: var(--active-file-bg-color);
}
.file-list-item.active {
    background-color: rgb(34, 34, 34);
    background-color: var(--active-file-bg-color);
}


Copyright (c) 2010-2013, Sorkin Type Co (www.sorkintype.com) with Reserved Font Name 'Merriweather'

Merriweather is a trademark of Sorkin Type Co.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL

-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.


import selection from '../selection'
import { findNearestParagraph, findOutMostParagraph } from '../utils/domManipulate'
import { isCursorAtBegin, onlyHaveOneLine, setCursorAtLastLine } from '../codeMirror'

const backspaceCtrl = ContentState => {
  ContentState.prototype.checkBackspaceCase = function () {
    const node = selection.getSelectionStart()
    const nearestParagraph = findNearestParagraph(node)
    const outMostParagraph = findOutMostParagraph(node)
    const block = this.getBlock(nearestParagraph.id)
    const outBlock = this.findOutMostBlock(block)
    const parent = this.getParent(block)

    const { left: outLeft } = selection.getCaretOffsets(outMostParagraph)
    const { left: inLeft } = selection.getCaretOffsets(nearestParagraph)

    if (parent && parent.type === 'li' && inLeft === 0 && this.isFirstChild(block)) {
      if (this.isOnlyChild(parent)) {
        return { type: 'LI', info: 'REPLACEMENT' }
      } else if (this.isFirstChild(parent)) {
        return { type: 'LI', info: 'REMOVE_INSERT_BEFORE' }
      } else {
        return { type: 'LI', info: 'INSERT_PRE_LIST' }
      }
    }
    if (parent && parent.type === 'blockquote' && inLeft === 0) {
      if (this.isOnlyChild(block)) {
        return { type: 'BLOCKQUOTE', info: 'REPLACEMENT' }
      } else if (this.isFirstChild(block)) {
        return { type: 'BLOCKQUOTE', info: 'INSERT_BEFORE' }
      }
    }
    if (!outBlock.preSibling && outLeft === 0) {
      return { type: 'STOP' }
    }
  }

  ContentState.prototype.backspaceHandler = function (event) {
    const node = selection.getSelectionStart()
    const paragraph = findNearestParagraph(node)
    const id = paragraph.id
    const block = this.getBlock(id)
    const parent = this.getBlock(block.parent)
    const preBlock = this.getBlock(block.preSibling)
    const selectionState = selection.exportSelection(paragraph)
    const { left } = selection.getCaretOffsets(paragraph)
    const inlineDegrade = this.checkBackspaceCase()

    if (block.type === 'pre') {
      const cm = this.codeBlocks.get(id)
      // if event.preventDefault(), U can not use backspace in language input.
      if (isCursorAtBegin(cm) && onlyHaveOneLine(cm)) {
        event.preventDefault()
        const value = cm.getValue()
        const newBlock = this.createBlock('p')
        if (value) newBlock.text = value
        this.insertBefore(newBlock, block)
        this.removeBlock(block)
        this.codeBlocks.delete(id)
        this.cursor.key = newBlock.key
        this.cursor.range = { start: 0, end: 0 }
        this.render()
      }
    } else if (
      left === 0 &&
      preBlock &&
      preBlock.type === 'pre'
    ) {
      event.preventDefault()
      const text = block.text
      const cm = this.codeBlocks.get(preBlock.key)
      const value = cm.getValue() + text
      cm.setValue(value)
      this.removeBlock(block)
      setCursorAtLastLine(cm)
    } else if (inlineDegrade) {
      event.preventDefault()
      switch (inlineDegrade.type) {
        case 'STOP': // at begin of article
          // do nothing...
          break
        case 'LI': {
          if (inlineDegrade.info === 'REPLACEMENT') {
            const children = parent.children
            const parentsparent = this.getBlock(parent.parent)
            children.forEach(child => {
              this.insertBefore(child, parentsparent)
            })
            this.removeBlock(parentsparent)
          } else if (inlineDegrade.info === 'REMOVE_INSERT_BEFORE') {
            const children = parent.children
            const parentsparent = this.getBlock(parent.parent)
            children.forEach(child => {
              this.insertBefore(child, parentsparent)
            })
            this.removeBlock(parent)
          } else if (inlineDegrade.info === 'INSERT_PRE_LIST') {
            const parPre = this.getBlock(parent.preSibling)
            const children = parent.children
            children.forEach(child => {
              this.appendChild(parPre, child)
            })
            this.removeBlock(parent)
            console.log(JSON.stringify(this.blocks, null, 2))
          }
          break
        }
        case 'BLOCKQUOTE':
          if (inlineDegrade.info === 'REPLACEMENT') {
            this.replaceBlock(block, parent)
          } else if (inlineDegrade.info === 'INSERT_BEFORE') {
            this.removeBlock(block)
            this.insertBefore(block, parent)
          }
          break
      }
      this.cursor = {
        key: block.key,
        range: selectionState
      }
      this.render()
    } else if (left === 0) {
      this.removeBlock(block)
    }
  }
}

export default backspaceCtrl


keep keep keep keep keep replace keep keep keep keep

const isUpdated = this.codeBlockUpdate(block)


import mousetrap from 'mousetrap'
import {
  getUniqueId
} from './utils'

class Event {
  constructor () {
    this.events = []
    this.listeners = {}
    this.eventIds = new Set() // use to store eventId
    // bind mousetrap methods to event instance.
    ;['bind', 'unbind', 'trigger', 'stopCallback', 'reset', 'handleKey', 'addKeycodes'].forEach(mothod => {
      this[mothod] = mousetrap[mothod]
    })
  }
  /**
   * [attachDOMEvent] bind event listener to target, and return a unique ID,
   * this ID
   */
  attachDOMEvent (target, event, listener, capture) {
    if (this.checkHasBind(target, event, listener, capture)) return false
    const eventId = getUniqueId(this.eventIds)
    target.addEventListener(event, listener, capture)
    this.events.push({
      eventId,
      target,
      event,
      listener,
      capture
    })
    return eventId
  }
  /**
   * [detachDOMEvent removeEventListener]
   * @param  {[type]} eventId [unique eventId]
   */
  detachDOMEvent (eventId) {
    if (!eventId) return false
    const removeEvent = this.events.filter(e => e.eventId === eventId)[0]
    if (removeEvent) {
      const { target, event, listener, capture } = removeEvent
      target.removeEventListener(event, listener, capture)
    }
  }
  /**
   * [detachAllDomEvents remove all the DOM events handler]
   */
  detachAllDomEvents () {
    this.events.forEach(event => this.detachDOMEvent(event.eventId))
    this.reset()
  }
  /**
   * [subscribe] subscribe custom event
   */
  subscribe (event, listener) {
    const eventListener = this.listeners[event]
    if (eventListener && Array.isArray(eventListener)) {
      eventListener.push(listener)
    } else {
      this.listeners[event] = [listener]
    }
  }
  /**
   * dispatch custom event
   * customEvent includes: {
   *   'markedTextChange',
   *   'paragraphChange',
   *   'arrow',
   *   'elementUpdate': 'update `p` elementNode to other elementNode. ex: `h1`, `ul-li`, `blockquote` or reversed'
   *   'enter',
   *   'delete',
   *   'tab',
   *   'backspace', 'paragraphBlur'
   * }
   */
  dispatch (event, ...data) {
    const eventListener = this.listeners[event]
    if (eventListener && Array.isArray(eventListener)) {
      eventListener.forEach(listener => listener(...data))
    }
  }
  // Determine whether the event has been bind
  checkHasBind (cTarget, cEvent, cListener, cCapture) {
    let i
    let len = this.events.length
    for (i = 0; i < len; i++) {
      const { target, event, listener, capture } = this.events[i]
      if (target === cTarget && event === cEvent && listener === cListener && capture === cCapture) {
        return true
      }
    }
    return false
  }
}

export default new Event()


import Vue from 'vue'
import axios from 'axios'

import App from './App'
import store from './store'

if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.http = Vue.prototype.$http = axios
Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  components: { App },
  store,
  template: '<App/>'
}).$mount('#app')


import { getUniqueId } from '../utils'
import StateRender from '../parser/StateRender'
import enterCtrl from './enterCtrl'
import updateCtrl from './updateCtrl'
import garbageCtrl from './garbageCtrl'
import backspaceCtrl from './backspaceCtrl'
import codeBlockCtrl from './codeBlockCtrl'
import History from './history'
import historyCtrl from './historyCtrl'
import arrowCtrl from './arrowCtrl'
import importMarkdown from '../utils/importMarkdown'

const ctrls = [
  enterCtrl,
  updateCtrl,
  garbageCtrl,
  backspaceCtrl,
  codeBlockCtrl,
  historyCtrl,
  arrowCtrl,
  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
}

class ContentState {
  constructor () {
    this.keys = new Set()
    this.blocks = [ this.createBlock() ]
    this.stateRender = new StateRender()
    this.codeBlocks = new Map()
    this.history = new History(this)
    const lastBlock = this.getLastBlock()
    this.cursor = {
      key: lastBlock.key,
      range: {
        start: lastBlock.text.length,
        end: lastBlock.text.length
      }
    }
    this.history.push({
      type: 'normal',
      blocks: this.blocks,
      cursor: this.cursor
    })
  }

  render () {
    const { blocks, cursor, codeBlocks } = this
    const activeBlockKey = this.getActiveBlockKey()
    this.stateRender.render(blocks, cursor, activeBlockKey, codeBlocks)
    this.pre2CodeMirror()
  }

  createBlock (type = 'p', text = '') {
    const key = getUniqueId(this.keys)
    return {
      key,
      type,
      text,
      parent: null,
      preSibling: null,
      nextSibling: null,
      children: []
    }
  }

  // getBlocks
  getBlocks () {
    let key
    let cm
    for ([ key, cm ] of this.codeBlocks.entries()) {
      const value = cm.getValue()
      const block = this.getBlock(key)
      if (block) block.text = value
    }
    return this.blocks
  }

  getCursor () {
    return this.cursor
  }

  getArrayBlocks () {
    return convertBlocksToArray(this.blocks)
  }

  getBlock (key) {
    return this.getArrayBlocks().filter(block => block.key === key)[0]
  }

  getParent (block) {
    if (block.parent) {
      return this.getBlock(block.parent)
    }
    return null
  }

  getPreSibling (block) {
    return block.preSibling ? this.getBlock(block.preSibling) : null
  }

  getNextSibling (block) {
    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')
    }
  }

  removeBlock (block) {
    const remove = (blocks, block) => {
      const len = blocks.length
      let i
      for (i = 0; i < len; i++) {
        if (blocks[i].key === block.key) {
          const preSibling = this.getBlock(block.preSibling)
          const nextSibling = this.getBlock(block.nextSibling)

          if (preSibling) {
            preSibling.nextSibling = nextSibling ? nextSibling.key : null
          }
          if (nextSibling) {
            nextSibling.preSibling = preSibling ? preSibling.key : null
          }

          return blocks.splice(i, 1)
        } else {
          if (blocks[i].children.length) {
            remove(blocks[i].children, block)
          }
        }
      }
    }
    remove(this.blocks, block)
  }

  getActiveBlockKey () {
    let block = this.getBlock(this.cursor.key)
    if (!block) return null
    while (block.parent) {
      block = this.getBlock(block.parent)
    }
    return block.key
  }

  getCursorBlock () {
    return this.getBlock(this.cursor.key) || null
  }

  insertAfter (newBlock, oldBlock) {
    const siblings = oldBlock.parent ? this.getBlock(oldBlock.parent).children : this.blocks
    const oldNextSibling = this.getBlock(oldBlock.nextSibling)
    const index = this.findIndex(siblings, oldBlock)
    siblings.splice(index + 1, 0, newBlock)
    oldBlock.nextSibling = newBlock.key
    newBlock.parent = oldBlock.parent
    newBlock.preSibling = oldBlock.key
    if (oldNextSibling) {
      newBlock.nextSibling = oldNextSibling.key
      oldNextSibling.preSibling = newBlock.key
    }
  }

  insertBefore (newBlock, oldBlock) {
    const siblings = oldBlock.parent ? this.getBlock(oldBlock.parent).children : this.blocks
    const oldPreSibling = this.getBlock(oldBlock.preSibling)
    const index = this.findIndex(siblings, oldBlock)
    siblings.splice(index, 0, newBlock)
    oldBlock.preSibling = newBlock.key
    newBlock.parent = oldBlock.parent
    newBlock.nextSibling = oldBlock.key

    if (oldPreSibling) {
      oldPreSibling.nextSibling = newBlock.key
      newBlock.preSibling = oldPreSibling.key
    }
  }

  findOutMostBlock (block) {
    if (!block.parent) return block
    else {
      const parent = this.getBlock(block.parent)
      return this.findOutMostBlock(parent)
    }
  }

  findIndex (children, block) {
    const len = children.length
    let i
    for (i = 0; i < len; i++) {
      if (children[i].key === block.key) return i
    }
    return -1
  }

  appendChild (parent, block) {
    const len = parent.children.length
    const lastChild = parent.children[len - 1]
    parent.children.push(block)
    block.parent = parent.key
    if (lastChild) {
      lastChild.nextSibling = block.key
      block.preSibling = lastChild.key
    }
  }

  replaceBlock (newBlock, oldBlock) {
    let index
    if (!oldBlock.parent) {
      index = this.findIndex(this.blocks, oldBlock)
      this.blocks.splice(index, 1, newBlock)
    } else {
      const parent = this.getBlock(oldBlock.parent)
      index = this.findIndex(parent.children, oldBlock)
      parent.children.splice(index, 1, newBlock)
    }
    newBlock.parent = oldBlock.parent
    newBlock.preSibling = oldBlock.preSibling
    newBlock.nextSibling = oldBlock.nextSibling
  }

  isFirstChild (block) {
    return !block.preSibling
  }

  isLastChild (block) {
    return !block.nextSibling
  }

  isOnlyChild (block) {
    return !block.nextSibling && !block.preSibling
  }

  getLastBlock () {
    const arrayBlocks = this.getArrayBlocks()
    const len = arrayBlocks.length
    return arrayBlocks[len - 1]
  }

  clear () {
    this.keys.clear()
    this.codeBlocks.clear()
  }
}

ctrls.forEach(ctrl => ctrl(ContentState))

export default ContentState


import { LOWERCASE_TAGS, EVENT_KEYS, CLASS_OR_ID } from './config'

import { throttle, debounce } from './utils'

import { checkEditLanguage, replaceLanguage } from './codeMirror/language'

import Emoji, { checkEditEmoji, setInlineEmoji } from './emojis'

import { findNearestParagraph, operateClassName } from './utils/domManipulate'

import ExportMarkdown from './utils/exportMarkdown'

keep keep keep replace keep keep keep keep

import { checkEditLanguage } from './codeMirror/language'


/* meyer reset -- http://meyerweb.com/eric/tools/css/reset/ , v2.0 | 20110126 | License: none (public domain) */

@include-when-export url(https://fonts.googleapis.com/css?family=PT+Serif:400,400italic,700,700italic&subset=latin,cyrillic-ext,cyrillic,latin-ext);

/* =========== */

@font-face {
    font-family: 'PT Serif';
    font-style: normal;
    font-weight: normal;
    src: local('PT Serif'), local('PTSerif-Regular'), url(./newsprint/PT_Serif-Web-Regular.ttf);
}
@font-face {
    font-family: 'PT Serif';
    font-style: italic;
    font-weight: normal;
    src: local('PT Serif'), local('PTSerif-Italic'), url('./newsprint/PT_Serif-Web-Italic.ttf')
}

@font-face {
    font-family: 'PT Serif';
    font-style: normal;
    font-weight: bold;
    src: local('PT Serif'), local('PTSerif-Bold'), url('./newsprint/PT_Serif-Web-Bold.ttf')
}

@font-face {
    font-family: 'PT Serif';
    font-style: italic;
    font-weight: bold;
    src: local('PT Serif'), local('PTSerif-BoldItalic'), url('./newsprint/PT_Serif-Web-BoldItalic.ttf')
}

:root {
	--active-file-bg-color: #dadada;
	--active-file-bg-color: rgba(32, 43, 51, 0.63);
	--active-file-text-color: white;
	--bg-color: #f3f2ee;
	--text-color: #1f0909;
}

html {
	font-size: 16px;
}

html, body {
	background-color: #f3f2ee;
	font-family: "PT Serif", 'Times New Roman', Times;
	color: #1f0909;
	line-height: 1.5em;
}

/*#ag-editor-id {
	overflow-x: auto;
    max-width: initial;
	padding-left: calc(50% - 17em);
    padding-right: calc(50% - 17em);
}

@media (max-width: 36em) {
 	#ag-editor-id {
 		padding-left: 1em;
    	padding-right: 1em;
 	}
}*/

#ag-editor-id {
	max-width: 40em;
}

ol,
ul {
	list-style: none;
}
blockquote,
q {
	quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
	content: '';
	content: none;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}
/* styles */

/* ====== */

/* headings */

h1,
h2,
h3,
h4,
h5,
h6 {
	font-weight: bold;
}
h1 {
	font-size: 1.875em;
	/*30 / 16*/
	line-height: 1.6em;
	/* 48 / 30*/
	margin-top: 2em;
}
h2,
h3 {
	font-size: 1.3125em;
	/*21 / 16*/
	line-height: 1.15;
	/*24 / 21*/
	margin-top: 2.285714em;
	/*48 / 21*/
	margin-bottom: 1.15em;
	/*24 / 21*/
}
h3 {
	font-weight: normal;
}
h4 {
	font-size: 1.125em;
	/*18 / 16*/
	margin-top: 2.67em;
	/*48 / 18*/
}
h5,
h6 {
	font-size: 1em;
	/*16*/
}
h1 {
	border-bottom: 1px solid;
	margin-bottom: 1.875em;
	padding-bottom: 0.8125em;
}
/* links */

a {
	text-decoration: none;
	color: #065588;
}
a:hover,
a:active {
	text-decoration: underline;
}
/* block spacing */

p,
blockquote,
.md-fences {
	margin-bottom: 1.5em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
	margin-bottom: 1.5em;
}
/* blockquote */

blockquote {
	font-style: italic;
	border-left: 5px solid;
	margin-left: 2em;
	padding-left: 1em;
}
/* lists */

ul,
ol {
	margin: 0 0 1.5em 1.5em;
}
ol li {
	list-style-type: decimal;
	list-style-position: outside;
}
ul li {
	list-style-type: disc;
	list-style-position: outside;
}
/* tables */
.md-meta,.md-before, .md-after {
	color:#999;
}

table {
	margin-bottom: 1.5em;
	/*24 / 16*/
	font-size: 1em;
	/* width: 100%; */
}
thead th,
tfoot th {
	padding: .25em .25em .25em .4em;
	text-transform: uppercase;
}
th {
	text-align: left;
}
td {
	vertical-align: top;
	padding: .25em .25em .25em .4em;
}

code,
.md-fences {
	background-color: #dadada;
	padding-left: 1ch;
	padding-right: 1ch;
}
.md-fences {
	margin-left: 2em;
	margin-bottom: 3em;
}

.md-fences .CodeMirror.CodeMirror-wrap {
	top: -0.8em;
}

pre,
code,
tt {
	font-size: .875em;
	line-height: 1.714285em;
}
/* some fixes */

h1 {
	line-height: 1.3em;
	font-weight: normal;
	margin-bottom: 0.5em;
}

p + ul,
p + ol{
	margin-top: -1em;
}

h3 + ul,
h4 + ul,
h5 + ul,
h6 + ul,
h3 + ol,
h4 + ol,
h5 + ol,
h6 + ol {
	margin-top: .5em;
}

li > ul,
li > ol {
	margin-top: inherit;
}

h2,
h3 {
	margin-bottom: .75em;
}
hr {
	border-top: none;
	border-right: none;
	border-bottom: 1px solid;
	border-left: none;
}
h1 {
	border-color: #c5c5c5;
}
blockquote {
	border-color: #bababa;
	color: #656565;
}
thead.md-table-edit {
	background-color: transparent;
}
thead {
	background-color: #dadada;
}
tr:nth-child(even) {
	background: #e8e7e7;
}
hr {
	border-color: #c5c5c5;
}
.task-list{
	padding-left: 1rem;
}

.task-list-item {
	padding-left: 1.5rem;
	list-style-type: none;
}

.task-list-item input:before {
	content: '\221A';
	display: inline-block;
	width: 1.25rem;
  	height: 1.6rem;
	vertical-align: middle;
	text-align: center;
	color: #ddd;
	background-color: #F3F2EE;
}

.task-list-item input:checked:before,
.task-list-item input[checked]:before{
	color: inherit;
}

#ag-editor-id pre.md-meta-block {
	min-height: 1.875rem;
	color: #555;
	border: 0px;
	background: transparent;
	margin-top: -4px;
	margin-left: 1em;
	margin-top: 1em;
}

.md-image>.md-meta {
	color: #9B5146;
}

.md-image>.md-meta{
	font-family: Menlo, 'Ubuntu Mono', Consolas, 'Courier New', 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
}

#ag-editor-id>h3.md-focus:before{
	left: -1.5rem;
	color:#999;
	border-color:#999;
}
#ag-editor-id>h4.md-focus:before{
	left: -1.5rem;
	top: .25rem;
	color:#999;
	border-color:#999;
}
#ag-editor-id>h5.md-focus:before{
	left: -1.5rem;
	top: .0.3125rem;
	color:#999;
	border-color:#999;
}
#ag-editor-id>h6.md-focus:before{
	left: -1.5rem;
	top: 0.3125rem;
	color:#999;
	border-color:#999;
}

.md-toc:focus .md-toc-content{
	margin-top: 19px;
}

.md-toc-content:empty:before{
	color: #065588;
}
.md-toc-item {
	color: #065588;
}
#ag-editor-id div.md-toc-tooltip {
	background-color: #f3f2ee;
}

#typora-sidebar {
	background-color: #f3f2ee;
	-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.375);
  	box-shadow: 0 6px 12px rgba(0, 0, 0, 0.375);
}

.pin-outline #typora-sidebar {
	background: inherit;
	box-shadow: none;
	border-right: 1px dashed;
}

.pin-outline #typora-sidebar:hover .outline-title-wrapper {
	border-left:1px dashed;
}

.outline-item:hover {
  background-color: #dadada;
  border-left: 28px solid #dadada;
  border-right: 18px solid #dadada;
}

.typora-node .outline-item:hover {
  	border-right: 28px solid #dadada;
}

.outline-expander:before {
  content: "\f0da";
  font-family: FontAwesome;
  font-size:14px;
  top: 1px;
}

.outline-expander:hover:before,
.outline-item-open>.outline-item>.outline-expander:before {
  content: "\f0d7";
}

.modal-content {
	background-color: #f3f2ee;
}

.auto-suggest-container ul li {
	list-style-type: none;
}

/** UI for electron */

.megamenu-menu,
#top-titlebar, #top-titlebar *,
.megamenu-content {
	background: #f3f2ee;
	color: #1f0909;
}

.megamenu-menu-header {
	border-bottom: 1px dashed #202B33;
}

.megamenu-menu {
	box-shadow: none;
	border-right: 1px dashed;
}

header, .context-menu, .megamenu-content, footer {
	font-family: "PT Serif", 'Times New Roman', Times;
    color: #1f0909;
}

#megamenu-back-btn {
	color: #1f0909;
	border-color: #1f0909;
}

.megamenu-menu-header #megamenu-menu-header-title:before {
	color: #1f0909;
}

.megamenu-menu-list li a:hover, .megamenu-menu-list li a.active {
	color: inherit;
	background-color: #e8e7df;
}

.long-btn:hover {
	background-color: #e8e7df;
}

#recent-file-panel tbody tr:nth-child(2n-1) {
    background-color: transparent !important;
}

.megamenu-menu-panel tbody tr:hover td:nth-child(2) {
    color: inherit;
}

.megamenu-menu-panel .btn {
	background-color: #D2D1D1;
}

.btn-default {
	background-color: transparent;
}

.typora-sourceview-on #toggle-sourceview-btn,
.show-word-count #footer-word-count {
	background: #c7c5c5;
}

#typora-quick-open {
    background-color: inherit;
}

.md-diagram-panel {
	margin-left: -1ch;
	margin-top: 24px;
}

.file-list-item-file-name {
	font-weight: initial;
}

.file-list-item-summary {
	opacity: 1;
}

.file-list-item {
	color: #777;
}

.file-list-item.active {
	background-color: inherit;
	color: black;
}

.file-list-item.active .file-list-item-file-name  {
	font-weight: bold;
}

.file-list-item{
    opacity:1 !important;
}

.file-library-node.active>.file-node-background{
	background-color: rgba(32, 43, 51, 0.63);
	background-color: var(--active-file-bg-color);
}

.file-tree-node.active>.file-node-content{
	color: white;
	color: var(--active-file-text-color);
}


import selection from '../selection'
import { findNearestParagraph } from '../utils/domManipulate'

const historyCtrl = ContentState => {
  ContentState.prototype.historyHandler = function (event) {
    if (
      event.altKey ||
      event.ctrlKey ||
      event.metaKey ||
      event.shiftKey ||
      (event.key === 'z' && event.metaKey) // when user press `commandOrCtrl + z` , don't push history
    ) return

    const node = selection.getSelectionStart()
    const paragraph = findNearestParagraph(node)
    const block = this.getBlock(paragraph.id)
    if (block.type !== 'pre') {
      this.history.push({
        type: 'normal',
        blocks: this.blocks,
        cursor: this.cursor
      })
    }
  }
}

export default historyCtrl


MIT License

Copyright (c) 2017 Jocs

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


const modes = search(lang).map(mode => {

return Object.assign(mode, { text: mode.name })


/**
 * translate markdown format to content state used by editor
 */
import Markdownit from 'markdown-it'
import parse5 from 'parse5'
// To be disabled rules when parse markdown, Becase content state don't need to parse inline rules
import { INLINE_RULES } from '../config'

const md = new Markdownit()
md.disable(INLINE_RULES)

const importRegistor = ContentState => {
  ContentState.prototype.importMarkdown = function (text) {
    this.keys = new Set()
    this.codeBlocks = new Map()
    // mock a root block...
    const rootState = {
      key: null,
      type: 'root',
      text: '',
      parent: null,
      preSibling: null,
      nextSibling: null,
      children: []
    }

    const htmlText = md.render(text)
    const domAst = parse5.parseFragment(htmlText)

    const childNodes = domAst.childNodes

    const getLang = node => {
      let lang = ''
      if (node.nodeName === 'code') {
        const classAttr = node.attrs.filter(attr => attr.name === 'class')[0]
        if (classAttr && /^language/.test(classAttr.value)) {
          lang = classAttr.value.split('-')[1]
        }
      }
      return lang
    }

    const travel = (parent, childNodes) => {
      const len = childNodes.length
      let i

      for (i = 0; i < len; i++) {
        const child = childNodes[i]
        let block
        let value
        console.log(child)
        switch (child.nodeName) {
          case 'p':
          case 'h1':
          case 'h2':
          case 'h3':
          case 'h4':
          case 'h5':
          case 'h6':
            const textValue = child.childNodes[0].value
            const match = /\d/.exec(child.nodeName)
            value = match ? '#'.repeat(+match[0]) + textValue : textValue
            block = this.createBlock(child.nodeName, value)
            this.appendChild(parent, block)
            break

          case 'hr':
            const initValue = '---'
            block = this.createBlock(child.nodeName, initValue)
            this.appendChild(parent, block)
            break

          case 'li':
            block = this.createBlock('li')
            this.appendChild(parent, block)
            if (child.childNodes.length === 1) {
              value = child.childNodes[0].value
              const pBlock = this.createBlock('p', value)
              this.appendChild(block, pBlock)
            } else {
              travel(block, child.childNodes)
            }
            break

          case 'ul':
            block = this.createBlock('ul')
            travel(block, child.childNodes)
            this.appendChild(parent, block)
            break

          case 'ol':
            block = this.createBlock('ol')
            child.attrs.forEach(attr => {
              block[attr.name] = attr.value
            })
            if (!block.start) {
              block.start = 1
            }
            travel(block, child.childNodes)
            this.appendChild(parent, block)
            break

          case 'blockquote':
            block = this.createBlock('blockquote')
            travel(block, child.childNodes)
            this.appendChild(parent, block)
            break

          case 'pre':
            const codeNode = child.childNodes[0]
            value = codeNode.childNodes[0].value

            if (value.endsWith('\n')) {
              value = value.replace(/\n+$/, '')
            }
            block = this.createBlock('pre', value)
            block.lang = getLang(codeNode)
            this.appendChild(parent, block)
            break
          default:
            if (child.tagName) {
              throw new Error(`unhandle node type ${child.tagName}`)
            }
            break
        }
      }
    }

    travel(rootState, childNodes)
    this.blocks = rootState.children
    const lastBlock = this.getLastBlock()
    this.cursor = {
      key: lastBlock.key,
      range: {
        start: lastBlock.text.length,
        end: lastBlock.text.length
      }
    }
    this.render()
  }
}

export default importRegistor