# Tandem Operational Transform Engine - v0.1.0 - #2012-12-04
# https://www.stypi.com/
# Copyright (c) 2012
# Byron Milligan, Salesforce.com
# Jason Chen, Salesforce.com

if exports?
  _ = require('underscore')._
  diff_match_patch = require('../lib/diff_match_patch')
  dmp = new diff_match_patch.diff_match_patch
else
  _ = window._
  diff_match_patch = window.diff_match_patch
  diff_match_patch.DIFF_DELETE = -1
  diff_match_patch.DIFF_EQUAL = 0
  diff_match_patch.DIFF_INSERT = 1
  dmp = new diff_match_patch

class Op
  @compact: (ops) ->
    compacted = []
    _.each(Op.normalize(ops), (op) ->
      if compacted.length == 0
        compacted.push(op) unless RetainOp.isRetain(op) && op.start == op.end
      else
        if RetainOp.isRetain(op) && op.start == op.end
          return
        last = _.last(compacted)
        if Delta.isInsert(last) && Delta.isInsert(op) && last.attributesMatch(op)
          # If two neighboring inserts, combine
          last.value = last.value + op.value
        else if RetainOp.isRetain(last) && RetainOp.isRetain(op) && last.end == op.start && last.attributesMatch(op)
          # If two neighboring ranges first's end + 1 == second's start, combine
          last.end = op.end
        else
          # Cannot coalesce with previous
          compacted.push(op)
    )
    return compacted

  @normalize: (ops) ->
    normalizedOps = _.map(ops, (op) ->
      switch typeof op
        when 'string' then return new InsertOp(op)
        when 'number' then return new RetainOp(op, op + 1)
        when 'object'
          if op.value?
            return new InsertOp(op.value, op.attributes)
          else if op.start? && op.end?
            return new RetainOp(op.start, op.end, op.attributes)
        else
          return null
    )
    return _.reject(normalizedOps, (op) -> !op? || op.getLength() == 0)

  constructor: (attributes = {}) ->
    @attributes = _.clone(attributes)

  addAttributes: (attributes) ->
    addedAttributes = {}
    for key, value of attributes when @attributes[key] == undefined
      addedAttributes[key] = value
    return addedAttributes

  attributesMatch: (other) ->
    otherAttributes = other.attributes || {}
    return _.isEqual(@attributes, otherAttributes)

  composeAttributes: (attributes) ->
    that = this
    resolveAttributes = (oldAttrs, newAttrs) ->
      return oldAttrs if !newAttrs
      resolvedAttrs = _.clone(oldAttrs)
      for key, value of newAttrs
        if Delta.isInsert(that) && value == null
          delete resolvedAttrs[key]
        else if typeof value != 'undefined'
          if typeof resolvedAttrs[key] == 'object' and typeof value == 'object' and _.all([resolvedAttrs[key], newAttrs[key]], ((val) -> val != null))
            resolvedAttrs[key] = resolveAttributes(resolvedAttrs[key], value)
          else
            resolvedAttrs[key] = value
      return resolvedAttrs
    return resolveAttributes(@attributes, attributes)

  numAttributes: () ->
    _.keys(@attributes).length

  toString: ->
    printAttrs = (attrs) ->
      attr_str = ""
      for key, value of @attributes
        attr_str += key + ":"
        if typeof value == 'object' and value != null
          attr_str += "{" + printAttrs(value) + "},"
        else
          attr_str += value + ","
      return "{" + attr_str + "}"
    return printAttrs(@attributes)

# Used to represent retains in the delta. [inclusive, exclusive)
class RetainOp extends Op
  @copy: (subject) ->
    console.assert(RetainOp.isRetain(subject), "Copy called on non-retain", subject)
    return new RetainOp(subject.start, subject.end, subject.attributes)

  @isRetain: (r) ->
    return r? && typeof r.start == "number" && typeof r.end == "number"

  constructor: (@start, @end, attributes = {}) ->
    console.assert(@start >= 0, "RetainOp start cannot be negative!", @start)
    console.assert(@end >= @start, "RetainOp end must be >= start!", @start, @end)
    @attributes = _.clone(attributes)

  getAt: (start, length) ->
    return new RetainOp(@start + start, @start + start + length, @attributes)

  getLength: ->
    return @end - @start

  split: (offset) ->
    console.assert(offset <= @end, "Split called with offset beyond end of retain")
    left = new RetainOp(@start, @start + offset, @attributes)
    right = new RetainOp(@start + offset, @end, @attributes)
    return [left, right]

  toString: ->
    return "{{#{@start} - #{@end}), #{super()}}"

class InsertOp extends Op
  @copy: (subject) ->
    return new InsertOp(subject.value, subject.attributes)

  @isInsert: (i) ->
    return i? && typeof i.value == "string"

  constructor: (@value, attributes = {}) ->
    @attributes = _.clone(attributes)

  getAt: (start, length) ->
    return new InsertOp(@value.substr(start, length), @attributes)

  getLength: ->
    return @value.length

  join: (other) ->
    if _.isEqual(@attributes, other.attributes)
      return new InsertOp(@value + second.value, @attributes)
    else
      throw Error

  split: (offset) ->
    console.assert(offset <= @value.length, "Split called with offset beyond end of insert")
    left = new InsertOp(@value.substr(0, offset), @attributes)
    right = new InsertOp(@value.substr(offset), @attributes)
    return [left, right]

  toString: ->
    return "{#{@value}, #{super()}}"

class Delta
  @copy: (subject) ->
    changes = []
    for op in subject.ops
      if Delta.isRetain(op)
        changes.push(RetainOp.copy(op))
      else
        changes.push(InsertOp.copy(op))
    return new Delta(subject.startLength, subject.endLength, changes)

  @getIdentity: (length) ->
    delta = new Delta(length, length, [new RetainOp(0, length)])
    return delta

  @getInitial: (contents) ->
    return new Delta(0, contents.length, [new InsertOp(contents)])

  @isDelta: (delta) ->
    if (delta? && typeof delta == "object" && typeof delta.startLength == "number" &&
        typeof delta.endLength == "number" && typeof delta.ops == "object")
      for op in delta.ops
        if !Delta.isRetain(op) && !Delta.isInsert(op)
          return false
      return true
    return false

  @isInsert: (change) ->
    return InsertOp.isInsert(change)

  @isRetain: (change) ->
    return RetainOp.isRetain(change) || typeof(change) == "number"

  @makeDelta: (obj) ->
    return new Delta(obj.startLength, obj.endLength, obj.ops)

  constructor: (@startLength, @endLength, ops) ->
    unless ops?
      ops = @endLength
      @endLength = null
    @ops = Op.compact(ops)
    length = _.reduce(@ops, (count, op) ->
      return count + op.getLength()
    , 0)
    if @endLength?
      console.assert(length == @endLength, "Expecting end length of", length, this)
    else
      @endLength = length

  applyToText: (text) ->
    delta = this
    console.assert(text.length == delta.startLength, "Start length of delta: " + delta.startLength + " is not equal to the text: " + text.length)
    appliedText = []
    for elem in delta.ops
      if Delta.isInsert(elem)
        appliedText.push(elem.value)
      else
        appliedText.push(text.substring(elem.start, elem.end))
    result = appliedText.join("")
    if delta.endLength != result.length
      console.log "Delta", delta
      console.log "text", text
      console.log "result", result
      console.assert(false, "End length of delta: " + delta.endLength + " is not equal to result text: " + result.length )
    return result

  clearOpsCache: ->
    @savedOpOffset = @savedOpIndex = undefined

  # Inserts in deltaB are given priority. Retains in deltaB are indexes into A,
  # and we take whatever is there (insert or retain).
  compose: (deltaB) ->
    console.assert(Delta.isDelta(deltaB), "Compose called when deltaB is not a Delta, type: " + typeof deltaB)
    console.assert(@endLength == deltaB.startLength, "startLength #{deltaB.startLength} / endlength #{this.endLength} mismatch")

    deltaA = new Delta(@startLength, @endLength, Op.normalize(@ops))
    deltaB = new Delta(deltaB.startLength, deltaB.endLength, Op.normalize(deltaB.ops))

    composed = []
    for elem in deltaB.ops
      elem = new InsertOp(elem) if typeof elem == 'string'
      if Delta.isInsert(elem)
        composed.push(elem)
      else if Delta.isRetain(elem)
        opsInRange = deltaA.getOpsAt(elem.start, elem.end - elem.start)
        opsInRange = _.map(opsInRange, (op) ->
          if Delta.isInsert(op)
            return new InsertOp(op.value, op.composeAttributes(elem.attributes))
          else
            return new RetainOp(op.start, op.end, op.composeAttributes(elem.attributes))
        )
        composed = composed.concat(opsInRange)
      else
        console.assert(false, "Invalid op in deltaB when composing", deltaB)

    deltaC = new Delta(deltaA.startLength, deltaB.endLength, Op.compact(composed))
    console.assert(Delta.isDelta(deltaC), "Composed returning invalid Delta", deltaC)
    return deltaC

  # For each element in deltaC, compare it to the current element in deltaA in
  # order to construct deltaB. Given A and C, there is more than one valid B.
  # Its impossible to guarantee that decompose yields the actual B that was
  # used in the original composition. However, the function is deterministic in
  # which of the possible B's it chooses. How it works:
  # 1. Inserts in deltaC are matched against the current elem in deltaA. If
  #    there is a match, we create a corresponding retain in deltaB. Otherwise,
  #    we create an insertion in deltaB.
  # 2. Retains in deltaC become retains in deltaB, which reference the original
  #    retain in deltaA.
  decompose: (deltaA) ->
    deltaC = this
    console.assert(Delta.isDelta(deltaA), "Decompose2 called when deltaA is not a Delta, type: " + typeof deltaA)
    console.assert(deltaA.startLength == @startLength, "startLength #{deltaA.startLength} / startLength #{@startLength} mismatch")
    console.assert(_.all(deltaA.ops, ((op) -> return op.value?)), "DeltaA has retain in decompose")
    console.assert(_.all(deltaC.ops, ((op) -> return op.value?)), "DeltaC has retain in decompose")

    decomposeAttributes = (attrA, attrC) ->
      decomposedAttributes = {}
      for key, value of attrC
        if attrA[key] == undefined or attrA[key] != value
          if attrA[key] != null and typeof attrA[key] == 'object' and value != null and typeof value == 'object'
            decomposedAttributes[key] = decomposeAttributes(attrA[key], value)
          else
            decomposedAttributes[key] = value
      for key, value of attrA
        if attrC[key] == undefined
          decomposedAttributes[key] = null
      return decomposedAttributes

    insertDelta = deltaA.diff(deltaC)
    ops = []
    offset = 0
    _.each(insertDelta.ops, (op) ->
      opsInC = deltaC.getOpsAt(offset, op.getLength())
      offsetC = 0
      _.each(opsInC, (opInC) ->
        if Delta.isInsert(op)
          d = new InsertOp(op.value.substring(offsetC, offsetC + opInC.getLength()), opInC.attributes)
          ops.push(d)
        else if Delta.isRetain(op)
          opsInA = deltaA.getOpsAt(op.start + offsetC, opInC.getLength())
          offsetA = 0
          _.each(opsInA, (opInA) ->
            attributes = decomposeAttributes(opInA.attributes, opInC.attributes)
            start = op.start + offsetA + offsetC
            e = new RetainOp(start, start + opInA.getLength(), attributes)
            ops.push(e)
            offsetA += opInA.getLength()
          )
        else
          console.error("Invalid delta in deltaB when composing", deltaB)
        offsetC += opInC.getLength()
      )
      offset += op.getLength()
    )

    deltaB = new Delta(insertDelta.startLength, insertDelta.endLength, Op.compact(ops))
    return deltaB

  diff: (other) ->
    diffToDelta = (diff) ->
      console.assert(diff.length > 0, "diffToDelta called with diff with length <= 0")
      originalLength = 0
      finalLength = 0
      ops = []
      # For each difference apply them separately so we do not disrupt the cursor
      for [operation, value] in diff
        switch operation
          when diff_match_patch.DIFF_DELETE
            # Deletes implied
            originalLength += value.length
          when diff_match_patch.DIFF_INSERT
            ops.push(new InsertOp(value))
            finalLength += value.length
          when diff_match_patch.DIFF_EQUAL
            ops.push(new RetainOp(originalLength, originalLength + value.length))
            originalLength += value.length
            finalLength += value.length
      return new Delta(originalLength, finalLength, ops)

    deltaToText = (delta) ->
      return _.map(delta.ops, (op) ->
        return if op.value? then op.value else ""
      ).join('')

    diffTexts = (oldText, newText) ->
      diff = dmp.diff_main(oldText, newText)
      if (diff.length > 2)
        dmp.diff_cleanupEfficiency(diff)
      return diff

    textA = deltaToText(this)
    textC = deltaToText(other)
    unless textA == '' and textC == ''
      diff = diffTexts(textA, textC)
      insertDelta = diffToDelta(diff)
    else
      insertDelta = new Delta(0, 0, [])
    return insertDelta

  # We compute the follow according to the following rules:
  # 1. Insertions in deltaA become retained characters in the follow set
  # 2. Insertions in deltaB become inserted characters in the follow set
  # 3. Characters retained in deltaA and deltaB become retained characters in
  #    the follow set
  follows: (deltaA, aIsRemote) ->
    deltaB = this
    console.assert(Delta.isDelta(deltaA), "Follows called when deltaA is not a Delta, type: " + typeof deltaA, deltaA)
    console.assert(aIsRemote?, "Remote delta not specified")

    deltaA = new Delta(deltaA.startLength, deltaA.endLength, Op.normalize(deltaA.ops))
    deltaB = new Delta(deltaB.startLength, deltaB.endLength, Op.normalize(deltaB.ops))
    followStartLength = deltaA.endLength
    followSet = []
    indexA = indexB = 0 # Tracks character offset in the 'document'
    elemIndexA = elemIndexB = 0 # Tracks offset into the ops list
    while elemIndexA < deltaA.ops.length and elemIndexB < deltaB.ops.length
      elemA = deltaA.ops[elemIndexA]
      elemB = deltaB.ops[elemIndexB]

      if Delta.isInsert(elemA) and Delta.isInsert(elemB)
        length = Math.min(elemA.getLength(), elemB.getLength())
        if aIsRemote
          followSet.push(new RetainOp(indexA, indexA + length))
          indexA += length
          if length == elemA.getLength()
            elemIndexA++
          else
            console.assert(length < elemA.getLength())
            deltaA.ops[elemIndexA] = _.last(elemA.split(length))
        else
          followSet.push(_.first(elemB.split(length)))
          indexB += length
          if length == elemB.getLength()
            elemIndexB++
          else
            deltaB.ops[elemIndexB] = _.last(elemB.split(length))

      else if Delta.isRetain(elemA) and Delta.isRetain(elemB)
        if elemA.end < elemB.start
          # Not a match, can't save. Throw away lower and adv.
          indexA += elemA.getLength()
          elemIndexA++
        else if elemB.end < elemA.start
          # Not a match, can't save. Throw away lower and adv.
          indexB += elemB.getLength()
          elemIndexB++
        else
          # A subrange or the entire range matches
          if elemA.start < elemB.start
            indexA += elemB.start - elemA.start
            elemA = deltaA.ops[elemIndexA] = new RetainOp(elemB.start, elemA.end, elemA.attributes)
          else if elemB.start < elemA.start
            indexB += elemA.start - elemB.start
            elemB = deltaB.ops[elemIndexB] = new RetainOp(elemA.start, elemB.end, elemB.attributes)

          console.assert(elemA.start == elemB.start, "RetainOps must have same
          start length when propagating into followset", elemA, elemB)
          length = Math.min(elemA.end, elemB.end) - elemA.start
          addedAttributes = elemA.addAttributes(elemB.attributes)
          followSet.push(new RetainOp(indexA, indexA + length, addedAttributes)) # Keep the retain
          indexA += length
          indexB += length
          if (elemA.end == elemB.end)
            elemIndexA++
            elemIndexB++
          else if (elemA.end < elemB.end)
            elemIndexA++
            deltaB.ops[elemIndexB] = _.last(elemB.split(length))
          else
            deltaA.ops[elemIndexA] = _.last(elemA.split(length))
            elemIndexB++

      else if Delta.isInsert(elemA) and Delta.isRetain(elemB)
        followSet.push(new RetainOp(indexA, indexA + elemA.getLength()))
        indexA += elemA.getLength()
        elemIndexA++
      else if Delta.isRetain(elemA) and Delta.isInsert(elemB)
        followSet.push(elemB)
        indexB += elemB.getLength()
        elemIndexB++
      else
        console.warn("Mismatch. elemA is: " + typeof(elemA) + ", elemB is:  " + typeof(elemB))

    # Remaining loops account for different length ops, only inserts will be
    # accepted
    while elemIndexA < deltaA.ops.length
      elemA = deltaA.ops[elemIndexA]
      followSet.push(new RetainOp(indexA, indexA + elemA.getLength())) if Delta.isInsert(elemA) # retain elemA
      indexA += elemA.getLength()
      elemIndexA++

    while elemIndexB < deltaB.ops.length
      elemB = deltaB.ops[elemIndexB]
      followSet.push(elemB) if Delta.isInsert(elemB) # insert elemB
      indexB += elemB.getLength()
      elemIndexB++

    followEndLength = 0
    for elem in followSet
      followEndLength += elem.getLength()

    follow = new Delta(followStartLength, followEndLength, Op.compact(followSet))
    console.assert(Delta.isDelta(follow), "Follows returning invalid Delta", follow)
    return follow

  getOpsAt: (index, length) ->
    changes = []
    if @savedOpOffset? and @savedOpOffset < index
      offset = @savedOpOffset
    else
      offset = @savedOpOffset = @savedOpIndex = 0
    for op in @ops.slice(@savedOpIndex)
      break if offset >= index + length
      opLength = op.getLength()
      if index < offset + opLength
        start = Math.max(index - offset, 0)
        getLength = Math.min(opLength - start, index + length - offset - start)
        changes.push(op.getAt(start, getLength))
      offset += opLength
      @savedOpIndex += 1
      @savedOpOffset += opLength
    return changes

  isIdentity: ->
    if @startLength == @endLength
      if @ops.length == 0
        return true
      index = 0
      for op in @ops
        if !RetainOp.isRetain(op) then return false
        if op.start != index then return false
        if !(op.numAttributes() == 0 || (op.numAttributes() == 1 && _.has(op.attributes, 'authorId')))
          return false
        index = op.end
      if index != @endLength then return false
      return true
    return false

  toString: ->
    return "{(#{@startLength}->#{@endLength})[#{@ops.join(', ')}]}"

# Expose this code to other files
root = if (typeof exports != "undefined" && exports != null) then exports else window
root.Tandem = {
  Delta: Delta
  Op: Op
  InsertOp: InsertOp
  RetainOp: RetainOp
}


_.each(op.attributes, (value, format) =>

@doc.formatText(op.start, op.end - op.start, format, value) if value?

keep keep keep keep replace keep keep keep

# If we had to force newline, pretend user added it

addNewlineDelta = new Tandem.Delta(delta.endLength, [

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

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


console.assert(delta.endLength == this.getLength(), "Applying delta resulted in incorrect end length", delta, this.getLength())

class ScribeEditor extends EventEmitter2
  @editors: []

  @CONTAINER_ID: 'Scribe-container'
  @ID_PREFIX: 'editor-'
  @CURSOR_PREFIX: 'cursor-'
  @DEFAULTS:
    cursor: 0
    enabled: true
    styles: {}
  @events:
    TEXT_CHANGE      : 'text-change'
    SELECTION_CHANGE : 'selection-change'

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

  reset: (keepHTML = false) ->
    @ignoreDomChanges = true
    options = _.clone(@options)
    options.keepHTML = keepHTML
    @renderer = new Scribe.Renderer(@iframeContainer, options)
    @contentWindow = @renderer.iframe.contentWindow
    @root = @contentWindow.document.getElementById(ScribeEditor.CONTAINER_ID)
    @doc = new Scribe.Document(@root)
    @selection = new Scribe.Selection(this)
    @keyboard = new Scribe.Keyboard(this)
    @undoManager = new Scribe.UndoManager(this)
    @pasteManager = new Scribe.PasteManager(this)
    this.initListeners()
    @ignoreDomChanges = false
    ScribeEditor.editors.push(this)

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

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

  initListeners: ->
    onEditOnce = =>
    onEdit = =>
      onEditOnce = _.once(onEdit)
      return if @ignoreDomChanges
      this.update()
    onSubtreeModified = =>
      return if @ignoreDomChanges
      toCall = onEditOnce
      _.defer( =>
        toCall.call(null)
      )
    onEditOnce = _.once(onEdit)
    @root.addEventListener('DOMSubtreeModified', onSubtreeModified)

  keepNormalized: (fn) ->
    fn.call(this)
    @doc.rebuildDirty()
    @doc.forceTrailingNewline()

  applyDelta: (delta, external = true) ->
    return if delta.isIdentity()
    this.doSilently( =>
      @selection.preserve( =>
        console.assert(delta.startLength == @doc.length, "Trying to apply delta to incorrect doc length", delta, @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
        oldDelta = @doc.toDelta()
        retains = []
        _.each(delta.ops, (op) =>
          if Tandem.Delta.isInsert(op)
            @doc.insertText(index + offset, op.value)
            retains.push(new Tandem.RetainOp(index + offset, index + offset + op.getLength(), op.attributes))
            offset += op.getLength()
          else if Tandem.Delta.isRetain(op)
            if op.start > index
              @doc.deleteText(index + offset, op.start - index)
              offset -= (op.start - index)
            retains.push(new Tandem.RetainOp(op.start + offset, op.end + offset, op.attributes))
            index = op.end
          else
            console.warn('Unrecognized type in delta', op)
        )
        # If end of text was deleted
        if delta.endLength < delta.startLength + offset
          @doc.deleteText(delta.endLength, delta.startLength + offset - delta.endLength)
        retainDelta = new Tandem.Delta(delta.endLength, delta.endLength, retains)
        _.each(retainDelta.ops, (op) =>
          _.each(op.attributes, (value, format) =>
            @doc.formatText(op.start, op.end - op.start, format, value) if value == null
          )
          _.each(op.attributes, (value, format) =>
            @doc.formatText(op.start, op.end - op.start, format, value) if value?
          )
        )
        @doc.forceTrailingNewline()
        @undoManager.record(delta, oldDelta)
        unless external
          this.emit(ScribeEditor.events.TEXT_CHANGE, delta)
      )
    )

  deleteAt: (index, length) ->
    this.doSilently( =>
      @selection.preserve( =>
        return this.trackDelta( =>
          this.keepNormalized( =>
            @doc.deleteText(index, length)
          )
        )
      )
    )

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

  # formatAt: (Number index, Number length, String name, Mixed value) ->
  formatAt: (index, length, name, value) ->
    this.doSilently( =>
      @selection.preserve( =>
        return this.trackDelta( =>
          this.keepNormalized( =>
            @doc.formatText(index, length, name, value)
          )
        )
      )
    )

  getDelta: ->
    return @doc.toDelta()

  getLength: ->
    return @doc.length

  getSelection: ->
    return @selection.getRange()

  insertAt: (index, text) ->
    this.doSilently( =>
      @selection.preserve( =>
        return this.trackDelta( =>
          this.keepNormalized( =>
            @doc.insertText(index, text)
          )
        )
      )
    )

  setSelection: (range) ->
    @selection.setRange(range)

  trackDelta: (fn) ->
    oldDelta = @doc.toDelta()
    fn()
    newDelta = @doc.toDelta()
    decompose = newDelta.decompose(oldDelta)
    compose = oldDelta.compose(decompose)
    newDelta.clearOpsCache()
    compose.clearOpsCache()
    console.assert(_.isEqual(compose, newDelta), oldDelta, newDelta, decompose, compose)
    @undoManager.record(decompose, oldDelta)
    this.emit(ScribeEditor.events.TEXT_CHANGE, decompose) unless  decompose.isIdentity()
    return decompose

  update: ->
    this.doSilently( =>
      @selection.preserve( =>
        return this.trackDelta( =>
          Scribe.Document.normalizeHtml(@root)
          lines = @doc.lines.toArray()
          lineNode = @root.firstChild
          _.each(lines, (line, index) =>
            while line.node != lineNode
              if line.node.parentNode == @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
        )
      )
    )

window.Scribe ||= {}
window.Scribe.Editor = ScribeEditor


describe('Leaf', ->
  it('Leaf Iterator', ->
    lines = ['

', '
12
', '
45
', '

', '

', '

78

', '

']
    $('#editor-container').html(lines.join(''))
    editor = new Scribe.Editor('editor-container')
    lines = editor.doc.lines.toArray()
    leaves = _.reduce(lines, (leaves, line) ->
      leaves.push(_.map(line.leaves.toArray(), (leaf) ->
        return leaf.id
      ))
      return leaves
    , [])
    leaves = _.flatten(leaves)
    leafIterator = new Scribe.LeafIterator(lines[0].leaves.first, lines[lines.length-1].leaves.last)
    arr = _.map(leafIterator.toArray(), (leaf) ->
      return leaf.id
    )
    expect(arr).to.deep.equal(leaves)
  )
)


/*global module:false*/
module.exports = function(grunt) {

  // Project configuration.
  grunt.initConfig({
    meta: {
      version: '0.1.0',
      banner: '/*! Stypi Editor - v<%= meta.version %> - ' +
        '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
        '* https://www.stypi.com/\n' +
        '* Copyright (c) <%= grunt.template.today("yyyy") %> ' +
        'Jason Chen, Salesforce.com */'
    },
    concat: {
      dist: {
        src: ['<banner:meta.banner>', 'bin/src/*.js'],
        dest: 'build/scribe.js'
      }
    },
    uglify: {}
  });

  // Default task.
  grunt.registerTask('default', 'concat');

};


#= require underscore

Stypi.events ||= {}

_.extend(Stypi.events,
  CLIENT_JOIN   : 'presence-client-join'
  CLIENT_LEAVE  : 'presence-client-leave'
  USER_JOIN     : 'presence-user-join'
  USER_LEAVE    : 'presence-user-leave'
)

Presence =
  COLORS: [
    "#33cc33"
    "#0099ff"
    "#ff00ff"
    "#ff9933"
    "#00ff99"
    "#3366ff"
    "#ff3399"
    "#ffff00"
    "#33cccc"
    "#9966ff"
    "#ff5050"
    "#99ff33"
  ]

  users: {}

  init: ->
    $(Stypi).on(Stypi.events.CLIENT_JOIN, (event, message) ->
      Presence.addUser(message.user)
    ).on(Stypi.events.CLIENT_LEAVE, (event, message) ->
      Presence.removeUser({id: message.userId})
    )

  addUser: (user) ->
    if @users[user.id]?
      @users[user.id].online += 1
    else
      @users[user.id] = user
    $(Stypi).trigger(Stypi.events.USER_JOIN, user) if @users[user.id].online == 1

  clearUsers: ->
    for id,user of @users
      $(Stypi).trigger(Stypi.events.USER_LEAVE, user)
    @users = {}

  getColorIndexForUser: (id) ->
    return parseInt(id.substring(24 - 6), 16) % Presence.COLORS.length

  getColorForUser: (id) ->
    return Presence.COLORS[this.getColorIndexForUser(id)]

  getUserName: (userId) ->
    if userId == Stypi.configs.userId
      return "me"
    else if @users[userId]? && (@users[userId].full_name || @users[userId].username)
      return @users[userId].full_name || @users[userId].username
    else
      # Mirrors method in user model
      return "User " + (parseInt(userId.substring(24 - 12), 16) % 1000000)

  removeUser: (user) ->
    if !@users[user.id]?
      console.warn "A nonexistent user left:", user
      return
    @users[user.id].online -= 1
    if @users[user.id].online <= 0
      $(Stypi).trigger(Stypi.events.USER_LEAVE, user)
      delete @users[user.id]

  setUsers: (users = {}) ->
    Stypi.Presence.clearUsers()
    for id,user of users
      Stypi.Presence.addUser(user)

Stypi.Presence = Presence


console.assert(_.isEqual(compose, newDelta), oldDelta, newDelta, decompose, compose)

this.emit(ScribeEditor.events.TEXT_CHANGE, decompose) unless  decompose.isIdentity()

keep keep keep keep keep replace keep keep keep


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

#content-container
  margin: auto
  width: 960px

.formatting-container
  background-color: #f5f5f5
  border-bottom: 1px solid #ccc
  height: 32px
  line-height: 32px
  padding: 0px 5px
  .active
    color: green
    font-weight: bold

.editor-container
  height: 600px

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


class ScribeRenderer
  @DEFAULTS:
    keepHTML: false

  @DEFAULT_STYLES:
    '.editor': {
      'cursor': 'text'
      'font-family': "'Helvetica', 'Arial', san-serif"
      'font-size': '13px'
      'line-height': '15px'
      'min-height': '100%'
      'outline': 'none'
      'padding': '10px 15px'
      'tab-size': '4'
      'white-space': 'pre-wrap'
    }
    'html' : { 'height': '100%' }
    'body' : { 'height': '100%', 'margin': '0px', 'padding': '0px' }
    'a'    : { 'text-decoration': 'underline' }
    'b'    : { 'font-weight': 'bold' }
    'i'    : { 'font-style': 'italic' }
    's'    : { 'text-decoration': 'line-through' }
    'u'    : { 'text-decoration': 'underline' }
    'ol'   : { 'margin': '0px', 'padding': '0px' }
    'ul'   : { 'list-style-type': 'disc', 'margin': '0px', 'padding': '0px' }
    'ol.indent-1' : { 'list-style-type': 'decimal' }
    'ol.indent-2' : { 'list-style-type': 'lower-alpha' }
    'ol.indent-3' : { 'list-style-type': 'lower-roman' }
    'ol.indent-4' : { 'list-style-type': 'decimal' }
    'ol.indent-5' : { 'list-style-type': 'lower-alpha' }
    'ol.indent-6' : { 'list-style-type': 'lower-roman' }
    'ol.indent-7' : { 'list-style-type': 'decimal' }
    'ol.indent-8' : { 'list-style-type': 'lower-alpha' }
    'ol.indent-9' : { 'list-style-type': 'lower-roman' }
    '.background-black'  : { 'background-color': 'black' }
    '.background-red'    : { 'background-color': 'red' }
    '.background-orange' : { 'background-color': 'orange' }
    '.background-yellow' : { 'background-color': 'yellow' }
    '.background-green'  : { 'background-color': 'green' }
    '.background-blue'   : { 'background-color': 'blue' }
    '.background-purple' : { 'background-color': 'purple' }
    '.color-white'       : { 'color': 'white' }
    '.color-red'         : { 'color': 'red' }
    '.color-orange'      : { 'color': 'orange' }
    '.color-yellow'      : { 'color': 'yellow' }
    '.color-green'       : { 'color': 'green' }
    '.color-blue'        : { 'color': 'blue' }
    '.color-purple'      : { 'color': 'purple' }
    '.family-monospace'  : { 'font-family': "'Courier New', monospace" }
    '.family-serif'      : { 'font-family': "'Times New Roman', serif" }
    '.size-huge'         : { 'font-size': '32px', 'line-height': '36px' }
    '.size-large'        : { 'font-size': '18px', 'line-height': '22px' }
    '.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' }

  @objToCss: (obj) ->
    return _.map(obj, (value, key) ->
      innerStr = _.map(value, (innerValue, innerKey) -> return "#{innerKey}: #{innerValue};" ).join(' ')
      return "#{key} { #{innerStr} }"
    ).join("\n")

  constructor: (@container, options) ->
    @options = _.extend(Scribe.Renderer.DEFAULTS, options)
    this.createFrame()

  addStyles: (styles) ->
    doc = @iframe.contentWindow.document
    head = doc.getElementsByTagName('head')[0]
    style = doc.createElement('style')
    style.type = 'text/css'
    css = Scribe.Renderer.objToCss(styles)
    if style.styleSheet?
      style.styleSheet.cssText = css
    else
      style.appendChild(doc.createTextNode(css))
    head.appendChild(style)

  createFrame: ->
    html = @container.innerHTML
    @container.innerHTML = ''
    @iframe = @container.ownerDocument.createElement('iframe')
    @iframe.frameborder = 0
    @iframe.src = 'javascript:;'
    @iframe.height = @iframe.width = '100%'
    @container.appendChild(@iframe)
    doc = @iframe.contentWindow.document
    styles = _.map(@options.styles, (value, key) ->
      obj = Scribe.Renderer.DEFAULT_STYLES[key] or {}
      return _.extend(obj, value)
    )
    styles = _.extend(Scribe.Renderer.DEFAULT_STYLES, styles)
    this.addStyles(styles)
    contentContainer = doc.createElement('div')
    contentContainer.id = Scribe.Editor.CONTAINER_ID
    contentContainer.classList.add('editor')
    doc.body.appendChild(contentContainer)
    contentContainer.innerHTML = html if @options.keepHTML

window.Scribe ||= {}
window.Scribe.Renderer = ScribeRenderer


#= 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()
)


keep keep keep keep keep replace keep keep keep keep keep

<ButtonGroup medium className='sidebar--custom-colors' onClick={(e): void => e.stopPropagation()}>

import type { IVoipRoom } from '@rocket.chat/core-typings';
import { ICallerInfo, VoIpCallerInfo, VoipClientEvents } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Box, Button, ButtonGroup, Icon, SidebarFooter, Menu, IconButton } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, MouseEvent, ReactNode } from 'react';

import type { VoipFooterMenuOptions } from '../../../../ee/client/hooks/useVoipFooterMenu';
import { CallActionsType } from '../../../contexts/CallContext';
import { useOmnichannelContactLabel } from './hooks/useOmnichannelContactLabel';

type VoipFooterPropsType = {
	caller: ICallerInfo;
	callerState: VoIpCallerInfo['state'];
	callActions: CallActionsType;
	title: string;
	subtitle: string;
	muted: boolean;
	paused: boolean;
	toggleMic: (state: boolean) => void;
	togglePause: (state: boolean) => void;
	callsInQueue: string;
	createRoom: (caller: ICallerInfo, callDirection?: IVoipRoom['direction']) => Promise<IVoipRoom['_id']>;
	openRoom: (rid: IVoipRoom['_id']) => void;
	dispatchEvent: (params: { event: VoipClientEvents; rid: string; comment?: string }) => void;
	openedRoomInfo: { v: { token?: string | undefined }; rid: string };
	isEnterprise: boolean;
	children?: ReactNode;
	options: VoipFooterMenuOptions;
};

export const VoipFooter = ({
	caller,
	callerState,
	callActions,
	title,
	subtitle,
	muted,
	paused,
	toggleMic,
	togglePause,
	createRoom,
	openRoom,
	callsInQueue,
	dispatchEvent,
	openedRoomInfo,
	isEnterprise = false,
	children,
	options,
}: VoipFooterPropsType): ReactElement => {
	const contactLabel = useOmnichannelContactLabel(caller);
	const t = useTranslation();

	const cssClickable =
		callerState === 'IN_CALL' || callerState === 'ON_HOLD'
			? css`
					cursor: pointer;
			  `
			: '';

	const handleHold = (e: MouseEvent<HTMLButtonElement>): void => {
		e.stopPropagation();
		const eventName = paused ? 'VOIP-CALL-UNHOLD' : 'VOIP-CALL-ON-HOLD';
		dispatchEvent({ event: VoipClientEvents[eventName], rid: openedRoomInfo.rid });
		togglePause(!paused);
	};

	const holdTitle = ((): string => {
		if (!isEnterprise) {
			return t('Hold_EE_only');
		}
		return paused ? t('Resume') : t('Hold');
	})();

	return (
		<SidebarFooter elevated>
			<Box
				className={cssClickable}
				onClick={(): void => {
					if (callerState === 'IN_CALL' || callerState === 'ON_HOLD') {
						openRoom(openedRoomInfo.rid);
					}
				}}
			>
				<Box display='flex' justifyContent='center' fontScale='c1' color='white' mbe='14px'>
					{callsInQueue}
				</Box>
				<Box display='flex' flexDirection='row' h='24px' mi='16px' mbs='12px' mbe='8px' justifyContent='space-between' alignItems='center'>
					<Box color='neutral-500' fontScale='c2' withTruncatedText>
						{title}
					</Box>
					{(callerState === 'IN_CALL' || callerState === 'ON_HOLD') && (
						<ButtonGroup medium className='sidebar--custom-colors'>
							<IconButton
								small
								disabled={paused}
								icon={muted ? 'mic-off' : 'mic'}
								color={muted ? 'neutral-500' : 'info'}
								data-tooltip={muted ? t('Turn_on_microphone') : t('Turn_off_microphone')}
								onClick={(e): void => {
									e.stopPropagation();
									toggleMic(!muted);
								}}
							/>
							<IconButton
								small
								data-tooltip={holdTitle}
								disabled={!isEnterprise}
								icon={paused ? 'pause' : 'pause-unfilled'}
								color={paused ? 'neutral-500' : 'info'}
								onClick={handleHold}
							/>
							{options && <Menu color='neutral-500' data-tooltip={t('More_options')} options={options} />}
						</ButtonGroup>
					)}
				</Box>
				<Box display='flex' flexDirection='row' mi='16px' mbe='12px' justifyContent='space-between' alignItems='center'>
					<Box>
						<Box color='white' fontScale='p2' withTruncatedText>
							{contactLabel || t('Anonymous')}
						</Box>
						<Box color='hint' fontScale='c1' withTruncatedText>
							{subtitle}
						</Box>
					</Box>

					<ButtonGroup medium>
						{(callerState === 'IN_CALL' || callerState === 'ON_HOLD' || callerState === 'OFFER_SENT') && (
							<Button
								small
								square
								danger
								disabled={paused}
								aria-label={t('End_call')}
								data-tooltip={t('End_Call')}
								onClick={(e): unknown => {
									e.stopPropagation();
									muted && toggleMic(false);
									paused && togglePause(false);
									return callActions.end();
								}}
							>
								<Icon name='phone-off' size='x16' />
							</Button>
						)}
						{callerState === 'OFFER_RECEIVED' && (
							<Button data-tooltip={t('Decline')} aria-label={t('Decline')} small square danger onClick={callActions.reject}>
								<Icon name='phone-off' size='x16' />
							</Button>
						)}
						{callerState === 'OFFER_RECEIVED' && (
							<Button
								small
								square
								success
								data-tooltip={t('Accept')}
								onClick={async (): Promise<void> => {
									callActions.pickUp();
									const rid = await createRoom(caller);
									dispatchEvent({ event: VoipClientEvents['VOIP-CALL-STARTED'], rid });
								}}
							>
								<Icon name='phone' size='x16' />
							</Button>
						)}
					</ButtonGroup>
				</Box>
			</Box>
			{children}
		</SidebarFooter>
	);
};


import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';

export function getConversationRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
	return {
		identifier: 'merged',
		order: 30,
		label: 'Conversations',
	};
}


import { States, StatesIcon, StatesSubtitle, StatesTitle, StatesActions, Button, Icon, Box } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement } from 'react';

const UpgradePageError = (): ReactElement => {
	const t = useTranslation();
	const handleReconnect = (): void => window.location.reload();

	return (
		<Box display='flex' justifyContent='center' height='full'>
			<States>
				<StatesIcon name='globe-cross' />
				<StatesTitle>{t('Connection_error')}</StatesTitle>
				<StatesSubtitle>{t('Upgrade_tab_connection_error_description')}</StatesSubtitle>
				<StatesSubtitle>
					<strong>{t('Upgrade_tab_connection_error_restore')}</strong>
				</StatesSubtitle>
				<StatesActions>
					<Button onClick={handleReconnect}>
						<Icon name='reload' /> {t('Refresh')}
					</Button>
				</StatesActions>
			</States>
		</Box>
	);
};

export default UpgradePageError;


import { registerModel } from '@rocket.chat/models';

import { db } from '../database/utils';
import { EmailInboxRaw } from './raw/EmailInbox';

registerModel('IEmailInboxModel', new EmailInboxRaw(db));


const openRoomInfo = useMutableCallback((username?: string) => {

(room.uids?.length ?? 0) > 2 ? open('user-info-group', username) : open('user-info', username);

case 'v':
open('voip-room-info', username);
break;


import type { Document } from 'mongodb';

import { api } from '../../../../apps/meteor/server/sdk/api';
import { broker } from '../../../../apps/meteor/ee/server/startup/broker';
import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo';
import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels';

(async () => {
	const db = await getConnection();

	const trash = await getCollection<Document>(Collections.Trash);

	registerServiceModels(db, trash);

	api.setBroker(broker);

	// need to import service after models are registered
	const { NotificationsModule } = await import('../../../../apps/meteor/server/modules/notifications/notifications.module');
	const { DDPStreamer } = await import('./DDPStreamer');
	const { Stream } = await import('./Streamer');

	const notifications = new NotificationsModule(Stream);

	notifications.configure();

	api.registerService(new DDPStreamer(notifications));

	await api.start();
})();


export { default } from './RoomMembersWithData';


return { id: rid || '', tab: 'voip-room-info' };

import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
import type { RoomCoordinator } from '../coordinator';

export function getVoipRoomType(coordinator: RoomCoordinator): IRoomTypeConfig {
	return {
		identifier: 'v',
		order: 6,
		icon: 'phone',
		label: 'Voip',
		route: {
			name: 'voip',
			path: '/voip/:id/:tab?/:context?',
			action: ({ id } = {}): void => {
				return coordinator.openRoom('v', id);
			},
			link({ rid }): Record<string, string> {
				return { id: rid || '' };
			},
		},

		notSubscribedTpl: 'livechatNotSubscribed',
		readOnlyTpl: 'ComposerNotAvailablePhoneCalls',
	};
}


import { settingsRegistry } from '../../settings/server';

settingsRegistry.addGroup('General', function () {
	this.section('Update', function () {
		this.add('Update_LatestAvailableVersion', '0.0.0', {
			type: 'string',
			readonly: true,
		});

		this.add('Update_EnableChecker', true, {
			type: 'boolean',
			enableQuery: {
				_id: 'Register_Server',
				value: true,
			},
			i18nDescription: 'Update_EnableChecker_Description',
		});
	});
});


export interface IMethodConnection {
	id: string;
	close(fn: (...args: any[]) => void): void;
	onClose(fn: (...args: any[]) => void): void;
	clientAddress: string;
	httpHeaders: Record<string, any>;
}

export interface IMethodThisType {
	isSimulation: boolean;
	userId: string | null;
	connection: IMethodConnection | null;
	setUserId(userId: string): void;
	unblock(): void;
	twoFactorChecked: boolean | undefined;
}


const modules = this.getNodeParameter('modules', index) as string;

import type {
	CodeExecutionMode,
	CodeNodeEditorLanguage,
	IExecuteFunctions,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';
import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription';
import { pythonCodeDescription } from './descriptions/PythonCodeDescription';
import { JavaScriptSandbox } from './JavaScriptSandbox';
import { PythonSandbox } from './PythonSandbox';
import { getSandboxContext } from './Sandbox';
import { standardizeOutput } from './utils';

export class Code implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Code',
		name: 'code',
		icon: 'fa:code',
		group: ['transform'],
		version: [1, 2],
		defaultVersion: 1,
		description: 'Run custom JavaScript code',
		defaults: {
			name: 'Code',
			color: '#FF9922',
		},
		inputs: ['main'],
		outputs: ['main'],
		parameterPane: 'wide',
		properties: [
			{
				displayName: 'Mode',
				name: 'mode',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Run Once for All Items',
						value: 'runOnceForAllItems',
						description: 'Run this code only once, no matter how many input items there are',
					},
					{
						name: 'Run Once for Each Item',
						value: 'runOnceForEachItem',
						description: 'Run this code as many times as there are input items',
					},
				],
				default: 'runOnceForAllItems',
			},
			{
				displayName: 'Language',
				name: 'language',
				type: 'options',
				noDataExpression: true,
				displayOptions: {
					show: {
						'@version': [2],
					},
				},
				options: [
					{
						name: 'JavaScript',
						value: 'javaScript',
					},
					{
						name: 'Python (Beta)',
						value: 'python',
					},
				],
				default: 'javaScript',
			},

			...javascriptCodeDescription,
			...pythonCodeDescription,
		],
	};

	async execute(this: IExecuteFunctions) {
		const nodeMode = this.getNodeParameter<CodeExecutionMode>('mode', 0);
		const workflowMode = this.getMode();

		const language: CodeNodeEditorLanguage =
			this.getNode()?.typeVersion === 2 ? this.getNodeParameter('language', 0) : 'javaScript';
		const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode';

		const getSandbox = (index = 0) => {
			const code = this.getNodeParameter<string>(codeParameterName, index);
			const context = getSandboxContext.call(this, index);
			if (language === 'python') {
				const modules = this.getNodeParameter<string>('modules', index);
				const moduleImports: string[] = modules ? modules.split(',').map((m) => m.trim()) : [];
				context.printOverwrite = workflowMode === 'manual' ? this.sendMessageToUI : null;
				return new PythonSandbox(context, code, moduleImports, index, this.helpers);
			} else {
				context.items = context.$input.all();
				const sandbox = new JavaScriptSandbox(context, code, index, workflowMode, this.helpers);
				if (workflowMode === 'manual') {
					sandbox.vm.on('console.log', this.sendMessageToUI);
				}
				return sandbox;
			}
		};

		// ----------------------------------
		//        runOnceForAllItems
		// ----------------------------------

		if (nodeMode === 'runOnceForAllItems') {
			const sandbox = getSandbox();
			let items: INodeExecutionData[];
			try {
				items = await sandbox.runCodeAllItems();
			} catch (error) {
				if (!this.continueOnFail()) throw error;
				items = [{ json: { error: error.message } }];
			}

			for (const item of items) {
				standardizeOutput(item.json);
			}

			return this.prepareOutputData(items);
		}

		// ----------------------------------
		//        runOnceForEachItem
		// ----------------------------------

		const returnData: INodeExecutionData[] = [];

		const items = this.getInputData();

		for (let index = 0; index < items.length; index++) {
			const sandbox = getSandbox(index);
			let result: INodeExecutionData | undefined;
			try {
				result = await sandbox.runCodeEachItem();
			} catch (error) {
				if (!this.continueOnFail()) throw error;
				returnData.push({ json: { error: error.message } });
			}

			if (result) {
				returnData.push({
					json: standardizeOutput(result.json),
					pairedItem: { item: index },
					...(result.binary && { binary: result.binary }),
				});
			}
		}

		return this.prepareOutputData(returnData);
	}
}


/**
 * Derive the middle key, i.e. the segment of the render key located between
 * the initial key (path to parameters root) and the property to render.
 *
 * Used by `nodeText()` to handle nested params.
 *
 * Location: `n8n-nodes-base.nodes.github.nodeView.<middleKey>.placeholder`
 */
export function deriveMiddleKey(path: string, parameter: { name: string; type: string }) {
	let middleKey = parameter.name;

	if (isTopLevelCollection(path, parameter) || isNestedInCollectionLike(path)) {
		const pathSegments = normalize(path).split('.');
		middleKey = insertOptionsAndValues(pathSegments).join('.');
	}

	if (isNestedCollection(path, parameter) || isFixedCollection(path, parameter)) {
		const pathSegments = [...normalize(path).split('.'), parameter.name];
		middleKey = insertOptionsAndValues(pathSegments).join('.');
	}

	return middleKey;
}

/**
 * Check if a param path is for a param nested inside a `collection` or
 * `fixedCollection` param.
 */
export const isNestedInCollectionLike = (path: string) => path.split('.').length >= 3;

const isTopLevelCollection = (path: string, parameter: { type: string }) =>
	path.split('.').length === 2 && parameter.type === 'collection';

const isNestedCollection = (path: string, parameter: { type: string }) =>
	path.split('.').length > 2 && parameter.type === 'collection';

/**
 * Check if the param is a normal `fixedCollection`, i.e. a FC other than the wrapper
 * that sits at the root of a node's top-level param and contains all of them.
 */
const isFixedCollection = (path: string, parameter: { type: string }) =>
	parameter.type === 'fixedCollection' && path !== 'parameters';

/**
 * Remove all indices and the `parameters.` prefix from a parameter path.
 *
 * Example: `parameters.a[0].b` → `a.b`
 */
export const normalize = (path: string) => path.replace(/\[.*?\]/g, '').replace('parameters.', '');

/**
 * Insert `'options'` and `'values'` on an alternating basis in a string array of
 * indefinite length. Helper to create a valid render key for a collection-like param.
 *
 * Example: `['a', 'b', 'c']` → `['a', 'options', 'b', 'values', 'c']`
 */
export const insertOptionsAndValues = (pathSegments: string[]) => {
	return pathSegments.reduce<string[]>((acc, cur, i) => {
		acc.push(cur);

		if (i === pathSegments.length - 1) return acc;

		acc.push(i % 2 === 0 ? 'options' : 'values');

		return acc;
	}, []);
};


import { closeBrackets, completionStatus, insertBracket } from '@codemirror/autocomplete';
import type { Extension } from '@codemirror/state';
import { codePointAt, codePointSize } from '@codemirror/state';
import { EditorView } from '@codemirror/view';

const handler = EditorView.inputHandler.of((view, from, to, insert) => {
	if (view.composing || view.state.readOnly) return false;

	// customization: do not autoclose tokens while autocompletion is active
	if (completionStatus(view.state) !== null) return false;

	const selection = view.state.selection.main;

	// customization: do not autoclose square brackets prior to `.json`
	if (
		insert === '[' &&
		view.state.doc.toString().slice(selection.from - '.json'.length, selection.to) === '.json'
	) {
		return false;
	}

	if (
		insert.length > 2 ||
		(insert.length === 2 && codePointSize(codePointAt(insert, 0)) === 1) ||
		from !== selection.from ||
		to !== selection.to
	) {
		return false;
	}

	const transaction = insertBracket(view.state, insert);

	if (!transaction) return false;

	view.dispatch(transaction);

	return true;
});

const [_, bracketState] = closeBrackets() as readonly Extension[];

/**
 * CodeMirror plugin for code node editor:
 *
 * - prevent token autoclosing during autocompletion
 * - prevent square bracket autoclosing prior to `.json`
 *
 * Other than segments marked `customization`, this is a copy of the [original](https://github.com/codemirror/closebrackets/blob/0a56edfaf2c6d97bc5e88f272de0985b4f41e37a/src/closebrackets.ts#L79).
 */
export const codeInputHandler = () => [handler, bracketState];


import { LicenseManager } from '@n8n_io/license-sdk';
import config from '@/config';
import { License } from '@/License';
import { N8N_VERSION } from '@/constants';

jest.mock('@n8n_io/license-sdk');

const MOCK_SERVER_URL = 'https://server.com/v1';
const MOCK_RENEW_OFFSET = 259200;
const MOCK_INSTANCE_ID = 'instance-id';
const MOCK_ACTIVATION_KEY = 'activation-key';
const MOCK_FEATURE_FLAG = 'feat:mock';
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';

describe('License', () => {
	beforeAll(() => {
		config.set('license.serverUrl', MOCK_SERVER_URL);
		config.set('license.autoRenewEnabled', true);
		config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
	});

	let license: License;

	beforeEach(async () => {
		license = new License();
		await license.init(MOCK_INSTANCE_ID);
	});

	test('initializes license manager', async () => {
		expect(LicenseManager).toHaveBeenCalledWith({
			autoRenewEnabled: true,
			autoRenewOffset: MOCK_RENEW_OFFSET,
			deviceFingerprint: expect.any(Function),
			productIdentifier: `n8n-${N8N_VERSION}`,
			logger: expect.anything(),
			loadCertStr: expect.any(Function),
			saveCertStr: expect.any(Function),
			server: MOCK_SERVER_URL,
			tenantId: 1,
		});
	});

	test('attempts to activate license with provided key', async () => {
		await license.activate(MOCK_ACTIVATION_KEY);

		expect(LicenseManager.prototype.activate).toHaveBeenCalledWith(MOCK_ACTIVATION_KEY);
	});

	test('renews license', async () => {
		await license.renew();

		expect(LicenseManager.prototype.renew).toHaveBeenCalled();
	});

	test('check if feature is enabled', async () => {
		await license.isFeatureEnabled(MOCK_FEATURE_FLAG);

		expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
	});

	test('check if sharing feature is enabled', async () => {
		await license.isFeatureEnabled(MOCK_FEATURE_FLAG);

		expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
	});

	test('check fetching entitlements', async () => {
		await license.getCurrentEntitlements();

		expect(LicenseManager.prototype.getCurrentEntitlements).toHaveBeenCalled();
	});

	test('check fetching feature values', async () => {
		await license.getFeatureValue(MOCK_FEATURE_FLAG, false);

		expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG, false);
	});

	test('check management jwt', async () => {
		await license.getManagementJwt();

		expect(LicenseManager.prototype.getManagementJwt).toHaveBeenCalled();
	});

	test('getMainPlan() returns the right entitlement', async () => {
		// mock entitlements response
		License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([
			{
				id: '84a9c852-1349-478d-9ad1-b3f55510e477',
				productId: '670650f2-72d8-4397-898c-c249906e2cc2',
				productMetadata: {},
				features: {},
				featureOverrides: {},
				validFrom: new Date(),
				validTo: new Date(),
			},
			{
				id: MOCK_MAIN_PLAN_ID,
				productId: '670650f2-72d8-4397-898c-c249906e2cc2',
				productMetadata: {
					terms: {
						isMainPlan: true,
					},
				},
				features: {},
				featureOverrides: {},
				validFrom: new Date(),
				validTo: new Date(),
			},
		]);
		jest.fn(license.getMainPlan).mockReset();

		const mainPlan = license.getMainPlan();
		expect(mainPlan?.id).toBe(MOCK_MAIN_PLAN_ID);
	});

	test('getMainPlan() returns undefined if there is no main plan', async () => {
		// mock entitlements response
		License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([
			{
				id: '84a9c852-1349-478d-9ad1-b3f55510e477',
				productId: '670650f2-72d8-4397-898c-c249906e2cc2',
				productMetadata: {}, // has no `productMetadata.terms.isMainPlan`!
				features: {},
				featureOverrides: {},
				validFrom: new Date(),
				validTo: new Date(),
			},
			{
				id: 'c1aae471-c24e-4874-ad88-b97107de486c',
				productId: '670650f2-72d8-4397-898c-c249906e2cc2',
				productMetadata: {}, // has no `productMetadata.terms.isMainPlan`!
				features: {},
				featureOverrides: {},
				validFrom: new Date(),
				validTo: new Date(),
			},
		]);
		jest.fn(license.getMainPlan).mockReset();

		const mainPlan = license.getMainPlan();
		expect(mainPlan).toBeUndefined();
	});
});


getNodeParameter<T = NodeParameterValueType | object>(

keep keep keep replace keep keep keep keep


import N8nLink from './Link.vue';

export default N8nLink;


import type { INodePropertyOptions } from 'n8n-workflow';

// Disable some charts that use different datasets for now
export const CHART_TYPE_OPTIONS: INodePropertyOptions[] = [
	{
		name: 'Bar Chart',
		value: 'bar',
	},
	{
		name: 'Doughnut Chart',
		value: 'doughnut',
	},
	{
		name: 'Line Chart',
		value: 'line',
	},
	{
		name: 'Pie Chart',
		value: 'pie',
	},
	{
		name: 'Polar Chart',
		value: 'polarArea',
	},
];

export const HORIZONTAL_CHARTS = ['bar', 'boxplot', 'violin'];
export const ITEM_STYLE_CHARTS = ['boxplot', 'horizontalBoxplot', 'violin', 'horizontalViolin'];
export const Fill_CHARTS = ['line'];
export const POINT_STYLE_CHARTS = ['line'];


import Container from 'typedi';
import { License } from '../../License';
import { generateKeyPairSync } from 'crypto';
import sshpk from 'sshpk';
import type { KeyPair } from './types/keyPair';

export function isVersionControlLicensed() {
	const license = Container.get(License);
	return license.isVersionControlLicensed();
}

export function isVersionControlEnabled() {
	// TODO: VERSION CONTROL check if enabled
	return true;
}

export function isVersionControlLicensedAndEnabled() {
	return isVersionControlLicensed() && isVersionControlEnabled();
}

export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
	const keyPair: KeyPair = {
		publicKey: '',
		privateKey: '',
	};
	let generatedKeyPair: KeyPair;
	switch (keyType) {
		case 'ed25519':
			generatedKeyPair = generateKeyPairSync('ed25519', {
				privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
				publicKeyEncoding: { format: 'pem', type: 'spki' },
			});
			break;
		case 'rsa':
			generatedKeyPair = generateKeyPairSync('rsa', {
				modulusLength: 4096,
				publicKeyEncoding: {
					type: 'spki',
					format: 'pem',
				},
				privateKeyEncoding: {
					type: 'pkcs8',
					format: 'pem',
				},
			});
			break;
	}
	const keyPublic = sshpk.parseKey(generatedKeyPair.publicKey, 'pem');
	keyPair.publicKey = keyPublic.toString('ssh');
	const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem');
	keyPair.privateKey = keyPrivate.toString('ssh-private');
	return {
		privateKey: keyPair.privateKey,
		publicKey: keyPair.publicKey,
	};
}


import type {
	ITriggerFunctions,
	IDataObject,
	INodeType,
	INodeTypeDescription,
	ITriggerResponse,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import { CronJob } from 'cron';
import moment from 'moment';
import type { IRecurencyRule } from './SchedulerInterface';
import { recurencyCheck } from './GenericFunctions';

export class ScheduleTrigger implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Schedule Trigger',
		name: 'scheduleTrigger',
		icon: 'fa:clock',
		group: ['trigger', 'schedule'],
		version: 1,
		description: 'Triggers the workflow on a given schedule',
		eventTriggerDescription: '',
		activationMessage:
			'Your schedule trigger will now trigger executions on the schedule you have defined.',
		defaults: {
			name: 'Schedule Trigger',
			color: '#31C49F',
		},
		// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
		inputs: [],
		outputs: ['main'],
		properties: [
			{
				displayName:
					'This workflow will run on the schedule you define here once you activate it.

For testing, you can also trigger it manually: by going back to the canvas and clicking ‘execute workflow’',
				name: 'notice',
				type: 'notice',
				default: '',
			},
			{
				displayName: 'Trigger Rules',
				name: 'rule',
				placeholder: 'Add Rule',
				type: 'fixedCollection',
				typeOptions: {
					multipleValues: true,
				},
				default: {
					interval: [
						{
							field: 'days',
						},
					],
				},
				options: [
					{
						name: 'interval',
						displayName: 'Trigger Interval',
						values: [
							{
								displayName: 'Trigger Interval',
								name: 'field',
								type: 'options',
								default: 'days',
								// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
								options: [
									{
										name: 'Seconds',
										value: 'seconds',
									},
									{
										name: 'Minutes',
										value: 'minutes',
									},
									{
										name: 'Hours',
										value: 'hours',
									},
									{
										name: 'Days',
										value: 'days',
									},
									{
										name: 'Weeks',
										value: 'weeks',
									},
									{
										name: 'Months',
										value: 'months',
									},
									{
										name: 'Custom (Cron)',
										value: 'cronExpression',
									},
								],
							},
							{
								displayName: 'Seconds Between Triggers',
								name: 'secondsInterval',
								type: 'number',
								default: 30,
								displayOptions: {
									show: {
										field: ['seconds'],
									},
								},
								description: 'Number of seconds between each workflow trigger',
							},
							{
								displayName: 'Minutes Between Triggers',
								name: 'minutesInterval',
								type: 'number',
								default: 5,
								displayOptions: {
									show: {
										field: ['minutes'],
									},
								},
								description: 'Number of minutes between each workflow trigger',
							},
							{
								displayName: 'Hours Between Triggers',
								name: 'hoursInterval',
								type: 'number',
								displayOptions: {
									show: {
										field: ['hours'],
									},
								},
								default: 1,
								description: 'Number of hours between each workflow trigger',
							},
							{
								displayName: 'Days Between Triggers',
								name: 'daysInterval',
								type: 'number',
								displayOptions: {
									show: {
										field: ['days'],
									},
								},
								default: 1,
								description: 'Number of days between each workflow trigger',
							},
							{
								displayName: 'Weeks Between Triggers',
								name: 'weeksInterval',
								type: 'number',
								displayOptions: {
									show: {
										field: ['weeks'],
									},
								},
								default: 1,
								description: 'Would run every week unless specified otherwise',
							},
							{
								displayName: 'Months Between Triggers',
								name: 'monthsInterval',
								type: 'number',
								displayOptions: {
									show: {
										field: ['months'],
									},
								},
								default: 1,
								description: 'Would run every month unless specified otherwise',
							},
							{
								displayName: 'Trigger at Day of Month',
								name: 'triggerAtDayOfMonth',
								type: 'number',
								displayOptions: {
									show: {
										field: ['months'],
									},
								},
								typeOptions: {
									minValue: 1,
									maxValue: 31,
								},
								default: 1,
								description: 'The day of the month to trigger (1-31)',
								hint: 'If a month doesn’t have this day, the node won’t trigger',
							},
							{
								displayName: 'Trigger on Weekdays',
								name: 'triggerAtDay',
								type: 'multiOptions',
								displayOptions: {
									show: {
										field: ['weeks'],
									},
								},
								typeOptions: {
									maxValue: 7,
								},
								options: [
									{
										name: 'Monday',
										value: 1,
									},
									{
										name: 'Tuesday',
										value: 2,
									},
									{
										name: 'Wednesday',
										value: 3,
									},
									{
										name: 'Thursday',
										value: 4,
									},
									{
										name: 'Friday',
										value: 5,
									},

									{
										name: 'Saturday',
										value: 6,
									},
									{
										name: 'Sunday',
										value: 0,
									},
								],
								default: [0],
							},
							{
								displayName: 'Trigger at Hour',
								name: 'triggerAtHour',
								type: 'options',
								default: 0,
								displayOptions: {
									show: {
										field: ['days', 'weeks', 'months'],
									},
								},
								options: [
									{
										name: 'Midnight',
										displayName: 'Midnight',
										value: 0,
									},
									{
										name: '1am',
										displayName: '1am',
										value: 1,
									},
									{
										name: '2am',
										displayName: '2am',
										value: 2,
									},
									{
										name: '3am',
										displayName: '3am',
										value: 3,
									},
									{
										name: '4am',
										displayName: '4am',
										value: 4,
									},
									{
										name: '5am',
										displayName: '5am',
										value: 5,
									},
									{
										name: '6am',
										displayName: '6am',
										value: 6,
									},
									{
										name: '7am',
										displayName: '7am',
										value: 7,
									},
									{
										name: '8am',
										displayName: '8am',
										value: 8,
									},
									{
										name: '9am',
										displayName: '9am',
										value: 9,
									},
									{
										name: '10am',
										displayName: '10am',
										value: 10,
									},
									{
										name: '11am',
										displayName: '11am',
										value: 11,
									},
									{
										name: 'Noon',
										displayName: 'Noon',
										value: 12,
									},
									{
										name: '1pm',
										displayName: '1pm',
										value: 13,
									},
									{
										name: '2pm',
										displayName: '2pm',
										value: 14,
									},
									{
										name: '3pm',
										displayName: '3pm',
										value: 15,
									},
									{
										name: '4pm',
										displayName: '4pm',
										value: 16,
									},
									{
										name: '5pm',
										displayName: '5pm',
										value: 17,
									},
									{
										name: '6pm',
										displayName: '6pm',
										value: 18,
									},
									{
										name: '7pm',
										displayName: '7pm',
										value: 19,
									},
									{
										name: '8pm',
										displayName: '8pm',
										value: 20,
									},
									{
										name: '9pm',
										displayName: '9pm',
										value: 21,
									},
									{
										name: '10pm',
										displayName: '10pm',
										value: 22,
									},
									{
										name: '11pm',
										displayName: '11pm',
										value: 23,
									},
								],
								description: 'The hour of the day to trigger',
							},
							{
								displayName: 'Trigger at Minute',
								name: 'triggerAtMinute',
								type: 'number',
								default: 0,
								displayOptions: {
									show: {
										field: ['hours', 'days', 'weeks', 'months'],
									},
								},
								typeOptions: {
									minValue: 0,
									maxValue: 59,
								},
								description: 'The minute past the hour to trigger (0-59)',
							},
							{
								displayName:
									'You can find help generating your cron expression here',
								name: 'notice',
								type: 'notice',
								displayOptions: {
									show: {
										field: ['cronExpression'],
									},
								},
								default: '',
							},
							{
								displayName: 'Expression',
								name: 'expression',
								type: 'string',
								default: '',
								placeholder: 'eg. 0 15 * 1 sun',
								displayOptions: {
									show: {
										field: ['cronExpression'],
									},
								},
								hint: 'Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
							},
						],
					},
				],
			},
		],
	};

	async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
		const rule = this.getNodeParameter('rule', []) as IDataObject;
		const interval = rule.interval as IDataObject[];
		const timezone = this.getTimezone();
		const cronJobs: CronJob[] = [];
		const intervalArr: NodeJS.Timeout[] = [];
		const staticData = this.getWorkflowStaticData('node') as {
			recurrencyRules: number[];
		};
		if (!staticData.recurrencyRules) {
			staticData.recurrencyRules = [];
		}
		const executeTrigger = async (recurency: IRecurencyRule) => {
			const resultData = {
				timestamp: moment.tz(timezone).toISOString(true),
				'Readable date': moment.tz(timezone).format('MMMM Do YYYY, h:mm:ss a'),
				'Readable time': moment.tz(timezone).format('h:mm:ss a'),
				'Day of week': moment.tz(timezone).format('dddd'),
				Year: moment.tz(timezone).format('YYYY'),
				Month: moment.tz(timezone).format('MMMM'),
				'Day of month': moment.tz(timezone).format('DD'),
				Hour: moment.tz(timezone).format('HH'),
				Minute: moment.tz(timezone).format('mm'),
				Second: moment.tz(timezone).format('ss'),
				Timezone: moment.tz(timezone).format('z Z'),
			};

			if (!recurency.activated) {
				this.emit([this.helpers.returnJsonArray([resultData])]);
			} else {
				if (recurencyCheck(recurency, staticData.recurrencyRules, timezone)) {
					this.emit([this.helpers.returnJsonArray([resultData])]);
				}
			}
		};

		for (let i = 0; i < interval.length; i++) {
			let intervalValue = 1000;
			if (interval[i].field === 'cronExpression') {
				const cronExpression = interval[i].expression as string;
				try {
					const cronJob = new CronJob(
						cronExpression,
						async () => executeTrigger({ activated: false } as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				} catch (error) {
					throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
						description: 'More information on how to build them at https://crontab.guru/',
					});
				}
			}

			if (interval[i].field === 'seconds') {
				const seconds = interval[i].secondsInterval as number;
				intervalValue *= seconds;
				const intervalObj = setInterval(
					async () => executeTrigger({ activated: false } as IRecurencyRule),
					intervalValue,
				) as NodeJS.Timeout;
				intervalArr.push(intervalObj);
			}

			if (interval[i].field === 'minutes') {
				const minutes = interval[i].minutesInterval as number;
				intervalValue *= 60 * minutes;
				const intervalObj = setInterval(
					async () => executeTrigger({ activated: false } as IRecurencyRule),
					intervalValue,
				) as NodeJS.Timeout;
				intervalArr.push(intervalObj);
			}

			if (interval[i].field === 'hours') {
				const hour = interval[i].hoursInterval as number;
				const minute = interval[i].triggerAtMinute?.toString() as string;
				const cronTimes: string[] = [minute, '*', '*', '*', '*'];
				const cronExpression: string = cronTimes.join(' ');
				if (hour === 1) {
					const cronJob = new CronJob(
						cronExpression,
						async () => executeTrigger({ activated: false } as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				} else {
					const cronJob = new CronJob(
						cronExpression,
						async () =>
							executeTrigger({
								activated: true,
								index: i,
								intervalSize: hour,
								typeInterval: 'hours',
							} as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				}
			}

			if (interval[i].field === 'days') {
				const day = interval[i].daysInterval as number;
				const hour = interval[i].triggerAtHour?.toString() as string;
				const minute = interval[i].triggerAtMinute?.toString() as string;
				const cronTimes: string[] = [minute, hour, '*', '*', '*'];
				const cronExpression: string = cronTimes.join(' ');
				if (day === 1) {
					const cronJob = new CronJob(
						cronExpression,
						async () => executeTrigger({ activated: false } as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				} else {
					const cronJob = new CronJob(
						cronExpression,
						async () =>
							executeTrigger({
								activated: true,
								index: i,
								intervalSize: day,
								typeInterval: 'days',
							} as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				}
			}

			if (interval[i].field === 'weeks') {
				const hour = interval[i].triggerAtHour?.toString() as string;
				const minute = interval[i].triggerAtMinute?.toString() as string;
				const week = interval[i].weeksInterval as number;
				const days = interval[i].triggerAtDay as IDataObject[];
				const day = days.length === 0 ? '*' : days.join(',');
				const cronTimes: string[] = [minute, hour, '*', '*', day];
				const cronExpression = cronTimes.join(' ');
				if (week === 1) {
					const cronJob = new CronJob(
						cronExpression,
						async () => executeTrigger({ activated: false } as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				} else {
					const cronJob = new CronJob(
						cronExpression,
						async () =>
							executeTrigger({
								activated: true,
								index: i,
								intervalSize: week,
								typeInterval: 'weeks',
							} as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				}
			}

			if (interval[i].field === 'months') {
				const month = interval[i].monthsInterval;
				const day = interval[i].triggerAtDayOfMonth?.toString() as string;
				const hour = interval[i].triggerAtHour?.toString() as string;
				const minute = interval[i].triggerAtMinute?.toString() as string;
				const cronTimes: string[] = [minute, hour, day, '*', '*'];
				const cronExpression: string = cronTimes.join(' ');
				if (month === 1) {
					const cronJob = new CronJob(
						cronExpression,
						async () => executeTrigger({ activated: false } as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				} else {
					const cronJob = new CronJob(
						cronExpression,
						async () =>
							executeTrigger({
								activated: true,
								index: i,
								intervalSize: month,
								typeInterval: 'months',
							} as IRecurencyRule),
						undefined,
						true,
						timezone,
					);
					cronJobs.push(cronJob);
				}
			}
		}

		async function closeFunction() {
			for (const cronJob of cronJobs) {
				cronJob.stop();
			}
			for (const entry of intervalArr) {
				clearInterval(entry);
			}
		}

		async function manualTriggerFunction() {
			void executeTrigger({ activated: false } as IRecurencyRule);
		}

		return {
			closeFunction,
			manualTriggerFunction,
		};
	}
}


diff --git a/js/helper.js b/js/helper.js
index a4a7a61f0e226d7cb45f8d5db34c35cc9e62cf96..38fd3ec2214970b1c5dd22fdff902ff3b5eeddde 100644
--- a/js/helper.js
+++ b/js/helper.js
@@ -15,6 +15,7 @@ function getBinaryPath() {
   const parts = [];
   parts.push(__dirname);
   parts.push('..');
+  parts.push('bin');
   parts.push(`sentry-cli${process.platform === 'win32' ? '.exe' : ''}`);
   return path.resolve(...parts);
 }


removeAttributes: (node, exception = []) ->

exception = [exception] if _.isString(exception)

Scribe = require('./scribe')

Scribe.DOM =
  NOBREAK_SPACE:  "\uFEFF"

  addClass: (node, cssClass) ->
    return if Scribe.DOM.hasClass(node, cssClass)
    if node.classList?
      node.classList.add(cssClass)
    else if node.className?
      node.className += ' ' + cssClass

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

  getClasses: (node) ->
    if node.classList
      return _.clone(node.classList)
    else if node.className?
      return node.className.split(' ')

  hasClass: (node, cssClass) ->
    if node.classList?
      return node.classList.contains(cssClass)
    else if node.className?
      return _.indexOf(Scribe.DOM.getClasses(node), cssClass) > -1
    return false

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

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

  normalize: (node) ->
    # Credit: Tim Down - http://stackoverflow.com/questions/2023255/node-normalize-crashes-in-ie6
    child = node.firstChild
    while (child)
      if (child.nodeType == 3)
        while ((nextChild = child.nextSibling) && nextChild.nodeType == 3)
          child.appendData(nextChild.data)
          node.removeChild(nextChild)
      child = child.nextSibling

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

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

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

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

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

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

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

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

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

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

module.exports = Scribe


Scribe = require('./scribe')
Scribe.Format ?= {}

class Scribe.Format.Leaf
  constructor: (@root, @keyName) ->

  clean: (node) ->
    Scribe.DOM.removeAttributes(node)

  createContainer: (value) ->
    console.error('Descendants should implement')

  matchContainer: (container) ->
    console.error('Descendants should implement')

class Scribe.Format.Tag extends Scribe.Format.Leaf
  constructor: (@root, @keyName, @tagName) ->
    super

  createContainer: (value) ->
    return @root.ownerDocument.createElement(@tagName)

  matchContainer: (container) ->
    return container.tagName == @tagName

class Scribe.Format.Span extends Scribe.Format.Tag
  constructor: (@root, @keyName) ->
    super(@root, @keyName, 'SPAN')

class Scribe.Format.Class extends Scribe.Format.Span
  constructor: (@root, @keyName) ->
    super

  clean: (node) ->
    Scribe.DOM.removeAttributes(node, 'class')

  createContainer: (value) ->
    container = super(value)
    Scribe.DOM.addClass(container, "#{@keyName}-#{value}")
    return container

  matchContainer: (container) ->
    if super(container)
      classList = Scribe.DOM.getClasses(container)
      for css in classList
        parts = css.split('-')
        if parts.length > 1 and parts[0] == @keyName
          return parts.slice(1).join('-')
    return false

class Scribe.Format.Style extends Scribe.Format.Span
  @getStyleObject: (container) ->
    styleString = container.getAttribute('style') or ''
    return _.reduce(styleString.split(';').slice(0, -1), (styles, str) ->
      [name, value] = str.split(':')
      value = value.slice(1) if value[0] == ' '
      styles[name] = value
      return styles
    , {})

  @getCamelCase: (cssName) ->
    nameArr = cssName.split('-')
    capitalNameArr = _.map(nameArr, (name) ->
      return name[0].toUpperCase() + name.slice(1)
    )
    return nameArr[0] + capitalNameArr.slice(1).join('')

  constructor: (@root, @keyName, @cssName, @styles, @matchFn) ->
    @matchFn or= (formatName, cssValue) =>
      return @styles[formatName] == cssValue
    super

  clean: (node) ->
    Scribe.DOM.removeAttributes(node, 'style')

  createContainer: (value) ->
    container = super(value)
    return container unless @styles[value]?
    cssName = Scribe.Format.Style.getCamelCase(@cssName)
    container.style[@cssName] = @styles[value] if @styles[value]
    return container

  matchContainer: (container) ->
    return false unless super(container)
    styles = Scribe.Format.Style.getStyleObject(container)
    if styles[@cssName]?
      for formatName of @styles
        return formatName if @matchFn(formatName, styles[@cssName])
    return false

class Scribe.Format.Bold extends Scribe.Format.Tag
  constructor: (@root) ->
    super(@root, 'bold', 'B')

class Scribe.Format.Italic extends Scribe.Format.Tag
  constructor: (@root) ->
    super(@root, 'italic', 'I')

class Scribe.Format.Strike extends Scribe.Format.Tag
  constructor: (@root) ->
    super(@root, 'strike', 'S')

class Scribe.Format.Underline extends Scribe.Format.Tag
  constructor: (@root) ->
    super(@root, 'underline', 'U')

class Scribe.Format.Link extends Scribe.Format.Tag
  constructor: (@root) ->
    super(@root, 'link', 'A')

  createContainer: (value) ->
    link = super(value)
    value = 'https://' + value unless value.match(/https?:\/\//)
    link.href = value
    link.href = 'about:blank' if (link.protocol != 'http:' && link.protocol != 'https:')
    link.title = link.href
    return link

  matchContainer: (container) ->
    return if super(container) then container.getAttribute('href') else false

class Scribe.Format.Background extends Scribe.Format.Style
  constructor: (@root) ->
    colors = _.clone(Scribe.Format.Color.COLORS)
    delete colors['white']
    super(@root, 'background', 'background-color', colors, Scribe.Format.Color.matchColor)

class Scribe.Format.Color extends Scribe.Format.Style
  @COLORS: {
    'black'   : '#000'
    'red'     : '#F00'
    'blue'    : '#00F'
    'lime'    : '#0F0'
    'teal'    : '#0FF'
    'magenta' : '#F0F'
    'yellow'  : '#FF0'
    'white'   : '#FFF'
  }

  @normalizeColor: (color) ->
    color = Scribe.Format.Color.COLORS[color] if Scribe.Format.Color.COLORS[color]?
    if color[0] == '#'
      color = color.slice(1)
      if color.length == 3
        color = _.map(color, (letter) ->
          letter + letter
        ).join('')
      return [
        parseInt(color.slice(0, 2), 16)
        parseInt(color.slice(2, 4), 16)
        parseInt(color.slice(4, 6), 16)
      ]
    else if color.indexOf('rgb(') == 0
      color = color.slice(4)
      return _.map(color.split(',').slice(0, 3), (part) ->
        parseInt(part, 10)
      )
    else
      return [0, 0, 0]

  @matchColor: (color1, color2) ->
    color1 = Scribe.Format.Color.normalizeColor(color1)
    color2 = Scribe.Format.Color.normalizeColor(color2)
    return _.isEqual(color1, color2)

  constructor: (@root) ->
    colors = _.clone(Scribe.Format.Color.COLORS)
    delete colors['black']
    super(@root, 'color', 'color', colors, Scribe.Format.Color.matchColor)

class Scribe.Format.Family extends Scribe.Format.Style
  constructor: (@root) ->
    super(@root, 'family', 'font-family', {
      'serif'     : "'Times New Roman', serif"
      'monospace' : "'Courier New', monospace"
    }, (formatValue, cssValue) =>
      return cssValue.indexOf(formatValue) >= 0
    )

class Scribe.Format.Size extends Scribe.Format.Style
  constructor: (@root) ->
    super(@root, 'size', 'font-size', {
      'huge'  : '32px'
      'large' : '18px'
      'small' : '10px'
    })

  createContainer: (value) ->
    container = super(value)
    return container

module.exports = Scribe


# So test files can use Tandem
window.Tandem = require('tandem-core')


/***** Begin Theme, feel free to edit in here! ******/

/* One container to bind them... */
.dk_container
  font-family: 'Helvetica', Arial, sans-serif
  font-size: 12px
  font-weight: bold
  line-height: 14px
  margin-bottom: 18px
  border-radius: 5px
  &:focus
    outline: 0
  a
    cursor: pointer
    text-decoration: none

/* Opens the dropdown and holds the menu label */
.dk_toggle
  background-image: url(../images/dk_arrows.png)
  background-repeat: no-repeat
  background-position: 90% center
  border: 1px solid #fff
  color: #333
  padding: 5px 10px 5px 10px
  &:hover
    background-image: url(../images/dk_arrows_blue.png)
    color: #06C
  .dk_label
    display: inline-block

/* Applied whenever the dropdown is open */
.dk_open
  /**
   * Help: Dropdown menu is covered by something
   * Try setting this value higher
   */
  z-index: 10
  .dk_toggle
    color: #ccc
    background-image: url(../images/dk_arrows_gray.png)
    border: 1px solid #ccc

/* The outer container of the options */
.dk_options
  box-shadow: rgba(0, 0, 0, .2) 0 2px 8px
  -moz-box-shadow: rgba(0, 0, 0, .2) 0 2px 8px
  -webkit-box-shadow: rgba(0, 0, 0, .2) 0 2px 8px
  a
    color: #333
    background-color: #fff
    font-weight: bold
    padding: 6px 10px
  li:last-child a
    border-bottom: none
.dk_options a:hover,
.dk_option_current a
  color: #06C
  text-decoration: none

/* Inner container for options, this is what makes the scrollbar possible. */
.dk_options_inner
  border: 1px solid #ccc
  border-bottom-width: 2px
  border-bottom-color: #bbb
  color: #333
  max-height: 250px
  text-shadow: #fff 0 1px 0

/* Set a max-height on the options inner */
.dk_options_inner,
.dk_touch .dk_options
  max-height: 250px

/******  End Theme ******/

/***** Critical to the continued enjoyment of working dropdowns ******/

.dk_container
  display: none
  float: left
  position: relative
  a
    outline: 0

.dk_toggle
  display: -moz-inline-stack
  display: inline-block
  *display: inline
  position: relative
  zoom: 1

.dk_open
  position: relative
  .dk_options
    display: block
  .dk_label
    color: inherit

.dk_options
  display: none
  margin-top: -1px
  position: absolute
  right: 0
  width: 100%
  a, a:link, a:visited
    display: block
.dk_options_inner
  overflow: auto
  position: relative

.dk_touch
  .dk_options
    overflow: hidden
  .dk_options_inner
    max-height: none
    overflow: visible

.dk_focus select
  position: relative
  top: -99999em
  visibility: hidden

/***** End Critical to the continued enjoyment of working dropdowns ******/


node.removeAttribute(attrNode.name) unless exception == attrNode.name

return unless Scribe.DOM.hasClass(node, cssClass)

node.removeAttribute(attrNode.name) unless _.indexOf(exception, attrNode.name) > -1


require "scribe-rails/version"

module Scribe
  module Rails
    class Engine < ::Rails::Engine
    end
  end
end


module Scribe
  module Rails
    VERSION = "0.6.4"
  end
end


listenEditor = (source, target) ->
  source.on(Scribe.Editor.events.USER_TEXT_CHANGE, (delta) ->
    console.info source.id, 'text change', delta
    target.applyDelta(delta)
    sourceDelta = source.doc.toDelta()
    targetDelta = target.doc.toDelta()
    console.assert(sourceDelta.isEqual(targetDelta), "Editor diversion!", source, target, sourceDelta, targetDelta)
  ).on(Scribe.Editor.events.SELECTION_CHANGE, (range) ->
    console.info source.id, 'selection change', range.start.index, range.end.index
    target.cursorManager.setCursor(source.id, range.end.index, source.id, 'blue')
  )

editors = _.map([1, 2], (num) ->
  editor = new Scribe.Editor('editor-container' + num)
  toolbar = new Scribe.Toolbar("formatting-container#{num}", editor)
  editor.cursorManager = new Scribe.MultiCursor(editor)
  editor.attributionManager = new Scribe.Attribution(editor, editor.id, 'blue')
  return editor
)

listenEditor(editors[0], editors[1])
listenEditor(editors[1], editors[0])
editors[0].attributionManager.addAuthor(editors[1].id, 'blue')
editors[1].attributionManager.addAuthor(editors[0].id, 'blue')