Scribe = require('./scribe')

Scribe.DOM =
  NOBREAK_SPACE:  "\uFEFF"

  findDeepestNode: (node, offset) ->
    if node.firstChild?
      for child in _.clone(node.childNodes)
        length = Scribe.Utils.getNodeLength(child)
        if offset < length
          return Scribe.DOM.findDeepestNode(child, offset)
        else
          offset -= length
      return Scribe.DOM.findDeepestNode(child, offset + length)
    else
      return [node, offset]

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    if (node1.tagName == 'OL' || node1.tagName == 'UL') && node1.childNodes.length == 2
      Scribe.DOM.mergeNodes(node1.firstChild, node1.lastChild)
    return node1

  moveChildren: (newParent, oldParent) ->
    _.each(_.clone(oldParent.childNodes), (child) ->
      newParent.appendChild(child)
    )

  removeAttributes: (node, exception = null) ->
    _.each(_.clone(node.attributes), (attrNode, value) ->
      node.removeAttribute(attrNode.name) unless exception == attrNode.name
    )

  splitAfter: (node, root) ->
    return false if node == root or node.parentNode == root
    parentNode = node.parentNode
    parentClone = parentNode.cloneNode(false)
    parentNode.parentNode.insertBefore(parentClone, parentNode.nextSibling)
    while node.nextSibling?
      parentClone.appendChild(node.nextSibling)
    Scribe.DOM.splitAfter(parentNode, root)

  splitNode: (node, offset, force = false) ->
    # Check if split necessary
    nodeLength = Scribe.Utils.getNodeLength(node)
    offset = Math.max(0, offset)
    offset = Math.min(offset, nodeLength)
    return [node.previousSibling, node, false] unless force or offset != 0
    return [node, node.nextSibling, false] unless force or offset != nodeLength
    if node.nodeType == node.TEXT_NODE
      after = node.splitText(offset)
      return [node, after, true]
    else
      left = node
      right = node.cloneNode(false)
      node.parentNode.insertBefore(right, left.nextSibling)
      [child, offset] = Scribe.Utils.getChildAtOffset(node, offset)
      [childLeft, childRight] = Scribe.DOM.splitNode(child, offset)
      while childRight != null
        nextRight = childRight.nextSibling
        right.appendChild(childRight)
        childRight = nextRight
      return [left, right, true]

  switchTag: (node, newTag) ->
    return if node.tagName == newTag
    newNode = node.ownerDocument.createElement(newTag)
    this.moveChildren(newNode, node)
    node.parentNode.replaceChild(newNode, node)
    newNode.className = node.className if node.className
    newNode.id = node.id if node.id
    return newNode

  traversePostorder: (root, fn, context = fn) ->
    return unless root?
    cur = root.firstChild
    while cur?
      Scribe.DOM.traversePostorder.call(context, cur, fn)
      cur = fn.call(context, cur)
      cur = cur.nextSibling if cur?

  traversePreorder: (root, offset, fn, context = fn, args...) ->
    return unless root?
    cur = root.firstChild
    while cur?
      nextOffset = offset + Scribe.Utils.getNodeLength(cur)
      curHtml = cur.innerHTML
      cur = fn.call(context, cur, offset, args...)
      Scribe.DOM.traversePreorder.call(null, cur, offset, fn, context, args...)
      if cur? && cur.innerHTML == curHtml
        cur = cur.nextSibling
        offset = nextOffset

  traverseSiblings: (curNode, endNode, fn) ->
    while curNode?
      nextSibling = curNode.nextSibling
      fn(curNode)
      break if curNode == endNode
      curNode = nextSibling

  unwrap: (node) ->
    ret = node.firstChild
    next = node.nextSibling
    _.each(_.clone(node.childNodes), (child) ->
      node.parentNode.insertBefore(child, next)
    )
    node.parentNode.removeChild(node)
    return ret

  wrap: (wrapper, node) ->
    node.parentNode.insertBefore(wrapper, node)
    wrapper.appendChild(node)
    return wrapper

module.exports = Scribe


this.record(delta, oldDelta) unless @ignoringChanges

keep keep keep keep add keep keep keep keep keep

clear: ->
@undoStack = []
@redoStack = []

Scribe = require('./scribe')
Tandem = require('tandem-core')

DEFAULT_API_OPTIONS = { silent: false, source: 'api' }

doAt = (fn, options) ->
  this.doSilently( =>
    trackDelta.call(this, =>
      fn.call(this)
    , options)
  )

initListeners = ->
  onEditOnce = =>
  onEdit = =>
    onEditOnce = _.once(onEdit)
    return if @ignoreDomChanges or !@renderer.iframe.parentNode?    # Make sure we have not been deleted
    this.update()
  onSubtreeModified = (arg1, arg2) =>
    return if @ignoreDomChanges
    toCall = onEditOnce
    _.defer( =>
      toCall.call(null)
    )
  onEditOnce = _.once(onEdit)
  @root.addEventListener('DOMSubtreeModified', onSubtreeModified)

deleteAt = (index, length) ->
  return if length <= 0
  @selection.preserve(index, -1 * length, =>
    [firstLine, offset] = @doc.findLineAtOffset(index)
    curLine = firstLine
    while curLine? and length > 0
      deleteLength = Math.min(length, curLine.length - offset)
      nextLine = curLine.next
      if curLine.length == deleteLength
        if curLine == @doc.lines.first and curLine == @doc.lines.last
          curLine.node.innerHTML = ''
          curLine.trailingNewline = false
          curLine.rebuild()
        else
          Scribe.Utils.removeNode(curLine.node)
          @doc.removeLine(curLine)
      else
        curLine.deleteText(offset, deleteLength)
      length -= deleteLength
      curLine = nextLine
      offset = 0
    if firstLine? and !firstLine.trailingNewline
      @doc.mergeLines(firstLine, firstLine.next)
    @doc.rebuildDirty()
  )

forceTrailingNewline = ->
  unless @doc.lines.last?.trailingNewline
    this.insertAt(this.getLength(), "\n", {}, false)

# formatAt (Number index, Number length, String name, Mixed value) ->
formatAt = (index, length, name, value) ->
  @selection.preserve(index, 0, =>
    [line, offset] = @doc.findLineAtOffset(index)
    while line? and length > 0
      if Scribe.Line.FORMATS[name]?
        # If newline character is being applied with formatting
        if length > line.length - offset
          line.format(name, value)
      else if @doc.formatManager.formats[name]?
        if line.length - offset >= length
          line.formatText(offset, length, name, value)
        else
          line.formatText(offset, line.length - offset, name, value)
      else
        console.warn 'Unsupported format', name, value
      length -= (line.length - offset)
      offset = 0
      line = line.next
    @doc.rebuildDirty()
  )

insertAt = (index, text, formatting = {}) ->
  @selection.preserve(index, text.length, =>
    text = text.replace(/\r\n/g, '\n')
    text = text.replace(/\r/g, '\n')
    lineTexts = text.split('\n')
    if index == this.getLength() and @doc.lines.last.trailingNewline
      if lineTexts[lineTexts.length - 1] == ''
        lineTexts.pop()
      line = @doc.splitLine(@doc.lines.last, @doc.lines.last.length)
      offset = 0
    else
      [line, offset] = @doc.findLineAtOffset(index)
    _.each(lineTexts, (lineText, i) =>
      line.insertText(offset, lineText, formatting)
      if i < lineTexts.length - 1
        if line.trailingNewline
          line = @doc.splitLine(line, offset + lineText.length)
        else
          line.trailingNewline = true
          line.length += 1
      offset = 0
    )
    @doc.rebuildDirty()
  )

trackDelta = (fn, options) ->
  oldDelta = @doc.toDelta()
  oldIndex = @selection.range?.start.index # We do not want new range value so we do not use getSelection
  fn()
  newDelta = @doc.toDelta()
  try
    newIndex = this.getSelection()?.start.index
    if oldIndex? and newIndex? and oldIndex <= oldDelta.endLength and newIndex <= newDelta.endLength
      [oldLeftDelta, oldRightDelta] = oldDelta.split(oldIndex)
      [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
      decomposeLeft = newLeftDelta.decompose(oldLeftDelta)
      decomposeRight = newRightDelta.decompose(oldRightDelta)
      decomposeA = decomposeLeft.merge(decomposeRight)
  catch ignored
  decomposeB = newDelta.decompose(oldDelta)
  if decomposeA and decomposeB
    decompose = if decomposeA.ops.length < decomposeB.ops.length then decomposeA else decomposeB
  else
    decompose = decomposeA or decomposeB
  compose = oldDelta.compose(decompose)
  console.assert(compose.isEqual(newDelta), oldDelta, newDelta, decompose, compose)
  if !decompose.isIdentity() and !options.silent
    eventName = if options.source == 'api' then Scribe.Editor.events.API_TEXT_CHANGE else Scribe.Editor.events.USER_TEXT_CHANGE
    this.emit(eventName, decompose)

class Scribe.Editor extends EventEmitter2
  @editors: []

  @ID_PREFIX: 'editor-'

  @DEFAULTS:
    cursor: 0
    enabled: true
    onReady: ->

    document: {}
    renderer: {}
    undoManager: {}

  @events:
    API_TEXT_CHANGE  : 'api-text-change'
    PRE_EVENT        : 'pre-event'
    POST_EVENT       : 'post-event'
    SELECTION_CHANGE : 'selection-change'
    USER_TEXT_CHANGE : 'user-text-change'

  constructor: (@iframeContainer, options = {}) ->
    @options = _.defaults(options, Scribe.Editor.DEFAULTS)
    @id = _.uniqueId(Scribe.Editor.ID_PREFIX)
    @iframeContainer = document.getElementById(@iframeContainer) if _.isString(@iframeContainer)
    this.reset(true)
    this.enable() if @options.enabled

  disable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', false)
    )

  enable: ->
    this.doSilently( =>
      @root.setAttribute('contenteditable', true)
    )

  reset: (keepHTML = false) ->
    @ignoreDomChanges = true
    @options.renderer.keepHTML = keepHTML
    @iframeContainer.innerHTML = @root.innerHTML if @root?
    @renderer = new Scribe.Renderer(@iframeContainer, @options.renderer)
    @contentWindow = @renderer.iframe.contentWindow
    @root = @renderer.root
    @doc = new Scribe.Document(@root, @options.document)
    @selection = new Scribe.Selection(this)
    @keyboard = new Scribe.Keyboard(this)
    @undoManager = new Scribe.UndoManager(this, @options.undoManager)
    @pasteManager = new Scribe.PasteManager(this)
    @renderer.runWhenLoaded(@options.onReady)
    initListeners.call(this)
    @ignoreDomChanges = false
    Scribe.Editor.editors.push(this)

  applyDelta: (delta, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    # Make exception for systems that assume editors start with empty text
    if delta.startLength == 0 and this.getLength() == 1
      return this.setDelta(delta, options)
    return if delta.isIdentity()
    this.doSilently( =>
      console.assert(delta.startLength == this.getLength(), "Trying to apply delta to incorrect doc length", delta, this.getLength())
      oldDelta = @doc.toDelta()
      delta.apply(insertAt, deleteAt, formatAt, this)
      unless options.silent
        eventName = if options.source == 'api' then Scribe.Editor.events.API_TEXT_CHANGE else Scribe.Editor.events.USER_TEXT_CHANGE
        this.emit(eventName, delta)
      # TODO enable when we figure out addNewline issue, currently will fail if we do add newline
      # console.assert(delta.endLength == this.getLength(), "Applying delta resulted in incorrect end length", delta, this.getLength())
      forceTrailingNewline.call(this)
    )

  emit: (eventName, args...) ->
    super(Scribe.Editor.PRE_EVENT, eventName, args...)
    super(eventName, args...)
    super(Scribe.Editor.POST_EVENT, eventName, args...)

  deleteAt: (index, length, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    doAt.call(this, =>
      deleteAt.call(this, index, length)
      forceTrailingNewline.call(this)
    , options)

  doSilently: (fn) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    fn()
    @ignoreDomChanges = oldIgnoreDomChange

  formatAt: (index, length, name, value, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    doAt.call(this, =>
      formatAt.call(this, index, length, name, value)
    , options)

  getDelta: ->
    return @doc.toDelta()

  getLength: ->
    return @doc.toDelta().endLength

  getSelection: ->
    return @selection.getRange()

  insertAt: (index, text, formatting = {}, options = {}) ->
    options = _.defaults(options, DEFAULT_API_OPTIONS)
    doAt.call(this, =>
      insertAt.call(this, index, text, formatting)
      forceTrailingNewline.call(this)
    , options)

  setDelta: (delta, options = {}) ->
    oldLength = delta.startLength
    delta.startLength = this.getLength()
    this.applyDelta(delta, options)
    delta.startLength = oldLength

  setSelection: (range, silent = false) ->
    @selection.setRange(range, silent)

  update: ->
    this.doSilently( =>
      trackDelta.call(this, =>
        @selection.preserve( =>
          Scribe.Normalizer.breakBlocks(@root)
          lines = @doc.lines.toArray()
          lineNode = @root.firstChild
          _.each(lines, (line, index) =>
            while line.node != lineNode
              if line.node.parentNode == @root
                @doc.normalizer.normalizeLine(lineNode)
                newLine = @doc.insertLineBefore(lineNode, line)
                lineNode = lineNode.nextSibling
              else
                return @doc.removeLine(line)
            @doc.updateLine(line)
            lineNode = lineNode.nextSibling
          )
          while lineNode != null
            newLine = @doc.appendLine(lineNode)
            lineNode = lineNode.nextSibling
        )
      , { silent: false, source: 'user' })
    )

module.exports = Scribe


@renderer.iframe.focus()  # Mobile Safari requires this

Quill = require('../quill')
_     = Quill.require('lodash')
dom   = Quill.require('dom')

class Toolbar
  @DEFAULTS:
    container: null

  @formats:
    LINE    : { 'align', 'bullet', 'list' }
    SELECT  : { 'align', 'background', 'color', 'font', 'size' }
    TOGGLE  : { 'bold', 'bullet', 'image', 'italic', 'link', 'list', 'strike', 'underline' }
    TOOLTIP : { 'image', 'link' }

  constructor: (@quill, @options) ->
    throw new Error('container required for toolbar', @options) unless @options.container?
    @container = if _.isString(@options.container) then document.querySelector(@options.container) else @options.container
    @inputs = {}
    @preventUpdate = false
    @triggering = false
    _.each(@quill.options.formats, (format) =>
      return if Toolbar.formats.TOOLTIP[format]?
      this.initFormat(format, (range, value) =>
        return if @triggering
        if range.isCollapsed()
          @quill.prepareFormat(format, value)
        else if Toolbar.formats.LINE[format]?
          @quill.formatLine(range, format, value, 'user')
        else
          @quill.formatText(range, format, value, 'user')
        _.defer( =>
          this.updateActive(range)  # Clear exclusive formats
          this.setActive(format, value)
        )
      )
    )
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, _.bind(this.updateActive, this))
    dom(@container).addClass('ql-toolbar-container')
    dom(@container).addClass('ios') if dom.isIOS()  # Fix for iOS not losing hover state after click
    if dom.isIE(11) or dom.isIOS()
      dom(@container).on('mousedown', =>
        # IE destroys selection by default when we click away
        # Also fixes bug in iOS where preformating prevents subsequent typing
        return false
      )

  initFormat: (format, callback) ->
    selector = ".ql-#{format}"
    if Toolbar.formats.SELECT[format]?
      selector = "select#{selector}"    # Avoid selecting the picker container
      eventName = 'change'
    else
      eventName = 'click'
    input = @container.querySelector(selector)
    return unless input?
    @inputs[format] = input
    dom(input).on(eventName, =>
      value = if eventName == 'change' then dom(input).value() else !dom(input).hasClass('ql-active')
      @preventUpdate = true
      @quill.focus()
      range = @quill.getSelection()
      callback(range, value) if range?
      @preventUpdate = false
      return true
    )

  setActive: (format, value) ->
    input = @inputs[format]
    return unless input?
    $input = dom(input)
    if input.tagName == 'SELECT'
      @triggering = true
      selectValue = $input.value(input)
      value = '' if _.isArray(value)
      if value != selectValue
        if value?
          $input.option(value)
        else
          $input.reset()
      @triggering = false
    else
      $input.toggleClass('ql-active', value or false)

  updateActive: (range) ->
    return unless range? and !@preventUpdate
    activeFormats = this._getActive(range)
    _.each(@inputs, (input, format) =>
      this.setActive(format, activeFormats[format])
      return true
    )

  _getActive: (range) ->
    leafFormats = this._getLeafActive(range)
    lineFormats = this._getLineActive(range)
    return _.defaults({}, leafFormats, lineFormats)

  _getLeafActive: (range) ->
    if range.isCollapsed()
      [line, offset] = @quill.editor.doc.findLineAt(range.start)
      if offset == 0
        contents = @quill.getContents(range.start, range.end + 1)
      else
        contents = @quill.getContents(range.start - 1, range.end)
    else
      contents = @quill.getContents(range)
    formatsArr = _.map(contents.ops, 'attributes')
    return this._intersectFormats(formatsArr)

  _getLineActive: (range) ->
    formatsArr = []
    [firstLine, offset] = @quill.editor.doc.findLineAt(range.start)
    [lastLine, offset] = @quill.editor.doc.findLineAt(range.end)
    lastLine = lastLine.next if lastLine? and lastLine == firstLine
    while firstLine? and firstLine != lastLine
      formatsArr.push(_.clone(firstLine.formats))
      firstLine = firstLine.next
    return this._intersectFormats(formatsArr)

  _intersectFormats: (formatsArr) ->
    return _.reduce(formatsArr.slice(1), (activeFormats, formats) ->
      activeKeys = _.keys(activeFormats)
      formatKeys = _.keys(formats)
      intersection = _.intersection(activeKeys, formatKeys)
      missing = _.difference(activeKeys, formatKeys)
      added = _.difference(formatKeys, activeKeys)
      _.each(intersection, (name) ->
        if Toolbar.formats.SELECT[name]?
          if _.isArray(activeFormats[name])
            activeFormats[name].push(formats[name]) if _.indexOf(activeFormats[name], formats[name]) < 0
          else if activeFormats[name] != formats[name]
            activeFormats[name] = [activeFormats[name], formats[name]]
      )
      _.each(missing, (name) ->
        if Toolbar.formats.TOGGLE[name]?
          delete activeFormats[name]
        else if Toolbar.formats.SELECT[name]? and !_.isArray(activeFormats[name])
          activeFormats[name] = [activeFormats[name]]
      )
      _.each(added, (name) ->
        activeFormats[name] = [formats[name]] if Toolbar.formats.SELECT[name]?
      )
      return activeFormats
    , formatsArr[0] or {})

Quill.registerModule('toolbar', Toolbar)
module.exports = Toolbar


  <button class="ql-bold">Bold</button>
  <button class="ql-link">Link</button>
  <select class="ql-size">
    <option value="10px">Small</option>
    <option value="13px" selected>Normal</option>
    <option value="18px">Large</option>
    <option value="32px">Huge</option>
  </select>


describe('Quill', ->
  beforeEach( ->
    @container = $('#editor-container').html('

0123

5678

    ').get(0)
    @quill = new Quill(@container.firstChild)
  )

  describe('constructor', ->
    it('string container', ->
      @container.innerHTML = '

'
      quill = new Quill('#target')
      expect(quill.editor.iframeContainer).toEqual(@container.firstChild)
    )

    it('invalid container', ->
      expect( =>
        @quill = new Quill('.none')
      ).toThrow(new Error('Invalid Quill container'))
    )
  )

  describe('modules', ->
    it('addContainer()', ->
      @quill.addContainer('test-container', true)
      expect(@quill.root.parentNode.querySelector('.test-container')).toEqual(@quill.root.parentNode.firstChild)
    )

    it('addModule()', ->
      obj =
        before: ->
        after: ->
      spyOn(obj, 'before')
      spyOn(obj, 'after')
      expect(@quill.getModule('toolbar')).toBe(undefined)
      @quill.onModuleLoad('toolbar', obj.before)
      @quill.addModule('toolbar', { container: '#toolbar-container' })
      expect(@quill.getModule('toolbar')).not.toBe(null)
      expect(obj.before).toHaveBeenCalled()
      @quill.onModuleLoad('toolbar', obj.after)
      expect(obj.after).toHaveBeenCalled()
    )

    it('addModule() nonexistent', ->
      expect( =>
        @quill.addModule('nonexistent')
      ).toThrow(new Error("Cannot load nonexistent module. Are you sure you registered it?"))
    )
  )

  describe('manipulation', ->
    it('deleteText()', ->
      @quill.deleteText(2, 3)
      expect(@quill.root).toEqualHTML('
013

5678
', true)
    )

    it('formatLine()', ->
      @quill.formatLine(4, 6, 'align', 'right')
      expect(@quill.root).toEqualHTML('

0123

5678

      ', true)
    )

    it('formatText()', ->
      @quill.formatText(2, 4, 'bold', true)
      expect(@quill.root).toEqualHTML('
0123

5678
', true)
      @quill.formatText(2, 4, 'bold', false)
      expect(@quill.root).toEqualHTML('
0123

5678
', true)
      expect(@quill.getContents(0, 4)).toEqualDelta(Quill.Delta.makeInsertDelta(0, 0, '0123'))
    )

    it('formatText() default style', ->
      html = @quill.root.innerHTML
      @quill.formatText(2, 4, 'size', '13px')
      expect(@quill.root).toEqualHTML(html)
    )

    it('insertEmbed()', ->
      @quill.insertEmbed(2, 'image', 'http://quilljs.com/images/cloud.png')
      expect(@quill.root).toEqualHTML('
0123

5678
', true)
    )

    it('insertText()', ->
      @quill.insertText(2, 'A')
      expect(@quill.root).toEqualHTML('
01A23

5678
', true)
    )

    it('setContents() with delta', ->
      @quill.setContents({
        startLength: 0
        endLength: 1
        ops: [{ value: 'A', attributes: { bold: true } }]
      })
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('setContents() with ops', ->
      @quill.setContents([{ value: 'A', attributes: { bold: true } }])
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('setHTML()', ->
      @quill.setHTML('
A
')
      expect(@quill.root).toEqualHTML('
A
', true)
    )

    it('updateContents()', ->
      @quill.updateContents(Quill.Delta.makeInsertDelta(10, 2, 'A'))
      expect(@quill.root).toEqualHTML('
01A23

5678
', true)
    )
  )

  describe('retrievals', ->
    it('getContents() all', ->
      expect(@quill.getContents()).toEqualDelta(Quill.Delta.makeInsertDelta(0, 0, '0123\n5678\n'))
    )

    it('getContents() partial', ->
      expect(@quill.getContents(2, 7)).toEqualDelta(Quill.Delta.makeInsertDelta(0, 0, '23\n56'))
    )

    it('getHTML()', ->
      expect(@quill.getHTML()).toEqualHTML('
0123

5678
', true)
    )

    it('getLength()', ->
      expect(@quill.getLength()).toEqual(10)
    )

    it('getText()', ->
      expect(@quill.getText()).toEqual('0123\n5678\n')
    )
  )

  describe('selection', ->
    it('get/set range', ->
      @quill.setSelection(1, 2)
      range = @quill.getSelection()
      expect(range).not.toBe(null)
      expect(range.start).toEqual(1)
      expect(range.end).toEqual(2)
    )

    it('get/set index range', ->
      @quill.setSelection(new Quill.Lib.Range(2, 3))
      range = @quill.getSelection()
      expect(range).not.toBe(null)
      expect(range.start).toEqual(2)
      expect(range.end).toEqual(3)
    )

    it('get/set null', ->
      @quill.setSelection(1, 2)
      expect(range).not.toBe(null)
      @quill.setSelection(null)
      range = @quill.getSelection()
      expect(range).toBe(null)
    )
  )

  describe('_buildParams()', ->
    tests =
      'index range string formats'  : [1, 3, 'bold', true]
      'index range object formats'  : [1, 3, { bold: true }]
      'object range string formats' : [new Quill.Lib.Range(1, 3), 'bold', true]
      'object range object formats' : [new Quill.Lib.Range(1, 3), { bold: true }]

    _.each(tests, (args, name) ->
      it(name, ->
        [start, end, formats, source] = @quill._buildParams(args...)
        expect(start).toEqual(1)
        expect(end).toEqual(3)
        expect(formats).toEqual({ bold: true })
      )
    )

    it('source override', ->
      [start, end, formats, source] = @quill._buildParams(1, 2, {}, 'silent')
      expect(source).toEqual('silent')
    )
  )
)


_ = require('lodash')

lastKeyEvent = null    # Workaround for fact we can't dispatch keyboard event via javascript

class Wrapper
  constructor: (@node) ->

  addClass: (cssClass) ->
    return if this.hasClass(cssClass)
    if @node.classList?
      @node.classList.add(cssClass)
    else if @node.className?
      @node.className = (@node.className + ' ' + cssClass).trim()
    return this

  attributes: (attributes) ->
    if attributes
      _.each(attributes, (value, name) =>
        @node.setAttribute(name, value)
      )
      return this
    else
      return {} unless @node.attributes?
      attributes = {}
      for value,i in @node.attributes
        attr = @node.attributes[i]
        attributes[attr.name] = attr.value
      return attributes

  child: (offset) ->
    child = @node.firstChild
    length = dom(child).length()
    while child?
      break if offset < length
      offset -= length
      child = child.nextSibling
      length = dom(child).length()
    unless child?
      child = @node.lastChild
      offset = dom(child).length()
    return [child, offset]

  childNodes: ->
    return _.map(@node.childNodes)

  classes: ->
    return @node.className.split(/\s+/)

  descendants: ->
    return _.map(@node.getElementsByTagName('*'))

  get: ->
    return @node

  hasClass: (cssClass) ->
    if @node.classList?
      return @node.classList.contains(cssClass)
    else if @node.className?
      return _.indexOf(this.classes(), cssClass) > -1
    return false

  isAncestor: (ancestor, inclusive = false) ->
    return inclusive if ancestor == @node
    node = @node
    while node
      return true if node == ancestor
      node = node.parentNode
    return false

  isElement: ->
    return @node?.nodeType == dom.ELEMENT_NODE

  isTextNode: ->
    return @node?.nodeType == dom.TEXT_NODE

  length: ->
    return 0 unless @node?
    length = this.text().length
    if this.isElement()
      length += @node.querySelectorAll(_.keys(dom.EMBED_TAGS).join(',')).length
    return length

  merge: (node) ->
    $node = dom(node)
    if this.isElement()
      $node.moveChildren(@node)
      this.normalize()
    else
      this.text(this.text() + $node.text())
    $node.remove()
    return this

  moveChildren: (newParent) ->
    _.each(this.childNodes(), (child) ->
      newParent.appendChild(child)
    )
    return this

  nextLineNode: (root) ->
    nextNode = @node.nextSibling
    if !nextNode? and @node.parentNode != root
      nextNode = @node.parentNode.nextSibling
    if nextNode? and dom.LIST_TAGS[nextNode.tagName]?
      nextNode = nextNode.firstChild
    return nextNode

  # IE normalize is broken
  normalize: ->
    curNode = @node.firstChild
    while curNode?
      nextNode = curNode.nextSibling
      $node = dom(curNode)
      if nextNode? and dom(nextNode).isTextNode()
        if $node.text().length == 0
          $node.remove()
        else if $node.isTextNode()
          followingNode = nextNode.nextSibling
          $node.merge(nextNode)
          nextNode = followingNode
      curNode = nextNode
    return this

  on: (eventName, listener) ->
    @node.addEventListener(eventName, (event) ->
      arg = if lastKeyEvent and (eventName == 'keydown' or eventName == 'keyup') then lastKeyEvent else event
      propogate = listener(arg)
      unless propogate
        event.preventDefault()
        event.stopPropagation()
      return propogate
    )
    return this

  remove: ->
    @node.parentNode?.removeChild(@node)
    @node = null
    return null

  removeClass: (cssClass) ->
    return unless this.hasClass(cssClass)
    if @node.classList?
      return @node.classList.remove(cssClass)
    else if @node.className?
      classArray = this.classes()
      classArray.splice(_.indexOf(classArray, cssClass), 1)
      @node.className = classArray.join(' ')
    return this

  replace: (newNode) ->
    @node.parentNode.replaceChild(newNode, @node)
    @node = newNode
    return newNode

  # @node is node after split point, root is parent of eldest node we want split (root will not be split)
  splitAncestors: (root, force = false) ->
    return this if @node == root or @node.parentNode == root
    if @node.previousSibling? or force
      parentNode = @node.parentNode
      parentClone = parentNode.cloneNode(false)
      parentNode.parentNode.insertBefore(parentClone, parentNode.nextSibling)
      refNode = @node
      while refNode?
        nextNode = refNode.nextSibling
        parentClone.appendChild(refNode)
        refNode = nextNode
      return dom(parentClone).splitAncestors(root)
    else
      return dom(@node.parentNode).splitAncestors(root)

  split: (offset, force = false) ->
    # Check if split necessary
    nodeLength = this.length()
    offset = Math.max(0, offset)
    offset = Math.min(offset, nodeLength)
    return [@node.previousSibling, @node, false] unless force or offset != 0
    return [@node, @node.nextSibling, false] unless force or offset != nodeLength
    if @node.nodeType == dom.TEXT_NODE
      after = @node.splitText(offset)
      return [@node, after, true]
    else
      left = @node
      right = @node.cloneNode(false)
      @node.parentNode.insertBefore(right, left.nextSibling)
      [child, offset] = this.child(offset)
      [childLeft, childRight] = dom(child).split(offset)
      while childRight != null
        nextRight = childRight.nextSibling
        right.appendChild(childRight)
        childRight = nextRight
      return [left, right, true]

  styles: (styles, overwrite = false) ->
    if styles
      styles = _.defaults(styles, this.styles()) if !overwrite
      styleString = _.map(styles, (style, name) ->
        return "#{name}: #{style}"
      ).join('; ') + ';'
      @node.setAttribute('style', styleString)
      return this
    else
      styleString = @node.getAttribute('style') or ''
      obj = _.reduce(styleString.split(';'), (styles, str) ->
        [name, value] = str.split(':')
        if name and value
          name = name.trim()
          value = value.trim()
          styles[name.toLowerCase()] = value
        return styles
      , {})
      return obj

  switchTag: (newTag) ->
    newTag = newTag.toUpperCase()
    return this if @node.tagName == newTag
    newNode = @node.ownerDocument.createElement(newTag)
    attributes = this.attributes()
    this.moveChildren(newNode) unless dom.VOID_TAGS[newTag]?
    this.replace(newNode)
    return this.attributes(attributes).get()

  text: (text) ->
    if text?
      switch @node.nodeType
        when dom.ELEMENT_NODE
          @node.textContent = text
        when dom.TEXT_NODE then @node.data = text
      return this
    else
      switch @node.nodeType
        when dom.ELEMENT_NODE
          return "" if @node.tagName == dom.DEFAULT_BREAK_TAG
          return dom.EMBED_TEXT if dom.EMBED_TAGS[@node.tagName]?
          return @node.textContent if @node.textContent?
          return ""
        when dom.TEXT_NODE then return @node.data or ""
        else return ""

  textNodes: ->
    walker = @node.ownerDocument.createTreeWalker(@node, NodeFilter.SHOW_TEXT, null, false)
    textNodes = []
    while textNode = walker.nextNode()
      textNodes.push(textNode)
    return textNodes

  toggleClass: (className, state) ->
    state = !this.hasClass(className) unless state?
    if state
      this.addClass(className)
    else
      this.removeClass(className)
    return this

  trigger: (eventName, options = {}) =>
    if _.indexOf(['keypress', 'keydown', 'keyup'], eventName) < 0
      event = @node.ownerDocument.createEvent('Event')
      event.initEvent(eventName, options.bubbles, options.cancelable)
    else
      event = @node.ownerDocument.createEvent('KeyboardEvent')
      lastKeyEvent = _.clone(options)
      if _.isNumber(options.key)
        lastKeyEvent.which = options.key
      else if _.isString(options.key)
        lastKeyEvent.which = options.key.toUpperCase().charCodeAt(0)
      else
        lastKeyEvent.which = 0
      if dom.isIE(10)
        modifiers = []
        modifiers.push('Alt') if options.altKey
        modifiers.push('Control') if options.ctrlKey
        modifiers.push('Meta') if options.metaKey
        modifiers.push('Shift') if options.shiftKey
        event.initKeyboardEvent(eventName, options.bubbles, options.cancelable, this.window(), 0, 0, modifiers.join(' '), null, null)
      else
        # FF uses initKeyEvent, Webkit uses initKeyboardEvent
        initFn = if _.isFunction(event.initKeyboardEvent) then 'initKeyboardEvent' else 'initKeyEvent'
        event[initFn](eventName, options.bubbles, options.cancelable, this.window(), options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, 0)
    @node.dispatchEvent(event)
    lastKeyEvent = null
    return this

  unwrap: ->
    ret = @node.firstChild
    next = @node.nextSibling
    _.each(this.childNodes(), (child) =>
      @node.parentNode.insertBefore(child, next)
    )
    this.remove()
    return ret

  window: ->
    return @node.ownerDocument.defaultView or @node.ownerDocument.parentWindow

  wrap: (wrapper) ->
    @node.parentNode.insertBefore(wrapper, @node) if @node.parentNode?
    parent = wrapper
    while parent.firstChild?
      parent = wrapper.firstChild
    parent.appendChild(@node)
    return this

class SelectWrapper extends Wrapper
  default: ->
    return @node.querySelector('option[selected]')

  option: (option, trigger = true) ->
    value = if _.isElement(option) then option.value else option
    if value
      @node.value = value
    else
      @node.selectedIndex = -1  # PhantomJS
    this.trigger('change') if trigger
    return this

  reset: (trigger = true) ->
    option = this.default()
    if option?
      option.selected = true
    else
      @node.selectedIndex = 0
    this.trigger('change') if trigger
    return this

  value: ->
    return if @node.selectedIndex > -1 then @node.options[@node.selectedIndex].value else ''

dom = (node) ->
  if node?.tagName == 'SELECT'
    return new SelectWrapper(node)
  else
    return new Wrapper(node)

dom = _.extend(dom,
  ELEMENT_NODE: 1
  NOBREAK_SPACE:  " "
  TEXT_NODE: 3
  ZERO_WIDTH_NOBREAK_SPACE:  "\uFEFF"

  DEFAULT_BLOCK_TAG: 'DIV'
  DEFAULT_BREAK_TAG: 'BR'
  DEFAULT_INLINE_TAG: 'SPAN'
  EMBED_TEXT: '!' # No reason we picked ! besides it being one character (so delta cannot split it up)

  FONT_SIZES:
    '10px': 1
    '13px': 2
    '16px': 3
    '18px': 4
    '24px': 5
    '32px': 6
    '48px': 7

  KEYS:
    BACKSPACE : 8
    TAB       : 9
    ENTER     : 13
    ESCAPE    : 27
    LEFT      : 37
    UP        : 38
    RIGHT     : 39
    DOWN      : 40
    DELETE    : 46

  BLOCK_TAGS: {
    'ADDRESS'
    'ARTICLE'
    'ASIDE'
    'AUDIO'
    'BLOCKQUOTE'
    'CANVAS'
    'DD'
    'DIV'
    'DL'
    'FIGCAPTION'
    'FIGURE'
    'FOOTER'
    'FORM'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'HEADER'
    'HGROUP'
    'LI'
    'OL'
    'OUTPUT'
    'P'
    'PRE'
    'SECTION'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
    'VIDEO'
  }

  EMBED_TAGS: {
    'IMG'
  }

  LINE_TAGS: {
    'DIV'
    'LI'
  }

  LIST_TAGS: {
    'OL'
    'UL'
  }

  VOID_TAGS: {
    'AREA'
    'BASE'
    'BR'
    'COL'
    'COMMAND'
    'EMBED'
    'HR'
    'IMG'
    'INPUT'
    'KEYGEN'
    'LINK'
    'META'
    'PARAM'
    'SOURCE'
    'TRACK'
    'WBR'
  }

  convertFontSize: (size) ->
    if _.isString(size) and size.indexOf('px') > -1
      sources = _.keys(dom.FONT_SIZES)
      targets = _.values(dom.FONT_SIZES)
    else
      targets = _.keys(dom.FONT_SIZES)
      sources = _.values(dom.FONT_SIZES)
    for i,s of sources
      return targets[i] if parseInt(size) <= parseInt(s)
    return _.last(targets)

  isIE: (maxVersion) ->
    version = document.documentMode
    return version and maxVersion >= version

  isIOS: ->
    return /iPhone|iPad/i.test(navigator.userAgent)

  isMac: ->
    return /Mac/i.test(navigator.platform)
)

module.exports = dom


dom(@container).addClass('ios') if dom.isIOS()  # Fix for iOS not losing hover state after click

# IE destroys selection by default when we click away


body
  font-family: 'Helvetica', 'Arial', san-serif
  font-size: 13px
  padding: 25px

#content-container
  margin: auto
  width: 960px

#formatting-container
  background-color: #f5f5f5
  border-bottom: 1px solid #ccc
  height: 22px
  padding: 5px 12px
  .ql-active, button:hover
    color: green
    font-weight: bold

#editor-container
  height: 600px

#editor-wrapper
  border: 1px solid #aaa
  box-shadow: 0 0 2px 2px #ddd


basicEditor = new Quill('.basic-wrapper .editor-container',
  modules:
    authorship: { authorId: 'basic' }
    toolbar: { container: '.basic-wrapper .toolbar-container' }
)

advancedEditor = new Quill('.advanced-wrapper .editor-container',
  modules:
    'authorship': { authorId: 'advanced', enabled: true }
    'toolbar': { container: '.advanced-wrapper .toolbar-container' }
    'link-tooltip': true
    'image-tooltip': true
    'multi-cursor': true
  theme: 'snow'
)

authorship = advancedEditor.getModule('authorship')
authorship.addAuthor('basic', 'rgba(255,153,51,0.4)')

cursorManager = advancedEditor.getModule('multi-cursor')
cursorManager.setCursor('basic', 0, 'basic', 'rgba(255,153,51,0.9)')

basicEditor.on('selection-change', (range) ->
  console.info 'basic', 'selection', range
  cursorManager.moveCursor('basic', range.end) if range?
)

basicEditor.on('text-change', (delta, source) ->
  console.info 'basic', 'text', delta, source
  return if source == 'api'
  advancedEditor.updateContents(delta)
  sourceDelta = basicEditor.getContents()
  targetDelta = advancedEditor.getContents()
  console.assert(sourceDelta.isEqual(targetDelta), "Editor diversion!", sourceDelta, targetDelta)
)

advancedEditor.on('selection-change', (range) ->
  console.info 'advanced', 'selection', range
)

advancedEditor.on('text-change', (delta, source) ->
  console.info 'advanced', 'text', delta, source
  return if source == 'api'
  basicEditor.updateContents(delta)
  sourceDelta = advancedEditor.getContents()
  targetDelta = basicEditor.getContents()
  console.assert(sourceDelta.isEqual(targetDelta), "Editor diversion!", sourceDelta, targetDelta)
)


dom = Quill.Lib.DOM

describe('UndoManager', ->
  beforeEach( ->
    @container = $('#editor-container').get(0)
    @container.innerHTML = '

The lazy fox

'
    @quill = new Quill(@container.firstChild, {
      modules: {
        'undo-manager': { delay: 400 }
      }
    })
    @undoManager = @quill.getModule('undo-manager')
    @original = @quill.getContents()
  )

  tests =
    'insert':
      delta: Quill.Delta.makeInsertDelta(13, 9, 'hairy ')
      index: 15
    'delete':
      delta: Quill.Delta.makeDeleteDelta(13, 4, 5)
      index: 4
    'format':
      delta: Quill.Delta.makeRetainDelta(13, 4, 5, { bold: true })
      index: 9
    'multiple':
      delta: Quill.Delta.makeDelta({
        startLength: 13
        endLength: 14
        ops: [
          { start: 0, end: 4, attributes: { bold: true } }
          { value: 'hairy' }
          { start: 8, end: 13 }
        ]
      })
      index: 9

  describe('_getLastChangeIndex', ->
    _.each(tests, (test, name) ->
      it(name, ->
        index = @undoManager._getLastChangeIndex(test.delta)
        expect(index).toEqual(test.index)
      )
    )
  )

  describe('undo/redo', ->
    _.each(tests, (test, name) ->
      it(name, ->
        @quill.updateContents(test.delta)
        changed = @quill.getContents()
        expect(changed).not.toEqualDelta(@original)
        @undoManager.undo()
        expect(@quill.getContents()).toEqualDelta(@original)
        @undoManager.redo()
        expect(@quill.getContents()).toEqualDelta(changed)
      )
    )

    it('user change', ->
      @quill.root.firstChild.innerHTML = 'The lazy foxes'
      @quill.editor.checkUpdate()
      changed = @quill.getContents()
      expect(changed).not.toEqualDelta(@original)
      @undoManager.undo()
      expect(@quill.getContents()).toEqualDelta(@original)
      @undoManager.redo()
      expect(@quill.getContents()).toEqualDelta(changed)
    )

    it('merge changes', ->
      expect(@undoManager.stack.undo.length).toEqual(0)
      @quill.updateContents(Quill.Delta.makeInsertDelta(13, 12, 'e'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      @quill.updateContents(Quill.Delta.makeInsertDelta(14, 13, 's'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      @undoManager.undo()
      expect(@quill.getContents()).toEqual(@original)
      expect(@undoManager.stack.undo.length).toEqual(0)
    )

    it('dont merge changes', (done) ->
      expect(@undoManager.stack.undo.length).toEqual(0)
      @quill.updateContents(Quill.Delta.makeInsertDelta(13, 12, 'e'))
      expect(@undoManager.stack.undo.length).toEqual(1)
      setTimeout( =>
        @quill.updateContents(Quill.Delta.makeInsertDelta(14, 13, 's'))
        expect(@undoManager.stack.undo.length).toEqual(2)
        done()
      , @undoManager.options.delay * 1.25)
    )

    it('multiple undos', (done) ->
      expect(@undoManager.stack.undo.length).toEqual(0)
      @quill.updateContents(Quill.Delta.makeInsertDelta(13, 12, 'e'))
      contents = @quill.getContents()
      setTimeout( =>
        @quill.updateContents(Quill.Delta.makeInsertDelta(14, 13, 's'))
        @undoManager.undo()
        expect(@quill.getContents()).toEqual(contents)
        @undoManager.undo()
        expect(@quill.getContents()).toEqual(@original)
        done()
      , @undoManager.options.delay * 1.25)
    )

    it('hotkeys', ->
      @quill.updateContents(Quill.Delta.makeInsertDelta(13, 0, 'A '))
      changed = @quill.getContents()
      expect(changed).not.toEqualDelta(@original)
      dom(@quill.root).trigger('keydown', Quill.Module.UndoManager.hotkeys.UNDO)
      expect(@quill.getContents()).toEqualDelta(@original)
      dom(@quill.root).trigger('keydown', Quill.Module.UndoManager.hotkeys.REDO)
      expect(@quill.getContents()).toEqualDelta(changed)
    )
  )
)


# Also fixes bug in iOS where preformating prevents subsequent typing

_          = require('lodash')
dom        = require('../lib/dom')
Document   = require('./document')
Line       = require('./line')
Renderer   = require('./renderer')
Selection  = require('./selection')
Tandem     = require('tandem-core')

class Editor
  constructor: (@iframeContainer, @quill, @options = {}) ->
    @renderer = new Renderer(@iframeContainer, @options)
    dom(@iframeContainer).on('focus', this.focus.bind(this))
    @root = @renderer.root
    @doc = new Document(@root, @options)
    @delta = @doc.toDelta()
    @selection = new Selection(@doc, @renderer.iframe, @quill)
    @timer = setInterval(_.bind(this.checkUpdate, this), @options.pollInterval)
    @quill.on(@quill.constructor.events.SELECTION_CHANGE, (range) =>
      @savedRange = range
    )
    this.enable() unless @options.readOnly

  disable: ->
    this.enable(false)

  enable: (enabled = true) ->
    @root.setAttribute('contenteditable', enabled)

  applyDelta: (delta, source) ->
    localDelta = this._update()
    if localDelta
      tempDelta = localDelta
      localDelta = localDelta.transform(delta, true)
      delta = delta.transform(tempDelta, false)
      @delta = @doc.toDelta()   # Only for our error check below, otherwise dont need to update yet
    unless delta.isIdentity()   # Follows may have turned delta into the identity
      if delta.startLength != @delta.endLength
        console.warn("Trying to apply delta to incorrect doc length", delta, @delta)
      delta = this._trackDelta( =>
        delta.apply(this._insertAt, this._deleteAt, this._formatAt, this)
        @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
      )
      @delta = @doc.toDelta()
      @innerHTML = @root.innerHTML
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source) if delta and source != 'silent'
    if localDelta and !localDelta.isIdentity() and source != 'silent'
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, localDelta, 'user')

  checkUpdate: (source = 'user') ->
    return clearInterval(@timer) if !@renderer.iframe.parentNode? or !@root.parentNode?
    delta = this._update()
    if delta
      oldDelta = @delta
      @delta = oldDelta.compose(delta)
      @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source)
    source = 'silent' if delta
    @selection.update(source)

  focus: ->
    @selection.setRange(@selection.range) if dom.isIE(11)
    @root.focus()

  getDelta: ->
    return @delta

  _deleteAt: (index, length) ->
    return if length <= 0
    @selection.shiftAfter(index, -1 * length, =>
      [firstLine, offset] = @doc.findLineAt(index)
      curLine = firstLine
      mergeFirstLine = firstLine.length - offset <= length and offset > 0
      while curLine? and length > 0
        nextLine = curLine.next
        deleteLength = Math.min(curLine.length - offset, length)
        if offset == 0 and length >= curLine.length
          @doc.removeLine(curLine)
        else
          curLine.deleteText(offset, deleteLength)
        length -= deleteLength
        curLine = nextLine
        offset = 0
      @doc.mergeLines(firstLine, firstLine.next) if mergeFirstLine and firstLine.next
    )

  _formatAt: (index, length, name, value) ->
    @selection.shiftAfter(index, 0, =>
      [line, offset] = @doc.findLineAt(index)
      while line? and length > 0
        formatLength = Math.min(length, line.length - offset - 1)
        line.formatText(offset, formatLength, name, value)
        length -= formatLength
        line.format(name, value) if length > 0
        length -= 1
        offset = 0
        line = line.next
    )

  _insertAt: (index, text, formatting = {}) ->
    @selection.shiftAfter(index, text.length, =>
      text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
      lineTexts = text.split('\n')
      [line, offset] = @doc.findLineAt(index)
      _.each(lineTexts, (lineText, i) =>
        if !line? or line.length <= offset    # End of document
          if i < lineTexts.length - 1 or lineText.length > 0
            line = @doc.appendLine(@root.ownerDocument.createElement(dom.DEFAULT_BLOCK_TAG))
            offset = 0
            line.insertText(offset, lineText, formatting)
            line.format(formatting)
            nextLine = null
        else
          line.insertText(offset, lineText, formatting)
          if i < lineTexts.length - 1       # Are there more lines to insert?
            nextLine = @doc.splitLine(line, offset + lineText.length)
            _.each(_.defaults({}, formatting, line.formats), (value, format) ->
              line.format(format, formatting[format])
            )
            offset = 0
        line = nextLine
      )
    )

  _trackDelta: (fn) ->
    oldIndex = @savedRange?.start
    fn()
    newDelta = @doc.toDelta()
    try
      newIndex = @selection.getRange()?.start
      if oldIndex? and newIndex? and oldIndex <= @delta.endLength and newIndex <= newDelta.endLength
        [oldLeftDelta, oldRightDelta] = @delta.split(oldIndex)
        [newLeftDelta, newRightDelta] = newDelta.split(newIndex)
        decomposeLeft = newLeftDelta.decompose(oldLeftDelta)
        decomposeRight = newRightDelta.decompose(oldRightDelta)
        decomposeA = decomposeLeft.merge(decomposeRight)
    catch ignored
    decomposeB = newDelta.decompose(@delta)
    if decomposeA and decomposeB
      # Choose delta with fewest insertions
      [lengthA, lengthB] = _.map([decomposeA, decomposeB], (delta) ->
        return _.reduce(delta.ops, (count, op) ->
          count += op.value.length if op.value?
          return count
        , 0)
      )
      decompose = if lengthA < lengthA then decomposeA else decomposeB
    else
      decompose = decomposeA or decomposeB
    return decompose

  _update: ->
    return false if @innerHTML == @root.innerHTML
    delta = this._trackDelta( =>
      @selection.preserve(_.bind(@doc.rebuild, @doc))
      @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
    )
    @innerHTML = @root.innerHTML
    return if delta.isIdentity() then false else delta

module.exports = Editor


_ = require('lodash')

class Range
  @compare: (r1, r2) ->
    return true if r1 == r2           # Covers both is null case
    return false unless r1? and r2?   # If either is null they are not equal
    return r1.equals(r2)

  constructor: (@start, @end) ->

  equals: (range) ->
    return false unless range?
    return @start == range.start and @end == range.end

  shift: (index, length) ->
    [@start, @end] = _.map([@start, @end], (pos) ->
      return pos if index > pos
      if length >= 0
        return pos + length
      else
        return Math.max(index, pos + length)
    )

  isCollapsed: ->
    return @start == @end

module.exports = Range


describe('Selection', ->
  beforeEach( ->
    @container = $('#editor-container').get(0)
  )

  describe('helpers', ->
    beforeEach( ->
      @container.innerHTML = '

0123

One

Two

<s>89</s>ab

      '
      @quill = new Quill(@container.firstChild)   # Need Quill to create iframe for focus logic
      @selection = @quill.editor.selection
    )

    tests =
      'text node':
        native: ->
          return [@quill.root.querySelector('s').firstChild, 1]
        encoded: ->
          return [@quill.root.querySelector('s').firstChild, 1]
        index: 17
      'between leaves':
        native: ->
          return [@quill.root.querySelector('s').firstChild, 2]
        encoded: ->
          return [@quill.root.querySelector('s').firstChild, 2]
        index: 18
      'break node':
        native: ->
          return [@quill.root.querySelector('br').parentNode, 0]
        encoded: ->
          return [@quill.root.querySelector('br'), 0]
        index: 5
      'before image':
        native: ->
          return [@quill.root.querySelector('img').parentNode, 0]
        encoded: ->
          return [@quill.root.querySelector('img'), 0]
        index: 6
      'after image':
        native: ->
          return [@quill.root.querySelector('img').parentNode, 1]
        encoded: ->
          return [@quill.root.querySelector('img'), 1]
        index: 7
      'bullet':
        native: ->
          return [@quill.root.querySelectorAll('li')[1].firstChild, 1]
        encoded: ->
          return [@quill.root.querySelectorAll('li')[1].firstChild, 1]
        index: 13
      'last node':
        native: ->
          return [@quill.root.querySelector('i').firstChild, 2]
        encoded: ->
          return [@quill.root.querySelector('i').firstChild, 2]
        index: 20

    describe('_decodePosition()', ->
      _.each(tests, (test, name) ->
        it(name, ->
          [node, offset] = test.encoded.call(this)
          [resultNode, resultOffset] = @selection._decodePosition(node, offset)
          [expectedNode, expectedOffset] = test.native.call(this)
          expect(resultNode).toEqual(expectedNode)
          expect(resultOffset).toEqual(expectedOffset)
        )
      )
    )

    describe('_encodePosition()', ->
      _.each(tests, (test, name) ->
        it(name, ->
          [node, offset] = test.native.call(this)
          [resultNode, resultOffset] = @selection._encodePosition(node, offset)
          [expectedNode, expectedOffset] = test.encoded.call(this)
          expect(resultNode).toEqual(expectedNode)
          expect(resultOffset).toEqual(expectedOffset)
        )
      )

      it('empty document', ->
        @container.innerHTML = '

'
        quill = new Quill(@container.firstChild)
        quill.root.innerHTML = ''
        quill.editor.doc.rebuild()
        [resultNode, resultIndex] = quill.editor.selection._encodePosition(quill.root, 0)
        expect(resultNode).toEqual(quill.root)
        expect(resultIndex).toEqual(0)
      )

      it('end of document', ->
        [resultNode, resultOffset] = @selection._encodePosition(@quill.root, 5)
        expect(resultNode).toEqual(@quill.root.querySelector('i').firstChild)
        expect(resultOffset).toEqual(2)
      )
    )

    describe('_positionToIndex()', ->
      _.each(tests, (test, name) ->
        it(name, ->
          [node, offset] = test.native.call(this)
          index = @selection._positionToIndex(node, offset)
          expect(index).toEqual(test.index)
        )
      )

      it('empty document', ->
        @container.innerHTML = '

'
        quill = new Quill(@container.firstChild)
        index = quill.editor.selection._positionToIndex(quill.root, 0)
        expect(index).toEqual(0)
      )
    )

    describe('_indexToPosition()', ->
      _.each(tests, (test, name) ->
        it(name, ->
          [node, offset] = @selection._indexToPosition(test.index)
          [expectedNode, expectedOffset] = test.native.call(this)
          expect(node).toEqual(expectedNode)
          expect(offset).toEqual(expectedOffset)
        )
      )

      it('empty document', ->
        @container.innerHTML = '

'
        quill = new Quill(@container.firstChild)
        quill.root.innerHTML = ''
        quill.editor.doc.rebuild()
        [node, offset] = quill.editor.selection._indexToPosition(0)
        expect(node).toEqual(quill.root)
        expect(offset).toEqual(0)
      )

      it('multiple consecutive images', ->
        @container.innerHTML = '

'
        quill = new Quill(@container.firstChild)
        [node, offset] = quill.editor.selection._indexToPosition(1)
        expect(node).toEqual(quill.root.firstChild)
        expect(offset).toEqual(1)
      )
    )

    describe('get/set range', ->
      _.each(tests, (test, name) ->
        it(name, ->
          @selection.setRange(new Quill.Lib.Range(test.index, test.index))
          expect(@selection.checkFocus()).toBe(true)
          range = @selection.getRange()
          expect(range).not.toEqual(null)
          expect(range.start).toEqual(test.index)
          expect(range.end).toEqual(test.index)
        )
      )

      it('entire document', ->
        @selection.setRange(new Quill.Lib.Range(0, 20))
        expect(@selection.checkFocus()).toBe(true)
        range = @selection.getRange()
        expect(range).not.toEqual(null)
        expect(range.start).toEqual(0)
        expect(range.end).toEqual(20)
      )

      it('null range', ->
        @selection.setRange(new Quill.Lib.Range(0, 0))
        expect(@selection.checkFocus()).toBe(true)
        @selection.setRange(null)
        expect(@selection.checkFocus()).toBe(false)
        range = @selection.getRange()
        expect(range).toBe(null)
      )

      it('empty document', ->
        @container.innerHTML = '

'
        quill = new Quill(@container.firstChild)
        quill.editor.selection.setRange(new Quill.Lib.Range(0, 0))
        expect(quill.editor.selection.checkFocus()).toBe(true)
        range = quill.editor.selection.getRange()
        expect(range).not.toEqual(null)
        expect(range.start).toEqual(0)
        expect(range.end).toEqual(0)
      )
    )
  )

  describe('preserve', ->
    describe('shiftAfter()', ->
      it('line optimization', ->
        @container.innerHTML = '

1234

'
        quill = new Quill(@container.firstChild)
        quill.editor.selection.setRange(new Quill.Lib.Range(0, 3))
        quill.editor._insertAt(0, Quill.Lib.DOM.EMBED_TEXT, { image: 'http://quilljs.com/images/cloud.png' })
        quill.editor._formatAt(2, 4, 'bold', true)
        expect(quill.root).toEqualHTML('

1234

        ', true)
        range = quill.editor.selection.getRange()
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(4)
        quill.editor.selection.shiftAfter(0, 0, _.bind(quill.editor.doc.optimizeLines, quill.editor.doc))
        range = quill.editor.selection.getRange()
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(4)
      )
    )

    describe('preserve()', ->
      beforeEach( ->
        @container.innerHTML = '

'
        @quill = new Quill(@container.firstChild)
        @doc = @quill.editor.doc
        @selection = @quill.editor.selection
      )

      it('wrapInline() text', ->
        @quill.root.innerHTML = 'Test'
        textNode = @quill.root.firstChild
        @selection._setNativeRange(textNode, 0, textNode, 4)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range.start).toEqual(0)
        expect(range.end).toEqual(4)
      )

      it('wrapInline() image', ->
        @quill.root.innerHTML = ''
        @selection._setNativeRange(@quill.root, 0, @quill.root, 1)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range.start).toEqual(0)
        expect(range.end).toEqual(1)
      )

      it('handleBreaks()', ->
        @quill.root.innerHTML = '
01
34
'
        firstTextNode = @quill.root.firstChild.firstChild
        lastTextNode = @quill.root.firstChild.lastChild
        @selection._setNativeRange(firstTextNode, 1, lastTextNode, 1)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(4)
      )

      it('pullBlocks()', ->
        @quill.root.innerHTML = '

01

34

'
        firstTextNode = @quill.root.firstChild.firstChild.firstChild
        lastTextNode = @quill.root.firstChild.lastChild.firstChild
        @selection._setNativeRange(firstTextNode, 1, lastTextNode, 1)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(4)
      )

      it('wrapText()', ->
        @quill.root.innerHTML = '
0123

5678
'
        firstTextNode = @quill.root.firstChild.firstChild
        lastTextNode = @quill.root.lastChild.firstChild
        @selection._setNativeRange(firstTextNode, 2, lastTextNode, 2)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range.start).toEqual(2)
        expect(range.end).toEqual(7)
      )

      it('no range', ->
        @quill.root.innerHTML = '
Test
'
        textNode = @quill.root.querySelector('div').firstChild
        @selection._setNativeRange(null)
        @selection.preserve(_.bind(@doc.rebuild, @doc))
        range = @selection.getRange()
        expect(range).toBe(null)
      )
    )
  )

  describe('save / restore selection', ->
    it('focus on another input', (done) ->
      @container.innerHTML = '<input type="textbox">
0123
'
      @quill = new Quill(@container.lastChild)
      @quill.setSelection(2, 3)
      @container.firstChild.focus()
      _.defer( =>
        expect(@quill.editor.selection.checkFocus()).not.toBeTruthy()
        expect(@quill.getSelection()).not.toBeTruthy()
        savedRange = @quill.editor.selection.getRange(true)
        expect(savedRange).toBeTruthy()
        expect(savedRange.start).toEqual(2)
        expect(savedRange.end).toEqual(3)
        @quill.focus()
        range = @quill.getSelection()
        expect(range).toBeTruthy()
        expect(range.start).toEqual(2)
        expect(range.end).toEqual(3)
        done()
      )
    )
  )
)


describe('PasteManager', ->
  it('_paste()', (done) ->
    container = $('#editor-container').get(0)
    container.innerHTML = '

0123

'
    quill = new Quill(container.firstChild)
    pasteManager = quill.getModule('paste-manager')
    quill.setSelection(2, 2)
    pasteManager._paste()
    pasteManager.container.innerHTML = '
      Pasted

Text

    '
    quill.on(Quill.events.TEXT_CHANGE, (delta) ->
      expect(delta).toEqualDelta(Quill.Delta.makeDelta({
        startLength: 5
        endLength: 16
        ops: [
          { start: 0, end: 2 }
          { value: 'Pasted', attributes: { bold: true } }
          { value: '\nText' }
          { start: 2, end: 5 }
        ]
      }))
      _.defer( ->
        range = quill.getSelection()
        expect(range.start).toEqual(13)
        expect(range.end).toEqual(13)
        done()
      )
    )
  )
)


keep keep keep add keep keep keep keep keep

"use strict";

jest.mock("libnpm/run-script", () => jest.fn(() => Promise.resolve()));

const loggingOutput = require("@lerna-test/logging-output");
const runScript = require("libnpm/run-script");
const npmConf = require("@lerna/npm-conf");
const Package = require("@lerna/package");
const runLifecycle = require("../run-lifecycle");

describe("runLifecycle()", () => {
  it("skips packages without scripts", async () => {
    const pkg = {
      name: "no-scripts",
    };

    await runLifecycle(pkg, "foo", new Map());

    expect(runScript).not.toHaveBeenCalled();
  });

  it("skips packages without matching script", async () => {
    const pkg = {
      name: "missing-script",
      scripts: {
        test: "foo",
      },
    };

    await runLifecycle(pkg, "bar", new Map());

    expect(runScript).not.toHaveBeenCalled();
  });

  it("calls npm-lifecycle with prepared arguments", async () => {
    const pkg = new Package(
      {
        name: "test-name",
        version: "1.0.0-test",
        scripts: {
          preversion: "test",
        },
        engines: {
          node: ">= 8.9.0",
        },
      },
      "/test/location"
    );
    const stage = "preversion";
    const opts = npmConf({ "custom-cli-flag": true });

    await runLifecycle(pkg, stage, opts);

    expect(runScript).toHaveBeenLastCalledWith(
      expect.objectContaining({
        name: pkg.name,
        version: pkg.version,
        engines: {
          node: ">= 8.9.0",
        },
        _id: `${pkg.name}@${pkg.version}`,
      }),
      stage,
      pkg.location,
      expect.objectContaining({
        config: expect.objectContaining({
          prefix: pkg.location,
          "custom-cli-flag": true,
        }),
        dir: pkg.location,
        failOk: false,
        log: expect.any(Object),
        unsafePerm: true,
      })
    );
  });

  it("camelCases dashed-options", async () => {
    const pkg = {
      name: "dashed-name",
      version: "1.0.0-dashed",
      location: "dashed-location",
      scripts: {
        "dashed-options": "test",
      },
    };
    const dir = pkg.location;
    const stage = "dashed-options";
    const opts = new Map([
      ["ignore-prepublish", true],
      ["ignore-scripts", false],
      ["node-options", true],
      ["script-shell", true],
      ["scripts-prepend-node-path", true],
      ["unsafe-perm", false],
    ]);

    await runLifecycle(pkg, stage, opts);

    expect(runScript).toHaveBeenLastCalledWith(expect.objectContaining(pkg), stage, dir, {
      config: expect.objectContaining({
        prefix: dir,
      }),
      dir,
      failOk: false,
      log: expect.any(Object),
      nodeOptions: true,
      scriptShell: true,
      scriptsPrependNodePath: true,
      unsafePerm: false,
    });
  });

  it("ignores prepublish when configured", async () => {
    const pkg = {
      name: "ignore-prepublish",
      scripts: {
        prepublish: "test",
      },
    };
    const stage = "prepublish";
    const opts = new Map().set("ignore-prepublish", true);

    await runLifecycle(pkg, stage, opts);

    expect(runScript).not.toHaveBeenCalled();
  });

  it("ignores scripts when configured", async () => {
    const pkg = {
      name: "ignore-scripts",
      scripts: {
        ignored: "test",
      },
    };
    const stage = "ignored";
    const opts = new Map().set("ignore-scripts", true);

    await runLifecycle(pkg, stage, opts);

    expect(runScript).not.toHaveBeenCalled();
  });

  it("omits circular opts", async () => {
    const pkg = {
      name: "circular-name",
      version: "1.0.0-circular",
      location: "circular-location",
      scripts: {
        prepack: "test",
      },
    };
    const stage = "prepack";
    const opts = new Map();

    await runLifecycle(pkg, stage, opts);

    const callOpts = runScript.mock.calls.pop().pop();

    expect(callOpts).not.toHaveProperty("config.log");
    expect(callOpts).not.toHaveProperty("config.logstream");
  });
});

describe("createRunner", () => {
  const runPackageLifecycle = runLifecycle.createRunner({ "other-cli-flag": 0 });

  it("creates partially-applied function with npm conf", async () => {
    const pkg = {
      name: "partially-applied",
      version: "1.2.3",
      location: "test",
      scripts: { version: "echo yay" },
    };
    const stage = "version";

    await runPackageLifecycle(pkg, stage);

    expect(runScript).toHaveBeenLastCalledWith(
      expect.any(Object),
      stage,
      pkg.location,
      expect.objectContaining({
        config: expect.objectContaining({
          "other-cli-flag": 0,
        }),
      })
    );
  });

  it("skips missing scripts block", async () => {
    const pkg = {
      name: "missing-scripts-block",
      version: "1.0.0",
      location: "test",
    };

    await runPackageLifecycle(pkg, "prepare");
    expect(runScript).not.toHaveBeenCalled();
  });

  it("skips missing script", async () => {
    const pkg = {
      name: "missing-script",
      version: "1.0.0",
      location: "test",
      scripts: { test: "echo foo" },
    };

    await runPackageLifecycle(pkg, "prepare");
    expect(runScript).not.toHaveBeenCalled();
  });

  it("logs script error and re-throws", async () => {
    runScript.mockImplementationOnce(({ scripts }, stage) => {
      const err = new Error("boom");

      // https://git.io/fAE3f
      err.errno = 123;
      err.script = scripts[stage];

      return Promise.reject(err);
    });

    const pkg = {
      name: "has-script-error",
      version: "1.0.0",
      location: "test",
      scripts: { prepublishOnly: "exit 123" },
    };

    try {
      await runPackageLifecycle(pkg, "prepublishOnly");
    } catch (err) {
      expect(err.code).toBe(123);
      expect(err.script).toBe("exit 123");
      expect(process.exitCode).toBe(123);
    }

    const [errorLog] = loggingOutput("error");
    expect(errorLog).toBe('"prepublishOnly" errored in "has-script-error", exiting 123');
  });

  it("defaults error exit code to 1", async () => {
    runScript.mockImplementationOnce(({ scripts }, stage) => {
      const err = new Error("kersplode");

      // errno only gets added when a proc closes, not from error
      err.script = scripts[stage];

      return Promise.reject(err);
    });

    const pkg = {
      name: "has-execution-error",
      version: "1.0.0",
      location: "test",
      scripts: { prepack: "a-thing-that-ends-poorly" },
    };

    try {
      await runPackageLifecycle(pkg, "prepack");
    } catch (err) {
      expect(err.code).toBe(1);
      expect(err.script).toBe("a-thing-that-ends-poorly");
    }

    const [errorLog] = loggingOutput("error");
    expect(errorLog).toBe('"prepack" errored in "has-execution-error", exiting 1');
  });
});


"use strict";

const execa = require("execa");
const fs = require("fs-extra");
const path = require("path");
const initFixture = require("@lerna-test/init-fixture")(__dirname);
const gitCheckout = require("../lib/git-checkout");

test("gitCheckout files", async () => {
  const cwd = await initFixture("normal-no-inter-dependencies");
  const files = ["package-1", "package-2"].map(name => path.join("packages", name, "package.json"));

  await Promise.all(files.map(fp => fs.writeJSON(path.join(cwd, fp), { foo: "bar" })));
  await gitCheckout(files, { cwd });

  const modified = await execa.stdout("git", ["ls-files", "--modified"], { cwd });
  expect(modified).toBe("");
});


lerna-monorepo

lerna dog-fooding lerna

npm test -- --ci --maxWorkers=2 && npm run integration -- --ci

jest --config jest.integration.js --maxWorkers=2

eslint . --ignore-path .gitignore --cache


keep keep keep keep replace keep keep keep


a private package


keep keep keep replace replace keep keep keep keep keep

["node-options", "--a-thing"],
["script-shell", "fish"],


@lerna/link

Symlink together all packages that are dependencies of each other


@lerna/npm-dist-tag

An internal Lerna tool


expect(runScript).toHaveBeenLastCalledWith(expect.objectContaining(pkg), stage, dir, {

"node-options": "--a-thing",
"script-shell": "fish",


chore(release): %s


}),
dir,
failOk: false,
log: expect.any(Object),
nodeOptions: true,
scriptShell: true,
scriptsPrependNodePath: true,
unsafePerm: false,
});
});

keep keep keep replace replace replace keep keep keep keep

log,
nodeOptions: "--a-thing",
scriptShell: "fish",


"use strict";

const log = require("libnpm/log");

module.exports = logPackageError;

function logPackageError(err, stream = false) {
  log.error(err.cmd, `exited ${err.code} in '${err.pkg.name}'`);

  if (stream) {
    // Streaming has already printed all stdout/stderr
    return;
  }

  if (err.stdout) {
    log.error(err.cmd, "stdout:");
    directLog(err.stdout);
  }

  if (err.stderr) {
    log.error(err.cmd, "stderr:");
    directLog(err.stderr);
  }

  // Below is just to ensure something sensible is printed after the long stream of logs
  log.error(err.cmd, `exited ${err.code} in '${err.pkg.name}'`);
}

function directLog(message) {
  log.pause();
  console.error(message); // eslint-disable-line no-console
  log.resume();
}


keep keep keep keep replace replace replace keep keep keep keep

"use strict";

const log = require("libnpm/log");
const runScript = require("libnpm/run-script");
const figgyPudding = require("figgy-pudding");
const npmConf = require("@lerna/npm-conf");

module.exports = runLifecycle;
module.exports.createRunner = createRunner;

const LifecycleConfig = figgyPudding(
  {
    log: { default: log },
    // provide aliases for some dash-cased props
    "ignore-prepublish": {},
    ignorePrepublish: "ignore-prepublish",
    "ignore-scripts": {},
    ignoreScripts: "ignore-scripts",
    "node-options": {},
    nodeOptions: "node-options",
    "script-shell": {},
    scriptShell: "script-shell",
    "scripts-prepend-node-path": {},
    scriptsPrependNodePath: "scripts-prepend-node-path",
    "unsafe-perm": {
      // when running scripts explicitly, assume that they're trusted
      default: true,
    },
    unsafePerm: "unsafe-perm",
  },
  {
    other() {
      // open up the pudding
      return true;
    },
  }
);

function runLifecycle(pkg, stage, _opts) {
  // back-compat for @lerna/npm-conf instances
  // https://github.com/isaacs/proto-list/blob/27764cd/proto-list.js#L14
  if ("root" in _opts) {
    // eslint-disable-next-line no-param-reassign
    _opts = _opts.snapshot;
  }

  const opts = LifecycleConfig(_opts);
  const dir = pkg.location;
  const config = {};

  if (opts.ignoreScripts) {
    opts.log.verbose("lifecycle", "%j ignored in %j", stage, pkg.name);

    return Promise.resolve();
  }

  if (!pkg.scripts || !pkg.scripts[stage]) {
    opts.log.silly("lifecycle", "No script for %j in %j, continuing", stage, pkg.name);

    return Promise.resolve();
  }

  if (stage === "prepublish" && opts.ignorePrepublish) {
    opts.log.verbose("lifecycle", "%j ignored in %j", stage, pkg.name);

    return Promise.resolve();
  }

  // https://github.com/zkat/figgy-pudding/blob/7d68bd3/index.js#L42-L64
  for (const [key, val] of opts) {
    // omit falsy values and circular objects
    if (val != null && key !== "log" && key !== "logstream") {
      config[key] = val;
    }
  }

  // env.npm_config_prefix should be the package directory
  config.prefix = dir;

  /* istanbul ignore else */
  // eslint-disable-next-line no-underscore-dangle
  if (pkg.__isLernaPackage) {
    // To ensure npm-lifecycle creates the correct npm_package_* env vars,
    // we must pass the _actual_ JSON instead of our fancy Package thingy
    // eslint-disable-next-line no-param-reassign
    pkg = pkg.toJSON();
  }

  // TODO: remove pkg._id when npm-lifecycle no longer relies on it
  pkg._id = `${pkg.name}@${pkg.version}`; // eslint-disable-line

  opts.log.silly("lifecycle", "%j starting in %j", stage, pkg.name);

  return runScript(pkg, stage, dir, {
    config,
    dir,
    failOk: false,
    log: opts.log,
    // bring along camelCased aliases
    nodeOptions: opts.nodeOptions,
    scriptShell: opts.scriptShell,
    scriptsPrependNodePath: opts.scriptsPrependNodePath,
    unsafePerm: opts.unsafePerm,
  }).then(
    () => {
      opts.log.silly("lifecycle", "%j finished in %j", stage, pkg.name);
    },
    err => {
      // propagate the exit code
      const exitCode = err.errno || 1;

      // error logging has already occurred on stderr, but we need to stop the chain
      log.error("lifecycle", "%j errored in %j, exiting %d", stage, pkg.name, exitCode);

      // ensure clean logging, avoiding spurious log dump
      err.name = "ValidationError";

      // our yargs.fail() handler expects a numeric .code, not .errno
      err.code = exitCode;
      process.exitCode = exitCode;

      // stop the chain
      throw err;
    }
  );
}

function createRunner(commandOptions) {
  const cfg = npmConf(commandOptions).snapshot;

  return (pkg, stage) => runLifecycle(pkg, stage, cfg);
}


@lerna/npm-run-script

An internal Lerna tool


'parchment': path.resolve(__dirname, '..', 'node_modules/parchment/src/parchment')

{ test: /parchment\/src\/.*\.ts$/, loader: 'ts' },

isparta: {
embedSource: true,
noAutoWrap: true,
babel: {
presets: ['es2015']
}
},

var config = require('./webpack.config.js');
var path = require('path');

config.babel = {
  presets: ['es2015']
};
config.module.preLoaders = [
  { test: /\.js$/, exclude: path.resolve('node_modules'), loader: 'isparta' }
];

module.exports = config;


import extend from 'extend';

class Module {
  constructor(quill, options = {}) {
    this.quill = quill;
    this.options = extend({}, this.constructor.DEFAULTS, options);
  }
}
Module.DEFAULTS = {};

export default Module;


.ql-snow
  .ql-image-tooltip
    padding: 10px
    text-align: center
    width: 300px
    input[type=text]
      height: 30px
      width: 100%
    a.ql-cancel, a.ql-action
      border: 1px solid activeColor
      display: inline-block
      float: left
      line-height: 18px
      padding: 5px
      width: 50%
    a.ql-cancel::before
      color: activeColor
      content: 'Cancel'
    a.ql-action
      background-color: activeColor
    a.ql-action::before
      color: #fff
      content: 'Insert'
    .ql-preview
      height: 200px
      margin: 10px 0px
      position: relative
      img
        bottom: 0
        left: 0
        margin: auto
        max-height: 100%
        max-width: 100%
        position: absolute
        right: 0
        top: 0
    .ql-empty.ql-preview::before
      border: 1px dashed borderColor
      color: borderColor
      content: 'Preview'
      display: block
      line-height: 200px


var config = require('./webpack.config.js');

{ test: /\.js$/, exclude: path.resolve('node_modules'), loader: 'isparta' }

keep keep keep replace replace replace replace replace replace keep keep

quill

webpack --config _develop/webpack.config.js --devtool hidden-source-map

npm run build; foreman start -f _develop/procfile

npm run build; karma start _develop/karma.config.js

karma start _develop/karma.config.js --reporters dots,saucelabs


<svg viewbox="0 0 18 18">
  <rect class="ql-stroke" height="4" width="12" x="3" y="7"></rect>
  <path class="ql-fill ql-transparent" d="M2,2V16H16V2H2ZM14,14H10V11H8v3H4V4H8V7h2V4h4V14Z"></path>
  <line class="ql-stroke" x1="12" x2="12" y1="11" y2="7"></line>
  <line class="ql-stroke" x1="9" x2="9" y1="11" y2="7"></line>
  <line class="ql-stroke" x1="6" x2="6" y1="11" y2="7"></line>
</svg>


<svg viewbox="0 0 18 18">
  <polyline class="ql-stroke" points="3.5 14 7 4 10.5 14"></polyline>
  <line class="ql-stroke" x1="9.45" x2="4.55" y1="11" y2="11"></line>
  <rect class="ql-fill" height="5" rx="0.5" ry="0.5" width="1" x="13" y="4"></rect>
  <rect class="ql-fill" height="5" rx="0.5" ry="0.5" transform="translate(20 -7) rotate(90)" width="1" x="13" y="4"></rect>
</svg>


import Delta from 'rich-text/lib/delta';
import Emitter from '../core/emitter';
import Module from '../core/module';
import Parchment from 'parchment';
import logger from '../core/logger';

let debug = logger('quill:clipboard');

const DOM_KEY = '__ql-matcher';
const BLOCK_ELEMENTS = {
  'ADDRESS': true,
  'ARTICLE': true,
  'ASIDE': true,
  'BLOCKQUOTE': true,
  'CANVAS': true,
  'DIV': true,
  'DL': true,
  'FIELDSET': true,
  'FIGCAPTION': true,
  'FIGURE': true,
  'FOOTER': true,
  'FORM': true,
  'HEADER': true,
  'H1': true, 'H2': true, 'H3': true, 'H4': true, 'H5': true, 'H6': true,
  'HGROUP': true,
  'HR': true,
  'LI': true,
  'MAIN': true,
  'NAV': true,
  'NOSCRIPT': true,
  'OL': true,
  'OUTPUT': true,
  'P': true,
  'PRE': true,
  'SECTION': true,
  'TABLE': true,
  'TFOOT': true,
  'UL': true,
  'VIDEO': true
};

class Clipboard extends Module {
  constructor(quill, options) {
    super(quill, options);
    this.quill.root.addEventListener('copy', this.onCopy.bind(this));
    this.quill.root.addEventListener('cut', this.onCut.bind(this));
    this.quill.root.addEventListener('paste', this.onPaste.bind(this));
    this.container = this.quill.addContainer('ql-clipboard');
    this.container.setAttribute('contenteditable', true);
    this.matchers = [
      [Node.TEXT_NODE, matchText],
      [Node.ELEMENT_NODE, matchNewline],
      [Node.ELEMENT_NODE, matchBlot],
      [Node.ELEMENT_NODE, matchSpacing],
      ['b, i', matchAliases]
    ];
  }

  addMatcher(selector, matcher) {
    this.matchers.push([selector, matcher]);
  }

  convert(container) {
    this.matchers.forEach((pair) => {
      let [selector, matcher] = pair;
      if (typeof selector === 'string') {
        [].forEach.call(container.querySelectorAll(selector), (node) => {
          // TODO use weakmap
          node[DOM_KEY] = node[DOM_KEY] || [];
          node[DOM_KEY].push(matcher);
        });
      }
    });
    let traverse = (node) => {  // Post-order
      return [].reduce.call(node.childNodes || [], (delta, childNode) => {
        let childrenDelta = traverse(childNode);
        childrenDelta = this.matchers.reduce(function(childrenDelta, pair) {
          let [type, matcher] = pair;
          if (type === true || childNode.nodeType === type) {
            childrenDelta = matcher(childNode, childrenDelta);
          }
          return childrenDelta;
        }, childrenDelta);
        childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {
          return matcher(childNode, childrenDelta);
        }, childrenDelta);
        return delta.concat(childrenDelta);
      }, new Delta());
    };
    return traverse(container);
  }

  onCopy(e) {
    let range = this.quill.getSelection();
    if (range == null || range.length === 0 || e.defaultPrevented) return;
    let clipboard = e.clipboardData || window.clipboardData;
    clipboard.setData('text', this.quill.getText(range));
    if (e.clipboardData) {  // IE11 does not let us set non-text data
      clipboard.setData('application/json', JSON.stringify(this.quill.getContents(range)));
    }
    e.preventDefault();
  }

  onCut(e) {
    if (e.defaultPrevented) return;
    this.onCopy(e);
    let range = this.quill.getSelection();
    this.quill.deleteText(range, Emitter.sources.USER);
    this.quill.setSelection(range.index, Emitter.sources.SILENT);
  }

  onPaste(e) {
    if (e.defaultPrevented) return;
    let range = this.quill.getSelection();
    let clipboard = e.clipboardData || window.clipboardData;
    let delta = new Delta().retain(range.index).delete(range.length);
    let callback = (delta) => {
      this.quill.updateContents(delta, Emitter.sources.USER);
      // range.length contributes to delta.length()
      this.quill.setSelection(delta.length() - range.length*2, Emitter.sources.SILENT);
      this.quill.selection.scrollIntoView();
    };
    let intercept = (delta) => {
      this.container.focus();
      setTimeout(() => {
        delta = delta.concat(this.convert(this.container));
        debug.info('paste', this.container.innerHTML, delta);
        this.container.innerHTML = '';
        callback(delta);
      }, 1);
    };
    // Firefox types is an iterable, not array
    // IE11 types can be null
    if ([].indexOf.call(clipboard.types || [], 'application/json') > -1) {
      try {
        let pasteJSON = JSON.parse(clipboard.getData('application/json'));
        callback(delta.concat(pasteJSON));
      } catch(err) {
        intercept(delta);
      }
      e.preventDefault();
      return;
    }
    intercept(delta);
  }
}

function deltaEndsWith(delta, text) {
  let lastOp = delta.ops[delta.ops.length - 1];
  // TODO fix case where delta lastOp length < text.length
  let endText = (lastOp == null || typeof lastOp.insert !== 'string') ? '' : lastOp.insert;
  return endText.slice(-1*text.length) === text;
}

function matchAliases(node, delta) {
  let formats = {};
  switch(node.tagName) {
    case 'B':
      formats = { bold: true };
      break;
    case 'I':
      formats = { italic: true };
      break;
    default: return delta;
  }
  return delta.compose(new Delta().retain(delta.length(), formats));
}

function matchBlot(node, delta) {
  let match = Parchment.query(node);
  if (match == null) return delta;
  if (match.prototype instanceof Parchment.Embed) {
    let embed = {};
    embed[match.blotName] = match.value(node);
    delta.insert(embed, match.formats(node));
  } else if (typeof match.formats === 'function') {
    delta = delta.compose(new Delta().retain(delta.length(), match.formats(node)));
  }
  return delta;
}

function matchNewline(node, delta) {
  if (BLOCK_ELEMENTS[node.tagName] || node.style.display === 'block') {
    if (!deltaEndsWith(delta, '\n')) {
      delta.insert('\n');
    }
  }
  return delta;
}

function matchSpacing(node, delta) {
  if (node.style.paddingBottom || (node.style.marginBottom && !deltaEndsWith(delta, '\n\n'))) {
    delta.insert('\n');
  }
  return delta;
}

function matchText(node, delta) {
  let text = node.data.replace(/\s\s+/g, ' ');
  return delta.insert(text);
}

export default Clipboard;


<svg viewbox="0 0 18 18">
  <rect class="ql-stroke" height="12" width="12" x="3" y="3"></rect>
  <rect class="ql-fill" height="2" width="3" x="5" y="5"></rect>
  <rect class="ql-fill" height="2" width="4" x="9" y="5"></rect>
  <g class="ql-fill ql-transparent">
    <rect height="2" width="3" x="5" y="8"></rect>
    <rect height="2" width="4" x="9" y="8"></rect>
    <rect height="2" width="3" x="5" y="11"></rect>
    <rect height="2" width="4" x="9" y="11"></rect>
  </g>
</svg>


import CodeBlock from '../formats/code';
import Emitter from '../core/emitter';
import Module from '../core/module';
import Parchment from 'parchment';

class HighlightCodeBlock extends CodeBlock {
  highlight() {
    if (this.cachedHTML !== this.domNode.innerHTML) {
      this.domNode.innerText = this.domNode.innerText;
      hljs.highlightBlock(this.domNode);
      this.attach();
      this.cachedHTML = this.domNode.innerHTML;
    }
  }
}

let CodeToken = new Parchment.Attributor.Class('token', 'hljs', {
  scope: Parchment.Scope.INLINE
});

class CodeHighlighter extends Module {
  constructor(quill, options) {
    super(quill, options);
    let timer = null;
    this.quill.on(Emitter.events.SCROLL_OPTIMIZE, () => {
      if (timer != null) return;
      timer = setTimeout(() => {
        this.highlight();
        timer = null;
      }, 100);
    });
    this.highlight();
  }

  highlight() {
    let range = this.quill.getSelection();
    this.quill.scroll.descendants(HighlightCodeBlock).forEach(function(code) {
      code.highlight();
    });
    if (range != null) {
      this.quill.setSelection(range, Emitter.sources.SILENT);
    }
  }
}

export { HighlightCodeBlock as CodeBlock, CodeToken, CodeHighlighter as default};


import Delta from 'rich-text/lib/delta';
import Parchment from 'parchment';
import Block from '../blots/block';
import Inline from '../blots/inline';

class Code extends Inline {}
Code.blotName = 'code';
Code.tagName = 'CODE';

class CodeBlock extends Block {
  delta() {
    return this.domNode.innerText.split('\n').reduce((delta, text) => {
      return delta.insert(text).insert('\n', this.formats());
    }, new Delta());
  }

  format(name, value) {
    if (name !== this.statics.blotName) return;
    super.formatAt(name, value);
  }

  formatAt(index, length, name, value) {
    if (name !== this.statics.blotName) return;
    super.formatAt(index, length, name, value);
  }

  insertAt(index, value, def) {
    if (def != null) return;  // Cannot insert embeds into code
    if (index < this.length() - 1) {
      let [child, offset] = this.children.find(index);
      child.insertAt(offset, value);
    } else {
      this.children.tail.insertAt(this.children.tail.length(), value);
    }
  }

  optimize(mutations) {
    super.optimize(mutations);
    let next = this.next;
    if (next instanceof CodeBlock && next.prev === this) {
      next.moveChildren(this);
      next.remove();
    }
  }
}
CodeBlock.blotName = 'code-block';
CodeBlock.tagName = 'PRE';

export { Code, CodeBlock as default };


"build": "webpack --config _develop/webpack.config.js --devtool hidden-source-map",

"start": "npm run build; foreman start -f _develop/procfile",

"test": "npm run build; karma start _develop/karma.config.js",

"travis": "karma start _develop/karma.config.js --reporters dots,saucelabs"

"test:coverage": "webpack --config _develop/webpack.coverage.js; karma start _develop/karma.config.js --reporters coverage",

var path = require('path');
var pkg = require('../package.json');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

var constantPack = new webpack.DefinePlugin({
  QUILL_VERSION: JSON.stringify(pkg.version)
});

module.exports = {
  context: path.resolve(__dirname, '..'),
  entry: {
    'quill.js': ['./quill.js'],
    'quill.css': './assets/core.styl',
    'quill.bubble.css': './assets/bubble.styl',
    'quill.snow.css': './assets/snow.styl',
    'unit.js': './test/unit.js'
  },
  output: {
    filename: '[name]',
    library: 'Quill',
    libraryTarget: 'umd',
    path: 'dist/'
  },
  resolve: {
    alias: {
      'parchment': path.resolve(__dirname, '..', 'node_modules/parchment/src/parchment')
    },
    extensions: ['', '.js', '.styl', '.ts']
  },
  module: {
    loaders: [
      { test: /parchment\/src\/.*\.ts$/, loader: 'ts' },
      { test: /\.styl$/, loader: ExtractTextPlugin.extract('style', 'css!stylus') },
      { test: /(?!node_modules)\/.*\.js$/, loader: 'babel?presets[]=es2015' },
      { test: /\.svg$/, loader: 'html?minimize=true' }
    ],
    noParse: [
      /\/node_modules\/clone\/clone\.js$/,
      /\/node_modules\/eventemitter3\/index\.js$/,
      /\/node_modules\/extend\/index\.js$/
    ]
  },
  ts: {
    configFileName: 'nonexistent.json',   // Parchment tsconfig wants to build tests, we don't
    compilerOptions: {
      target: 'es5',
      module: 'commonjs'
    },
    silent: true
  },
  plugins: [ constantPack, new ExtractTextPlugin('[name]', { allChunks: true }) ],
  devtool: 'source-map',
  devServer: {
    hot: false,
    port: 9080,
    stats: {
      assets: false,
      chunks: false,
      errorDetails: true,
      errors: true,
      hash: false,
      timings: false,
      version: false,
      warnings: true
    }
  }
};


var http = require('http');
var httpProxy = require('http-proxy');

var proxy = httpProxy.createProxyServer({});

var server = http.createServer(function(req, res) {
  if (/\/\d+\.\d+\.\d+/.test(req.url) || req.url.startsWith('/karma/base/dist')) {
    proxy.web(req, res, {
      ignorePath: true,
      target: 'http://localhost:9080/' + req.url.split('/').pop()
    });
  } else if (req.url.startsWith('/karma') || req.url === '/assets/favicon.png') {
    proxy.web(req, res, { ignorePath: false, target: { port: 9876 } });
  } else {
    proxy.web(req, res, { ignorePath: false, target: { port: 4000 } });
  }
});

server.on('upgrade', function (req, socket, head) {
  proxy.ws(req, socket, head);
});

proxy.on('error', function(e) {
  console.error(e);
});

console.log('Proxy listening on 9000');
server.listen(9000);


<svg viewbox="0 0 18 18">
  <polyline class="ql-stroke" points="3.5 14 7 4 10.5 14"></polyline>
  <line class="ql-stroke" x1="9.45" x2="4.55" y1="11" y2="11"></line>
  <rect class="ql-fill" height="5" rx="0.5" ry="0.5" transform="translate(20 -7) rotate(90)" width="1" x="13" y="4"></rect>
</svg>