else if node.previousSibling == null && node.nextSibling == null && !TandemLine.isLineNode(node.parentNode)

if node.parentNode[key][attribute] == true

keep keep keep keep replace keep keep keep

if node.parentNode[key]? && node.parentNode[key][attribute] == true

ALPHABET = 'abcdefghijklmnopqrstuvwxyz'.split('').concat(_.map([0..15], -> return "\n"))
NUM_OPERATIONS = 100

getRandomLength = ->
  rand = Math.random()
  if rand < 0.5
    return Math.floor(Math.random() * 10)
  else if rand < 0.7
    return Math.floor(Math.random() * 25)
  else if rand < 0.9
    return Math.floor(Math.random() * 50)
  else
    return Math.floor(Math.random() * 100)

getRandomOperation = (editor) ->
  index = Math.floor(Math.random() * (editor.doc.length + 1))
  length = getRandomLength() + 1
  rand = Math.random()
  if rand < 0.5
    return {op: 'insertAt', args: [index, getRandomString(length)]}
  length = Math.min(length, editor.doc.length - index)
  return null if length <= 0
  if rand < 0.75
    return {op: 'deleteAt', args: [index, length]}
  else
    attributes = ['bold', 'italic', 'strike', 'underline']
    attribute = attributes[Math.floor(Math.random() * attributes.length)]
    return {op: 'applyAttribute', args: [index, length, attribute, (Math.random() < 0.8)]}

getRandomString = (length) ->
  return _.map([0..(length - 1)], ->
    return ALPHABET[Math.floor(Math.random()*length)]
  ).join('')

$(document).ready( ->
  rangy.init()
  $editors = $('.editor-container')
  writer = new Tandem.Editor($editors.get(0))
  reader = new Tandem.Editor($editors.get(1))

  writer.on(writer.events.API_TEXT_CHANGE, (delta) ->
    reader.applyDelta(delta)
  )

  finishedFuzzing = _.after(NUM_OPERATIONS, ->
    _.defer( ->
      writerHtml = Tandem.Utils.cleanHtml(writer.doc.root.innerHTML)
      readerHtml = Tandem.Utils.cleanHtml(reader.doc.root.innerHTML)
      writerDelta = writer.getDelta()
      readerDelta = reader.getDelta()

      if writerHtml == readerHtml && _.isEqual(writerDelta, readerDelta)
        console.log "Fuzzing passed"
      else
        console.error "Fuzzing failed", writer, reader
    )
  )

  _.each([1..NUM_OPERATIONS], ->
    _.defer( ->
      operation = getRandomOperation(writer)
      if operation?
        writer[operation.op].apply(writer, operation.args)
      finishedFuzzing()
    )
  )
)


index = Math.floor(Math.random() * (editor.doc.length + 1))

return {op: 'insertAt', args: [index, getRandomString(length)]}

keep keep keep replace keep keep keep keep

index = Math.floor(Math.random() * editor.doc.length)


writer = new Tandem.Editor($editors.get(0))

reader = new Tandem.Editor($editors.get(1))


if writerHtml == readerHtml && _.isEqual(writerDelta, readerDelta)

console.error "Fuzzing failed", writer, reader

keep keep keep keep keep replace keep keep keep keep

console.info (NUM_OPERATIONS / time).toPrecision(2), 'ops/sec'

#= require underscore
#= require linked_list
#= require jetsync
#= require tandem/tags

class TandemLine extends LinkedList.Node
  @CLASS_NAME : 'tandem-line'
  @ID_PREFIX  : 'tandem-line-'
  @counter    : 0

  @isLineNode: (node) ->
    return node? && node.classList? && node.classList.contains(TandemLine.CLASS_NAME)

  constructor: (@doc, @node) ->
    @id = TandemLine.ID_PREFIX + TandemLine.counter
    @node.id = @id
    @node.classList.add(TandemLine.CLASS_NAME)
    TandemLine.counter += 1
    this.rebuild()
    super(@node)

  buildLeaves: (node, attributes) ->
    _.each(node.childNodes, (node) =>
      nodeAttributes = _.clone(attributes)
      attr = Tandem.Utils.getAttributeForContainer(node)
      nodeAttributes[attr] = true if attr?
      if Tandem.Leaf.isLeafNode(node)
        @leaves.append(new Tandem.Leaf(this, node, nodeAttributes, @numLeaves))
        @numLeaves += 1
      else
        this.buildLeaves(node, nodeAttributes)
    )

  applyRules: ->
    Tandem.Utils.traversePreorder(@node, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE
        rules = Tandem.Tags.LINE_RULES[node.tagName]
        if rules?
          _.each(rules, (data, rule) ->
            switch rule
              when 'rename' then node = Tandem.Utils.switchTag(node, data)
              else return
          )
        else
          node = Tandem.Utils.unwrap(node)
      return node
    )

  findLeaf: (leafNode) ->
    curLeaf = @leaves.first
    while curLeaf?
      return curLeaf if curLeaf.node == leafNode
      curLeaf = curLeaf.next
    return null

  normalizeHtml: ->
    return if @node.childNodes.length == 1 && @node.firstChild.tagName == 'BR'
    this.applyRules()
    this.removeRedundant()
    this.mergeAdjacent()
    this.wrapText()
    @node.appendChild(@node.ownerDocument.createElement('br')) if @node.firstChild == null

  splitContents: (offset) ->
    [node, offset] = Tandem.Utils.getChildAtOffset(@node, offset)
    return Tandem.Utils.splitNode(node, offset)

  toDelta: ->
    deltas = _.map(@leaves.toArray(), (leaf) ->
      return new JetInsert(leaf.text, leaf.attributes)
    )
    delta = new JetDelta(0, @length, deltas)
    delta.compact()
    return delta

  rebuild: ->
    # TODO memory leak issue?
    @leaves = new LinkedList()
    @numLeaves = 0
    this.normalizeHtml()
    this.buildLeaves(@node, {})
    this.resetContent()

  resetContent: ->
    @length = @node.textContent.length
    @innerHTML = @node.innerHTML
    @delta = this.toDelta()

  mergeAdjacent: (node = @node.firstChild) ->
    while node? && node.nextSibling?
      next = node.nextSibling
      if node.tagName == next.tagName && Tandem.Utils.getAttributeForContainer(node) == Tandem.Utils.getAttributeForContainer(next)
        node = Tandem.Utils.mergeNodes(node, next)
      else
        node = next
        _.each(_.clone(node.childNodes), (childNode) =>
          this.mergeAdjacent(node)
        )

  removeRedundant: ->
    key = '_tandemAttributes' + Math.floor(Math.random() * 100000000)
    @node[key] = {}
    Tandem.Utils.traversePreorder(@node, 0, (node) =>
      if node.nodeType == node.ELEMENT_NODE && node.textContent.length == 0
        return Tandem.Utils.unwrap(node)
      if node.tagName == 'SPAN'
        # Check if children need us
        if node.childNodes.length == 0 || !_.any(node.childNodes, (child) -> child.nodeType != child.ELEMENT_NODE)
          return Tandem.Utils.unwrap(node)
        # Check if parent needs us
        else if node.previousSibling == null && node.nextSibling == null && !TandemLine.isLineNode(node.parentNode)
          return Tandem.Utils.unwrap(node)
      attribute = Tandem.Utils.getAttributeForContainer(node)
      if attribute?
        if node.parentNode[key][attribute] == true
          return Tandem.Utils.unwrap(node)
        node[key] = _.clone(node.parentNode[key])
        node[key][attribute] = true
      return node
    )
    delete @node[key]
    Tandem.Utils.traversePreorder(@node, 0, (node) ->
      delete node[key]
      return node
    )

  renameEquivalent: ->

  wrapText: ->
    Tandem.Utils.traversePreorder(@node, 0, (node) =>
      node.normalize()
      if node.nodeType == node.TEXT_NODE && (node.nextSibling? || node.previousSibling? || node.parentNode == @node)
        span = node.ownerDocument.createElement('span')
        Tandem.Utils.wrap(span, node)
        node = span
      return node
    )

window.Tandem ||= {}
window.Tandem.Line = TandemLine


#= require underscore
#= require rangy/rangy-core
#= require diff_match_patch
#= require eventemitter2
#= require jetsync
#= require tandem/document
#= require tandem/range
#= require tandem/keyboard

class TandemEditor extends EventEmitter2
  @editors: []

  events:
    API_TEXT_CHANGE       : 'api-text-change'
    USER_SELECTION_CHANGE : 'user-selection-change'
    USER_TEXT_CHANGE      : 'user-text-change'

  options:
    POLL_INTERVAL: 500

  constructor: (@container) ->
    @container = document.getElementById(@container) if _.isString(@container)
    @iframe = this.createIframe(@container)
    @doc = new Tandem.Document(this, @iframe.contentWindow.document.body)
    @ignoreDomChanges = false
    @currentSelection = null
    @keyboard = new Tandem.Keyboard(this)
    this.initContentListeners()
    this.initSelectionListeners()
    TandemEditor.editors.push(this)

  createIframe: (parent) ->
    html = parent.innerHTML
    parent.innerHTML = ''
    iframe = document.createElement('iframe')
    iframe.frameborder = 0
    iframe.src = 'javascript:;'
    iframe.height = iframe.width = '100%'
    parent.appendChild(iframe)
    doc = iframe.contentWindow.document
    head = doc.getElementsByTagName('head')[0]
    style = doc.createElement('style')
    style.type = 'text/css'
    css = "
      body {
        font-family: 'Helvetica', 'Arial', san-serif;
        font-size: 13px;
        line-height: 15px;
        margin: 0px;
        padding: 10px;
      }

      a { text-decoration: underline; }
      b { font-weight: bold; }
      i { font-style: italic; }
      s { text-decoration: line-through; }
      u { text-decoration: underline; }

      ol, ul { margin: 0px; padding: 0px; }
      ul { list-style-type: disc; }

      ol.indent-1, ol.indent-4, ol.indent-7 { list-style-type: decimal; }
      ol.indent-2, ol.indent-5, ol.indent-8 { list-style-type: lower-alpha; }
      ol.indent-3, ol.indent-6, ol.indent-9 { list-style-type: lower-roman; }

      .font-background.black  { background-color: black; }
      .font-background.red    { background-color: red; }
      .font-background.orange { background-color: orange; }
      .font-background.yellow { background-color: yellow; }
      .font-background.green  { background-color: green; }
      .font-background.blue   { background-color: blue; }
      .font-background.purple { background-color: purple; }
      .font-color.white       { color: white; }
      .font-color.red         { color: red; }
      .font-color.orange      { color: orange; }
      .font-color.yellow      { color: yellow; }
      .font-color.green       { color: green; }
      .font-color.blue        { color: blue; }
      .font-color.purple      { color: purple; }
      .font-family.monospace  { font-family: 'Courier New', monospace; }
      .font-family.serif      { font-family: 'Times New Roman', serif; }
      .font-size.huge         { font-size: 32px; line-height: 36px; }
      .font-size.large        { font-size: 18px; line-height: 22px; }
      .font-size.small        { font-size: 10px; line-height: 12px; }

      .indent-1 { margin-left: 2em; }
      .indent-2 { margin-left: 4em; }
      .indent-3 { margin-left: 6em; }
      .indent-4 { margin-left: 8em; }
      .indent-5 { margin-left: 10em; }
      .indent-6 { margin-left: 12em; }
      .indent-7 { margin-left: 14em; }
      .indent-8 { margin-left: 16em; }
      .indent-9 { margin-left: 18em; }

      .tab { display: inline-block; margin: 0px; width: 2em; }
    "
    if style.styleSheet?
      style.styleSheet.cssText = css
    else
      style.appendChild(doc.createTextNode(css))
    head.appendChild(style)
    doc.body.setAttribute('contenteditable', true)
    doc.body.innerHTML = html
    return iframe

  initContentListeners: ->
    onEdit = _.debounce( =>
      delta = this.update()
      if !delta.isIdentity()
        this.checkSelectionChange()
        Tandem.Range.setSelection(this, @currentSelection)
        this.emit(this.events.USER_TEXT_CHANGE, delta)
    , 100)

    @doc.root.addEventListener('DOMSubtreeModified', =>
      return if @ignoreDomChanges
      onEdit()
    )

  initSelectionListeners: ->
    debounceCheckSelectionChange = _.debounce( =>
      this.checkSelectionChange()
    , 100)
    @doc.root.addEventListener('keyup', debounceCheckSelectionChange)
    @doc.root.addEventListener('mouseup', debounceCheckSelectionChange)
    setInterval((=> this.checkSelectionChange()), this.options.POLL_INTERVAL)

  # applyAttribute: (TandemRange range, String attr, Mixed value) ->
  # applyAttribute: (Number startIndex, Number length, String attr, Mixed value) ->
  applyAttribute: (startIndex, length, attr, value, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    if !_.isNumber(startIndex)
      [range, attr, value] = [startIndex, length, attr]
      startIndex = range.start.getIndex()
      length = range.end.getIndex() - startIndex
    else
      range = new Tandem.Range(this, startIndex, startIndex + length)

    this.preserveSelection(range.start, 0, =>
      [startLine, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      [endLine, endLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex + length)
      if startLine == endLine
        this.applyAttributeToLine(startLine, startLineOffset, endLineOffset, attr, value)
      else
        curLine = startLine.nextSibling
        while curLine? && curLine != endLine
          nextSibling = curLine.nextSibling
          this.applyAttributeToLine(curLine, 0, curLine.textContent.length, attr, value)
          curLine = nextSibling
        this.applyAttributeToLine(startLine, startLineOffset, startLine.textContent.length, attr, value)
        this.applyAttributeToLine(endLine, 0, endLineOffset, attr, value)
      Tandem.Document.fixListNumbering(@doc.root) if attr == 'list'
      @doc.rebuildDirty()
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if startIndex > 0
      attribute = {}
      attribute[attr] = value
      deltas.push(new JetRetain(startIndex, startIndex + length, attribute))
      deltas.push(new JetRetain(startIndex + length, @doc.length)) if startIndex + length < @doc.length
      delta = new JetDelta(@doc.length, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  applyAttributeToLine: (lineNode, startOffset, endOffset, attr, value) ->
    line = @doc.findLine(lineNode)
    if _.indexOf(Tandem.Constants.LINE_ATTRIBUTES, attr, true) > -1
      this.applyLineAttribute(line, attr, value)
    else
      return if startOffset == endOffset
      [prevNode, startNode] = line.splitContents(startOffset)
      [endNode, nextNode] = line.splitContents(endOffset)
      if value && Tandem.Utils.getAttributeDefault(attr) != value
        fragment = @doc.root.ownerDocument.createDocumentFragment()
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          fragment.appendChild(node)
        )
        attrNode = Tandem.Utils.createContainerForAttribute(@doc.root.ownerDocument, attr, value)
        attrNode.appendChild(fragment)
        lineNode.insertBefore(attrNode, nextNode)
      else
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          Tandem.Utils.removeAttributeFromSubtree(node, attr)
        )
    @doc.updateLine(line)

  applyDelta: (delta) ->
    console.assert(delta.startLength == @doc.length, "Trying to apply delta to incorrect doc length", delta, @doc, @doc.root)
    index = 0       # Stores where the last retain end was, so if we see another one, we know to delete
    offset = 0      # Tracks how many characters inserted to correctly offset new text
    _.each(delta.deltas, (delta) =>
      if JetDelta.isInsert(delta)
        this.insertAt(index + offset, delta.text, false)
        _.each(delta.attributes, (value, attr) =>
          this.applyAttribute(index + offset, delta.text.length, attr, value, false)
        )
        offset += delta.getLength()
      else if JetDelta.isRetain(delta)
        if delta.start > index
          this.deleteAt(index + offset, delta.start - index, false)
          offset -= (delta.start - index)
        else
          _.each(delta.attributes, (value, attr) =>
            this.applyAttribute(index + offset, delta.end - delta.start, attr, value, false)
          )
        index = delta.end
      else
        console.warn('Unrecognized type in delta', delta)
    )
    # If end of text was deleted
    if delta.endLength < delta.startLength + offset
      this.deleteAt(delta.endLength, delta.startLength + offset - delta.endLength, false)

  applyLineAttribute: (line, attr, value) ->
    curIndent = if line.attributes[attr]? then line.attributes[attr] else 0
    nextIndent = if _.isNumber(value) then value else (if value then 1 else 0)
    if attr == 'indent'
      Tandem.Utils.setIndent(line.node, nextIndent)
    else if Tandem.Constants.INDENT_ATTRIBUTES[attr]?
      lineNode = line.node
      expectedTag = if value then (if attr == 'list' then 'OL' else 'UL') else 'DIV'
      if lineNode.tagName != expectedTag
        if value && lineNode.firstChild.tagName != 'LI'
          li = lineNode.ownerDocument.createElement('li')
          Tandem.Utils.wrapChildren(li, lineNode)
        else if !value && lineNode.firstChild.tagName == 'LI'
          Tandem.Utils.unwrap(lineNode.firstChild)
        line.node = Tandem.Utils.switchTag(lineNode, expectedTag)
      Tandem.Utils.setIndent(line.node, nextIndent)

  checkSelectionChange: ->
    return if @ignoreDomChanges
    selection = this.getSelection()
    if selection != @currentSelection && ((selection? && !selection.equals(@currentSelection)) || !@currentSelection.equals(selection))
      this.emit(this.events.USER_SELECTION_CHANGE, selection)
      @currentSelection = selection

  deleteAt: (startIndex, length, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    originalDocLength = @doc.length
    if !_.isNumber(startIndex)
      range = startIndex
      startPos = range.start
      endPos = range.end
      startIndex = range.start.getIndex()
      length = range.end.getIndex() - startIndex
    else
      startPos = Tandem.Position.makePosition(this, startIndex)
      endPos = Tandem.Position.makePosition(this, startIndex + length)
    startIndex = startPos.getIndex()
    endIndex = endPos.getIndex()
    this.preserveSelection(startPos, 0 - length, =>
      [startLineNode, startOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      [endLineNode, endOffset] = Tandem.Utils.getChildAtOffset(@doc.root, endIndex)
      fragment = Tandem.Utils.extractNodes(startLineNode, startOffset, endLineNode, endOffset)
      lineNodes = _.values(fragment.childNodes).concat(_.uniq([startLineNode, endLineNode]))
      _.each(lineNodes, (lineNode) =>
        line = @doc.findLine(lineNode)
        @doc.updateLine(line) if line?
      )
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if 0 < startIndex
      deltas.push(new JetRetain(startIndex + length, originalDocLength)) if startIndex + length < originalDocLength
      delta = new JetDelta(originalDocLength, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  getAt: (startIndex, length) ->
    # - Returns array of {text: "", attr: {}}
    # 1. Get all nodes in the range
    # 2. For first and last, change the text
    # 3. Return array
    # - Helper to get nodes in given index range
    # - In the case of 0 lenght, text will always be "", but attributes should be properly applied

  getDelta: ->
    return @doc.toDelta()

  getSelection: ->
    return Tandem.Range.getSelection(this)

  insertAt: (startIndex, text, emitEvent = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    originalDocLength = @doc.length
    position = Tandem.Position.makePosition(this, startIndex)
    index = startIndex = position.getIndex()
    this.preserveSelection(position, text.length, =>
      lines = text.split("\n")
      _.each(lines, (line, lineIndex) =>
        strings = line.split("\t")
        _.each(strings, (str, strIndex) =>
          this.insertTextAt(index, str)
          index += str.length
          if strIndex < strings.length - 1
            this.insertTabAt(index)
            index += 1
        )
        if lineIndex < lines.length - 1
          this.insertNewlineAt(index)
          index += 1
        else
          # TODO could be more clever about if we need to call this
          Tandem.Document.fixListNumbering(@doc.root)
      )
    )
    if emitEvent
      deltas = []
      deltas.push(new JetRetain(0, startIndex)) if 0 < startIndex
      deltas.push(new JetInsert(text))
      deltas.push(new JetRetain(startIndex, originalDocLength)) if startIndex < originalDocLength
      delta = new JetDelta(originalDocLength, @doc.length, deltas)
      this.emit(this.events.API_TEXT_CHANGE, delta)
    @ignoreDomChanges = oldIgnoreDomChange

  insertNewlineAt: (startIndex) ->
    [line, offset] = @doc.findLineAtOffset(startIndex)
    @doc.splitLine(line, offset)

  insertTabAt: (startIndex) ->
    [startLineNode, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
    startLine = @doc.findLine(startLineNode)
    [prevNode, startNode] = startLine.splitContents(startLineOffset)
    tab = startLineNode.ownerDocument.createElement('span')
    tab.classList.add(Tandem.Leaf.TAB_NODE_CLASS)
    startLineNode.insertBefore(tab, startNode)
    @doc.updateLine(startLine)

  # insertTextAt: (Number startIndex, String text) ->
  # insertTextAt: (TandemRange startIndex, String text) ->
  insertTextAt: (startIndex, text) ->
    return if text.length <= 0
    position = Tandem.Position.makePosition(this, startIndex)
    startIndex = position.getIndex()
    leaf = position.getLeaf()
    if _.keys(leaf.attributes).length > 0
      [lineNode, lineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      line = @doc.findLine(lineNode)
      [beforeNode, afterNode] = line.splitContents(lineOffset)
      span = lineNode.ownerDocument.createElement('span')
      span.textContent = text
      lineNode.insertBefore(span, afterNode)
    else
      if leaf.node.nodeName == 'BR'
        parent = leaf.node.parentNode
        parent.removeChild(leaf.node)
        leaf.node = parent.ownerDocument.createElement('span')
        leaf.setText(text)
        parent.appendChild(leaf.node)
      else
        leaf.setText(leaf.node.textContent.substr(0, position.offset) + text + leaf.node.textContent.substr(position.offset))
    @doc.updateLine(leaf.line)

  preserveSelection: (modificationStart, charAdditions, fn) ->
    if @currentSelection?
      startLeaf = @currentSelection.start.getLeaf()
      if startLeaf?
        [selStart, selEnd] = this.transformSelection(modificationStart, @currentSelection, charAdditions)
        fn()
      else
        nativeSel = Tandem.Range.getNativeSelection(@currentSelection.editor)
        anchorNode = if nativeSel.anchorNode.parentNode? then nativeSel.anchorNode else nativeSel.anchorParent
        focusNode = if nativeSel.focusNode.parentNode? then nativeSel.focusNode else nativeSel.focusParent
        fn()
        startPos = new Tandem.Position(this, anchorNode, nativeSel.anchorOffset)
        endPos = new Tandem.Position(this, focusNode, nativeSel.focusOffset)
        range = new Tandem.Range(this, startPos, endPos)
        [selStart, selEnd] = this.transformSelection(modificationStart, range, charAdditions)
      savedSelectionRange = new Tandem.Range(@currentSelection.editor, selStart, selEnd)
      Tandem.Range.setSelection(this, savedSelectionRange)
    else
      fn()

  transformSelection: (modificationStart, selectionRange, charAdditions) ->
    modPos = if modificationStart? then modificationStart.getIndex() else 0
    selStart = selectionRange.start.getIndex()
    selEnd = selectionRange.end.getIndex()
    selStart = Math.max(selStart + charAdditions, modPos) if modPos <= selStart
    selEnd = Math.max(selEnd + charAdditions, modPos) if modPos < selEnd
    selEnd = Math.max(selStart, selEnd)
    return [selStart, selEnd]

  update: ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    oldDelta = @doc.toDelta()
    this.preserveSelection(null, 0, =>
      Tandem.Document.normalizeHtml(@doc.root)
      lines = @doc.lines.toArray()
      lineNode = @doc.root.firstChild
      _.each(lines, (line, index) =>
        while line.node != lineNode
          if line.node.parentNode == @doc.root
            newLine = @doc.insertLineBefore(lineNode, line)
            lineNode = lineNode.nextSibling
          else
            @doc.removeLine(line)
            return
        @doc.updateLine(line)
        lineNode = lineNode.nextSibling
      )
      while lineNode != null
        newLine = @doc.appendLine(lineNode)
        lineNode = lineNode.nextSibling
    )
    @ignoreDomChanges = oldIgnoreDomChange

    newDelta = @doc.toDelta()
    decompose = JetSync.decompose(oldDelta, newDelta)
    compose = JetSync.compose(oldDelta, decompose)
    if !_.isEqual(compose, newDelta)
      console.info(JSON.stringify(oldDelta))
      console.info(JSON.stringify(newDelta))
      console.info(JSON.stringify(decompose))
      console.info(JSON.stringify(compose))
      console.assert(false, oldDelta, newDelta, decompose, compose)
    return decompose

window.Tandem ||= {}
window.Tandem.Editor = TandemEditor


;!function(exports, undefined) {

  var isArray = Array.isArray ? Array.isArray : function _isArray(obj) {
    return Object.prototype.toString.call(obj) === "[object Array]";
  };
  var defaultMaxListeners = 10;

  function init() {
    this._events = new Object;
  }

  function configure(conf) {
    if (conf) {
      conf.delimiter && (this.delimiter = conf.delimiter);
      conf.wildcard && (this.wildcard = conf.wildcard);
      if (this.wildcard) {
        this.listenerTree = new Object;
      }
    }
  }

  function EventEmitter(conf) {
    this._events = new Object;
    configure.call(this, conf);
  }

  //
  // Attention, function return type now is array, always !
  // It has zero elements if no any matches found and one or more
  // elements (leafs) if there are matches
  //
  function searchListenerTree(handlers, type, tree, i) {
    if (!tree) {
      return [];
    }
    var listeners=[], leaf, len, branch, xTree, xxTree, isolatedBranch, endReached,
        typeLength = type.length, currentType = type[i], nextType = type[i+1];
    if (i === typeLength && tree._listeners) {
      //
      // If at the end of the event(s) list and the tree has listeners
      // invoke those listeners.
      //
      if (typeof tree._listeners === 'function') {
        handlers && handlers.push(tree._listeners);
        return [tree];
      } else {
        for (leaf = 0, len = tree._listeners.length; leaf < len; leaf++) {
          handlers && handlers.push(tree._listeners[leaf]);
        }
        return [tree];
      }
    }

    if ((currentType === '*' || currentType === '**') || tree[currentType]) {
      //
      // If the event emitted is '*' at this part
      // or there is a concrete match at this patch
      //
      if (currentType === '*') {
        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+1));
          }
        }
        return listeners;
      } else if(currentType === '**') {
        endReached = (i+1 === typeLength || (i+2 === typeLength && nextType === '*'));
        if(endReached && tree._listeners) {
          // The next element has a _listeners, add it to the handlers.
          listeners = listeners.concat(searchListenerTree(handlers, type, tree, typeLength));
        }

        for (branch in tree) {
          if (branch !== '_listeners' && tree.hasOwnProperty(branch)) {
            if(branch === '*' || branch === '**') {
              if(tree[branch]._listeners && !endReached) {
                listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], typeLength));
              }
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            } else if(branch === nextType) {
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+2));
            } else {
              // No match on this one, shift into the tree but not in the type array.
              listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i));
            }
          }
        }
        return listeners;
      }

      listeners = listeners.concat(searchListenerTree(handlers, type, tree[currentType], i+1));
    }

    xTree = tree['*'];
    if (xTree) {
      //
      // If the listener tree will allow any match for this part,
      // then recursively explore all branches of the tree
      //
      searchListenerTree(handlers, type, xTree, i+1);
    }

    xxTree = tree['**'];
    if(xxTree) {
      if(i < typeLength) {
        if(xxTree._listeners) {
          // If we have a listener on a '**', it will catch all, so add its handler.
          searchListenerTree(handlers, type, xxTree, typeLength);
        }

        // Build arrays of matching next branches and others.
        for(branch in xxTree) {
          if(branch !== '_listeners' && xxTree.hasOwnProperty(branch)) {
            if(branch === nextType) {
              // We know the next element will match, so jump twice.
              searchListenerTree(handlers, type, xxTree[branch], i+2);
            } else if(branch === currentType) {
              // Current node matches, move into the tree.
              searchListenerTree(handlers, type, xxTree[branch], i+1);
            } else {
              isolatedBranch = {};
              isolatedBranch[branch] = xxTree[branch];
              searchListenerTree(handlers, type, { '**': isolatedBranch }, i+1);
            }
          }
        }
      } else if(xxTree._listeners) {
        // We have reached the end and still on a '**'
        searchListenerTree(handlers, type, xxTree, typeLength);
      } else if(xxTree['*'] && xxTree['*']._listeners) {
        searchListenerTree(handlers, type, xxTree['*'], typeLength);
      }
    }

    return listeners;
  }

  function growListenerTree(type, listener) {

    type = typeof type === 'string' ? type.split(this.delimiter) : type.slice();

    //
    // Looks for two consecutive '**', if so, don't add the event at all.
    //
    for(var i = 0, len = type.length; i+1 < len; i++) {
      if(type[i] === '**' && type[i+1] === '**') {
        return;
      }
    }

    var tree = this.listenerTree;
    var name = type.shift();

    while (name) {

      if (!tree[name]) {
        tree[name] = new Object;
      }

      tree = tree[name];

      if (type.length === 0) {

        if (!tree._listeners) {
          tree._listeners = listener;
        }
        else if(typeof tree._listeners === 'function') {
          tree._listeners = [tree._listeners, listener];
        }
        else if (isArray(tree._listeners)) {

          tree._listeners.push(listener);

          if (!tree._listeners.warned) {

            var m = defaultMaxListeners;

            if (typeof this._events.maxListeners !== 'undefined') {
              m = this._events.maxListeners;
            }

            if (m > 0 && tree._listeners.length > m) {

              tree._listeners.warned = true;
              console.error('(node) warning: possible EventEmitter memory ' +
                            'leak detected. %d listeners added. ' +
                            'Use emitter.setMaxListeners() to increase limit.',
                            tree._listeners.length);
              console.trace();
            }
          }
        }
        return true;
      }
      name = type.shift();
    }
    return true;
  };

  // By default EventEmitters will print a warning if more than
  // 10 listeners are added to it. This is a useful default which
  // helps finding memory leaks.
  //
  // Obviously not all Emitters should be limited to 10. This function allows
  // that to be increased. Set to zero for unlimited.

  EventEmitter.prototype.delimiter = '.';

  EventEmitter.prototype.setMaxListeners = function(n) {
    this._events || init.call(this);
    this._events.maxListeners = n;
  };

  EventEmitter.prototype.event = '';

  EventEmitter.prototype.once = function(event, fn) {
    this.many(event, 1, fn);
    return this;
  };

  EventEmitter.prototype.many = function(event, ttl, fn) {
    var self = this;

    if (typeof fn !== 'function') {
      throw new Error('many only accepts instances of Function');
    }

    function listener() {
      if (--ttl === 0) {
        self.off(event, listener);
      }
      fn.apply(this, arguments);
    };

    listener._origin = fn;

    this.on(event, listener);

    return self;
  };

  EventEmitter.prototype.emit = function() {
    this._events || init.call(this);

    var type = arguments[0];

    if (type === 'newListener') {
      if (!this._events.newListener) { return false; }
    }

    // Loop through the *_all* functions and invoke them.
    if (this._all) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
      for (i = 0, l = this._all.length; i < l; i++) {
        this.event = type;
        this._all[i].apply(this, args);
      }
    }

    // If there is no 'error' event listener then throw.
    if (type === 'error') {

      if (!this._all &&
        !this._events.error &&
        !(this.wildcard && this.listenerTree.error)) {

        if (arguments[1] instanceof Error) {
          throw arguments[1]; // Unhandled 'error' event
        } else {
          throw new Error("Uncaught, unspecified 'error' event.");
        }
        return false;
      }
    }

    var handler;

    if(this.wildcard) {
      handler = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handler, ns, this.listenerTree, 0);
    }
    else {
      handler = this._events[type];
    }

    if (typeof handler === 'function') {
      this.event = type;
      if (arguments.length === 1) {
        handler.call(this);
      }
      else if (arguments.length > 1)
        switch (arguments.length) {
          case 2:
            handler.call(this, arguments[1]);
            break;
          case 3:
            handler.call(this, arguments[1], arguments[2]);
            break;
          // slower
          default:
            var l = arguments.length;
            var args = new Array(l - 1);
            for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
            handler.apply(this, args);
        }
      return true;
    }
    else if (handler) {
      var l = arguments.length;
      var args = new Array(l - 1);
      for (var i = 1; i < l; i++) args[i - 1] = arguments[i];

      var listeners = handler.slice();
      for (var i = 0, l = listeners.length; i < l; i++) {
        this.event = type;
        listeners[i].apply(this, args);
      }
      return (listeners.length > 0) || this._all;
    }
    else {
      return this._all;
    }

  };

  EventEmitter.prototype.on = function(type, listener) {

    if (typeof type === 'function') {
      this.onAny(type);
      return this;
    }

    if (typeof listener !== 'function') {
      throw new Error('on only accepts instances of Function');
    }
    this._events || init.call(this);

    // To avoid recursion in the case that type == "newListeners"! Before
    // adding it to the listeners, first emit "newListeners".
    this.emit('newListener', type, listener);

    if(this.wildcard) {
      growListenerTree.call(this, type, listener);
      return this;
    }

    if (!this._events[type]) {
      // Optimize the case of one listener. Don't need the extra array object.
      this._events[type] = listener;
    }
    else if(typeof this._events[type] === 'function') {
      // Adding the second element, need to change to array.
      this._events[type] = [this._events[type], listener];
    }
    else if (isArray(this._events[type])) {
      // If we've already got an array, just append.
      this._events[type].push(listener);

      // Check for listener leak
      if (!this._events[type].warned) {

        var m = defaultMaxListeners;

        if (typeof this._events.maxListeners !== 'undefined') {
          m = this._events.maxListeners;
        }

        if (m > 0 && this._events[type].length > m) {

          this._events[type].warned = true;
          console.error('(node) warning: possible EventEmitter memory ' +
                        'leak detected. %d listeners added. ' +
                        'Use emitter.setMaxListeners() to increase limit.',
                        this._events[type].length);
          console.trace();
        }
      }
    }
    return this;
  };

  EventEmitter.prototype.onAny = function(fn) {

    if(!this._all) {
      this._all = [];
    }

    if (typeof fn !== 'function') {
      throw new Error('onAny only accepts instances of Function');
    }

    // Add the function to the event listener collection.
    this._all.push(fn);
    return this;
  };

  EventEmitter.prototype.addListener = EventEmitter.prototype.on;

  EventEmitter.prototype.off = function(type, listener) {
    if (typeof listener !== 'function') {
      throw new Error('removeListener only takes instances of Function');
    }

    var handlers,leafs=[];

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);
    }
    else {
      // does not use listeners(), so no side effect of creating _events[type]
      if (!this._events[type]) return this;
      handlers = this._events[type];
      leafs.push({_listeners:handlers});
    }

    for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
      var leaf = leafs[iLeaf];
      handlers = leaf._listeners;
      if (isArray(handlers)) {

        var position = -1;

        for (var i = 0, length = handlers.length; i < length; i++) {
          if (handlers[i] === listener ||
            (handlers[i].listener && handlers[i].listener === listener) ||
            (handlers[i]._origin && handlers[i]._origin === listener)) {
            position = i;
            break;
          }
        }

        if (position < 0) {
          return this;
        }

        if(this.wildcard) {
          leaf._listeners.splice(position, 1)
        }
        else {
          this._events[type].splice(position, 1);
        }

        if (handlers.length === 0) {
          if(this.wildcard) {
            delete leaf._listeners;
          }
          else {
            delete this._events[type];
          }
        }
      }
      else if (handlers === listener ||
        (handlers.listener && handlers.listener === listener) ||
        (handlers._origin && handlers._origin === listener)) {
        if(this.wildcard) {
          delete leaf._listeners;
        }
        else {
          delete this._events[type];
        }
      }
    }

    return this;
  };

  EventEmitter.prototype.offAny = function(fn) {
    var i = 0, l = 0, fns;
    if (fn && this._all && this._all.length > 0) {
      fns = this._all;
      for(i = 0, l = fns.length; i < l; i++) {
        if(fn === fns[i]) {
          fns.splice(i, 1);
          return this;
        }
      }
    } else {
      this._all = [];
    }
    return this;
  };

  EventEmitter.prototype.removeListener = EventEmitter.prototype.off;

  EventEmitter.prototype.removeAllListeners = function(type) {
    if (arguments.length === 0) {
      !this._events || init.call(this);
      return this;
    }

    if(this.wildcard) {
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0);

      for (var iLeaf=0; iLeaf<leafs.length; iLeaf++) {
        var leaf = leafs[iLeaf];
        leaf._listeners = null;
      }
    }
    else {
      if (!this._events[type]) return this;
      this._events[type] = null;
    }
    return this;
  };

  EventEmitter.prototype.listeners = function(type) {
    if(this.wildcard) {
      var handlers = [];
      var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice();
      searchListenerTree.call(this, handlers, ns, this.listenerTree, 0);
      return handlers;
    }

    this._events || init.call(this);

    if (!this._events[type]) this._events[type] = [];
    if (!isArray(this._events[type])) {
      this._events[type] = [this._events[type]];
    }
    return this._events[type];
  };

  EventEmitter.prototype.listenersAny = function() {

    if(this._all) {
      return this._all;
    }
    else {
      return [];
    }

  };

  if (typeof define === 'function' && define.amd) {
    define(function() {
      return EventEmitter;
    });
  } else {
    exports.EventEmitter2 = EventEmitter;
  }

}(typeof process !== 'undefined' && typeof process.title !== 'undefined' && typeof exports !== 'undefined' ? exports : window);


GIT
  remote: git://github.com/manufaktor/guard-haml.git
  revision: ee19e704c5afface900d18ba7f9ad90993712d02
  branch: development
  specs:
    guard-haml (0.4)
      guard (>= 1.1)
      haml (>= 3.0)

GEM
  remote: http://rubygems.org/
  specs:
    coffee-script (2.2.0)
      coffee-script-source
      execjs
    coffee-script-source (1.3.3)
    execjs (1.4.0)
      multi_json (~> 1.0)
    ffi (1.1.5)
    guard (1.3.0)
      listen (>= 0.4.2)
      thor (>= 0.14.6)
    guard-coffeescript (1.2.0)
      coffee-script (>= 2.2.0)
      guard (>= 1.1.0)
    guard-sass (1.0.0)
      guard (>= 1.1.0)
      sass (>= 3.1)
    haml (3.1.7)
    listen (0.4.7)
      rb-fchange (~> 0.0.5)
      rb-fsevent (~> 0.9.1)
      rb-inotify (~> 0.8.8)
    multi_json (1.3.6)
    rb-fchange (0.0.5)
      ffi
    rb-fsevent (0.9.1)
    rb-inotify (0.8.8)
      ffi (>= 0.5.0)
    sass (3.1.20)
    thor (0.15.4)

PLATFORMS
  ruby

DEPENDENCIES
  guard
  guard-coffeescript
  guard-haml!
  guard-sass


#= underscore
#= require rangy/rangy-core

class TandemRange
  @getNativeSelection: (editor) ->
    rangySel = rangy.getSelection(editor.iframe.contentWindow)
    return null unless rangySel.anchorNode? && rangySel.focusNode?
    if !rangySel.isBackwards()
      [anchorNode, anchorOffset, focusNode, focusOffset] = [rangySel.anchorNode, rangySel.anchorOffset, rangySel.focusNode, rangySel.focusOffset]
    else
      [focusNode, focusOffset, anchorNode, anchorOffset] = [rangySel.anchorNode, rangySel.anchorOffset, rangySel.focusNode, rangySel.focusOffset]
    return {
      anchorNode    : anchorNode
      anchorParent  : anchorNode.parentNode
      anchorOffset  : anchorOffset
      focusNode     : focusNode
      focusParent   : focusNode.parentNode
      focusOffset   : focusOffset
    }

  @getSelection: (editor) ->
    nativeSel = TandemRange.getNativeSelection(editor)
    return null unless nativeSel?
    start = new Tandem.Position(editor, nativeSel.anchorNode, nativeSel.anchorOffset)
    end = new Tandem.Position(editor, nativeSel.focusNode, nativeSel.focusOffset)
    return new TandemRange(editor, start, end)

  @setSelection: (editor, range) ->
    rangySel = rangy.getSelection(editor.iframe.contentWindow)
    rangySelRange = range.getRangy()
    rangySel.setSingleRange(rangySelRange)

  # constructor: (TandemEditor editor, Number startIndex, Number endIndex) ->
  # constructor: (TandemEditor editor, Object start, Object end) ->
  constructor: (@editor, @start, @end) ->
    # TODO initialize with index
    @start = new Tandem.Position(@editor, @start) if _.isNumber(@start)
    @end = new Tandem.Position(@editor, @end) if _.isNumber(@end)

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

  getAttributeIntersection: (ignoreValue = false) ->
    startLeaf = this.start.getLeaf()
    endLeaf = this.end.getLeaf()
    # TODO Fix race condition that makes check necessary... should always be able to return attribute intersection
    return {} if !startLeaf? || !endLeaf?
    if this.isCollapsed()
      return startLeaf.getAttributes()
    leaves = this.getLeaves()
    leaves.pop() if leaves.length > 0 && @end.offset == 0
    leaves.splice(0, 1) if leaves.length > 0 && @start.offset == leaves[0].text.length
    attributes = if leaves.length > 0 then leaves[0].getAttributes() else {}
    _.all(leaves, (leaf) ->
      _.each(attributes, (value, key) ->
        delete attributes[key] if leaf.attributes[key] != value && !ignoreValue
      )
      return _.keys(attributes).length > 0
    )
    return attributes

  getLeafNodes: ->
    range = this.getRangy()
    if range.collapsed
      return [@start.leafNode]
    else
      nodes = _.map(range.getNodes([3]), (node) ->
        return node.parentNode
      )
      nodes.pop() if nodes[nodes.length - 1] != @end.leafNode || @end.offset == 0
      return nodes

  getLeaves: ->
    itr = new Tandem.LeafIterator(@start.getLeaf(), @end.getLeaf())
    arr = itr.toArray()
    return arr

  getLineNodes: ->
    startLine = @editor.doc.findLineNode(@start.leafNode)
    endLine = @editor.doc.findLineNode(@end.leafNode)
    if startLine == endLine
      return [startLine]
    lines = []
    while startLine != endLine
      lines.push(startLine)
      startLine = startLine.nextSibling
    lines.push(endLine)
    return lines

  getLines: ->
    return _.map(this.getLineNodes(), (lineNode) ->
      return @editor.doc.findLine(lineNode)
    )

  getRangy: ->
    range = rangy.createRangyRange(@editor.iframe.contentWindow)
    if @start.leafNode.nodeName != 'BR'
      range.setStart(@start.leafNode.firstChild, @start.offset)
    else
      range.setStart(@start.leafNode, 0)
    if @end.leafNode.nodeName != 'BR'
      range.setEnd(@end.leafNode.firstChild, @end.offset)
    else
      range.setEnd(@end.leafNode, 0)
    return range

  getText: ->
    leaves = this.getLeaves()
    return "" if leaves.length == 0
    line = leaves[0].line
    return _.map(leaves, (leaf) =>
      part = leaf.text
      if leaf == @end.getLeaf()
        part = part.substring(0, @end.offset)
      if leaf == @start.getLeaf()
        part = part.substring(@start.offset)
      if line != leaf.line
        part = "\n" + part
        line = leaf.line
      return part
    ).join('')

  isCollapsed: ->
    return @start.leafNode == @end.leafNode && @start.offset == @end.offset

window.Tandem ||= {}
window.Tandem.Range = TandemRange


attrNode = Tandem.Utils.createContainerForAttribute(@doc.root.ownerDocument, attr, value)

#= require underscore
#= require linked_list
#= require jetsync
#= require tandem/constants

class TandemLine extends LinkedList.Node
  @CLASS_NAME : 'line'
  @DIRTY_CLASS: 'dirty'
  @ID_PREFIX  : 'line-'

  @applyRules: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE
        rules = Tandem.Constants.LINE_RULES[node.tagName]
        if rules?
          _.each(rules, (data, rule) ->
            switch rule
              when 'rename' then node = Tandem.Utils.switchTag(node, data)
              else return
          )
        else
          node = Tandem.Utils.unwrap(node)
      return node
    )

  @isLineNode: (node) ->
    return node? && node.classList? && node.classList.contains(TandemLine.CLASS_NAME)

  @mergeAdjacent: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      if node.nodeType == node.ELEMENT_NODE && !TandemLine.isLineNode(node)
        next = node.nextSibling
        if next?.tagName == node.tagName && node.tagName != 'LI' && !Tandem.Utils.isIgnoreNode(node) && !Tandem.Utils.isIgnoreNode(next)
          [nodeAttr, nodeValue] = Tandem.Utils.getAttributeForContainer(node)
          [nextAttr, nextValue] = Tandem.Utils.getAttributeForContainer(next)
          if nodeAttr == nextAttr && nodeValue == nextValue
            node = Tandem.Utils.mergeNodes(node, next)
      return node
    )

  @normalizeHtml: (root) ->
    return if root.childNodes.length == 1 && root.firstChild.tagName == 'BR'
    this.applyRules(root)
    this.removeRedundant(root)
    # TODO need wrapText before and after for these cases:
    # Before: TestWhat -> TestWhat
    # After: BoldTest -> BoldTest
    this.wrapText(root)
    this.mergeAdjacent(root)
    this.wrapText(root)
    root.appendChild(root.ownerDocument.createElement('br')) if root.firstChild == null

  @removeRedundant: (root) ->
    tandemKey = '_tandemAttributes' + Math.floor(Math.random() * 100000000)
    root[tandemKey] = {}

    isRedudant = (node) ->
      if node.nodeType == node.ELEMENT_NODE && !Tandem.Utils.isIgnoreNode(node)
        if node.textContent.length == 0
          return true
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        if attrName?
          return node.parentNode[tandemKey][attrName]?     # Parent attribute value will overwrite child's so no need to check attrValue
        else if node.tagName == 'SPAN'
          # Check if children need us
          if node.childNodes.length == 0 || !_.any(node.childNodes, (child) -> child.nodeType != child.ELEMENT_NODE)
            return true
          # Check if parent needs us
          if node.previousSibling == null && node.nextSibling == null && !TandemLine.isLineNode(node.parentNode) && node.parentNode.tagName != 'LI'
            return true
      return false

    Tandem.Utils.traversePreorder(root, 0, (node) =>
      if isRedudant(node)
        node = Tandem.Utils.unwrap(node)
      if node?
        node[tandemKey] = _.clone(node.parentNode[tandemKey])
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        node[tandemKey][attrName] = attrValue if attrName?
      return node
    )
    delete root[tandemKey]
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      delete node[tandemKey]
      return node
    )

  @wrapText: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) =>
      node.normalize()
      if node.nodeType == node.TEXT_NODE && (node.nextSibling? || node.previousSibling? || node.parentNode == root || node.parentNode.tagName == 'LI')
        span = node.ownerDocument.createElement('span')
        Tandem.Utils.wrap(span, node)
        node = span
      return node
    )

  constructor: (@doc, @node) ->
    @id = _.uniqueId(Tandem.Line.ID_PREFIX)
    @node.id = @id
    @node.classList.add(TandemLine.CLASS_NAME)
    this.rebuild()
    super(@node)

  buildLeaves: (node, attributes) ->
    _.each(_.clone(node.childNodes), (node) =>
      node.normalize()
      nodeAttributes = _.clone(attributes)
      [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(node)
      nodeAttributes[attrName] = attrVal if attrName?
      if Tandem.Leaf.isLeafNode(node)
        @leaves.append(new Tandem.Leaf(this, node, nodeAttributes))
      else
        this.buildLeaves(node, nodeAttributes)
    )

  findLeaf: (leafNode) ->
    curLeaf = @leaves.first
    while curLeaf?
      return curLeaf if curLeaf.node == leafNode
      curLeaf = curLeaf.next
    return null

  rebuild: ->
    if @node.parentNode == @doc.root
      return if @outerHTML == @node.outerHTML
      while @leaves? && @leaves.length > 0
        @leaves.remove(@leaves.first)
      @leaves = new LinkedList()
      TandemLine.normalizeHtml(@node)
      this.buildLeaves(@node, {})
      this.resetContent()
    else
      @doc.removeLine(this)

  resetContent: ->
    @length = _.reduce(@leaves.toArray(), ((length, leaf) -> leaf.length + length), 0)
    @outerHTML = @node.outerHTML
    @attributes = {}
    [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(@node)
    @attributes[attrName] = attrVal if attrName?
    this.setDirty(false)
    @delta = this.toDelta()

  setDirty: (isDirty = true) ->
    if isDirty
      @node.classList.add(TandemLine.DIRTY_CLASS)
    else
      @node.classList.remove(TandemLine.DIRTY_CLASS)

  splitContents: (offset) ->
    this.setDirty()
    [node, offset] = Tandem.Utils.getChildAtOffset(@node, offset)
    return Tandem.Utils.splitNode(node, offset)

  toDelta: ->
    deltas = _.map(@leaves.toArray(), (leaf) ->
      return new JetInsert(leaf.text, leaf.getAttributes())
    )
    delta = new JetDelta(0, @length, deltas)
    delta.compact()
    return delta

window.Tandem ||= {}
window.Tandem.Line = TandemLine


#= underscore
#= tandem/constants

TandemUtils =
  # All block nodes inside nodes are moved out
  breakBlocks: (root) ->
    lineNodes = [root]
    this.traversePreorder(root, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE && !Tandem.Utils.isIgnoreNode(node)
        toBreak = []
        if _.indexOf(Tandem.Constants.BLOCK_TAGS, node.tagName, true) > -1
          [left1, left2, didLeftSplit] = this.splitNode(root, index)
          if didLeftSplit
            toBreak = toBreak.concat([left1, left2])
          else
            [right1, right2, didRightSplit] = this.splitNode(root, node.textContent.length)
            toBreak = toBreak.concat([right1, right2]) if didRightSplit
        else if _.indexOf(Tandem.Constants.BREAK_TAGS, node.tagName, true) > -1
          next = node.nextSibling
          if node.previousSibling && node.nextSibling
            [left, right, didSplit] = this.splitNode(root, index, true)
            node.parentNode.removeChild(node)
            toBreak = [left, right]
            node = next
          else if node.previousSibling || node.nextSibling
            node.parentNode.removeChild(node)
            node = next
        _.each(toBreak, (line) =>
          if line? && line != root
            newLineNodes = this.breakBlocks(line)
            lineNodes.push(line)
            lineNodes = _.uniq(lineNodes.concat(newLineNodes))
        )
      return node
    )
    return lineNodes

  cleanHtml: (html, keepIdClass = false) ->
    # Remove leading and tailing whitespace
    html = html.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
    # Remove whitespace between tags
    html = html.replace(/>\s\s+</gi, '><')
    # Remove id or class classname
    html = html.replace(/\ (class|id)="[a-z0-9\-_]+"/gi, '') unless keepIdClass == true
    # Standardize br
    html = html.replace(/
<\/br>/, '
')
    return html

  createContainerForAttribute: (doc, attribute, value) ->
    switch (attribute)
      when 'bold'       then return doc.createElement('b')
      when 'italic'     then return doc.createElement('i')
      when 'strike'     then return doc.createElement('s')
      when 'underline'  then return doc.createElement('u')
      when 'link'
        link = doc.createElement('a')
        link.setAttribute('href', value)
        return link
      else
        span = doc.createElement('span')
        span.classList.add(attribute)
        span.classList.add(value)
        return span

  extractNodes: (startLineNode, startOffset, endLineNode, endOffset) ->
    [leftStart, rightStart] = Tandem.Utils.splitNode(startLineNode, startOffset, true)
    if startLineNode == endLineNode
      endLineNode = rightStart
      endOffset -= leftStart.textContent.length if leftStart? && startLineNode != rightStart
    [leftEnd, rightEnd] = Tandem.Utils.splitNode(endLineNode, endOffset, true)
    fragment = startLineNode.ownerDocument.createDocumentFragment()
    while rightStart != rightEnd
      next = rightStart.nextSibling
      fragment.appendChild(rightStart)
      rightStart = next
    Tandem.Utils.mergeNodes(leftStart, rightEnd)
    return fragment

  getAncestorCount: (container, tagName) ->
    count = 0
    while container.parentNode?
      if container.parentNode.tagName == tagName
        count += 1
      container = container.parentNode
    return count

  getAttributeDefault: (attribute) ->
    switch attribute
      when 'font-background'  then return 'white'
      when 'font-color'       then return 'black'
      when 'font-family'      then return 'san-serif'
      when 'font-size'        then return 'normal'
      else                         return false

  getAttributeForContainer: (container) ->
    switch container.tagName
      when 'A'  then return ['link', container.getAttribute('href')]
      when 'B'  then return ['bold', true]
      when 'I'  then return ['italic', true]
      when 'S'  then return ['strike', true]
      when 'U'  then return ['underline', true]
      when 'OL' then return ['list', Tandem.Utils.getIndent(container)]
      when 'UL' then return ['bullet', Tandem.Utils.getIndent(container)]
      when 'DIV'
        indent = Tandem.Utils.getIndent(container)
        if indent > 0
          return ['indent', indent]
        else
          return []
      when 'SPAN'
        attribute = []
        _.all(Tandem.Constants.SPAN_ATTRIBUTES, (list, attrName) ->
          if container.classList.contains(attrName)
            return _.all(container.classList, (css) ->
              if _.indexOf(list, css) > -1
                attribute = [attrName, css]
                return false
              return true
            )
          return true
        )
        return attribute
      else
        return []

  getChildAtOffset: (node, offset) ->
    child = node.firstChild
    while offset > child.textContent.length
      offset -= child.textContent.length
      offset -= 1 if Tandem.Line.isLineNode(child)
      child = child.nextSibling
    return [child, offset]

  getIndent: (list) ->
    indent = 0
    _.any(list.classList, (css) ->
      if css.substring(0, Tandem.Document.INDENT_PREFIX.length) == Tandem.Document.INDENT_PREFIX
        indent = parseInt(css.substring(Tandem.Document.INDENT_PREFIX.length))
        return true
      return false
    )
    return indent

  isIgnoreNode: (node) ->
    return _.any(Tandem.Constants.IGNORE_CLASSES, (cssClass) ->
      return node.classList.contains(cssClass)
    )

  isTextNodeParent: (node) ->
    return node.childNodes.length == 1 && node.firstChild.nodeType == node.TEXT_NODE

  mergeNodes: (node1, node2) ->
    return node2 if !node1?
    return node1 if !node2?
    this.moveChildren(node1, node2)
    node2.parentNode.removeChild(node2)
    return node1

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

  removeAttributeFromSubtree: (subtree, attribute) ->
    children = _.clone(subtree.childNodes)
    attributes = Tandem.Utils.getAttributeForContainer(subtree)
    [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(subtree)
    if attrName == attribute
      Tandem.Utils.unwrap(subtree)
    _.each(children, (child) ->
      Tandem.Utils.removeAttributeFromSubtree(child, attribute)
    )

  setIndent: (list, indent) ->
    _.each(_.clone(list.classList), (css) ->
      if css.substring(0, Tandem.Document.INDENT_PREFIX.length) == Tandem.Document.INDENT_PREFIX
        list.classList.remove(css)
    )
    list.classList.add(Tandem.Document.INDENT_PREFIX + indent) if indent

  splitNode: (node, offset, force = false) ->
    if offset > node.textContent.length
      throw new Error('Splitting at offset greater than node length')

    if node.nodeType == node.TEXT_NODE
      node = this.wrap(node.ownerDocument.createElement('span'), node)

    # Check if split necessary
    if !force
      if offset == 0
        return [node.previousSibling, node, false]
      if offset == node.textContent.length
        return [node, node.nextSibling, false]

    left = node
    right = node.cloneNode(false)
    node.parentNode.insertBefore(right, left.nextSibling)

    if TandemUtils.isTextNodeParent(node)
      # Text split
      beforeText = node.textContent.substring(0, offset)
      afterText = node.textContent.substring(offset)
      left.textContent = beforeText
      right.textContent = afterText
      return [left, right, true]
    else
      # Node split
      [child, offset] = TandemUtils.getChildAtOffset(node, offset)
      [childLeft, childRight] = TandemUtils.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

  traversePreorder: (root, offset, fn, context = fn, args...) ->
    return unless root?
    cur = root.firstChild
    while cur?
      nextOffset = offset + cur.textContent.length
      curHtml = cur.innerHTML
      cur = fn.apply(context, [cur, offset].concat(args))
      TandemUtils.traversePreorder.apply(TandemUtils.traversePreorder, [cur, offset, fn, context].concat(args))
      if cur? && cur.innerHTML == curHtml
        cur = cur.nextSibling
        offset = nextOffset

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

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

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

  wrapChildren: (wrapper, node) ->
    Tandem.Utils.moveChildren(wrapper, node)
    node.appendChild(wrapper)
    return wrapper

window.Tandem ||= {}
window.Tandem.Utils = TandemUtils


/*global setTimeout: false, console: false */
(function () {

    var async = {};

    // global on the server, window in the browser
    var root = this,
        previous_async = root.async;

    if (typeof module !== 'undefined' && module.exports) {
        module.exports = async;
    }
    else {
        root.async = async;
    }

    async.noConflict = function () {
        root.async = previous_async;
        return async;
    };

    //// cross-browser compatiblity functions ////

    var _forEach = function (arr, iterator) {
        if (arr.forEach) {
            return arr.forEach(iterator);
        }
        for (var i = 0; i < arr.length; i += 1) {
            iterator(arr[i], i, arr);
        }
    };

    var _map = function (arr, iterator) {
        if (arr.map) {
            return arr.map(iterator);
        }
        var results = [];
        _forEach(arr, function (x, i, a) {
            results.push(iterator(x, i, a));
        });
        return results;
    };

    var _reduce = function (arr, iterator, memo) {
        if (arr.reduce) {
            return arr.reduce(iterator, memo);
        }
        _forEach(arr, function (x, i, a) {
            memo = iterator(memo, x, i, a);
        });
        return memo;
    };

    var _keys = function (obj) {
        if (Object.keys) {
            return Object.keys(obj);
        }
        var keys = [];
        for (var k in obj) {
            if (obj.hasOwnProperty(k)) {
                keys.push(k);
            }
        }
        return keys;
    };

    //// exported async module functions ////

    //// nextTick implementation with browser-compatible fallback ////
    if (typeof process === 'undefined' || !(process.nextTick)) {
        async.nextTick = function (fn) {
            setTimeout(fn, 0);
        };
    }
    else {
        async.nextTick = process.nextTick;
    }

    async.forEach = function (arr, iterator, callback) {
        callback = callback || function () {};
        if (!arr.length) {
            return callback();
        }
        var completed = 0;
        _forEach(arr, function (x) {
            iterator(x, function (err) {
                if (err) {
                    callback(err);
                    callback = function () {};
                }
                else {
                    completed += 1;
                    if (completed === arr.length) {
                        callback(null);
                    }
                }
            });
        });
    };

    async.forEachSeries = function (arr, iterator, callback) {
        callback = callback || function () {};
        if (!arr.length) {
            return callback();
        }
        var completed = 0;
        var iterate = function () {
            iterator(arr[completed], function (err) {
                if (err) {
                    callback(err);
                    callback = function () {};
                }
                else {
                    completed += 1;
                    if (completed === arr.length) {
                        callback(null);
                    }
                    else {
                        iterate();
                    }
                }
            });
        };
        iterate();
    };

    async.forEachLimit = function (arr, limit, iterator, callback) {
        callback = callback || function () {};
        if (!arr.length || limit <= 0) {
            return callback();
        }
        var completed = 0;
        var started = 0;
        var running = 0;

        (function replenish () {
            if (completed === arr.length) {
                return callback();
            }

            while (running < limit && started < arr.length) {
                started += 1;
                running += 1;
                iterator(arr[started - 1], function (err) {
                    if (err) {
                        callback(err);
                        callback = function () {};
                    }
                    else {
                        completed += 1;
                        running -= 1;
                        if (completed === arr.length) {
                            callback();
                        }
                        else {
                            replenish();
                        }
                    }
                });
            }
        })();
    };

    var doParallel = function (fn) {
        return function () {
            var args = Array.prototype.slice.call(arguments);
            return fn.apply(null, [async.forEach].concat(args));
        };
    };
    var doSeries = function (fn) {
        return function () {
            var args = Array.prototype.slice.call(arguments);
            return fn.apply(null, [async.forEachSeries].concat(args));
        };
    };

    var _asyncMap = function (eachfn, arr, iterator, callback) {
        var results = [];
        arr = _map(arr, function (x, i) {
            return {index: i, value: x};
        });
        eachfn(arr, function (x, callback) {
            iterator(x.value, function (err, v) {
                results[x.index] = v;
                callback(err);
            });
        }, function (err) {
            callback(err, results);
        });
    };
    async.map = doParallel(_asyncMap);
    async.mapSeries = doSeries(_asyncMap);

    // reduce only has a series version, as doing reduce in parallel won't
    // work in many situations.
    async.reduce = function (arr, memo, iterator, callback) {
        async.forEachSeries(arr, function (x, callback) {
            iterator(memo, x, function (err, v) {
                memo = v;
                callback(err);
            });
        }, function (err) {
            callback(err, memo);
        });
    };
    // inject alias
    async.inject = async.reduce;
    // foldl alias
    async.foldl = async.reduce;

    async.reduceRight = function (arr, memo, iterator, callback) {
        var reversed = _map(arr, function (x) {
            return x;
        }).reverse();
        async.reduce(reversed, memo, iterator, callback);
    };
    // foldr alias
    async.foldr = async.reduceRight;

    var _filter = function (eachfn, arr, iterator, callback) {
        var results = [];
        arr = _map(arr, function (x, i) {
            return {index: i, value: x};
        });
        eachfn(arr, function (x, callback) {
            iterator(x.value, function (v) {
                if (v) {
                    results.push(x);
                }
                callback();
            });
        }, function (err) {
            callback(_map(results.sort(function (a, b) {
                return a.index - b.index;
            }), function (x) {
                return x.value;
            }));
        });
    };
    async.filter = doParallel(_filter);
    async.filterSeries = doSeries(_filter);
    // select alias
    async.select = async.filter;
    async.selectSeries = async.filterSeries;

    var _reject = function (eachfn, arr, iterator, callback) {
        var results = [];
        arr = _map(arr, function (x, i) {
            return {index: i, value: x};
        });
        eachfn(arr, function (x, callback) {
            iterator(x.value, function (v) {
                if (!v) {
                    results.push(x);
                }
                callback();
            });
        }, function (err) {
            callback(_map(results.sort(function (a, b) {
                return a.index - b.index;
            }), function (x) {
                return x.value;
            }));
        });
    };
    async.reject = doParallel(_reject);
    async.rejectSeries = doSeries(_reject);

    var _detect = function (eachfn, arr, iterator, main_callback) {
        eachfn(arr, function (x, callback) {
            iterator(x, function (result) {
                if (result) {
                    main_callback(x);
                    main_callback = function () {};
                }
                else {
                    callback();
                }
            });
        }, function (err) {
            main_callback();
        });
    };
    async.detect = doParallel(_detect);
    async.detectSeries = doSeries(_detect);

    async.some = function (arr, iterator, main_callback) {
        async.forEach(arr, function (x, callback) {
            iterator(x, function (v) {
                if (v) {
                    main_callback(true);
                    main_callback = function () {};
                }
                callback();
            });
        }, function (err) {
            main_callback(false);
        });
    };
    // any alias
    async.any = async.some;

    async.every = function (arr, iterator, main_callback) {
        async.forEach(arr, function (x, callback) {
            iterator(x, function (v) {
                if (!v) {
                    main_callback(false);
                    main_callback = function () {};
                }
                callback();
            });
        }, function (err) {
            main_callback(true);
        });
    };
    // all alias
    async.all = async.every;

    async.sortBy = function (arr, iterator, callback) {
        async.map(arr, function (x, callback) {
            iterator(x, function (err, criteria) {
                if (err) {
                    callback(err);
                }
                else {
                    callback(null, {value: x, criteria: criteria});
                }
            });
        }, function (err, results) {
            if (err) {
                return callback(err);
            }
            else {
                var fn = function (left, right) {
                    var a = left.criteria, b = right.criteria;
                    return a < b ? -1 : a > b ? 1 : 0;
                };
                callback(null, _map(results.sort(fn), function (x) {
                    return x.value;
                }));
            }
        });
    };

    async.auto = function (tasks, callback) {
        callback = callback || function () {};
        var keys = _keys(tasks);
        if (!keys.length) {
            return callback(null);
        }

        var results = {};

        var listeners = [];
        var addListener = function (fn) {
            listeners.unshift(fn);
        };
        var removeListener = function (fn) {
            for (var i = 0; i < listeners.length; i += 1) {
                if (listeners[i] === fn) {
                    listeners.splice(i, 1);
                    return;
                }
            }
        };
        var taskComplete = function () {
            _forEach(listeners.slice(0), function (fn) {
                fn();
            });
        };

        addListener(function () {
            if (_keys(results).length === keys.length) {
                callback(null, results);
                callback = function () {};
            }
        });

        _forEach(keys, function (k) {
            var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k];
            var taskCallback = function (err) {
                if (err) {
                    callback(err);
                    // stop subsequent errors hitting callback multiple times
                    callback = function () {};
                }
                else {
                    var args = Array.prototype.slice.call(arguments, 1);
                    if (args.length <= 1) {
                        args = args[0];
                    }
                    results[k] = args;
                    taskComplete();
                }
            };
            var requires = task.slice(0, Math.abs(task.length - 1)) || [];
            var ready = function () {
                return _reduce(requires, function (a, x) {
                    return (a && results.hasOwnProperty(x));
                }, true) && !results.hasOwnProperty(k);
            };
            if (ready()) {
                task[task.length - 1](taskCallback, results);
            }
            else {
                var listener = function () {
                    if (ready()) {
                        removeListener(listener);
                        task[task.length - 1](taskCallback, results);
                    }
                };
                addListener(listener);
            }
        });
    };

    async.waterfall = function (tasks, callback) {
        callback = callback || function () {};
        if (!tasks.length) {
            return callback();
        }
        var wrapIterator = function (iterator) {
            return function (err) {
                if (err) {
                    callback(err);
                    callback = function () {};
                }
                else {
                    var args = Array.prototype.slice.call(arguments, 1);
                    var next = iterator.next();
                    if (next) {
                        args.push(wrapIterator(next));
                    }
                    else {
                        args.push(callback);
                    }
                    async.nextTick(function () {
                        iterator.apply(null, args);
                    });
                }
            };
        };
        wrapIterator(async.iterator(tasks))();
    };

    async.parallel = function (tasks, callback) {
        callback = callback || function () {};
        if (tasks.constructor === Array) {
            async.map(tasks, function (fn, callback) {
                if (fn) {
                    fn(function (err) {
                        var args = Array.prototype.slice.call(arguments, 1);
                        if (args.length <= 1) {
                            args = args[0];
                        }
                        callback.call(null, err, args);
                    });
                }
            }, callback);
        }
        else {
            var results = {};
            async.forEach(_keys(tasks), function (k, callback) {
                tasks[k](function (err) {
                    var args = Array.prototype.slice.call(arguments, 1);
                    if (args.length <= 1) {
                        args = args[0];
                    }
                    results[k] = args;
                    callback(err);
                });
            }, function (err) {
                callback(err, results);
            });
        }
    };

    async.series = function (tasks, callback) {
        callback = callback || function () {};
        if (tasks.constructor === Array) {
            async.mapSeries(tasks, function (fn, callback) {
                if (fn) {
                    fn(function (err) {
                        var args = Array.prototype.slice.call(arguments, 1);
                        if (args.length <= 1) {
                            args = args[0];
                        }
                        callback.call(null, err, args);
                    });
                }
            }, callback);
        }
        else {
            var results = {};
            async.forEachSeries(_keys(tasks), function (k, callback) {
                tasks[k](function (err) {
                    var args = Array.prototype.slice.call(arguments, 1);
                    if (args.length <= 1) {
                        args = args[0];
                    }
                    results[k] = args;
                    callback(err);
                });
            }, function (err) {
                callback(err, results);
            });
        }
    };

    async.iterator = function (tasks) {
        var makeCallback = function (index) {
            var fn = function () {
                if (tasks.length) {
                    tasks[index].apply(null, arguments);
                }
                return fn.next();
            };
            fn.next = function () {
                return (index < tasks.length - 1) ? makeCallback(index + 1): null;
            };
            return fn;
        };
        return makeCallback(0);
    };

    async.apply = function (fn) {
        var args = Array.prototype.slice.call(arguments, 1);
        return function () {
            return fn.apply(
                null, args.concat(Array.prototype.slice.call(arguments))
            );
        };
    };

    var _concat = function (eachfn, arr, fn, callback) {
        var r = [];
        eachfn(arr, function (x, cb) {
            fn(x, function (err, y) {
                r = r.concat(y || []);
                cb(err);
            });
        }, function (err) {
            callback(err, r);
        });
    };
    async.concat = doParallel(_concat);
    async.concatSeries = doSeries(_concat);

    async.whilst = function (test, iterator, callback) {
        if (test()) {
            iterator(function (err) {
                if (err) {
                    return callback(err);
                }
                async.whilst(test, iterator, callback);
            });
        }
        else {
            callback();
        }
    };

    async.until = function (test, iterator, callback) {
        if (!test()) {
            iterator(function (err) {
                if (err) {
                    return callback(err);
                }
                async.until(test, iterator, callback);
            });
        }
        else {
            callback();
        }
    };

    async.queue = function (worker, concurrency) {
        var workers = 0;
        var q = {
            tasks: [],
            concurrency: concurrency,
            saturated: null,
            empty: null,
            drain: null,
            push: function (data, callback) {
                if(data.constructor !== Array) {
                    data = [data];
                }
                _forEach(data, function(task) {
                    q.tasks.push({
                        data: task,
                        callback: typeof callback === 'function' ? callback : null
                    });
                    if (q.saturated && q.tasks.length == concurrency) {
                        q.saturated();
                    }
                    async.nextTick(q.process);
                });
            },
            process: function () {
                if (workers < q.concurrency && q.tasks.length) {
                    var task = q.tasks.shift();
                    if(q.empty && q.tasks.length == 0) q.empty();
                    workers += 1;
                    worker(task.data, function () {
                        workers -= 1;
                        if (task.callback) {
                            task.callback.apply(task, arguments);
                        }
                        if(q.drain && q.tasks.length + workers == 0) q.drain();
                        q.process();
                    });
                }
            },
            length: function () {
                return q.tasks.length;
            },
            running: function () {
                return workers;
            }
        };
        return q;
    };

    var _console_fn = function (name) {
        return function (fn) {
            var args = Array.prototype.slice.call(arguments, 1);
            fn.apply(null, args.concat([function (err) {
                var args = Array.prototype.slice.call(arguments, 1);
                if (typeof console !== 'undefined') {
                    if (err) {
                        if (console.error) {
                            console.error(err);
                        }
                    }
                    else if (console[name]) {
                        _forEach(args, function (x) {
                            console[name](x);
                        });
                    }
                }
            }]));
        };
    };
    async.log = _console_fn('log');
    async.dir = _console_fn('dir');
    /*async.info = _console_fn('info');
    async.warn = _console_fn('warn');
    async.error = _console_fn('error');*/

    async.memoize = function (fn, hasher) {
        var memo = {};
        var queues = {};
        hasher = hasher || function (x) {
            return x;
        };
        var memoized = function () {
            var args = Array.prototype.slice.call(arguments);
            var callback = args.pop();
            var key = hasher.apply(null, args);
            if (key in memo) {
                callback.apply(null, memo[key]);
            }
            else if (key in queues) {
                queues[key].push(callback);
            }
            else {
                queues[key] = [callback];
                fn.apply(null, args.concat([function () {
                    memo[key] = arguments;
                    var q = queues[key];
                    delete queues[key];
                    for (var i = 0, l = q.length; i < l; i++) {
                      q[i].apply(null, arguments);
                    }
                }]));
            }
        };
        memoized.unmemoized = fn;
        return memoized;
    };

    async.unmemoize = function (fn) {
      return function () {
        return (fn.unmemoized || fn).apply(null, arguments);
      };
    };

}());


!!! 5
%html
  %head
    %title Stypi Editor Utils Test
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../../../lib/mocha.css'}

%body
  #mocha
  #test-container
    #split-test-base<>
      %b<> Bold
    #split-test-text<>
      %b<> Bold
    #split-test-node
      %b<>
        %i<> Italic
        %s<> Strike
    #split-test-both
      %b<>
        %i<> Italic
    #split-test-complex
      %b<>
        %i<>
          %s<>
            %u<> One
            %u<> Two
          %s<> Three
    #split-test-lines
      .line<>
        %b<> 123
        %i<> 456

  %script{:type => 'text/javascript', :src => '../../../lib/jquery.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/underscore.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/eventemitter2.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/rangy/rangy-core.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/diff_match_patch.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/jetsync.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/linked_list.js'}
  %script{:type => 'text/javascript', :src => '../../src/constants.js'}
  %script{:type => 'text/javascript', :src => '../../src/utils.js'}
  %script{:type => 'text/javascript', :src => '../../src/leaf.js'}
  %script{:type => 'text/javascript', :src => '../../src/line.js'}
  %script{:type => 'text/javascript', :src => '../../src/document.js'}
  %script{:type => 'text/javascript', :src => '../../src/position.js'}
  %script{:type => 'text/javascript', :src => '../../src/range.js'}
  %script{:type => 'text/javascript', :src => '../../src/keyboard.js'}
  %script{:type => 'text/javascript', :src => '../../src/editor.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/mocha.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/chai.js'}
  :javascript
    mocha.setup('bdd');
    expect = chai.expect;
  %script{:type => 'text/javascript', :src => 'scripts/utils.js'}
  :javascript
    mocha.run();


[node, offset] = Tandem.Utils.getChildAtOffset(@node, offset)

deltas = _.map(@leaves.toArray(), (leaf) ->

return new JetInsert(leaf.text, leaf.getAttributes())

keep keep keep replace keep keep keep keep keep

splitNode = if @node.tagName == 'OL' || @node.tagName == 'UL' then @node.firstChild else @node

[node, offset] = Tandem.Utils.getChildAtOffset(splitNode, offset)


#= require async

# Manages network connections
#
# Creates socketio connection, reusable by many modules
# Handles configurations, authentication, and connectivity notifications

class Connection
  constructor: (@socket) ->
    @sendQueue = []
    @ready = false
    @stats =
      sent      : {}
      recieved  : {}
      acked     : {}

  addStat: (type, route) ->
    @stats[type] = {} if !@stats[type]?
    @stats[type][route] = 0 if !@stats[type][route]?
    @stats[type][route] += 1

  setReady: ->
    async.until( =>
      return @sendQueue.length == 0
    , (callback) =>
      elem = @sendQueue.splice(0, 1)
      [route, packet, sendCallback] = elem[0]
      console.info "Sending from queue:", route, packet
      @socket.emit(route, packet, (args...) =>
        sendCallback.apply(this, args)
        callback()
      )
    , (err) =>
      @ready = true
    )

  send: (route, packet, priority, callback) ->
    if @ready
      console.info "Sending:", route, packet
      @socket.emit(route, packet, callback)
    else
      console.info "Queued:", route, packet, priority
      if priority
        @sendQueue.unshift([route, packet, callback])
      else
        @sendQueue.push([route, packet, callback])

class NetworkClass
  @ACKED    : 'acked'
  @RECIEVED : 'recieved'
  @SENT     : 'sent'

  constructor: ->
    this.reset()

  authenticate: (docId) ->
    authPacket =
      auth        : @auth
      docId       : docId
      referrer    : window.location.href      # In case access method is obscure url
      sessionId   : @sessionId
      userId      : @userId
    console.info "Connecting to", docId, authPacket
    if @connections[docId]?
      @connections[docId].socket.emit('auth', authPacket, (response) =>
        if !response.error || response.error.length == 0
          console.info "Connected!", response
          @connections[docId].setReady() if @connections[docId].ready == false
        else
          $(document).trigger("notify", ["error", "Could not access this document.", Infinity])
      )
    else
      console.warn "No connection to authenticate for docId", docId

  reset: ->
    @connections  = {}
    @unacked      = 0
    @auth         = Stypi.configs.auth
    @host         = Stypi.configs.nodeHost
    @sessionId    = Stypi.configs.sessionId
    @userId       = Stypi.configs.userId
    @options      = {
      'max reconnection attempts' : Infinity
      'reconnection limit'        : 30000
      'sync disconnect on unload' : false
      'secure'                    : true
      'port'                      : 443
      'force new connection'      : true
    }
    window.onunload = =>
      conn.socket.socket.disconnectSync() for conn in @connections

  getOrCreateConnection: (docId) ->
    if !@connections[docId]?
      socket = io.connect("https://#{@host}", @options)
      @connections[docId] = new Connection(socket)
      socket.on('reconnecting', =>
        @connections[docId].ready = false if  @connections[docId]?
      ).on('reconnect', =>
        this.authenticate(docId) if @connections[docId].ready == false
      )
      this.authenticate(docId)
    return @connections[docId]

  send: (docId, route, packet, priority, sendCallback) ->
    if typeof priority != 'boolean'
      sendCallback = priority
      priority = false
    connection = this.getOrCreateConnection(docId)
    connection.addStat(NetworkClass.SENT, route)
    @unacked += 1
    connection.send(route, packet, priority, (packet, callback) =>
      console.info "Callback", route, packet
      connection.addStat(NetworkClass.ACKED, route)
      @unacked -= 1
      sendCallback(packet, callback) if sendCallback?
    )

  on: (docId, route, onCallback) ->
    connection = this.getOrCreateConnection(docId)
    connection.socket.addListener(route, (args...) =>
      console.info "Got", route, args
      connection.addStat(NetworkClass.RECIEVED, route)
      onCallback.apply(this, args) if onCallback?
    )

  off: (docId, route) ->
    @connections[docId].socket.removeAllListeners(route) if @connections[docId]?

$(document).ready( ->
  window.NetworkClass = NetworkClass     # Necessary for tests to work
  window.Network = new NetworkClass()
)


// Inspired by http://blog.jcoglan.com/2007/07/23/writing-a-linked-list-in-javascript/

function LinkedList() {}
LinkedList.prototype = {
  length: 0,
  first: null,
  last: null
};

LinkedList.prototype.append = function(node) {
  if (typeof this.first === 'undefined' || this.first === null) {
    this.first = node;
    this.last = node;
  } else {
    node.prev = this.last;
    node.next = null;
    this.last.next = node;
    this.last = node;
  }
  this.length++;
};

LinkedList.prototype.insertAfter = function(node, newNode) {
  newNode.prev = node;
  if (node) {
    newNode.next = node.next;
    if (typeof node.next !== 'undefined' && node.next !== null) {
      node.next.prev = newNode;
    }
    node.next = newNode;
    if (node === this.last) {
      this.last = newNode;
    }
  }
  else {
    // Insert after null implies inserting at position 0
    newNode.next = this.first;
    this.first.prev = newNode;
    this.first = newNode;
  }
  this.length++;
};

LinkedList.prototype.remove = function(node) {
  if (this.length > 1) {
    if (typeof node.prev !== 'undefined' && node.prev !== null) {
      node.prev.next = node.next;
    }
    if (typeof node.next !== 'undefined' && node.next !== null) {
      node.next.prev = node.prev;
    }
    if (node == this.first) { this.first = node.next; }
    if (node == this.last) { this.last = node.prev; }
  } else {
    this.first = null;
    this.last = null;
  }
  node.prev = null;
  node.next = null;
  this.length--;
};

LinkedList.prototype.toArray = function() {
  var arr = [];
  var cur = this.first;
  while (cur) {
    arr.push(cur);
    cur = cur.next;
  }
  return arr;
}

LinkedList.Node = function(data) {
  this.prev = null; this.next = null;
  this.data = data;
};


body
  font-family: 'Helvetica'
  font-size: 13px
  padding: 0
  margin: 0

#content-container
  width: 100%
  height: 100%


import Process from '@prisma/engine-core/dist/process'

import { NodeEngine } from '@prisma/engine-core/dist/NodeEngine'
import path from 'path'
import { DMMF, ExternalDMMF } from '../runtime/dmmf-types'
import { externalToInternalDmmf } from '../runtime/externalToInternalDmmf'
import { transformDmmf } from '../runtime/transformDmmf'

const modelBlacklist = {
  // helper types
  Enumerable: true,
  MergeTruthyValues: true,
  CleanupNever: true,
  AtLeastOne: true,
  OnlyOne: true,
  // scalar filters
  StringFilter: true,
  IDFilter: true,
  FloatFilter: true,
  IntFilter: true,
  BooleanFilter: true,
  DateTimeFilter: true,
  // nullable scalar filters
  NullableStringFilter: true,
  NullableIDFilter: true,
  NullableFloatFilter: true,
  NullableIntFilter: true,
  NullableBooleanFilter: true,
  NullableDateTimeFilter: true,
  // photon classes
  PhotonFetcher: true,
  Photon: true,
  Engine: true,
  PhotonOptions: true,
}

const fieldBlacklist = {
  AND: true,
  OR: true,
  NOT: true,
}

/**
 * Checks if a model or field shouldn't be there
 * @param document DMMF.Document
 */
function checkBlacklist(sdl: DMMF.Datamodel) {
  for (const model of sdl.models) {
    if (modelBlacklist[model.name]) {
      throw new Error(`Model name ${model.name} is a reserved name and not allowed in the datamodel`)
    }
    for (const field of model.fields) {
      if (fieldBlacklist[field.name]) {
        throw new Error(`Field ${model.name}.${field.name} is a reserved name and not allowed in the datamodel`)
      }
    }
  }
}

export async function getRawDMMF(
  datamodel: string,
  cwd = process.cwd(),
  prismaPath = path.join(__dirname, '../../runtime/prisma'), // improve the binary resolution
): Promise<ExternalDMMF.Document> {
  const engine = new NodeEngine({
    datamodel,
    prismaPath,
    cwd,
  })

  await engine.start()
  const externalDmmf: ExternalDMMF.Document = await engine.getDmmf()
  await engine.stop()
  return externalDmmf
}

export interface GetDmmfOptions {
  datamodel: string
  cwd?: string
  prismaPath?: string
}

export async function getDMMF({ datamodel, cwd, prismaPath }: GetDmmfOptions): Promise<DMMF.Document> {
  const externalDmmf = await getRawDMMF(datamodel, cwd, prismaPath)
  const dmmf = transformDmmf(externalToInternalDmmf(externalDmmf))
  checkBlacklist(dmmf.datamodel)

  return dmmf
}


const { ensureBinaries } = require('@prisma/fetch-engine')
const path = require('path')
const debug = require('debug')('download')
// Until https://github.com/zeit/ncc/issues/390 is resolved we have to do this 🙈
const runtimePath = eval(`path.join(__dirname, '../runtime')`)
debug(`Downloading binaries to ${runtimePath}`)
ensureBinaries(runtimePath)


import * as fs from 'fs'

export function copy(src, target) {
  return new Promise((resolve, reject) => {
    fs.copyFile(src, target, err => {
      if (err) {
        reject(err)
      } else {
        resolve()
      }
    })
  })
}


prismaPath = path.join(__dirname, '../../runtime/prisma'), // improve the binary resolution

keep keep keep keep keep replace replace replace replace replace keep

const child = new Process(prismaPath, 'cli', '--dmmf')


datasource db {
  provider = "sqlite"
  url      = "file:db/migration_engine.db"
  default  = true
}

generator photon {
  provider = "javascript"
  output   = "@generated/photon"
}

model Blog {
  id        Int      @id
  name      String
  viewCount Int
  posts     Post[]
  authors   Author[]
}

model Author {
  id    Int     @id
  name  String?
  posts Post[]
  blog  Blog
}

model Post {
  id    Int      @id
  title String
  tags  String[]
  blog  Blog
}

model PostNews {
  new        Int     @id
  otherField String?
}


tabWidth: 2
trailingComma: all
singleQuote: true
semi: false
printWidth: 120


node_modules
dist
build
runtime
browser-runtime
download
generator
cli
.DS_Store
!src/runtime
yarn-error.log
src/utils/transformed-dmmf.json
*-dmmf.json
db
failed-*.md


const externalDmmf: ExternalDMMF.Document = await engine.getDmmf()

keep replace replace replace replace keep keep keep keep

function concat(fn: (result: string) => void): NodeJS.WriteStream {

function transform(chunk, _enc, callback) {

import fs from 'fs'
import { blog } from '../fixtures/blog'
import { getDMMF, getRawDMMF } from './getDMMF'

async function main() {
  console.log('fetching dmmf')
  const dmmf = await getRawDMMF(blog)
  console.log({ dmmf })
  fs.writeFileSync(__dirname + '/blog-dmmf.json', JSON.stringify(dmmf, null, 2))
}

main().catch(console.error)


prototype: true
databases:
  default:
    connector: sqlite-native
    databaseFile: ./db/Chinook.db
    migrations: true
    active: true
    rawAccess: true


fs.writeFileSync(__dirname + '/blog-dmmf.json', JSON.stringify(dmmf, null, 2))


type Album {   id: Int! @id @map(name: "AlbumId")   Title: String!   Artist: Artist! @map(name: "ArtistId")   Tracks: [Track] }  type Track {   id: Int! @id @map(name: "TrackId")   Name: String!   Album: Album @map(name: "AlbumId")   AlbumId: Int   MediaType: MediaType! @map(name: "MediaTypeId")   Genre: Genre @map(name: "GenreId")   Composer: String   Milliseconds: Int!   UnitPrice: Float!   Playlists: [Playlist] @relation(name: "PlaylistTrack") }  type MediaType {   id: Int! @id @map(name: "MediaTypeId")   Name: String }  type Genre {   id: Int! @id @map(name: "GenreId")   Name: String   Tracks: [Track] }  type Artist {   id: Int! @id @map(name: "ArtistId")   Name: String   Albums: [Album] }  type Customer {   id: Int! @id @map(name: "CustomerId")   FirstName: String!   LastName: String!   Company: String   Address: String   City: String   State: String   Country: String   PostalCode: String   Phone: String   Fax: String   Email: String!   SupportRep: Employee @map(name: "SupportRepId") }  type Employee {   id: Int! @id @map(name: "EmployeeId")   FirstName: String!   LastName: String!   Title: String   ReportsTo: Employee   BirthDate: DateTime   HireDate: DateTime   Address: String   City: String   State: String   Country: String   PostalCode: String   Phone: String   Fax: String   Email: String }  type Invoice {   id: Int! @id @map(name: "InvoiceId")   Customer: Customer! @map(name: "CustomerId")   InvoiceDate: DateTime!   BillingAddress: String   BillingCity: String   BillingState: String   BillingCountry: String   BillingPostalCode: String   Total: Float!   Lines: [InvoiceLine] }  type InvoiceLine {   id: Int! @id @map(name: "InvoiceLineId")   Invoice: Invoice! @map(name: "InvoiceId")   Track: Track! @map(name: "TrackId")   UnitPrice: Float!   Quantity: Int! }  type Playlist {   id: Int! @id @map(name: "PlaylistId")   Name: String   Tracks: [Track] @relation(name: "PlaylistTrack") }  type PlaylistTrack @relationTable {   PlaylistId: Playlist   TrackId: Track }


# @prisma/engine-download

Responsible for downloading and caching the latest Rust binary


import path from 'path'
import { spawn } from 'child_process'
import byline from './byline'

export function getInternalDatamodelJson(
  datamodel: string,
  schemaInferrerPath: string = path.join(__dirname, '../schema-inferrer-bin'),
): Promise<string> {
  return new Promise((resolve, reject) => {
    const proc = spawn(schemaInferrerPath, { stdio: ['pipe', 'pipe', process.stderr] })

    proc.on('error', function(err) {
      console.error('[schema-inferrer-bin] error: %s', err)
      reject(err)
    })

    proc.on('exit', function(code, signal) {
      if (code !== 0) {
        console.error('[schema-inferrer-bin] exit: code=%s signal=%s', code, signal)
      }
      reject()
    })

    const out = byline(proc.stdout)
    out.on('data', line => {
      const result = JSON.parse(line)
      const resultB64 = Buffer.from(JSON.stringify(result)).toString('base64')
      resolve(resultB64)
    })

    const cut = datamodel.replace(/\n/g, ' ')

    proc.stdin.write(JSON.stringify({ dataModel: cut }) + '\n')
  })
}