/**
 * 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,
  findNearestParagraph,
  getClosestBlockContainer,
  getCursorPositionWithinMarkedText,
  compareParagraphsOrder,
  getTextContent
} from './utils/domManipulate'

import { CLASS_OR_ID } from './config'

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)
  }

  // https://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
  }

  // https://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 preceded 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
  }

  // https://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 beginning 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
    }
  }

  // https://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) {
    const { rangeCount } = this.doc.getSelection()
    if (!rangeCount) return
    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)
  }

  // https://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
  }

  setCursorRange (cursorRange) {
    const { start, end } = cursorRange
    const startParagraph = document.querySelector(`#${start.key}`)
    const endParagraph = document.querySelector(`#${end.key}`)

    const getNodeAndOffset = (node, offset) => {
      if (node.nodeType === 3) {
        return {
          node,
          offset
        }
      }
      const childNodes = node.childNodes
      const len = childNodes.length
      let i
      let count = 0
      for (i = 0; i < len; i++) {
        const child = childNodes[i]
        if (count + getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'] ]).length >= offset) {
          return getNodeAndOffset(child, offset - count)
        } else {
          count += getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'] ]).length
        }
      }
      return { node, offset }
    }

    let { node: startNode, offset: startOffset } = getNodeAndOffset(startParagraph, start.offset)
    let { node: endNode, offset: endOffset } = getNodeAndOffset(endParagraph, end.offset)
    startOffset = Math.min(startOffset, startNode.textContent.length)
    endOffset = Math.min(endOffset, endNode.textContent.length)

    this.select(startNode, startOffset, endNode, endOffset)
  }

  getCursorRange () {
    let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()

    // when the first paragraph is task list, press ctrl + a, then press backspace will cause bug
    // use code bellow to fix the bug
    const findFirstTextNode = anchor => {
      if (anchor.nodeType === 3) return anchor
      const children = anchor.childNodes
      for (const node of children) {
        if (node.nodeName !== 'INPUT') {
          return findFirstTextNode(node)
        }
      }
    }
    if (anchorNode.nodeName === 'LI') {
      anchorNode = findFirstTextNode(anchorNode)
    }

    let startParagraph = findNearestParagraph(anchorNode)
    let endParagraph = findNearestParagraph(focusNode)

    const getOffsetOfParagraph = (node, paragraph) => {
      let offset = 0
      let preSibling = node

      if (node === paragraph) return offset

      do {
        preSibling = preSibling.previousSibling
        if (preSibling) {
          offset += getTextContent(preSibling, [ CLASS_OR_ID['AG_MATH_RENDER'] ]).length
        }
      } while (preSibling)
      return (node === paragraph || node.parentNode === paragraph)
        ? offset
        : offset + getOffsetOfParagraph(node.parentNode, paragraph)
    }

    if (startParagraph === endParagraph) {
      const key = startParagraph.id
      const offset1 = getOffsetOfParagraph(anchorNode, startParagraph) + anchorOffset
      const offset2 = getOffsetOfParagraph(focusNode, endParagraph) + focusOffset
      return {
        start: { key, offset: Math.min(offset1, offset2) },
        end: { key, offset: Math.max(offset1, offset2) }
      }
    } else {
      const order = compareParagraphsOrder(startParagraph, endParagraph)

      const rawCursor = {
        start: {
          key: startParagraph.id,
          offset: getOffsetOfParagraph(anchorNode, startParagraph) + anchorOffset
        },
        end: {
          key: endParagraph.id,
          offset: getOffsetOfParagraph(focusNode, endParagraph) + focusOffset
        }
      }
      if (order) {
        return rawCursor
      } else {
        return { start: rawCursor.end, end: rawCursor.start }
      }
    }
  }

  getCursorCoords () {
    const sel = this.doc.getSelection()
    let range
    let x = 0
    let y = 0

    if (sel.rangeCount) {
      range = sel.getRangeAt(0).cloneRange()
      if (range.getClientRects) {
        range.collapse(true)
        const rects = range.getClientRects()

        if (rects.length) {
          const { left, top, x: rectX, y: rectY } = rects[0]
          x = rectX || left
          y = rectY || top
        }
      }
    }

    return { x, y }
  }

  getSelectionEnd () {
    const node = this.doc.getSelection().focusNode
    const endNode = (node && node.nodeType === 3 ? node.parentNode : node)

    return endNode
  }
}

export default new Selection(document)


import fileIcons from '../fileIcons'
import {
CLASS_OR_ID
} from '../config'
import './index.css'

keep replace replace replace keep keep keep

# Mark Text on Linux

## AppImage

Download the AppImage and type the following:

1. `chmod +x marktext-%version%-x86_64.AppImage`
2. `./marktext-%version%-x86_64.AppImage`
3. Now you can decide whether you want to install Mark Text (yes) or just execute it (no).

### Installation

You cannot really install a AppImage. It's just a file which can be integrated with your desktop environment. The only thing you have to do is to execute Mark Text and click `Yes` on dialog.

### Uninstall

1. Delete AppImage file
2. Delete `~/.local/share/applications/appimagekit-marktext.desktop`
3. Delete your user settings: `~/.config/marktext`

### Extract application

1. `./marktext-%version%-x86_64.AppImage --appimage-extract`
2. Move Mark Text into another location: `mkdir ~/bin/marktext && mv squashfs-root/app/* ~/bin/marktext`
3. Delete `squashfs-root` folder

### Known issues

Some error output during application update. Just ignore it if everything is ok.

## Snappy

Downlaod snap file and execute `sudo snap install --classic --dangerous marktext_%version%_amd64.snap`.

Please note that snappy packages may not work on not Debian-based distros and SELinux distros.

### Known issues

- Library problems on SELinux distros (even if SELinux is disabled) and not Debian-based distros.
- User directories (like documents and downloads) are always saved under the english name.


<template>

    <el-dialog
      :visible.sync="dialogTableVisible"
      :show-close="isShowClose"
      :modal="true"
      custom-class="ag-dialog-table"
      width="450px"
      center
    >

        <svg class="icon" aria-hidden="true">
          <use xlink:href="#icon-table-3d"></use>
        </svg>

      <el-form :model="tableChecker" :inline="true">
        <el-form-item label="Rows">
          <el-input-number
            ref="rowInput"
            size="mini"
            v-model="tableChecker.rows"
            controls-position="right"
            :min="2"
            :max="20"
          ></el-input-number>
        </el-form-item>
        <el-form-item label="Columns">
          <el-input-number
            size="mini"
            v-model="tableChecker.columns"
            controls-position="right"
            :min="2"
            :max="20"
          ></el-input-number>
        </el-form-item>
      </el-form>

        <el-button @click="dialogTableVisible = false" size="mini">
          <svg class="icon" aria-hidden="true">
            <use xlink:href="#icon-close"></use>
          </svg>
        </el-button>
        <el-button type="primary" @click="handleDialogTableConfirm" size="mini">
          <svg class="icon" aria-hidden="true">
            <use xlink:href="#icon-gou"></use>
          </svg>
        </el-button>

    </el-dialog>

</template>


const defaultSearchOption = {
  caseSensitive: false,
  selectHighlight: false,
  highlightIndex: -1
}

const searchCtrl = ContentState => {
  ContentState.prototype.replaceOne = function (match, value) {
    const {
      start,
      end,
      key
    } = match
    const block = this.getBlock(key)
    const {
      text
    } = block

    block.text = text.substring(0, start) + value + text.substring(end)
  }

  ContentState.prototype.replace = function (replaceValue, opt = { isSingle: true }) {
    const { isSingle, caseSensitive } = opt
    const { matches, value, index } = this.searchMatches
    if (matches.length) {
      if (isSingle) {
        this.replaceOne(matches[index], replaceValue)
      } else {
        // replace all
        for (const match of matches) {
          this.replaceOne(match, replaceValue)
        }
      }
      const highlightIndex = index < matches.length - 1 ? index : index - 1
      this.search(value, { caseSensitive, highlightIndex: isSingle ? highlightIndex : -1 })
    }
  }

  ContentState.prototype.setCursorToHighlight = function () {
    const { matches, index } = this.searchMatches
    const match = matches[index]

    if (!match) return
    const { key, start, end } = match

    this.cursor = {
      start: {
        key,
        offset: start
      },
      end: {
        key,
        offset: end
      }
    }
  }

  ContentState.prototype.find = function (action/* prev next */) {
    let { matches, index } = this.searchMatches
    const len = matches.length
    if (!len) return
    index = action === 'next' ? index + 1 : index - 1
    if (index < 0) index = len - 1
    if (index >= len) index = 0
    this.searchMatches.index = index

    this.setCursorToHighlight()
  }

  ContentState.prototype.search = function (value, opt = {}) {
    value = value.trim()
    let matches = []
    const { caseSensitive, highlightIndex } = Object.assign(defaultSearchOption, opt)
    const { blocks } = this
    const search = blocks => {
      for (const block of blocks) {
        let { text, key } = block
        if (!caseSensitive) {
          text = text.toLowerCase()
          value = value.toLowerCase()
        }
        if (text) {
          let i = text.indexOf(value)
          while (i > -1) {
            matches.push({
              key,
              start: i,
              end: i + value.length
            })
            i = text.indexOf(value, i + value.length)
          }
        }
        if (block.children.length) {
          search(block.children)
        }
      }
    }
    if (value) search(blocks)
    let index = -1
    if (highlightIndex !== -1) {
      index = highlightIndex
    } else if (matches.length) {
      index = 0
    }

    Object.assign(this.searchMatches, { value, matches, index })
    if (value) this.setCursorToHighlight()
    return matches
  }
}

export default searchCtrl


import 'codemirror/addon/edit/closebrackets'
import 'codemirror/addon/edit/closetag'
import 'codemirror/addon/selection/active-line'
import 'codemirror/mode/meta'
import codeMirror from 'codemirror/lib/codemirror'

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

loadmode(codeMirror)
overlayMode(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(({ name }) => getModeFromName(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
  return new Promise(resolve => {
    codeMirror.requireMode(mode, () => {
      doc.setOption('mode', mime || mode)
      codeMirror.autoLoadMode(doc, mode)
      resolve(m)
    })
  })
}

export default codeMirror


<template>
<div class="editor-container">
<title-bar
:pathname="pathname"
:active="windowActive"
:word-count="wordCount"
></title-bar>
<editor

marktext

A markdown editor

node .electron-vue/build.js && electron-builder --dir

cross-env BUILD_TARGET=clean node .electron-vue/build.js

cross-env BUILD_TARGET=web node .electron-vue/build.js

eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test

eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test

npm run pack:main && npm run pack:renderer

cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js

cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js

Output: build

md markdown txt mmd text mdown mdtxt mdtext


MIT License

Copyright (c) 2017-2018 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.


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 } from './codeMirror/language'
import Emoji, { checkEditEmoji, setInlineEmoji } from './emojis'
import floatBox from './floatBox'
import { findNearestParagraph, operateClassName } from './utils/domManipulate'
import ExportMarkdown from './utils/exportMarkdown'
import ExportStyledHTML from './utils/exportStyledHTML'
import exportHtml from './utils/exportUnstylishHtml'
import tablePicker from './tablePicker'

import './assets/symbolIcon' // import symbol icons
import './assets/symbolIcon/index.css'

class Aganippe {
  constructor (container, options) {
    this.container = container
    this.eventCenter = eventCenter
    this.floatBox = floatBox
    this.tablePicker = tablePicker
    this.contentState = new ContentState()
    this.emoji = new Emoji() // emoji instance: has search(text) clear() methods.
    this.focusMode = false
    // private property
    this._isEditChinese = false
    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.dispatchEditEmoji()
    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()
      this.dispatchChange()
    }, 300))

    eventCenter.attachDOMEvent(container, 'paste', event => {
      this.contentState.pasteHandler(event)
    })

    this.recordEditChinese()
    this.imageClick()
    this.listItemCheckBoxClick()
    this.dispatchArrow()
    this.dispatchBackspace()
    this.dispatchEnter()
    this.dispatchUpdateState()
    this.dispatchCopyCut()
    this.dispatchTableToolBar()
    this.dispatchCodeBlockClick()
  }

  /**
   * [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
  }

  dispatchChange () {
    const { eventCenter } = this
    const markdown = this.getMarkdown()
    const wordCount = this.getWordCount()
    eventCenter.dispatch('change', markdown, wordCount)
  }

  dispatchCopyCut () {
    const { container, eventCenter } = this
    const handler = event => {
      this.contentState.copyCutHandler(event)
      if (event.type === 'cut') {
        // when user use `cut` function, the dom has been deleted by default.
        // But should update content state manually.
        this.contentState.cutHandler()
      }
    }
    eventCenter.attachDOMEvent(container, 'cut', handler)
    eventCenter.attachDOMEvent(container, 'copy', handler)
  }

  /**
   * dispatchEditEmoji
  */
  dispatchEditEmoji () {
    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, top
        }, cb)
        this.floatBox.setOptions(list)
      } else {
        this.floatBox.hideIfNeeded()
      }
    }
  }

  dispatchHideFloatBox () {
    const { container, eventCenter } = this
    let cacheTop = null
    const handler = event => {
      if (event.type === 'scroll') {
        const scrollTop = container.scrollTop
        if (cacheTop && Math.abs(scrollTop - cacheTop) > 10) {
          cacheTop = null
          return eventCenter.dispatch('hideFloatBox')
        } else {
          cacheTop = scrollTop
          return
        }
      }
      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)
    eventCenter.attachDOMEvent(container, 'scroll', throttle(handler, 200))
  }

  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 => {
      this.contentState.selectLanguage(paragraph, item.name)
      this.floatBox.hideIfNeeded()
    }
    if (modes.length) {
      this.floatBox.showIfNeeded({
        left, top
      }, 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)
  }

  recordEditChinese () {
    const { container, eventCenter } = this
    const handler = event => {
      if (event.type === 'compositionstart') {
        this._isEditChinese = true
      } else if (event.type === 'compositionend') {
        this._isEditChinese = false
      }
    }

    eventCenter.attachDOMEvent(container, 'compositionend', handler)
    eventCenter.attachDOMEvent(container, 'compositionstart', handler)
  }

  dispatchEnter (event) {
    const { container, eventCenter } = this

    const handler = event => {
      if (event.key === EVENT_KEYS.Enter && !this._isEditChinese) {
        this.contentState.enterHandler(event)
      }
    }

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

  // dispatch arrow event
  dispatchArrow () {
    const { container, eventCenter } = this
    const handler = event => {
      if (this._isEditChinese) return
      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
        case EVENT_KEYS.Tab:
          this.contentState.tabHandler(event)
          break
      }
    }
    eventCenter.attachDOMEvent(container, 'keydown', handler)
  }

  dispatchCodeBlockClick () {
    const { container, eventCenter } = this
    const handler = event => {
      const target = event.target
      if (target.tagName === 'PRE' && target.classList.contains(CLASS_OR_ID['AG_CODE_BLOCK'])) {
        this.contentState.focusCodeBlock(event)
      }
    }

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

  dispatchTableToolBar () {
    const { container, eventCenter } = this
    const getToolItem = target => {
      // poor implement， fix me
      const parent = target.parentNode
      const grandPa = parent && parent.parentNode
      if (target.hasAttribute('data-label')) return target
      if (parent && parent.hasAttribute('data-label')) return parent
      if (grandPa && grandPa.hasAttribute('data-label')) return grandPa
      return null
    }
    const handler = event => {
      const target = event.target
      const toolItem = getToolItem(target)
      if (toolItem) {
        event.preventDefault()
        event.stopPropagation()
        const type = toolItem.getAttribute('data-label')
        this.contentState.tableToolBarClick(type)
      }
    }

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

  dispatchUpdateState () {
    const { container, eventCenter } = this
    const changeHandler = event => {
      // const target = event.target
      // const style = getComputedStyle(target)
      // if (event.type === 'click' && !style.contenteditable) return
      if (!this._isEditChinese || event.type === 'input') {
        this.contentState.updateState(event)
      }
      if (event.type === 'click' || event.type === 'keyup') {
        const selectionChanges = this.contentState.selectionChange()
        const { formats } = this.contentState.selectionFormats()
        eventCenter.dispatch('selectionChange', selectionChanges)
        eventCenter.dispatch('selectionFormats', formats)
      }
    }

    eventCenter.attachDOMEvent(container, 'click', changeHandler)
    eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
    eventCenter.attachDOMEvent(container, 'input', 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)
  }

  listItemCheckBoxClick () {
    const { container, eventCenter } = this
    const handler = event => {
      const target = event.target
      if (target.tagName === 'INPUT' && target.classList.contains(CLASS_OR_ID['AG_TASK_LIST_ITEM_CHECKBOX'])) {
        this.contentState.listItemCheckBoxClick(target)
      }
    }

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

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

  async exportStyledHTML () {
    const html = await new ExportStyledHTML().generate()
    return html
  }

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

  getWordCount () {
    return this.contentState.wordCount()
  }

  setMarkdown (markdown, cursor) {
    // if markdown is blank, dont need to import markdown
    if (!markdown.trim()) return
    this.contentState.importMarkdown(markdown)
    this.contentState.importCursor(cursor)
    this.contentState.render()
    this.dispatchChange()
  }

  createTable (tableChecker) {
    const { eventCenter } = this
    this.contentState.createFigure(tableChecker)
    const selectionChanges = this.contentState.selectionChange()
    eventCenter.dispatch('selectionChange', selectionChanges)
  }

  getSelection () {
    return this.contentState.selectionChange()
  }

  setFocusMode (bool) {
    const { container, focusMode } = this
    if (bool && !focusMode) {
      container.classList.add(CLASS_OR_ID['AG_FOCUS_MODE'])
    } else {
      container.classList.remove(CLASS_OR_ID['AG_FOCUS_MODE'])
    }
    this.focusMode = bool
  }

  updateParagraph (type) {
    this.contentState.updateParagraph(type)
  }

  format (type) {
    this.contentState.format(type)
  }

  search (value, opt) {
    const { selectHighlight } = opt
    this.contentState.search(value, opt)
    this.contentState.render(!!selectHighlight)
    return this.contentState.searchMatches
  }

  replace (value, opt) {
    this.contentState.replace(value, opt)
    this.contentState.render(false)
    return this.contentState.searchMatches
  }

  find (action/* pre or next */) {
    this.contentState.find(action)
    this.contentState.render(false)
    return this.contentState.searchMatches
  }

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

  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


/* Flowchart variables */
/* Sequence Diagram variables */
/* Gantt chart variables */
.mermaid .label {
  color: #323D47;
}
.node rect,
.node circle,
.node ellipse,
.node polygon {
  fill: #BDD5EA;
  stroke: #81B1DB;
  stroke-width: 1px;
}
.edgePath .path {
  stroke: lightgrey;
}
.edgeLabel {
  background-color: #e8e8e8;
}
.cluster rect {
  fill: #6D6D65 !important;
  rx: 4 !important;
  stroke: rgba(255, 255, 255, 0.25) !important;
  stroke-width: 1px !important;
}
.cluster text {
  fill: #F9FFFE;
}
.actor {
  stroke: #81B1DB;
  fill: #BDD5EA;
}
text.actor {
  fill: black;
  stroke: none;
}
.actor-line {
  stroke: lightgrey;
}
.messageLine0 {
  stroke-width: 1.5;
  stroke-dasharray: "2 2";
  marker-end: "url(#arrowhead)";
  stroke: lightgrey;
}
.messageLine1 {
  stroke-width: 1.5;
  stroke-dasharray: "2 2";
  stroke: lightgrey;
}
#arrowhead {
  fill: lightgrey !important;
}
#crosshead path {
  fill: lightgrey !important;
  stroke: lightgrey !important;
}
.messageText {
  fill: lightgrey;
  stroke: none;
}
.labelBox {
  stroke: #81B1DB;
  fill: #BDD5EA;
}
.labelText {
  fill: #323D47;
  stroke: none;
}
.loopText {
  fill: lightgrey;
  stroke: none;
}
.loopLine {
  stroke-width: 2;
  stroke-dasharray: "2 2";
  marker-end: "url(#arrowhead)";
  stroke: #81B1DB;
}
.note {
  stroke: rgba(255, 255, 255, 0.25);
  fill: #fff5ad;
}
.noteText {
  fill: black;
  stroke: none;
  font-family: 'trebuchet ms', verdana, arial;
  font-size: 14px;
}
/** Section styling */
.section {
  stroke: none;
  opacity: 0.2;
}
.section0 {
  fill: rgba(255, 255, 255, 0.3);
}
.section2 {
  fill: #EAE8B9;
}
.section1,
.section3 {
  fill: white;
  opacity: 0.2;
}
.sectionTitle0 {
  fill: #F9FFFE;
}
.sectionTitle1 {
  fill: #F9FFFE;
}
.sectionTitle2 {
  fill: #F9FFFE;
}
.sectionTitle3 {
  fill: #F9FFFE;
}
.sectionTitle {
  text-anchor: start;
  font-size: 11px;
  text-height: 14px;
}
/* Grid and axis */
.grid .tick {
  stroke: rgba(255, 255, 255, 0.3);
  opacity: 0.3;
  shape-rendering: crispEdges;
}
.grid .tick text {
  fill: lightgrey;
  opacity: 0.5;
}
.grid path {
  stroke-width: 0;
}
/* Today line */
.today {
  fill: none;
  stroke: #DB5757;
  stroke-width: 2px;
}
/* Task styling */
/* Default task */
.task {
  stroke-width: 1;
}
.taskText {
  text-anchor: middle;
  font-size: 11px;
}
.taskTextOutsideRight {
  fill: #323D47;
  text-anchor: start;
  font-size: 11px;
}
.taskTextOutsideLeft {
  fill: #323D47;
  text-anchor: end;
  font-size: 11px;
}
/* Specific task settings for the sections*/
.taskText0,
.taskText1,
.taskText2,
.taskText3 {
  fill: #323D47;
}
.task0,
.task1,
.task2,
.task3 {
  fill: #BDD5EA;
  stroke: rgba(255, 255, 255, 0.5);
}
.taskTextOutside0,
.taskTextOutside2 {
  fill: lightgrey;
}
.taskTextOutside1,
.taskTextOutside3 {
  fill: lightgrey;
}
/* Active task */
.active0,
.active1,
.active2,
.active3 {
  fill: #81B1DB;
  stroke: rgba(255, 255, 255, 0.5);
}
.activeText0,
.activeText1,
.activeText2,
.activeText3 {
  fill: #323D47 !important;
}
/* Completed task */
.done0,
.done1,
.done2,
.done3 {
  fill: lightgrey;
}
.doneText0,
.doneText1,
.doneText2,
.doneText3 {
  fill: #323D47 !important;
}
/* Tasks on the critical line */
.crit0,
.crit1,
.crit2,
.crit3 {
  stroke: #E83737;
  fill: #E83737;
  stroke-width: 2;
}
.activeCrit0,
.activeCrit1,
.activeCrit2,
.activeCrit3 {
  stroke: #E83737;
  fill: #81B1DB;
  stroke-width: 2;
}
.doneCrit0,
.doneCrit1,
.doneCrit2,
.doneCrit3 {
  stroke: #E83737;
  fill: lightgrey;
  stroke-width: 1;
  cursor: pointer;
  shape-rendering: crispEdges;
}
.doneCritText0,
.doneCritText1,
.doneCritText2,
.doneCritText3 {
  fill: lightgrey !important;
}
.activeCritText0,
.activeCritText1,
.activeCritText2,
.activeCritText3 {
  fill: #323D47 !important;
}
.titleText {
  text-anchor: middle;
  font-size: 18px;
  fill: lightgrey;
}
/*

*/
.node text {
  font-family: 'trebuchet ms', verdana, arial;
  font-size: 14px;
}
div.mermaidTooltip {
  position: absolute;
  text-align: center;
  max-width: 200px;
  padding: 2px;
  font-family: 'trebuchet ms', verdana, arial;
  font-size: 12px;
  background: #6D6D65;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 2px;
  pointer-events: none;
  z-index: 100;
}


'pathname', 'filename', 'windowActive', 'wordCount',

<template>
  <div class="editor-container">
    <title-bar
      :pathname="pathname"
      :active="windowActive"
      :word-count="wordCount"
    ></title-bar>
    <editor
      :typewriter="typewriter"
      :focus="focus"
      :source-code="sourceCode"
      :markdown="markdown"
      :cursor="cursor"
      v-if="!sourceCode"
    ></editor>
    <source-code
      v-else
      :markdown="markdown"
      :cursor="cursor"
    ></source-code>
    <search
      v-if="!sourceCode"
    ></search>
  </div>
</template>

<script>
  import Editor from '@/components/editor'
  import TitleBar from '@/components/titleBar'
  import Search from '@/components/search'
  import SourceCode from '@/components/sourceCode'
  import { mapState } from 'vuex'

  export default {
    name: 'marktext',
    components: {
      Editor,
      TitleBar,
      Search,
      SourceCode
    },
    data () {
      return {}
    },
    computed: {
      ...mapState(['pathname', 'windowActive', 'wordCount', 'typewriter', 'focus', 'sourceCode', 'markdown', 'cursor'])
    },
    created () {
      const { dispatch } = this.$store

      dispatch('ASK_FOR_MODE')
      dispatch('LISTEN_FOR_CLOSE')
      dispatch('LISTEN_FOR_SAVE_AS')
      dispatch('LINTEN_WIN_STATUS')
      dispatch('LISTEN_FOR_SAVE')
      dispatch('GET_FILENAME')
      dispatch('LISTEN_FOR_FILE_LOAD')
      dispatch('LISTEN_FOR_FILE_CHANGE')
      dispatch('LISTEN_FOR_EDIT')
      dispatch('LISTEN_FOR_VIEW')
      dispatch('LISTEN_FOR_EXPORT')
      dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
    }
  }
</script>

<style>
  .editor-container {
    padding-top: 22px;
  }
  .editor-container .hide {
    z-index: -1;
    opacity: 0;
    position: absolute;
    left: -10000px;
  }
</style>


import { generateKeyHash, genUpper2LowerKeyHash } from './utils'

// export const markedSymbol = ['\\*', '\\-', '\\_', '\\!', '\\[', '\\]', '\\+',
//   '\\\\', '\\)', '\\(', '\\`', '\\{', '\\}', '\\#', '\\.']
/**
 * configs
 */
// export const INLINE_RULES = ['autolink', 'backticks', 'emphasis', 'escape', 'image', 'link', 'strikethrough']
export const PARAGRAPH_TYPES = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'ol', 'li', 'figure']
export const blockContainerElementNames = [
  // elements our editor generates
  'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
  // all other known block elements
  'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',
  'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',
  'noscript', 'output', 'section', 'video',
  'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
]

export const emptyElementNames = ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr']

export const EVENT_KEYS = generateKeyHash([
  'Enter',
  'Backspace',
  'Delete',
  'ArrowUp',
  'ArrowDown',
  'ArrowLeft',
  'ArrowRight',
  'Tab'
])

export const LOWERCASE_TAGS = generateKeyHash([
  ...blockContainerElementNames, ...emptyElementNames, 'div'
])
/**
 * ALL className, id, attributes need has a `AG_` prefix,
 * element's id will has a `_ID` postfix,
 * attribute will has a `_ATTR` postfix.
 * className has no postfix.
 * example:
 *   genUpper2LowerKeyHash(['AG_GRAY'])
 *   => {
 *     'AG_GRAY': 'ag-gray'
 *   }
 */

export const CLASS_OR_ID = genUpper2LowerKeyHash([
  'mousetrap',
  'AG_GRAY',
  'AG_HIDE',
  'AG_WARN',
  'AG_PARAGRAPH', // => 'ag-paragraph'
  'AG_ACTIVE',
  'AG_EDITOR_ID',
  'AG_FLOAT_BOX_ID',
  'AG_FLOAT_BOX',
  'AG_SHOW_FLOAT_BOX',
  'AG_FLOAT_ITEM', // LI element
  'AG_FLOAT_ITEM_ACTIVE',
  'AG_FLOAT_ITEM_ICON', // icon wrapper in li
  'AG_EMOJI_MARKED_TEXT',
  'AG_CODE_BLOCK',
  'AG_LANGUAGE',
  'AG_LANGUAGE_INPUT',
  'AG_TEMP',
  'AG_FOCUS_MODE',
  'AG_LINK_IN_BRACKET',
  'AG_BACKLASH',
  'AG_BUG', // for remove bug
  'AG_IMAGE_MARKED_TEXT',
  'AG_IMAGE_FAIL',
  'AG_REMOVE',
  'AG_EMOJI_MARKER',
  'AG_NOTEXT_LINK',
  'AG_ORDER_LIST',
  'AG_ORDER_LIST_ITEM',
  'AG_BULLET_LIST',
  'AG_BULLET_LIST_ITEM',
  'AG_TASK_LIST',
  'AG_TASK_LIST_ITEM',
  'AG_TASK_LIST_ITEM_CHECKBOX',
  'AG_CHECKBOX_CHECKED',
  'AG_TABLE_TOOL_BAR',
  'AG_SELECTION',
  'AG_HIGHLIGHT'
])

export const codeMirrorConfig = {
  autoCloseBrackets: true,
  lineWiseCopyCut: false,
  autoCloseTags: true,
  autofocus: true,
  tabSize: 2,
  extraKeys: {
    'Cmd-Z': false,
    'Cmd-Y': false
  }
}

export const DAED_REMOVE_SELECTOR = new Set([
  '.ag-image-marked-text::before',
  '.ag-image-marked-text.ag-image-fail::before',
  '.ag-hide',
  '.ag-gray',
  '.ag-warn'
])

export const htmlBeautifyConfig = {
  'indent_size': 4,
  'html': {
    'end_with_newline': true,
    'js': {
      'indent_size': 2
    },
    'css': {
      'indent_size': 2
    }
  },
  'css': {
    'indent_size': 1
  },
  'js': {
    'preserve-newlines': true
  }
}

export const turndownConfig = {
  headingStyle: 'atx',
  bulletListMarker: '*',
  codeBlockStyle: 'fenced',
  emDelimiter: '_',
  strongDelimiter: '**'
}


import * as actions from '../actions/format'

export default {
  label: 'Format',
  submenu: [{
    label: 'Strong',
    type: 'checkbox',
    accelerator: 'Shift+CmdOrCtrl+B',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'strong')
    }
  }, {
    label: 'Emphasis',
    type: 'checkbox',
    accelerator: 'CmdOrCtrl+E',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'em')
    }
  }, {
    label: 'Inline Code',
    type: 'checkbox',
    accelerator: 'CmdOrCtrl+`',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'inline_code')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Strike',
    type: 'checkbox',
    accelerator: 'CmdOrCtrl+D',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'del')
    }
  }, {
    label: 'Hyperlink',
    type: 'checkbox',
    accelerator: 'CmdOrCtrl+L',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'link')
    }
  }, {
    label: 'Image',
    type: 'checkbox',
    accelerator: 'Shift+CmdOrCtrl+I',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'image')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Clear Format',
    accelerator: 'Shift+CmdOrCtrl+R',
    click (menuItem, browserWindow) {
      actions.format(browserWindow, 'clear')
    }
  }]
}


const { y } = this.editor.getSelection().cursorCoords

animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 300)

keep keep keep replace replace replace replace replace keep keep keep keep keep


### 0.5.2

**Features**

- Add Typewriter Mode, The current line will always in the center of the document. If you change the current line, it will be auto scroll to the new line.

- Add Focus Mode, the current paragraph's will be focused.

**Optimization**

- Optimize the display of path name and file name in title bar.

- Eidtor will auto scroll to the highlight word when click Find Prev or Find Next.

**Bug fix**

### 0.4.0

**Feature**

- Search value in document, Use **FIND PREV** and **FIND NEXT** to selection previous one or next one.

  Add animation of highlight word.

  Auto focus the search input when open search panel.

  close the search panel will auto selection the last highlight word by ESC button.

- Replace value

  Replace All

  Replace one and auto highlight the next word.

**Bug fix**

- fix the bug that click at the edge of code block will caused the code block does not be focused.

**Optimization**

- Optimize the display of word count in title bar. we also delete the background color of title bar to make it more concise.

- Customize the style of checkbox in Task List Item.

- Change the display of Insert Table dialog.

### 0.3.0

**Features**

- Export PDF

**Bug fix**

- fix the bug that editor can only print the first page.


@include-when-export url(http://fonts.googleapis.com/css?family=Ubuntu:400,700,400italic,700italic);
@include-when-export url(http://fonts.googleapis.com/css?family=Raleway:600,400&subset=latin,latin-ext);

@charset "UTF-8";
@font-face {
    font-family: 'TeXGyreAdventor';
    src: url('texgyreadventor-regular.otf');
    font-weight: normal;
    font-style: normal;

}

@charset "UTF-8";
@font-face {
	font-family: 'TeXGyreAdventor';
	font-style: normal;
	font-weight: normal;
	src: url(./gothic/texgyreadventor-regular.otf);
}
@font-face {
	font-family: 'TeXGyreAdventor';
	font-style: normal;
	font-weight: bold;
	src: url(./gothic/texgyreadventor-bold.otf);
}
@font-face {
	font-family: 'TeXGyreAdventor';
	font-style: italic;
	font-weight: normal;
	src: url(./gothic/texgyreadventor-italic.otf);
}
@font-face {
	font-family: 'TeXGyreAdventor';
	font-style: italic;
	font-weight: bold;
	src: url(./gothic/texgyreadventor-bolditalic.otf);
}

:root {
	--window-border: none;
	--typora-center-window-title: true;
	--active-file-bg-color: #f3f3f3;
	--bg-color: #fcfcfc;
	--control-text-color: #777;
}

.mac-seamless-mode #typora-sidebar {
	top: var(--mac-title-bar-height);
	padding-top: 0;
	height: calc(100% - var(--mac-title-bar-height));
}

html,
body,
#ag-editor-id{
	background: #fcfcfc;
	font-family: 'TeXGyreAdventor', "Century Gothic", 'Yu Gothic', 'Raleway', "STHeiti", sans-serif;
	font-weight: 400;
}
h1,
h2,
h3,
h4,
h5,
h6 {
	color: #111;
	font-family: 'TeXGyreAdventor', "Century Gothic", 'Yu Gothic', 'Ubuntu', "STHeiti", sans-serif;
}

html {
	font-size:16px;
}

#ag-editor-id {
	max-width: 914px;
	text-align: justify;
}

#ag-editor-id>h1:first-child {
	margin-top: 2.75rem;
}
#ag-editor-id>h2:first-child {
	margin-top: 1.75rem;
}
#ag-editor-id>h3:first-child {
	margin-top: 1rem;
}
#ag-editor-id>h4:first-child {
	margin-top: 0.5rem;
}
h1 {
	font-weight: normal;
	line-height: 4rem;
	margin: 0 0 1.75rem;
	padding: 20px 30px;
	text-align: center;
	text-transform: uppercase;
	margin-top: 4rem;
}
h2 {
	font-weight: normal;
	line-height: 3rem;
	margin: 0 0 1.9375rem;
	padding: 0 30px;
	text-align: center;
	text-transform: uppercase;
	margin-top: 3rem
}
h3 {
	font-weight: normal;
}
h4 {
	font-weight: normal;
}
h5 {
	font-size: 1.125rem;
	font-weight: normal;
}
h6 {
	font-size: 1rem;
	font-weight: bold;
}
p {
	color: #111;
	font-size: 1rem;
	line-height: 1.75rem;
	margin: 0 0 1.25rem;
}
#ag-editor-id>h3.md-focus:before {
	left: -1.875rem;
	top: 0.5rem;
	padding: 2px;
}
#ag-editor-id>h4.md-focus:before {
	left: -1.875rem;
	top: 0.3125rem;
	padding: 2px;
}
#ag-editor-id>h5.md-focus:before {
	left: -1.875rem;
	top: 0.25rem;
	padding: 2px;
}
#ag-editor-id>h6.md-focus:before {
	left: -1.875rem;
	top: .125rem;
	padding: 2px;
}
/*@media screen and (min-width: 48em) {
	.h1,
	h1 {
		font-size: 3.250rem;
	}
	.h2,
	h2 {
		font-size: 2.298rem;
	}
	.h3,
	h3 {
		font-size: 1.625rem;
	}
	.h4,
	h4 {
		font-size: 1.250rem;
	}
	.h5,
	h5 {
		font-size: 1.150rem;
	}
	.h6,
	h6 {
		font-size: 1rem;
	}
	#ag-editor-id>h4.md-focus:before,
	#ag-editor-id>h5.md-focus:before,
	#ag-editor-id>h6.md-focus:before {
		top: 1px;
	}
	p {
		font-size: 1.25rem;
		line-height: 1.8;
	}
	table {
		font-size: 1.25rem;
	}
}*/
@media screen and (max-width: 48em) {
	blockquote {
		margin-left: 1rem;
		margin-right: 0;
		padding: 0.5em;
	}
	.h1,
	h1 {
		font-size: 2.827rem;
	}
	.h2,
	h2 {
		font-size: 1.999rem;
	}
	.h3,
	h3 {
		font-size: 1.413rem;
	}
	.h4,
	h4 {
		font-size: 1.250rem;
	}
	.h5,
	h5 {
		font-size: 1.150rem;
	}
	.h6,
	h6 {
		font-size: 1rem;
	}
}
a,
.md-def-url {
	color: #990000;
	text-decoration: none;
}
a:hover {
	text-decoration: underline;
}
table {
	margin-bottom: 20px
}
table th,
table td {
	padding: 8px;
	line-height: 1.25rem;
	vertical-align: top;
	border-top: 1px solid #ddd
}
table th {
	font-weight: bold
}
table thead th {
	vertical-align: bottom
}
table caption+thead tr:first-child th,
table caption+thead tr:first-child td,
table colgroup+thead tr:first-child th,
table colgroup+thead tr:first-child td,
table thead:first-child tr:first-child th,
table thead:first-child tr:first-child td {
	border-top: 0
}
table tbody+tbody {
	border-top: 2px solid #ddd
}
code, .md-fences {
	padding: .5em;
	/*background: #f0f0f0;*/
	border: 1px solid #ccc;
	padding: .1em;
	font-size: 0.9em;
	margin-left: 0.2em;
	margin-right: 0.2em;
}
.md-fences {
	margin: 0 0 20px;
	font-size: 1em;
	padding: 0.3em 1em;
  	padding-top: 0.4em;
}
.task-list{
	padding-left: 0;
}

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

.task-list-item input {
	top: 3px;

}

.task-list-item input:before {
	content: "";
	display: inline-block;
	width: 1rem;
	height: 1rem;
	vertical-align: middle;
	text-align: center;
	border: 1px solid gray;
	background-color: #fdfdfd;
	margin-left: 0;
	margin-top: -0.8rem;
}
.task-list-item input:checked:before,
.task-list-item input[checked]:before{
	content: '\25FC';
	/*◘*/
	font-size: 0.8125rem;
	line-height: 0.9375rem;
	margin-top: -1rem;
}

/* Chrome 29+ */
@media screen and (-webkit-min-device-pixel-ratio:0)
  and (min-resolution:.001dpcm) {
    .task-list-item input:before {
    	margin-top: -0.2rem;
    }

    .task-list-item input:checked:before,
	.task-list-item input[checked]:before {
		margin-top: -0.2rem;
	}
}

blockquote {
	margin: 0 0 1.11111rem;
	padding: 0.5rem 1.11111rem 0 1.05556rem;
	border-left: 1px solid gray;
}
blockquote,
blockquote p {
	line-height: 1.6;
	color: #6f6f6f;
}
#ag-editor-id pre.md-meta-block {
	min-height: 30px;
	background: #f8f8f8;
	padding: 1.5em;
	font-weight: 300;
	font-size: 1em;
	padding-bottom: 1.5em;
	padding-top: 3em;
    margin-top: -1.5em;
	color: #999;

	width: 100vw;
	max-width: calc(100% + 60px);
	margin-left: -30px;
	border-left: 30px #f8f8f8 solid;
	border-right: 30px #f8f8f8 solid;
}
.MathJax_Display {
	font-size: 0.9em;
	margin-top: 0.5em;
	margin-bottom: 0;
}
p.mathjax-block,
.mathjax-block {
	padding-bottom: 0;
}
.mathjax-block>.code-tooltip {
	bottom: 5px;
	box-shadow: none;
}
.md-image>.md-meta {
	padding-left: 0.5em;
	padding-right: 0.5em;
}
.md-image>img {
	margin-top: 2px;
}
.md-image>.md-meta:first-of-type:before {
	padding-left: 4px;
}

#typora-source {
	color: #555;
}

/** ui for windows **/
#md-searchpanel {
    border-bottom: 1px solid #ccc;
}

#md-searchpanel .btn {
    border: 1px solid #ccc;
}

#md-notification:before {
	top: 14px;
}

#md-notification {
	background: #eee;
}

.megamenu-menu-panel .btn {
	border: 1px solid #ccc;
}

#typora-sidebar {
	box-shadow: none;
}

.file-list-item ,
.show-folder-name .file-list-item {
	padding-top: 20px;
	padding-bottom: 20px;
	line-height: 20px;
}

.file-list-item-summary {
	height: 40px;
	line-height: 20px;
}


keep keep keep keep keep replace keep keep

// init set markdown and edit mode(typewriter mode and focus mode)


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(method => {
      this[method] = mousetrap[method]
    })
  }
  /**
   * [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
   */
  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()


:root {
  --side-bar-bg-color: #fafafa;
  --control-text-color: #777;
}

@include-when-export url(http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,700,400&subset=latin,latin-ext);
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: normal;
  src: local('Open Sans Regular'), url('./github/400.woff') format('woff')
}

@font-face {
  font-family: 'Open Sans';
  font-style: italic;
  font-weight: normal;
  src: local('Open Sans Italic'), url('./github/400i.woff') format('woff')
}

@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: bold;
  src: local('Open Sans Bold'), url('./github/700.woff') format('woff')
}

@font-face {
  font-family: 'Open Sans';
  font-style: italic;
  font-weight: bold;
  src: local('Open Sans Bold Italic'), url('./github/700i.woff') format('woff')
}

html {
  font-size: 16px;
}

body {
  font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: rgb(51, 51, 51);
  line-height: 1.6;
}

#ag-editor-id {
  max-width: 860px;
  margin: 0 auto;
  padding: 20px 30px 40px 30px;
  padding-top: 20px;
  padding-bottom: 100px;
}

#ag-editor-id>ul:first-child,
#ag-editor-id>ol:first-child {
  margin-top: 30px;
}

body>*:first-child {
  margin-top: 0 !important;
}

body>*:last-child {
  margin-bottom: 0 !important;
}

a {
  color: #4183C4;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  position: relative;
  margin-top: 1rem;
  margin-bottom: 1rem;
  font-weight: bold;
  line-height: 1.4;
  cursor: text;
}

h1:hover a.anchor,
h2:hover a.anchor,
h3:hover a.anchor,
h4:hover a.anchor,
h5:hover a.anchor,
h6:hover a.anchor {
  /*background: url("../../images/modules/styleguide/para.png") no-repeat 10px center;*/
  text-decoration: none;
}

h1 tt,
h1 code {
  font-size: inherit;
}

h2 tt,
h2 code {
  font-size: inherit;
}

h3 tt,
h3 code {
  font-size: inherit;
}

h4 tt,
h4 code {
  font-size: inherit;
}

h5 tt,
h5 code {
  font-size: inherit;
}

h6 tt,
h6 code {
  font-size: inherit;
}

h1 {
  padding-bottom: .3em;
  font-size: 2.25em;
  line-height: 1.2;
  border-bottom: 1px solid #eee;
}

h2 {
  padding-bottom: .3em;
  font-size: 1.75em;
  line-height: 1.225;
  border-bottom: 1px solid #eee;
}

h3 {
  font-size: 1.5em;
  line-height: 1.43;
}

h4 {
  font-size: 1.25em;
}

h5 {
  font-size: 1em;
}

h6 {
  font-size: 1em;
  color: #777;
}

p,
blockquote,
ul,
ol,
dl,
table {
  margin: 0.8em 0;
}

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

hr {
  height: 4px;
  padding: 0;
  margin: 16px 0;
  background-color: #e7e7e7;
  border: 0 none;
  overflow: hidden;
  box-sizing: content-box;
  border-bottom: 1px solid #ddd;
}

body>h2:first-child {
  margin-top: 0;
  padding-top: 0;
}

body>h1:first-child {
  margin-top: 0;
  padding-top: 0;
}

body>h1:first-child+h2 {
  margin-top: 0;
  padding-top: 0;
}

body>h3:first-child,
body>h4:first-child,
body>h5:first-child,
body>h6:first-child {
  margin-top: 0;
  padding-top: 0;
}

a:first-child h1,
a:first-child h2,
a:first-child h3,
a:first-child h4,
a:first-child h5,
a:first-child h6 {
  margin-top: 0;
  padding-top: 0;
}

h1 p,
h2 p,
h3 p,
h4 p,
h5 p,
h6 p {
  margin-top: 0;
}

li p.first {
  display: inline-block;
}

ul,
ol {
  padding-left: 30px;
}

ul:first-child,
ol:first-child {
  margin-top: 0;
}

ul:last-child,
ol:last-child {
  margin-bottom: 0;
}

blockquote {
  border-left: 4px solid #dddddd;
  padding: 0 15px;
  color: #777777;
}

blockquote blockquote {
  padding-right: 0;
}

table {
  padding: 0;
  word-break: initial;
}

table tr {
  margin: 0;
  padding: 0;
}

table thead tr,
table tr:nth-child(2n) {
  background-color: #fafafa;
}

table tr th {
  font-weight: bold;
  border: 1px solid #ebeef5;
  text-align: left;
  margin: 0;
  padding: 6px 13px;
}

table tr td {
  border: 1px solid #ebeef5;
  text-align: left;
  margin: 0;
  padding: 6px 13px;
}

table tr th:first-child,
table tr td:first-child {
  margin-top: 0;
}

table tr th:last-child,
table tr td:last-child {
  margin-bottom: 0;
}

.CodeMirror-gutters {
  border-right: 1px solid #ddd;
}

.md-fences,
code,
tt {
  border: 1px solid #ddd;
  background-color: #f8f8f8;
  border-radius: 3px;
  padding: 0;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
  padding: 2px 4px 0px 4px;
  font-size: 0.9em;
}

/* custom add */

.md-fences,
code {
  border: none;
  padding: 2px 4px;
  font-size: 90%;
  color: #c7254e;
  background-color: #f9f2f4;
  border-radius: 4px;
  font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

.md-fences {
  margin-bottom: 15px;
  margin-top: 15px;
  padding: 0.2em 1em;
  padding-top: 8px;
  padding-bottom: 6px;
  border: none;
}

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

.task-list-item {
  padding-left: 32px;
}

.task-list-item input {
  top: 3px;
  left: 8px;
}

@media screen and (min-width: 914px) {
  /*body {
    width: 854px;
    margin: 0 auto;
  }*/
}

@media print {
  html {
    font-size: 13px;
  }
  table,
  pre {
    page-break-inside: avoid;
  }
  pre {
    word-wrap: break-word;
  }
}

.md-fences {
  background-color: #f8f8f8;
}

#ag-editor-id pre.ag-code-block {
  padding: .4rem 1rem;
  font-size: 90%;
  line-height: 1.6;
  background-color: #f6f8fa;
  border: 0;
  border-radius: 3px;
  color: #777777;
  margin-top: 0;
}

.mathjax-block>.code-tooltip {
  bottom: .375rem;
}

#ag-editor-id>h3.md-focus:before {
  left: -1.5625rem;
  top: .375rem;
}

#ag-editor-id>h4.md-focus:before {
  left: -1.5625rem;
  top: .285714286rem;
}

#ag-editor-id>h5.md-focus:before {
  left: -1.5625rem;
  top: .285714286rem;
}

#ag-editor-id>h6.md-focus:before {
  left: -1.5625rem;
  top: .285714286rem;
}

.md-image>.md-meta {
  border: 1px solid #ddd;
  border-radius: 3px;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
  padding: 2px 4px 0px 4px;
  font-size: 0.9em;
  color: inherit;
}

.md-tag {
  color: inherit;
}

.md-toc {
  margin-top: 20px;
  padding-bottom: 20px;
}

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

#typora-quick-open {
  border: 1px solid #ddd;
  background-color: #f8f8f8;
}

#typora-quick-open-item {
  background-color: #FAFAFA;
  border-color: #FEFEFE #e5e5e5 #e5e5e5 #eee;
  border-style: solid;
  border-width: 1px;
}

#md-notification:before {
  top: 10px;
}

/** focus mode */

.on-focus-mode blockquote {
  border-left-color: rgba(85, 85, 85, 0.12);
}

header,
.context-menu,
.megamenu-content,
footer {
  font-family: "Segoe UI", "Arial", sans-serif;
}

.file-node-content:hover .file-node-icon,
.file-node-content:hover .file-node-open-state {
  visibility: visible;
}

.mac-seamless-mode #typora-sidebar {
  background-color: #fafafa;
  background-color: var(--side-bar-bg-color);
}


bus.$on('undo', () => this.editor.undo())

A markdown editor

Output: build


# Commented sections below can be used to run tests on the CI server
# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing
version: 0.1.{build}

branches:
  only:
    - master

image: Visual Studio 2017
platform:
  - x64

cache:
  - node_modules
  - '%APPDATA%\npm-cache'
  - '%USERPROFILE%\.electron'
  - '%USERPROFILE%\AppData\Local\Yarn\cache'

init:
  - git config --global core.autocrlf input

install:
  - ps: Install-Product node 8 x64
  - choco install yarn --ignore-dependencies
  - git reset --hard HEAD
  - yarn
  - node --version

build_script:
  #- yarn test
  - yarn build

test: off


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

      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
  }
}

export default History


import selection from '../selection'

const tabCtrl = ContentState => {
  ContentState.prototype.findNextCell = function (block) {
    if (!(/td|th/.test(block.type))) throw new Error('only th and td can have next cell')
    const nextSibling = this.getBlock(block.nextSibling)
    const parent = this.getBlock(block.parent)
    const tbOrTh = this.getBlock(parent.parent)
    if (nextSibling) {
      return nextSibling
    } else {
      if (parent.nextSibling) {
        const nextRow = this.getBlock(parent.nextSibling)
        return nextRow.children[0]
      } else if (tbOrTh.type === 'thead') {
        const tBody = this.getBlock(tbOrTh.nextSibling)
        if (tBody.children.length) {
          return tBody.children[0].children[0]
        }
      }
    }
    return false
  }

  ContentState.prototype.tabHandler = function (event) {
    const { start, end } = selection.getCursorRange()
    const startBlock = this.getBlock(start.key)
    const endBlock = this.getBlock(end.key)
    let nextCell
    if (start.key === end.key && /th|td/.test(startBlock.type)) {
      nextCell = this.findNextCell(startBlock)
    } else if (/th|td/.test(endBlock.type)) {
      nextCell = endBlock
    }
    if (nextCell) {
      event.preventDefault()
      const key = nextCell.key
      const textLength = nextCell.text.length
      this.cursor = {
        start: { key, offset: 0 },
        end: { key, offset: textLength }
      }
      this.render()
    }
  }
}

export default tabCtrl


const searchMatches = this.editor.replace(value, opt)

const anchor = document.querySelector('.ag-highlight')

keep keep keep keep add keep keep keep keep

const { y } = this.editor.getSelection().cursorCoords

animatedScrollTo(container, container.scrollTop + y - STANDAR_Y, 300)


import selection from '../selection'
import { tokenizer, generator } from '../parser/parse'

const FORMAT_TYPES = ['strong', 'em', 'del', 'inline_code', 'link', 'image']

const getOffset = (offset, { range: { start, end }, type, anchor, title }) => {
  const dis = offset - start
  const len = end - start
  switch (type) {
    case 'strong':
    case 'del':
    case 'em':
    case 'inline_code': {
      const MARKER_LEN = (type === 'strong' || type === 'del') ? 2 : 1
      if (dis < 0) return 0
      if (dis >= 0 && dis < MARKER_LEN) return -dis
      if (dis >= MARKER_LEN && dis <= len - MARKER_LEN) return -MARKER_LEN
      if (dis > len - MARKER_LEN && dis <= len) return len - dis - 2 * MARKER_LEN
      if (dis > len) return -2 * MARKER_LEN
      break
    }
    case 'link': {
      const MARKER_LEN = 1
      if (dis < MARKER_LEN) return 0
      if (dis >= MARKER_LEN && dis <= MARKER_LEN + anchor.length) return -1
      if (dis > MARKER_LEN + anchor.length) return anchor.length - dis
      break
    }
    case 'image': {
      const MARKER_LEN = 1
      if (dis < MARKER_LEN) return 0
      if (dis >= MARKER_LEN && dis < MARKER_LEN * 2) return -1
      if (dis >= MARKER_LEN * 2 && dis <= MARKER_LEN * 2 + title.length) return -2
      if (dis > MARKER_LEN * 2 + title.length) return title.length - dis
      break
    }
  }
}

const clearFormat = (token, { start, end }) => {
  if (start) {
    const deltaStart = getOffset(start.offset, token)
    start.delata += deltaStart
  }
  if (end) {
    const delataEnd = getOffset(end.offset, token)
    end.delata += delataEnd
  }
  switch (token.type) {
    case 'strong':
    case 'del':
    case 'em':
    case 'link':
    case 'image':
      const parent = token.parent
      const index = parent.indexOf(token)
      parent.splice(index, 1, ...token.children)
      break
    case 'inline_code':
      token.type = 'text'
      delete token.marker
      break
  }
}

const addFormat = (type, block, { start, end }) => {
  if (block.type === 'pre') return false
  const MARKER_MAP = {
    'em': '*',
    'inline_code': '`',
    'strong': '**',
    'del': '~~'
  }
  switch (type) {
    case 'em':
    case 'del':
    case 'inline_code':
    case 'strong': {
      const MARKER = MARKER_MAP[type]
      const oldText = block.text
      block.text = oldText.substring(0, start.offset) +
        MARKER + oldText.substring(start.offset, end.offset) +
        MARKER + oldText.substring(end.offset)
      start.offset += MARKER.length
      end.offset += MARKER.length
      break
    }
    case 'link':
    case 'image': {
      const oldText = block.text
      block.text = oldText.substring(0, start.offset) +
        (type === 'link' ? '[' : '![') +
        oldText.substring(start.offset, end.offset) + ']()' +
        oldText.substring(end.offset)
      start.offset += type === 'link' ? 1 : 2
      end.offset += type === 'link' ? 1 : 2
      break
    }
  }
}

const formatCtrl = ContentState => {
  ContentState.prototype.selectionFormats = function ({ start, end } = selection.getCursorRange()) {
    const startBlock = this.getBlock(start.key)
    const formats = []
    const neighbors = []
    let tokens = []
    if (start.key === end.key) {
      const text = startBlock.text
      tokens = tokenizer(text)
      ;(function iterator (tks) {
        for (const token of tks) {
          if (
            FORMAT_TYPES.includes(token.type) &&
            start.offset >= token.range.start &&
            end.offset <= token.range.end
          ) {
            formats.push(token)
          }
          if (
            FORMAT_TYPES.includes(token.type) &&
            ((start.offset >= token.range.start && start.offset <= token.range.end) ||
            (end.offset >= token.range.start && end.offset <= token.range.end) ||
            (start.offset <= token.range.start && token.range.end <= end.offset))
          ) {
            neighbors.push(token)
          }
          if (token.children && token.children.length) {
            iterator(token.children)
          }
        }
      })(tokens)
    }

    return { formats, tokens, neighbors }
  }

  ContentState.prototype.clearBlockFormat = function (block, { start, end } = selection.getCursorRange(), type) {
    if (block.type === 'pre') return false
    const { key } = block
    let tokens
    let neighbors
    if (start.key === end.key && start.key === key) {
      ({ tokens, neighbors } = this.selectionFormats({ start, end }))
    } else if (start.key !== end.key && start.key === key) {
      ({ tokens, neighbors } = this.selectionFormats({ start, end: { key: start.key, offset: block.text.length } }))
    } else if (start.key !== end.key && end.key === key) {
      ({ tokens, neighbors } = this.selectionFormats({
        start: {
          key: end.key,
          offset: 0
        },
        end
      }))
    } else {
      ({ tokens, neighbors } = this.selectionFormats({
        start: {
          key,
          offset: 0
        },
        end: {
          key,
          offset: block.text.length
        }
      }))
    }

    neighbors = type ? neighbors.filter(n => n.type === type) : neighbors

    for (const neighbor of neighbors) {
      clearFormat(neighbor, { start, end })
    }
    start.offset += start.delata
    end.offset += end.delata
    block.text = generator(tokens)
  }

  ContentState.prototype.format = function (type) {
    const { start, end } = selection.getCursorRange()
    const startBlock = this.getBlock(start.key)
    const endBlock = this.getBlock(end.key)
    start.delata = end.delata = 0
    if (start.key === end.key) {
      const { formats, tokens, neighbors } = this.selectionFormats()
      const currentFormats = formats.filter(format => format.type === type).reverse()
      const currentNeightbors = neighbors.filter(format => format.type === type).reverse()
      // cache delata
      if (type === 'clear') {
        for (const neighbor of neighbors) {
          clearFormat(neighbor, { start, end })
        }
        start.offset += start.delata
        end.offset += end.delata
        startBlock.text = generator(tokens)
      } else if (currentFormats.length) {
        for (const token of currentFormats) {
          clearFormat(token, { start, end })
        }
        start.offset += start.delata
        end.offset += end.delata
        startBlock.text = generator(tokens)
      } else {
        if (currentNeightbors.length) {
          for (const neighbor of currentNeightbors) {
            clearFormat(neighbor, { start, end })
          }
        }
        start.offset += start.delata
        end.offset += end.delata
        startBlock.text = generator(tokens)
        addFormat(type, startBlock, { start, end })
      }
      this.cursor = { start, end }
      this.render()
    } else {
      let nextBlock = startBlock
      const formatType = type !== 'clear' ? type : undefined
      while (nextBlock && nextBlock !== endBlock) {
        this.clearBlockFormat(nextBlock, { start, end }, formatType)
        nextBlock = this.findNextBlockInLocation(nextBlock)
      }
      this.clearBlockFormat(endBlock, { start, end }, formatType)

      if (type !== 'clear') {
        addFormat(type, startBlock, {
          start,
          end: { offset: startBlock.text.length }
        })
        nextBlock = this.findNextBlockInLocation(startBlock)
        while (nextBlock && nextBlock !== endBlock) {
          addFormat(type, nextBlock, {
            start: { offset: 0 },
            end: { offset: nextBlock.text.length }
          })
          nextBlock = this.findNextBlockInLocation(nextBlock)
        }
        addFormat(type, endBlock, {
          start: { offset: 0 },
          end
        })
      }
      console.log(start, end)
      this.cursor = { start, end }
      this.render()
    }
  }
}

export default formatCtrl


'use strict'

const chalk = require('chalk')
const electron = require('electron')
const path = require('path')
const { say } = require('cfonts')
const { spawn } = require('child_process')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const webpackHotMiddleware = require('webpack-hot-middleware')

const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')

let electronProcess = null
let manualRestart = false
let hotMiddleware

function logStats (proc, data) {
  let log = ''

  log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`)
  log += '\n\n'

  if (typeof data === 'object') {
    data.toString({
      colors: true,
      chunks: false
    }).split(/\r?\n/).forEach(line => {
      log += '  ' + line + '\n'
    })
  } else {
    log += `  ${data}\n`
  }

  log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'

  console.log(log)
}

function startRenderer () {
  return new Promise((resolve, reject) => {
    rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)

    const compiler = webpack(rendererConfig)
    hotMiddleware = webpackHotMiddleware(compiler, {
      log: false,
      heartbeat: 2500
    })

    compiler.plugin('compilation', compilation => {
      compilation.plugin('html-webpack-plugin-after-emit', (data, cb) => {
        hotMiddleware.publish({ action: 'reload' })
        cb()
      })
    })

    compiler.plugin('done', stats => {
      logStats('Renderer', stats)
    })

    const server = new WebpackDevServer(
      compiler,
      {
        contentBase: path.join(__dirname, '../'),
        quiet: true,
        setup (app, ctx) {
          app.use(hotMiddleware)
          ctx.middleware.waitUntilValid(() => {
            resolve()
          })
        }
      }
    )

    server.listen(9080)
  })
}

function startMain () {
  return new Promise((resolve, reject) => {
    mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)

    const compiler = webpack(mainConfig)

    compiler.plugin('watch-run', (compilation, done) => {
      logStats('Main', chalk.white.bold('compiling...'))
      hotMiddleware.publish({ action: 'compiling' })
      done()
    })

    compiler.watch({}, (err, stats) => {
      if (err) {
        console.log(err)
        return
      }

      logStats('Main', stats)

      if (electronProcess && electronProcess.kill) {
        manualRestart = true
        process.kill(electronProcess.pid)
        electronProcess = null
        startElectron()

        setTimeout(() => {
          manualRestart = false
        }, 5000)
      }

      resolve()
    })
  })
}

function startElectron () {
  electronProcess = spawn(electron, ['--inspect=5858', path.join(__dirname, '../dist/electron/main.js')])

  electronProcess.stdout.on('data', data => {
    electronLog(data, 'blue')
  })
  electronProcess.stderr.on('data', data => {
    electronLog(data, 'red')
  })

  electronProcess.on('close', () => {
    if (!manualRestart) process.exit()
  })
}

function electronLog (data, color) {
  let log = ''
  data = data.toString().split(/\r?\n/)
  data.forEach(line => {
    log += `  ${line}\n`
  })
  if (/[0-9A-z]+/.test(log)) {
    console.log(
      chalk[color].bold('┏ Electron -------------------') +
      '\n\n' +
      log +
      chalk[color].bold('┗ ----------------------------') +
      '\n'
    )
  }
}

function greeting () {
  const cols = process.stdout.columns
  let text = ''

  if (cols > 104) text = 'electron-vue'
  else if (cols > 76) text = 'electron-|vue'
  else text = false

  if (text) {
    say(text, {
      colors: ['yellow'],
      font: 'simple3d',
      space: false
    })
  } else console.log(chalk.yellow.bold('\n  electron-vue'))
  console.log(chalk.blue('  getting ready...') + '\n')
}

function init () {
  greeting()

  Promise.all([startRenderer(), startMain()])
    .then(() => {
      startElectron()
    })
    .catch(err => {
      console.error(err)
    })
}

init()


import { Menu } from 'electron'

export const getMenuItem = menuName => {
  const menus = Menu.getApplicationMenu()
  return menus.items.find(menu => menu.label === menuName)
}


const Asset = require('../Asset');
const path = require('path');

class RawAsset extends Asset {
  // Don't load raw assets. They will be copied by the RawPackager directly.
  load() {}

  generate() {
    return {
      js: `module.exports=${JSON.stringify(this.generateBundleName())};`
    };
  }
}

module.exports = RawAsset;