Scribe = require('./scribe')

_initDeletes = ->
  _.each([Scribe.Keyboard.KEYS.DELETE, Scribe.Keyboard.KEYS.BACKSPACE], (key) =>
    this.addHotkey(key, =>
      # Prevent deleting if editor is already blank (browser quirk fix)
      return @editor.getLength() > 1
    )
  )

_initHotkeys = ->
  this.addHotkey(Scribe.Keyboard.KEYS.TAB, =>
    @editor.selection.deleteRange()
    this.insertText("\t")
  )
  this.addHotkey(Scribe.Keyboard.HOTKEYS.BOLD, (selection) =>
    this.toggleFormat(selection, 'bold')
  )
  this.addHotkey(Scribe.Keyboard.HOTKEYS.ITALIC, (selection) =>
    this.toggleFormat(selection, 'italic')
  )
  this.addHotkey(Scribe.Keyboard.HOTKEYS.UNDERLINE, (selection) =>
    this.toggleFormat(selection, 'underline')
  )

_initListeners = ->
  @editor.root.addEventListener('keydown', (event) =>
    event ||= window.event
    if @hotkeys[event.which]?
      prevent = false
      _.each(@hotkeys[event.which], (hotkey) =>
        return if hotkey.meta? and (event.metaKey != hotkey.meta and event.ctrlKey != hotkey.meta)
        return if hotkey.shift? and event.shiftKey != hotkey.shift
        @editor.selection.update(true)
        selection = @editor.getSelection()
        return unless selection?
        prevent = hotkey.callback.call(null, selection) == false
      )
    event.preventDefault() if prevent
    return !prevent
  )

class Scribe.Keyboard
  @KEYS:
    BACKSPACE : 8
    TAB       : 9
    ENTER     : 13
    LEFT      : 37
    UP        : 38
    RIGHT     : 39
    DOWN      : 40
    DELETE    : 46

  @HOTKEYS:
    BOLD:       { key: 'B', meta: true }
    ITALIC:     { key: 'I', meta: true }
    UNDERLINE:  { key: 'U', meta: true }
    UNDO:       { key: 'Z', meta: true, shift: false }
    REDO:       { key: 'Z', meta: true, shift: true }

  @NAVIGATION: [@KEYS.UP, @KEYS.DOWN, @KEYS.LEFT, @KEYS.RIGHT]

  constructor: (@editor) ->
    @hotkeys = {}
    _initListeners.call(this)
    _initHotkeys.call(this)
    _initDeletes.call(this)

  addHotkey: (hotkey, callback) ->
    hotkey = if _.isObject(hotkey) then _.clone(hotkey) else { key: hotkey }
    hotkey.key = hotkey.key.toUpperCase().charCodeAt(0) if _.isString(hotkey.key)
    hotkey.callback = callback
    @hotkeys[hotkey.key] ?= []
    @hotkeys[hotkey.key].push(hotkey)

  indent: (selection, increment) ->
    lines = selection.getLines()
    applyIndent = (line, format) =>
      if increment
        indent = if _.isNumber(line.formats[format]) then line.formats[format] else (if line.formats[format] then 1 else 0)
        indent += increment
        indent = Math.min(Math.max(indent, Scribe.Line.MIN_INDENT), Scribe.Line.MAX_INDENT)
      else
        indent = false
      index = Position.getIndex(line.node, 0)
      @editor.formatAt(index, 0, format, indent)

    _.each(lines, (line) =>
      if line.formats.bullet?
        applyIndent(line, 'bullet')
      else if line.formats.list?
        applyIndent(line, 'list')
      else
        applyIndent(line, 'indent')
    )

  onIndentLine: (selection) ->
    return false if !selection?
    intersection = selection.getFormats()
    return intersection.bullet? || intersection.indent? || intersection.list?

  insertText: (text) ->
    selection = @editor.getSelection()
    @editor.insertAt(selection.start.index, text, {}, false) if selection?
    # Make sure selection is after our text
    range = new Scribe.Range(@editor, selection.start.index + text.length, selection.start.index + text.length)
    @editor.setSelection(range, true)

  toggleFormat: (selection, format) ->
    formats = selection.getFormats()
    @editor.selection.format(format, !formats[format], false)

module.exports = Scribe


'.cursor-caret': { 'display': 'inline-block', 'width': '2px', 'position': 'absolute', 'height': '100%', 'left': '-1px' }

'.cursor.top > .cursor-name': { 'top': '100%' }

keep keep keep replace keep keep keep keep

Scribe = require('./scribe')

class Scribe.Renderer
  @DEFAULTS:
    id: 'editor'
    keepHTML: false
    styles:
      'html' : { 'height': '100%' }
      'body': {
        'cursor': 'text'
        'font-family': "'Helvetica', 'Arial', sans-serif"
        'font-size': '13px'
        'height': '100%'
        'line-height': '1.154'
        'margin': '0px'
        'padding': '0px'
      }
      'div.editor': {
        'bottom': '10px'
        'left': '15px'
        'outline': 'none'
        'position': 'absolute'
        'right': '15px'
        'tab-size': '4'
        'top': '10px'
        'white-space': 'pre-wrap'
      }
      'div.line:last-child': { 'padding-bottom': '10px' }
      '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' }
      '.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' }

  @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 = _.defaults(options, Scribe.Renderer.DEFAULTS)
    this.createFrame()
    @formats = {}
    this.addStyles(Scribe.Renderer.DEFAULTS.styles)
    # Ensure user specified styles are added last
    this.runWhenLoaded( =>
      this.addStyles(options.styles) if options.styles?
    , -10)

  addContainer: (container, before = false) ->
    this.runWhenLoaded( =>
      refNode = if before then @root else null
      @root.parentNode.insertBefore(container, refNode)
    )

  addStyles: (styles) ->
    this.runWhenLoaded( =>
      style = @root.ownerDocument.createElement('style')
      style.type = 'text/css'
      css = Scribe.Renderer.objToCss(styles)
      if style.styleSheet?
        style.styleSheet.cssText = css
      else
        style.appendChild(@root.ownerDocument.createTextNode(css))
      # Firefox needs defer
      _.defer( =>
        @root.ownerDocument.head.appendChild(style)
      )
    )

  createFrame: ->
    html = @container.innerHTML
    @container.innerHTML = ''
    @iframe = @container.ownerDocument.createElement('iframe')
    @iframe.frameborder = 0
    @iframe.height = @iframe.width = '100%'
    @container.appendChild(@iframe)
    @root = this.getDocument().createElement('div')
    Scribe.DOM.addClass(@root, 'editor')
    @root.id = @options.id
    @root.innerHTML = Scribe.Normalizer.normalizeHtml(html) if @options.keepHTML
    this.runWhenLoaded( =>
      this.getDocument().body.appendChild(@root)
    )

  getDocument: ->
    # Firefox does not like us saving a reference to this result so retrieve every time
    # IE does not have contentWindow
    return @iframe.document or @iframe.contentWindow?.document

  runWhenLoaded: (fn, priority = 0) ->
    return fn.call(this) if this.getDocument()?.readyState == 'complete'
    if @callbacks?
      @callbacks[priority] ?= []
      @callbacks[priority].push(fn)
    else
      @callbacks = {}
      @callbacks[priority] = [fn]
      interval = setInterval( =>
        doc = this.getDocument()
        if doc?.readyState == 'complete'
          clearInterval(interval)
          _.defer( =>
            keys = _.keys(@callbacks).sort((a,b) -> b - a)
            _.each(keys, (key) =>
              _.each(@callbacks[key], (callback) =>
                callback.call(this)
              )
            )
            @callbacks = {}
          )
        else if !doc
          clearInterval(interval)
      , 100)

module.exports = Scribe


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

getLastChangeIndex = (delta) ->
  lastChangeIndex = index = offset = 0
  _.each(delta.ops, (op) ->
    # Insert
    if Tandem.Delta.isInsert(op)
      offset += op.getLength()
      lastChangeIndex = index + offset
    else if Tandem.Delta.isRetain(op)
      # Delete
      if op.start > index
        lastChangeIndex = index + offset
        offset -= (op.start - index)
      # Format
      if _.keys(op.attributes).length > 0
        lastChangeIndex = op.end + offset
      index = op.end
  )
  if delta.endLength < delta.startLength + offset
    lastChangeIndex = delta.endLength
  return lastChangeIndex

_change = (source, dest) ->
  if @stack[source].length > 0
    change = @stack[source].pop()
    @lastRecorded = 0
    _ignoreChanges.call(this, =>
      @editor.applyDelta(change[source], { source: 'user' })
      index = getLastChangeIndex(change[source])
      @editor.setSelection(new Scribe.Range(@editor, index, index))
    )
    @stack[dest].push(change)

_ignoreChanges = (fn) ->
  oldIgnoringChanges = @ignoringChanges
  @ignoringChanges = true
  fn.call(this)
  @ignoringChanges = oldIgnoringChanges

class Scribe.UndoManager
  @DEFAULTS:
    delay: 1000
    maxStack: 100

  constructor: (@editor, options = {}) ->
    this.clear()
    @options = _.defaults(options, Scribe.UndoManager.DEFAULTS)
    @lastRecorded = 0
    this.initListeners()

  initListeners: ->
    @editor.keyboard.addHotkey(Scribe.Keyboard.HOTKEYS.UNDO, =>
      this.undo()
      return false
    )
    @editor.keyboard.addHotkey(Scribe.Keyboard.HOTKEYS.REDO, =>
      this.redo()
      return false
    )
    @ignoringChanges = false
    @editor.on(Scribe.Editor.events.USER_TEXT_CHANGE, (delta) =>
      this.record(delta, @oldDelta) unless @ignoringChanges
      @oldDelta = @editor.getDelta()
    ).on(Scribe.Editor.events.API_TEXT_CHANGE, (delta) =>
      this.record(delta, @oldDelta)
      @oldDelta = @editor.getDelta()
    )

  clear: ->
    @stack =
      undo: []
      redo: []
    @oldDelta = @editor.getDelta()

  record: (changeDelta, oldDelta) ->
    return if changeDelta.isIdentity()
    @redoStack = []
    undoDelta = oldDelta.invert(changeDelta)
    timestamp = new Date().getTime()
    if @lastRecorded + @options.delay > timestamp and @stack.undo.length > 0
      change = @stack.undo.pop()
      undoDelta = undoDelta.compose(change.undo)
      changeDelta = change.redo.compose(changeDelta)
    else
      @lastRecorded = timestamp
    @stack.undo.push({
      redo: changeDelta
      undo: undoDelta
    })
    @stack.undo.unshift() if @stack.undo.length > @options.maxStack

  redo: ->
    _change.call(this, 'redo', 'undo')

  ###
  transformExternal: (delta) ->
    return if delta.isIdentity()
    @stack['undo'] = _.map(@stack['undo'], (change) ->
      return {
        redo: delta.follows(change.redo, true)
        undo: change.undo.follows(delta, true)
      }
    )
  ###

  undo: ->
    _change.call(this, 'undo', 'redo')

module.exports = Scribe


(function() {
  var _ = window._.noConflict();
  var EventEmitter2 = window.EventEmitter2;
  var LinkedList = window.LinkedList;
  var rangy = window.rangy;
  window.rangy = window.LinkedList = window.EventEmitter2 = undefined;


class SeleniumAdapter

  attr_accessor :doc_length, :cursor_pos

  def initialize(driver, editor)
    @cursor_pos = 0
    @driver = driver
    @editor = editor
    @doc_length = 1 # Initial val of 1 due to Scribe's "phantom" newline
  end

  # Assumes delta will contain only one document modifying op
  def apply_delta(delta)
    index = 0
    delta['ops'].each_with_index do |op, i|
      if op['value']
        while op and op['value']
          insert_at index, op['value']
          i += 1
          index += op['value'].length
          op = delta['ops'][i]
        end
        break
      elsif op['start'] > index
        delete_length = op['start'] - index
        delete_at(index, delete_length)
        break
      elsif !op['attributes'].empty?
        length = op['end'] - op['start']
        format_at(index, length, op['attributes'])
        break
      else
        index += op['end'] - op['start']
      end
    end
  end

  # private

  def insert_at(index, text)
    puts "Inserting #{text} at #{index}"
    move_cursor(index)
    type_text(text)
    move_cursor index + text.length
  end

  def delete_at(index, length)
    puts "Deleting #{length} at #{index}"
    move_cursor(index)
    delete(length)
  end

  def format_at(index, length, attributes)
    puts "Formatting #{length} starting at #{index} with #{attributes}"
    move_cursor(index)
    highlight(length)
    attributes.each do |attr, val|
      format(attr, val)
    end
    remove_highlighting
  end

  def remove_highlighting
    @editor.send_keys [] # Workaround for an issue in Ruby bindings.
    @editor.send_keys [:arrow_right]
  end

  def jump_to_start
    os = SeleniumAdapter.os()
    if os == :windows or os == :linux
      key = :home
    elsif os == :macosx
      key = :arrow_up
    end
    @driver.action.key_down(@@cmd_modifier).send_keys(key).key_up(@@cmd_modifier).perform
  end

  def move_cursor(dest_index)
    if dest_index < 0 or dest_index > doc_length
      raise IndexError, "Invalid dest_index #{dest_index} for doc_length #{doc_length}"
    end
    distance = (@cursor_pos - dest_index).abs
    if dest_index == 0
      jump_to_start()
    elsif @cursor_pos > dest_index
      distance.times do @editor.send_keys(:arrow_left) end
    else
      distance.times do @editor.send_keys(:arrow_right) end
    end
    @cursor_pos = dest_index
  end

  def highlight(length)
    keys = (0...length).to_a.map! { :arrow_right }
    @cursor_pos += length
    @driver.action.key_down(:shift).send_keys(keys).key_up(:shift).perform
  end

  def delete(length)
    highlight(length)
    @editor.send_keys(:delete)
    @doc_length -= length
    @cursor_pos -= length
  end

  def split_text(text)
    keys = []
    cur = ""
    text.each_char { |c|
      if c == "\n"
        keys << cur if cur.length > 0
        cur = ""
        keys << :enter
      elsif c == " "
        keys << cur if cur.length > 0
        cur = ""
        keys << :space
      else
        cur << c
      end
    }
    keys << cur if cur.length > 0
    return keys
  end

  def type_text(text)
    keys = split_text(text)
    if @cursor_pos == 0 && @doc_length == 0 && @driver.browser == :firefox
      # Hack to workaround inexplicable firefox behavior in which it appends a
      # newline if you append to the end of the empty document.
      @editor.send_keys(keys)
      @editor.send_keys([:arrow_down, :delete])
    else
      @editor.send_keys keys
    end
    len = keys.reduce(0) do |len, elem|
      if elem.is_a?(String) then len + elem.length else len + 1 end
    end
    @cursor_pos += len
    @doc_length += len
  end

  def click_button_from_toolbar(button_class)
    @driver.switch_to.default_content
    button = @driver.execute_script("return $('#editor-toolbar > .#{button_class}')[0]")
    button.click()
    @driver.switch_to.frame(@driver.find_element(:tag_name, "iframe"))
  end

  def select_from_dropdown(dropdown_class, value)
    @driver.switch_to.default_content
    dropdown = @driver.execute_script("return $('#editor-toolbar > .#{dropdown_class}')[0]")
    Selenium::WebDriver::Support::Select.new(dropdown).select_by(:value, value)
    @driver.switch_to.frame(@driver.find_element(:tag_name, "iframe"))
  end

  def format(format, value)
    case format
    when /bold|italic|underline/
      if Random.rand() < 0.5
        @driver.action.key_down(@@cmd_modifier).send_keys(format[0]).key_up(@@cmd_modifier).perform
      else
        click_button_from_toolbar(format)
      end
    when /link|strike/
      click_button_from_toolbar(format)
    when /family|size|background|color/
      select_from_dropdown(format, value)
    else
      raise "Unknown formatting op: #{format}"
    end
    self.focus # Need to reset focus to the editor after clicking toolbar
  end

  def focus
    @editor.send_keys ""
  end

  def self.os
    host_os = RbConfig::CONFIG['host_os']
    case host_os
    when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
      return :windows
    when /darwin|mac os/
      return :macosx
    when /linux/
      return :linux
    when /solaris|bsd/
      return :unix
    else
      raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
    end
  end

  def self.os_to_modifier
    case self.os()
    when :windows
      return :control
    when :linux
      return :control
    when :macosx
      return :command
    end
  end
  @@cmd_modifier = SeleniumAdapter.os_to_modifier()
end


Scribe = require('./scribe')

# DOM Selection API says offset is child index of container, not number of characters like Scribe.Position
normalizeNativePosition = (node, offset) ->
  if node?.nodeType == node.ELEMENT_NODE
    if offset == 0
      node = node.firstChild if node.firstChild?
    else
      node = node.childNodes[node.childNodes.length-1]
      offset = node.textContent.length
  return [node, offset]

normalizeNativeRange = (nativeRange) ->
  return null unless nativeRange?
  [startContainer, startOffset] = normalizeNativePosition(nativeRange.startContainer, nativeRange.startOffset)
  [endContainer, endOffset] = normalizeNativePosition(nativeRange.endContainer, nativeRange.endOffset)
  return {
    startContainer  : startContainer
    startOffset     : startOffset
    endContainer    : endContainer
    endOffset       : endOffset
  }

_nativeRangeToRange = (nativeRange) ->
  nativeRange = normalizeNativeRange(nativeRange)
  start = new Scribe.Position(@editor, nativeRange.startContainer, nativeRange.startOffset)
  end = new Scribe.Position(@editor, nativeRange.endContainer, nativeRange.endOffset)
  if start.index <= end.index
    return new Scribe.Range(@editor, start, end)
  else
    return new Scribe.Range(@editor, end, start)

_preserveWithIndex = (nativeRange, index, lengthAdded, fn) ->
  range = _nativeRangeToRange.call(this, nativeRange)
  [startIndex, endIndex] = _.map([range.start, range.end], (pos) ->
    if index >= pos.index
      return pos.index
    else
      return Math.max(pos.index + lengthAdded, index)
  )
  fn.call(null)
  this.setRange(new Scribe.Range(@editor, startIndex, endIndex), true)

_preserveWithLine = (nativeRange, fn) ->
  savedNativeRange = normalizeNativeRange(nativeRange)
  savedData = _.map([
    { container: savedNativeRange.startContainer, offset: savedNativeRange.startOffset }
    { container: savedNativeRange.endContainer,   offset: savedNativeRange.endOffset }
  ], (position) ->
    lineNode = Scribe.Utils.findAncestor(position.container, Scribe.Line.isLineNode) or @editor.root
    return {
      lineNode  : lineNode
      offset    : Scribe.Position.getIndex(position.container, position.offset, lineNode)
      nextLine  : position.container.previousSibling?.tagName == 'BR'  # Track special case for Firefox
    }
  )
  fn.call(null)
  nativeRange = normalizeNativeRange(this.getNativeRange())
  if !_.isEqual(nativeRange, savedNativeRange)
    [start, end] = _.map(savedData, (savedDatum) =>
      if savedDatum.nextLine and savedDatum.lineNode.nextSibling?
        savedDatum.lineNode = savedDatum.lineNode.nextSibling
        savedDatum.offset = 0
      return new Scribe.Position(@editor, savedDatum.lineNode, savedDatum.offset)
    )
    this.setRange(new Scribe.Range(@editor, start, end), true)

class Scribe.Selection
  constructor: (@editor) ->
    @range = null
    this.initListeners()
    @editor.renderer.runWhenLoaded( =>
      rangy.init()
      @nativeSelection = rangy.getIframeSelection(@editor.renderer.iframe)
      this.setRange(new Scribe.Range(@editor, 0, 0))    # Range gets set to end of doc in Firefox by default
    )

  initListeners: ->
    checkUpdate = =>
      this.update() if @editor.root.isContentEditable
    @editor.root.addEventListener('mouseup', checkUpdate)
    _.each(Scribe.Keyboard.NAVIGATION, (key) =>
      @editor.keyboard.addHotkey(key, =>
        _.defer(checkUpdate)
      )
    )

  format: (name, value) ->
    this.update()
    return unless @range
    start = @range.start.index
    end = @range.end.index
    formats = @range.getFormats()
    @editor.formatAt(start, end - start, name, value, { source: 'user' }) if end > start
    formats[name] = value
    @range.formats = formats
    this.setRange(new Scribe.Range(@editor, start, end))

  deleteRange: ->
    this.update()
    return false if @range.isCollapsed()
    @editor.deleteAt(@range.start.index, @range.end.index - @range.start.index)
    this.update()
    return @range

  getDimensions: ->
    rangyRange = this.getNativeRange()
    return rangyRange?.nativeRange?.getBoundingClientRect()

  getNativeRange: ->
    @nativeSelection.refresh()
    return if @nativeSelection?.rangeCount > 0 then @nativeSelection.getRangeAt(0) else null

  getRange: ->
    nativeRange = this.getNativeRange()
    return if nativeRange? then _nativeRangeToRange.call(this, nativeRange) else null

  preserve: (index, lengthAdded, fn) ->
    fn = index if _.isFunction(index)
    nativeRange = this.getNativeRange()
    if nativeRange?
      if _.isFunction(index)
        _preserveWithLine.call(this, nativeRange, index)
      else
        _preserveWithIndex.call(this, nativeRange, index, lengthAdded, fn)
    else
      fn.call(null)

  setRange: (range, silent = false) ->
    return unless @nativeSelection?
    unless silent
      # If we are not emitting change, we don't care what the cursor was
      this.update(true) unless silent
      return if range == @range or @range?.equals(range)
    @range = range
    if @range?
      nativeRange = rangy.createRangyRange()
      _.each([@range.start, @range.end], (pos, i) ->
        [node, offset] = Scribe.DOM.findDeepestNode(pos.leafNode, pos.offset)
        node = node.parentNode if node.tagName == "BR"      # Firefox does not like selections inside break tags
        offset = Math.min(node.textContent.length, offset)  # Should only occur at end of document
        fn = if i == 0 then 'setStart' else 'setEnd'
        nativeRange[fn].call(nativeRange, node, offset)
      )
      @nativeSelection.setSingleRange(nativeRange)
    else
      @nativeSelection.removeAllRanges()
    @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, @range) unless silent

  update: (silent = false) ->
    range = this.getRange()
    unless range == @range or @range?.equals(range)
      @editor.emit(Scribe.Editor.events.SELECTION_CHANGE, range) unless silent
      @range = range

module.exports = Scribe


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

class Scribe.Attribution
  constructor: (@editor, @authorId, color, enabled = false) ->
    @editor.on(Scribe.Editor.PRE_EVENT, (eventName, delta) =>
      if eventName == Scribe.Editor.events.USER_TEXT_CHANGE
        _.each(delta.ops, (op) =>
          if Tandem.InsertOp.isInsert(op) or _.keys(op.attributes).length > 0
            op.attributes['author'] = @authorId
        )
        delta.apply((index, text) =>
          @editor.formatAt(index, text.length, 'author', @authorId, { silent: true })
        , (=>), (index, length, name, value) =>
          @editor.formatAt(index, length, 'author', @authorId, { silent: true })
        )
    )
    @editor.doc.formatManager.addFormat('author', new Scribe.Format.Class(@editor.renderer.root, 'author'))
    this.addAuthor(@authorId, color)
    this.enable() if enabled

  addAuthor: (id, color) ->
    styles = {}
    styles[".editor.attribution .author-#{id}"] = { "background-color": "#{color}" }
    @editor.renderer.addStyles(styles)

  enable: ->
    Scribe.DOM.addClass(@editor.root, 'attribution')

  disable: ->
    Scribe.DOM.removeClass(@editor.root, 'attribution')

module.exports = Scribe


Scribe = require('./scribe')
class Scribe.Renderer
@DEFAULTS:
id: 'editor'
keepHTML: false
styles:
'html' : { 'height': '100%' }

keep keep keep replace keep keep keep keep keep

class Scribe.Renderer extends EventEmitter2

Scribe = require('../scribe')

_applyDelta = (delta) ->
  delta.apply((index, text, formatting) =>
    this.shiftCursors(index, text.length, formatting['author'], false)
  , (index, length) =>
    this.shiftCursors(index, -1 * length, null, false)
  , (index, length, name, value) =>
    this.shiftCursors(index, 0, null, false)
  )
  this.update()

_buildCursor = (name, color) ->
  cursor = @container.ownerDocument.createElement('span')
  Scribe.DOM.addClass(cursor, 'cursor')
  cursor.innerHTML = @options.template
  cursorName = cursor.querySelector('.cursor-name')
  cursorName.textContent = name
  cursorCaret = cursor.querySelector('.cursor-caret')
  cursorCaret.style.backgroundColor = cursorName.style.backgroundColor = color
  @container.appendChild(cursor)
  return cursor

_moveCursor = (cursor, referenceNode) ->
  cursor.elem.style.top = referenceNode.offsetTop
  cursor.elem.style.left = referenceNode.offsetLeft
  cursor.elem.style.height = referenceNode.offsetHeight
  if parseInt(cursor.elem.style.top) <= parseInt(cursor.elem.style.height)
    Scribe.DOM.addClass(cursor.elem, 'top')
  else
    Scribe.DOM.removeClass(cursor.elem, 'top')

_updateCursor = (cursor) ->
  @editor.doSilently( =>
    position = new Scribe.Position(@editor, cursor.index)
    span = @container.ownerDocument.createElement('span')
    span.textContent = Scribe.DOM.NOBREAK_SPACE
    if !position.leafNode.firstChild?
      position.leafNode.parentNode.insertBefore(span, position.leafNode)
      _moveCursor.call(this, cursor, span)
    else
      [leftText, rightText, didSplit] = Scribe.DOM.splitNode(position.leafNode.firstChild, position.offset)
      if rightText?
        rightText.parentNode.insertBefore(span, rightText)
        _moveCursor.call(this, cursor, span)
      else if leftText?
        leftText.parentNode.appendChild(span)
        _moveCursor.call(this, cursor, span)
    span.parentNode.removeChild(span)
    Scribe.DOM.normalize(position.leafNode) if didSplit
  )
  cursor.dirty = false

class Scribe.MultiCursor
  @DEFAULTS:
    template: ''
    timeout: 2500

  constructor: (@editor, options = {}) ->
    @options = _.defaults(options, Scribe.MultiCursor.DEFAULTS)
    @cursors = {}
    @container = @editor.root.ownerDocument.createElement('div')
    @container.id = 'cursor-container'
    @editor.renderer.addContainer(@container, true)
    @editor.renderer.addStyles({
      '#cursor-container': { 'position': 'absolute', 'z-index': '1000' }
      '.cursor': { 'display': 'inline-block', 'height': '15px', 'position': 'absolute', 'width': '0px' }
      '.cursor-name': {
        'font-family': "'Helvetica', 'Arial', san-serif"
        'font-size': '13px'
        'color': 'white'
        'display': 'inline-block'
        'left': '-1px'
        'line-height': '1.154'
        'padding': '2px 8px'
        'position': 'absolute'
        'top': '-18px'
        'white-space': 'nowrap'
      }
      '.cursor.hidden .cursor-name': { 'display': 'none' }
      '.cursor-caret': { 'display': 'inline-block', 'width': '2px', 'position': 'absolute', 'height': '100%', 'left': '-1px' }
      '.cursor.top > .cursor-name': { 'top': '100%' }
    })
    @editor.renderer.runWhenLoaded( =>
      _.defer( =>
        @container.style.top = @editor.root.offsetTop
        @container.style.left = @editor.root.offsetLeft
      )
    , -20)
    this.initListeners()

  initListeners: ->
    @editor.on(Scribe.Editor.events.API_TEXT_CHANGE, (delta) =>
      _applyDelta.call(this, delta)
    ).on(Scribe.Editor.events.USER_TEXT_CHANGE, (delta) =>
      _applyDelta.call(this, delta)
    )

  shiftCursors: (index, length, authorId = null, update = true) ->
    _.each(@cursors, (cursor, id) =>
      return unless cursor and (cursor.index > index or cursor.userId == authorId)
      cursor.index += Math.max(length, index - cursor.index)
      cursor.dirty = true
    )
    this.update() if update

  setCursor: (userId, index, name, color, update = true) ->
    cursor = @cursors[userId]
    unless cursor?
      @cursors[userId] = cursor = {
        userId: userId
        index: index
        elem: _buildCursor.call(this, name, color)
      }
    cursor.index = index
    cursor.dirty = true
    Scribe.DOM.removeClass(cursor.elem, 'hidden')
    clearTimeout(cursor.timer)
    cursor.timer = setTimeout( =>
      Scribe.DOM.addClass(cursor.elem, 'hidden')
      cursor.timer = null
    , @options.timeout)
    _updateCursor.call(this, cursor) if update

  clearCursors: ->
    _.each(_.keys(@cursors), (id) =>
      this.removeCursor(id)
    )
    @cursors = {}

  removeCursor: (userId) ->
    cursor = @cursors[userId]
    cursor.elem.parentNode.removeChild(cursor.elem) of cursor?
    delete @cursors[userId]

  update: ->
    _.each(@cursors, (cursor, id) =>
      _updateCursor.call(this, cursor) if cursor?.dirty
    )

module.exports = Scribe


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

#content-container
  margin: auto
  width: 960px

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

.editor-container
  height: 600px

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


# -*- encoding: utf-8 -*-
require File.expand_path('../lib/scribe-rails/version', __FILE__)

Gem::Specification.new do |gem|
  gem.authors       = ["Jason Chen"]
  gem.email         = ["jhchen7@gmail.com"]
  gem.description   = "Scribe rich text editor for Rails 3"
  gem.summary       = "Scribe rich text editor for Rails 3"
  gem.homepage      = "https://github.com/stypi/scribe"

  gem.files         = Dir["{lib,vendor}/**/*"] + ["README.md"]
  gem.name          = "scribe-rails"
  gem.require_paths = ["lib"]
  gem.version       = Scribe::Rails::VERSION

  gem.add_dependency "railties", "~> 3.1"
  gem.add_dependency "underscore-rails", "~> 1.4.3"
end


# Not require.js
# Allows other source files to use require.js style but allow coverage tests to separate files

window.Scribe = {}

window.require = (module) ->
  switch module
    when './scribe'     then return window.Scribe
    when '../scribe'    then return window.Scribe
    when 'tandem-core'  then return window.Tandem
    else                     return null

window.module = {
  exports: {}
}


# From canonical document, run every possible legal operation
#
# Insert: O(n * (k+2))
#   - At every location:
#     - insert letter
#     - insert letter of every format
#     - insert newline
# Delete: O(n^2)
#   - Delete at every possible index, length combination
# Format: O(n^2 * k)
#   - Format at every possible index, length, attribute, combination
# Apply delta: O(n^2 * k)
#   - for each above, generate and apply delta
#
# n - document length
# k - number of formats

class ScribeTestSuite
  constructor: (@options) ->
    @options.initial = Scribe.Utils.cleanHtml(@options.initial, true)
    $('#test-container').html(options.initial)
    doc = new Scribe.Document($('#test-container').get(0))
    @delta = doc.toDelta()
    @docLength = @delta.endLength
    @editorTest = new Scribe.Test.EditorTest(@options)

class ScribeInsertTestSuite extends ScribeTestSuite
  constructor: (options) ->
    super

  run: ->
    _.each([0..(@docLength - 1)], (index) =>
      return unless index >= 0
      tests = {
        "insert nothing":
          value: ''
          attributes: {}
        "insert A":
          value: 'A'
          attributes: {}
        "insert bold A":
          value: 'A'
          attributes: { bold: true }
        "insert red A":
          value: 'A'
          attributes: { color: '#f00' }
        "insert newline":
          value: '\n'
          attributes: {}
        "insert multiline text":
          value: 'A\nB'
          attributes: {}
        "insert triple line text":
          value: 'A\nB\nC'
          attributes: {}
      }
      _.each(tests, (test, name) =>
        insertDelta = Tandem.Delta.makeInsertDelta(@docLength, index, test.value, test.attributes)
        expected = @delta.compose(insertDelta)
        @editorTest.run("#{name} at #{index}",
          expected: expected
          fn: (testEditor, expectedEditor) ->
            testEditor.insertAt(index, test.value, test.attributes)
        )
      )
    )

class ScribeDeleteTestSuite extends ScribeTestSuite
  constructor: (options) ->
    super

  run: ->
    _.each([0..(@docLength - 2)], (index) =>
      return unless index >= 0
      _.each([0..(@docLength-index-1)], (length) =>
        deleteDelta = Tandem.Delta.makeDeleteDelta(@docLength, index, length)
        expected = @delta.compose(deleteDelta)
        @editorTest.run("Delete #{length} characters at #{index}",
          expected: expected
          fn: (testEditor, expectedEditor) ->
            testEditor.deleteAt(index, length)
        )
      )
    )

class ScribeFormatTestSuite extends ScribeTestSuite
  constructor: (options) ->
    super

  run: ->
    _.each([0..(@docLength - 2)], (index) =>
      return unless index >= 0
      _.each([0..(@docLength-index-1)], (length) =>
        formats =
          color: ['red', null]
          bold: [true, false]
        _.each(formats, (values, format) =>
          _.each(values, (value) =>
            attributes = {}
            attributes[format] = value
            retainDelta = Tandem.Delta.makeRetainDelta(@docLength, index, length, attributes)
            expected = @delta.compose(retainDelta)
            @editorTest.run("Apply #{format} #{value} to #{length} characters at #{index}",
              expected: expected
              fn: (testEditor, expectedEditor) ->
                testEditor.formatAt(index, length, format, value)
            )
          )
        )
      )
    )

window.Scribe or= {}
window.Scribe.Test or= {}
window.Scribe.Test.InsertTestSuite = ScribeInsertTestSuite
window.Scribe.Test.DeleteTestSuite = ScribeDeleteTestSuite
window.Scribe.Test.FormatTestSuite = ScribeFormatTestSuite


describe('Range', ->
  describe('Position', ->
    positionTests = new Scribe.Test.EditorTest(
      initial: [
        '
1234
'
        '
<s>56</s>78
'
        '

'
      ]
      expected: [0, 1, 2]
    )

    positionTests.run('should correctly initialize position from index',
      checker: (editor) ->
        expectedResults = [
          { text: '12', offset: 0 }
          { text: '12', offset: 1 }
          { text: '34', offset: 0 }
          { text: '34', offset: 1 }
          { text: '34', offset: 2 }
          { text: '56', offset: 0 }
          { text: '56', offset: 1 }
          { text: '78', offset: 0 }
          { text: '78', offset: 1 }
          { text: '78', offset: 2 }
          { text: '', offset: 0 }
        ]
        _.each(expectedResults, (result, i) ->
          position = new Scribe.Position(editor, i)
          expect(position.leafNode.textContent).to.equal(result.text)
          expect(position.offset).to.equal(result.offset)
        )
    )
  )

  describe('getText', ->
    textTests = new Scribe.Test.EditorTest(
      initial:
        '

          abc
          def

            <s>hi</s>

            jk
            lm

            <s>no</s>

          <s>p</s>
          q
          <s>r</s>
          s

'
      expected: [0]
    )
    text = "abcdef\nhijklmno\npqrs"

    textTests.run('should identifiy single node',
      checker: (editor) ->
        _.each(text.split(''), (char, index) ->
          range = new Scribe.Range(editor, index, index + 1)
          expect(range.getText()).to.equal(char)
        )
    )

    textTests.run('should identifiy entire document',
      checker: (editor) ->
        range = new Scribe.Range(editor, 0, text.length)
        expect(range.getText()).to.equal(text)
    )
  )

  describe('getFormats', ->
    formatTests = new Scribe.Test.EditorTest(
      initial:
        '

          123
          456

            <s>78</s>

            90
            12

            <s>34</s>

          <s>5</s>
          6
          <s>7</s>
          8

'
      expected: [0]
      checker: (testEditor, expectedEditor, start, end, text, formats) ->
        range = new Scribe.Range(testEditor, start, end)
        expect(range.getText()).to.equal(text)
        expect(range.getFormats()).to.eql(_.extend({}, Scribe.Leaf.DEFAULT_FORMATS, formats))
    )

    formatTests.run('inside of node', {}, 1, 2, '2', { bold: true })
    formatTests.run('start of node', {}, 0, 1, '1', { bold: true })
    formatTests.run('end of node', {}, 2, 3, '3', { bold: true })
    formatTests.run('entire node', {}, 0, 3, '123', { bold: true })
    formatTests.run('cursor inside of node', {}, 1, 1, '', { bold: true })
    formatTests.run('cursor at start of node', {}, 0, 0, '', { bold: true })
    formatTests.run('cursor at end of node', {}, 3, 3, '', { italic: true })
    formatTests.run('node at end of document', {}, 19, 20, '8', { underline: true })
    formatTests.run('cursor at end of document', {}, 20, 20, '', { underline: true })
    formatTests.run('part of two nodes', {}, 8, 10, '89', { bold: true })
    formatTests.run('node with preceding newline', {}, 6, 9, '\n78', { bold: true, strike: true })
    formatTests.run('node with trailing newline', {}, 13, 16, '34\n', { bold: true, strike: true })
    formatTests.run('line with preceding and trailing newline', {}, 6, 16, "\n78901234\n", { bold: true })
  )

  describe('getLeafNodes', ->
    leafTests = new Scribe.Test.EditorTest(
      initial: [
        '
123456
'
        '
<s>7</s>8<s>9</s>0
'
      ]
      expected: [0, 1]
    )

    leafTests.run('should select a single node at boundaries',
      checker: (editor) ->
        range = new Scribe.Range(editor, 0, 3)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(1)
        expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )

    leafTests.run('should select multiple nodes at boundaries',
      checker: (editor) ->
        range = new Scribe.Range(editor, 0, 6)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
        expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select a single node inside boundaries',
      checker: (editor) ->
        for i in [0..2]
          range = new Scribe.Range(editor, i, i+1)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(1)
          expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )

    leafTests.run('should select multipe nodes inside boundaries',
      checker: (editor) ->
        for i in [0..2]
          range = new Scribe.Range(editor, i, i+4)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(2)
          expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
          expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select multiple nodes across lines within boundaries',
      checker: (editor) ->
        range = new Scribe.Range(editor, 0, 6)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.childNodes[0])
        expect(nodes[1]).to.equal(editor.root.firstChild.childNodes[1])
    )

    leafTests.run('should select multiple nodes across lines outside boundaries',
      checker: (editor) ->
        range = new Scribe.Range(editor, 5, 8)
        nodes = range.getLeafNodes()
        expect(nodes.length).to.equal(2)
        expect(nodes[0]).to.equal(editor.root.firstChild.lastChild)
        expect(nodes[1]).to.equal(editor.root.lastChild.firstChild)
    )

    leafTests.run('should select node collapsed',
      checker: (editor) ->
        for i in [0..2]
          range = new Scribe.Range(editor, i, i)
          nodes = range.getLeafNodes()
          expect(nodes.length).to.equal(1)
          expect(nodes[0]).to.equal(editor.root.firstChild.firstChild)
    )
  )
)


# Ensure user specified styles are added last

addContainer: (container, before = false) ->

refNode = if before then @root else null

keep keep keep replace replace keep keep keep keep keep


#= require underscore
#= require eventemitter2
#= require linked_list
#= require rangy-core
#= require tandem-core
#= require tandem-core.module
#= require_tree ./scribe

# scribe/scribe will want to require('tandem-core') which in node.js will know to
# look in the tandem-core github repository. Rails sprockets does not do this and
# in fact it should not do anything since Tandem is already included in the above
# require. Thus we preemptively require it pointing to tandem-core.module.coffee
# as a workaround
window.Tandem = require('tandem-core')
window.Scribe = require('scribe/scribe')


buildString = (reference, arr) ->
  return _.map(arr, (elem) =>
    return if _.isNumber(elem) then reference[elem] else elem
  ).join('')

class ScribeHtmlTest
  @DEFAULTS:
    async         : false
    checker       : ->
    fn            : ->
    initial       : []
    expected      : []
    ignoreExpect  : false
    pre           : -> []

  constructor: (options = {}) ->
    console.assert(_.isObject(options), "Invalid options passed into constructor")
    @settings = _.defaults(options, ScribeHtmlTest.DEFAULTS)

  run: (name, options, args...) ->
    it(name, (done) =>
      this.runWithoutIt(options, args..., done)
    )

  runWithoutIt: (options, args..., done) ->
    options = _.defaults(options, @settings)
    options.initial = [options.initial] if _.isString(options.initial)
    options.expected = buildString(options.initial, if options.expected? then options.expected else @settings.expected)
    options.initial = options.initial.join('') if _.isArray(options.initial)
    options.initial = Scribe.Utils.cleanHtml(options.initial, true)
    options.expected = Scribe.Utils.cleanHtml(options.expected, true)
    testContainer = $('#test-container').html(options.initial).get(0)
    expectedContainer = $('#expected-container').html(options.expected).get(0)
    newArgs = options.pre.call(this, testContainer, expectedContainer)
    newArgs = [newArgs] unless _.isArray(newArgs)
    next = =>
      testHtml = Scribe.Utils.cleanHtml(testContainer.innerHTML)
      expectedHtml = Scribe.Utils.cleanHtml(expectedContainer.innerHTML)
      expect(testHtml).to.equal(expectedHtml) unless options.ignoreExpect
      options.checker.call(this, testContainer, expectedContainer, newArgs..., args...)
      done()
    options.fn.call(this, testContainer, expectedContainer, newArgs..., args..., next)
    next() unless options.async

class ScribeEditorTest extends ScribeHtmlTest
  @DEFAULTS:
    async         : true
    ignoreExpect  : false

  constructor: (options = {}) ->
    @settings = _.defaults(options, ScribeEditorTest.DEFAULTS)
    super(@settings)

  run: (name, options, args...) ->
    it(name, (done) =>
      this.runWithoutIt(options, args..., done)
    )

  runWithoutIt: (options, args..., done) ->
    console.assert(_.isObject(options), "Invalid options passed into run")
    @options = _.defaults(options, @settings)
    testEditor = expectedEditor = null
    htmlOptions = _.clone(@options)
    htmlOptions.initial = '' if Tandem.Delta.isDelta(htmlOptions.initial)
    htmlOptions.expected = '' if Tandem.Delta.isDelta(htmlOptions.expected)
    htmlOptions.fn = (testContainer, expectedContainer, args..., callback) =>
      async.map([testContainer, expectedContainer], (container, callback) =>
        editor = new Scribe.Editor(container,
          onReady: _.once( ->
            _.defer( ->
              callback(null, editor)
            )
          )
        )
      , (err, editors) =>
        [testEditor, expectedEditor] = editors
        testEditor.setDelta(options.initial) if Tandem.Delta.isDelta(@options.initial)
        expectedEditor.setDelta(options.expected) if Tandem.Delta.isDelta(@options.expected)
        @options.fn.call(this, testEditor, expectedEditor, args...)
        callback()
      )
    htmlOptions.checker = (testContainer, expectedContainer, args...) =>
      @options.checker.call(this, testEditor, expectedEditor, args...)
      unless @options.ignoreExpect
        testDelta = testEditor.getDelta()
        expectedDelta = expectedEditor.getDelta()
        console.error testDelta, expectedDelta unless testDelta.isEqual(expectedDelta)
        expect(testEditor.getDelta()).to.deep.equal(expectedEditor.getDelta())
        consistent = Scribe.Debug.checkDocumentConsistency(testEditor.doc)
        expect(consistent).to.be.true
    super(htmlOptions, args..., done)

window.Scribe or= {}
window.Scribe.Test =
  EditorTest  : ScribeEditorTest
  HtmlTest    : ScribeHtmlTest


Scribe = require('./scribe')

Scribe.Debug =
  checkDocumentConsistency: (doc, output = false) ->
    isConsistent = ->
      nodesByLine = _.map(_.clone(doc.root.childNodes), (lineNode) ->
        nodes = lineNode.querySelectorAll('*')
        return _.filter(nodes, (node) ->
          return node.nodeType == node.ELEMENT_NODE && (node.nodeName == 'BR' || !node.firstChild? || node.firstChild.nodeType == node.TEXT_NODE)
        )
      )
      lines = doc.lines.toArray()

      # doc.lines and doc.lineMap should match
      if lines.length != _.values(doc.lineMap).length
        console.error "doc.lines and doc.lineMap differ in length", lines.length, _.values(doc.lineMap).length
        return false
      return false if _.any(lines, (line) =>
        if !line? || doc.lineMap[line.id] != line
          if line?
            console.error line, "does not match", doc.lineMap[line.id]
          else
            console.error "null line"
          return true
        return false
      )
      # Line nodes and line's nodes should match
      if _.any(doc.root.childNodes, (lineNode, index) ->
        if lines[index].node != lineNode
          console.error "Line and nodes do not match", lines[index].node, lineNode
          return true
        return false
      )
        return false
      # All lines should still be in the DOM
      if _.any(lines, (line) ->
        if line.node.parentNode == null
          console.error 'Missing line from DOM', line
          return true
        return false
      )
        return false

      # All line lengths should be correct
      if _.any(lines, (line) ->
        lineLength = _.reduce(line.leaves.toArray(), (count, leaf) ->
          return leaf.length + count
        , 0)
        lineLength += 1 if line.trailingNewline
        if lineLength != line.length
          console.error 'incorrect line length', lineLength, line.length
          return true
        return false
      )
        return false

      # All lists should have li tag
      if _.any(lines, (line) ->
        if (line.node.tagName == 'OL' || line.node.tagName == 'UL') && line.node.firstChild.tagName != 'LI'
          console.error 'inconsistent bullet/list', line
          return true
        return false
      )
        return false

      # All leaves should still be in the DOM
      orphanedLeaf = orphanedLine = null
      if _.any(lines, (line) ->
        return _.any(line.leaves.toArray(), (leaf) ->
          if leaf.node.parentNode == null
            orphanedLeaf = leaf
            orphanedLine = line
            return true
          return false
        )
      )
        console.error 'Missing from DOM', orphanedLine, orphanedLeaf
        return false

      # doc.lines should match nodesByLine
      if lines.length != nodesByLine.length
        console.error "doc.lines and nodesByLine differ in length", lines, nodesByLine
        return false
      return false if _.any(lines, (line, index) =>
        calculatedLength = _.reduce(line.node.childNodes, ((length, node) -> Scribe.Utils.getNodeLength(node) + length), 0)
        calculatedLength += 1 if line.trailingNewline
        if line.length != calculatedLength
          console.error line, line.length, calculatedLength, 'differ in length'
          return true
        leaves = line.leaves.toArray()
        leafNodes = _.map(leaves, (leaf) -> return leaf.node)
        if !_.isEqual(leafNodes, nodesByLine[index])
          console.error line, leafNodes, 'nodes differ from', nodesByLine[index]
          return true
        leaves = _.map(leaves, (leaf) ->
          return _.pick(leaf, 'formats', 'length', 'line', 'node', 'text')
        )
        line.rebuild()
        rebuiltLeaves = _.map(line.leaves.toArray(), (leaf) ->
          return _.pick(leaf, 'formats', 'length', 'line', 'node', 'text')
        )
        if !_.isEqual(leaves, rebuiltLeaves)
          console.error leaves, 'leaves differ from', rebuiltLeaves
          return true
        return false
      )
      return true

    if isConsistent()
      return true
    else
      if output
        console.error doc.root
        console.error nodesByLine
        console.error lines
        console.error doc.lineMap
      return false

module.exports = Scribe


# Firefox does not like us saving a reference to this result so retrieve every time

return @iframe.document or @iframe.contentWindow?.document

return fn.call(this) if this.getDocument()?.readyState == 'complete'

keep keep keep keep keep replace keep keep


.editor-wrapper
  border: 1px solid #ccc
  float: left
  margin-left: 3%
  margin-top: 15px
  width: 45.5%

.editor-container
  height: 400px

.formatting-container
  border-bottom: 1px solid #ccc
  height: 36px
  line-height: 32px
  padding: 0px 5px
  .format-group
    display: inline-block
    height: 24px
    line-height: 24px
    margin-right: 15px
    margin-top: 4px
    vertical-align: middle
  .format-button
    background-repeat: repeat-x
    display: inline-block
    height: 24px
  .format
    background-position: center center
    background-repeat: no-repeat
    border-right: 1px solid #ddd
    cursor: pointer
    display: inline-block
    height: 16px
    padding: 0px 6px
    vertical-align: middle
    width: 16px
  .format.last
    border-right: 0px
  .font-family
    margin-right: 10px
  .font-family, .font-size
    visibility: hidden

  @each $format in bold, italic, strike, underline, list, bullet, outdent, indent, link
    .#{$format}
      background-image: url(../images/#{$format}.png)
    .#{$format}:hover, .#{$format}:active, .#{$format}.active
      background-image: url(../images/#{$format}-color.png)


java -jar ~/.browserstack/BrowserStackTunnel.jar m2LGGyTRgKd453bAQhcb localhost,<port>,0

You can now access your local server(s) in our remote browser:

./node_modules/.bin/browserstack launch --attach OS_X:Mountain_Lion:chrome:27.0 <url>

./node_modules/.bin/browserstack launch --attach OS_X:Mountain_Lion:firefox:21.0 <url>

./node_modules/.bin/browserstack launch --attach OS_X:Mountain_Lion:safari:6.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:8:chrome:27.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:8:firefox:21.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:8:safari:5.1 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:ie:10.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:chrome:27.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:firefox:21.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:safari:5.1 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:ie:9.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:7:ie:8.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:XP:chrome:27.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:XP:firefox:21.0 <url>

./node_modules/.bin/browserstack launch --attach Windows:XP:safari:5.1 <url>

./node_modules/.bin/browserstack launch --attach Windows:XP:ie:8.0 <url>

Mac Chrome
Mac Firefox
Mac Safari
Windows 8 Chrome
Windows 8 Firefox
Windows 8 Safari
Windows 8 IE 10
Windows 7 Chrome
Windows 7 Firefox
Windows 7 Safari
Windows 7 IE 10
Windows 7 IE 9
Windows 7 IE 8
Windows XP Chrome
Windows XP Firefox
Windows XP Safari
Windows XP IE 8


describe('Undo Manager', ->
  describe('undo/redo', ->
    undoTests = new Scribe.Test.EditorTest(
      initial: Tandem.Delta.getInitial("The lazy fox\n")
      ignoreExpect: true
      fn: (editor, expectedEditor, delta) ->
        editor.applyDelta(delta, { source: 'user' })
      checker: (editor, expectedEditor, delta) ->
        beforeDelta = editor.getDelta()
        editor.undoManager.undo()
        expect(editor.getDelta().isEqual(@options.initial)).to.be.true
        editor.undoManager.redo()
        expect(editor.getDelta().isEqual(beforeDelta)).to.be.true
    )

    undoTests.run('insert', {}, Tandem.Delta.makeInsertDelta(13, 9, 'hairy '))

    undoTests.run('delete', {}, Tandem.Delta.makeDeleteDelta(13, 4, 5))

    undoTests.run('replace', {}, new Tandem.Delta(13, [
      new Tandem.RetainOp(0, 4)
      new Tandem.InsertOp('hairy ')
      new Tandem.RetainOp(9, 13)
    ]))

    undoTests.run('replace all', {}, new Tandem.Delta(13, [new Tandem.InsertOp('A fast bunny\n')]))

    boldDelta = Tandem.Delta.makeRetainDelta(13, 0, 12, { bold: true })
    undoTests.run('insert attribute', {}, boldDelta)

    unboldDelta = Tandem.Delta.makeRetainDelta(13, 0, 12, { bold: false })
    undoTests.run('remove attribute', {
      initial: Tandem.Delta.getInitial("The lazy fox\n").compose(boldDelta)
    }, unboldDelta)

    blueStrikeDelta = Tandem.Delta.makeRetainDelta(13, 0, 12, { color: 'blue', strike: true })
    undoTests.run('insert color/strike', {}, blueStrikeDelta)

    undoBlueStrikeDelta = Tandem.Delta.makeRetainDelta(13, 0, 12, { color: false, strike: false })
    undoTests.run('remove color/strike', {
      initial: Tandem.Delta.getInitial("The lazy fox\n").compose(blueStrikeDelta)
    }, undoBlueStrikeDelta)

    # Multi-user
    undoTests.run('remove external insert',
      fn: (editor, expectedEditor, delta) ->
        editor.applyDelta(delta, { source: 'api' })
    , Tandem.Delta.makeInsertDelta(13, 9, 'hairy '))

    undoTests.run('remove external delete',
      fn: (editor, expectedEditor, delta) ->
        editor.applyDelta(delta, { source: 'api' })
    , Tandem.Delta.makeDeleteDelta(13, 4, 5))

    undoTests.run('remove external replace',
      fn: (editor, expectedEditor, delta) ->
        editor.applyDelta(delta, { source: 'api' })
    , new Tandem.Delta(13, [
        new Tandem.RetainOp(0, 4)
        new Tandem.InsertOp('hairy ')
        new Tandem.RetainOp(9, 13)
      ])
    )

    undoTests.run('remove external replace all',
      fn: (editor, expectedEditor, delta) ->
        editor.applyDelta(delta, { source: 'api' })
      , new Tandem.Delta(13, [new Tandem.InsertOp('A fast bunny\n')])
    )
  )
)


Scribe = require('./scribe')

class Scribe.Renderer
  @DEFAULTS:
    id: 'editor'
    keepHTML: false
    styles:
      'html' : { 'height': '100%' }
      'body': {
        'cursor': 'text'
        'font-family': "'Helvetica', 'Arial', sans-serif"
        'font-size': '13px'
        'height': '100%'
        'line-height': '1.154'
        'margin': '0px'
        'padding': '0px'
      }
      'div.editor': {
        'bottom': '10px'
        'left': '15px'
        'outline': 'none'
        'position': 'absolute'
        'right': '15px'
        'tab-size': '4'
        'top': '10px'
        'white-space': 'pre-wrap'
      }
      'div.line:last-child': { 'padding-bottom': '10px' }
      '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' }
      '.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' }

  @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 = _.defaults(options, Scribe.Renderer.DEFAULTS)
    this.createFrame()
    @formats = {}
    this.addStyles(Scribe.Renderer.DEFAULTS.styles)
    # Ensure user specified styles are added last
    this.runWhenLoaded( =>
      this.addStyles(options.styles) if options.styles?
    , -10)

  addContainer: (container, before = false) ->
    this.runWhenLoaded( =>
      refNode = if before then @root else null
      @root.parentNode.insertBefore(container, refNode)
    )

  addStyles: (styles) ->
    this.runWhenLoaded( =>
      style = @root.ownerDocument.createElement('style')
      style.type = 'text/css'
      css = Scribe.Renderer.objToCss(styles)
      if style.styleSheet?
        style.styleSheet.cssText = css
      else
        style.appendChild(@root.ownerDocument.createTextNode(css))
      # Firefox needs defer
      _.defer( =>
        @root.ownerDocument.head.appendChild(style)
      )
    )

  createFrame: ->
    html = @container.innerHTML
    @container.innerHTML = ''
    @iframe = @container.ownerDocument.createElement('iframe')
    @iframe.frameborder = 0
    @iframe.height = @iframe.width = '100%'
    @container.appendChild(@iframe)
    @root = this.getDocument().createElement('div')
    Scribe.DOM.addClass(@root, 'editor')
    @root.id = @options.id
    @root.innerHTML = Scribe.Normalizer.normalizeHtml(html) if @options.keepHTML
    this.runWhenLoaded( =>
      this.getDocument().body.appendChild(@root)
    )

  getDocument: ->
    # Firefox does not like us saving a reference to this result so retrieve every time
    # IE does not have contentWindow
    return @iframe.document or @iframe.contentWindow?.document

  runWhenLoaded: (fn, priority = 0) ->
    return fn.call(this) if this.getDocument()?.readyState == 'complete'
    if @callbacks?
      @callbacks[priority] ?= []
      @callbacks[priority].push(fn)
    else
      @callbacks = {}
      @callbacks[priority] = [fn]
      interval = setInterval( =>
        doc = this.getDocument()
        if doc?.readyState == 'complete'
          clearInterval(interval)
          _.defer( =>
            keys = _.keys(@callbacks).sort((a,b) -> b - a)
            _.each(keys, (key) =>
              _.each(@callbacks[key], (callback) =>
                callback.call(this)
              )
            )
            @callbacks = {}
          )
        else if !doc
          clearInterval(interval)
      , 100)

module.exports = Scribe


!!! 5
%html
  %head
    %title Stypi Editor Demo
    %link{:rel => 'stylesheet', :type => 'text/css', :href => 'styles/reset.css'}
    %link{:rel => 'stylesheet', :type => 'text/css', :href => 'styles/style.css'}
    %meta{:charset => 'utf-8'}

  %body
    #content-container
      .editor-wrapper
        #formatting-container.formatting-container
          %select.family
            %option{:value => false, :selected => true}  Sans Serif
            %option{:value => 'serif'}      Serif
            %option{:value => 'monospace'}  Monospace
          %select.size
            %option{:value => 'small'}   Small
            %option{:value => false, :selected => true} Normal
            %option{:value => 'large'}   Large
            %option{:value => 'huge'}    Huge
          %select.color
            %option{:value => 'white'}   White
            %option{:value => false, :selected => true}  Black
            %option{:value => 'red'}     Red
            %option{:value => 'blue'}    Blue
            %option{:value => 'lime'}    Lime
            %option{:value => 'teal'}    Teal
            %option{:value => 'magenta'} Magenta
            %option{:value => 'yellow'}  Yellow
          %select.background
            %option{:value => false, :selected => true}   White
            %option{:value => 'black'}   Black
            %option{:value => 'red'}     Red
            %option{:value => 'blue'}    Blue
            %option{:value => 'lime'}    Lime
            %option{:value => 'teal'}    Teal
            %option{:value => 'magenta'} Magenta
            %option{:value => 'yellow'}  Yellow
          %button.format-button.bold Bold
          %button.format-button.italic Italic
          %button.format-button.underline Under
          %button.format-button.strike Strike
          %button.format-button.link Link
        #editor-container.editor-container
          = Haml::Engine.new(File.read('demo/content.haml')).render

    %script{:type => 'text/javascript', :src => '../scribe.all.js'}
    :javascript
      var editor = new Scribe.Editor('editor-container');
      var toolbar = new Scribe.Toolbar('formatting-container', editor);
      editor.on(Scribe.Editor.events.USER_TEXT_CHANGE, function(delta) {
        console.info('User', delta);
      });


= Haml::Engine.new(File.read('tests/mocha.haml')).render do
  - Haml::Engine.new("%script{:type => 'text/javascript', :src => 'scripts/unit.js'}").render


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

  clean: (node) ->
    Scribe.DOM.removeAttributes(node, ['href', 'title'])

  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


CURSOR_COLOR = '#0054a6'

initToolbar = (container, editor, toolbar) ->
  $formattingContainer = $('.formatting-container', $(container))
  dropkickFormats = ['family', 'size']
  for format in dropkickFormats
    do (format) ->
      console.log format
      $(".#{format}", $formattingContainer).dropkick({
        change: (value) -> editor.selection.format(format, value)
        width: 75
      })
  editor.on(Scribe.Editor.events.SELECTION_CHANGE, ->
    $(".family, .size", $formattingContainer).each((select) ->
      $(this).dropkick('set', $('option:selected', this).text())
    )
  )

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, CURSOR_COLOR)
  )

$(document).ready( ->
  editors = []
  for num in [1, 2]
    wrapperClass = '.editor-wrapper'
    wrapperClass += if num == 1 then '.first' else '.last'
    container = document.querySelector(wrapperClass)
    editor = new Scribe.Editor(container.querySelector('.editor-container'), {
      renderer:
        styles:
          'a': { 'color': '#06c' }
          '#link-tooltip':
            'border': '1px solid #ccc'
            'box-shadow': '0px 0px 5px #ddd'
            'color': '#222'
          '#link-tooltip a':
            'color': '#06c'
          '#link-tooltip .input':
            'border': '1px solid #ccc'
            'margin': '0px'
            'padding': '3px'
    })
    toolbar = new Scribe.Toolbar(container.querySelector('.formatting-container'), editor)
    editor.cursorManager = new Scribe.MultiCursor(editor)
    editor.attributionManager = new Scribe.Attribution(editor, editor.id, CURSOR_COLOR)
    editors.push(editor)
    initToolbar(container, editor, toolbar)

  $('.dk_container').click( ->
    that = this
    $('.dk_container.dk_open').each( ->
      $(this).removeClass('dk_open') if that != this
    )
  )
  listenEditor(editors[0], editors[1])
  listenEditor(editors[1], editors[0])
  editors[0].attributionManager.addAuthor(editors[1].id, CURSOR_COLOR)
  editors[1].attributionManager.addAuthor(editors[0].id, CURSOR_COLOR)
)


!!! 5
%html
  %head
    %title Stypi Editor Testem Tests
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../../lib/mocha.css'}
    :css
      #expected-container, #test-container {
        float: left;
        width: 45%;
      }
      #expected-container, #test-container, #format-container {
        margin-left: 10px;
      }
      iframe { border: 1px solid gray; }
    %meta{:charset => 'utf-8'}

  %body
    #mocha
    #format-container
      %button.bold Bold
      %button.link Link
      %select.size
        %option{:value => 'small'}   Small
        %option{:value => false, :selected => true} Normal
        %option{:value => 'large'}   Large
        %option{:value => 'huge'}    Huge
    #test-container
    #expected-container

    // Need to separate files for code coverage
    %script{:type => 'text/javascript', :src => 'http://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js'}

    - libs = ['async', 'underscore', 'eventemitter2', 'linked_list', 'tandem-core']
    - libs.each do |lib|
      %script{:type => 'text/javascript', :src => "../../lib/#{lib}.js"}

    %script{:type => 'text/javascript', :src => "../../scribe.js"}

    %script{:type => 'text/javascript', :src => '../../lib/mocha.js'}
    %script{:type => 'text/javascript', :src => '../../lib/chai.js'}
    %script{:type => 'text/javascript', :src => '/testem.js'}
    :javascript
      mocha.setup('bdd');
      expect = chai.expect;
    %script{:type => 'text/javascript', :src => '../scripts/unit.js'}
    :javascript
      mocha.run();


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


keys = _.keys(@callbacks).sort((a,b) -> b - a)

keep keep keep replace replace replace replace replace keep


var fs = require('fs');
var path = require('path');
var jade = require('jade');
var reporter = require.resolve('mocha/lib/reporters/html-cov');
var file = path.join(path.dirname(reporter), 'templates/coverage.jade');
var fn = jade.compile(fs.readFileSync(file, 'utf-8'), {filename: file});

var data = '';

process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (chunk) {
  data += chunk;
});

process.stdin.on('end', function () {
  // remove unuse log
  data = data.replace(/^[\s\S]*?({)/, '$1');
  process.stdout.write(fn({
    cov: JSON.parse(data),
    coverageClass: coverageClass
  }));
});

function coverageClass(n) {
  if (n >= 75) return 'high';
  if (n >= 50) return 'medium';
  if (n >= 25) return 'low';
  return 'terrible';
}


Scribe = require('./scribe')

class Scribe.Normalizer
  @BLOCK_TAGS: [
    'ADDRESS'
    'BLOCKQUOTE'
    'DD'
    'DIV'
    'DL'
    'H1', 'H2', 'H3', 'H4', 'H5', 'H6'
    'LI'
    'OL'
    'P'
    'PRE'
    'TABLE'
    'TBODY'
    'TD'
    'TFOOT'
    'TH'
    'THEAD'
    'TR'
    'UL'
  ]

  # Missing rule implies removal
  @TAG_RULES: {
    'A'         : {}
    'ADDRESSS'  : {rename: 'div'}
    'B'         : {}
    'BLOCKQUOTE': {rename: 'div'}
    'BR'        : {}
    'BIG'       : {rename: 'span'}
    'CENTER'    : {rename: 'span'}
    'DD'        : {rename: 'div'}
    'DEL'       : {rename: 's'}
    'DIV'       : {}
    'DL'        : {rename: 'div'}
    'EM'        : {rename: 'i'}
    'H1'        : {rename: 'div'}
    'H2'        : {rename: 'div'}
    'H3'        : {rename: 'div'}
    'H4'        : {rename: 'div'}
    'H5'        : {rename: 'div'}
    'H6'        : {rename: 'div'}
    'HR'        : {rename: 'br'}
    'I'         : {}
    'INS'       : {rename: 'span'}
    'LI'        : {rename: 'div'}
    'OL'        : {rename: 'div'}
    'P'         : {rename: 'div'}
    'PRE'       : {rename: 'div'}
    'S'         : {}
    'SMALL'     : {rename: 'span'}
    'SPAN'      : {}
    'STRIKE'    : {rename: 's'}
    'STRONG'    : {rename: 'b'}
    'TABLE'     : {rename: 'div'}
    'TBODY'     : {rename: 'div'}
    'TD'        : {rename: 'span'}
    'TFOOT'     : {rename: 'div'}
    'TH'        : {rename: 'span'}
    'THEAD'     : {rename: 'div'}
    'TR'        : {rename: 'div'}
    'U'         : {}
    'UL'        : {rename: 'div'}
  }

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

  @breakBlocks: (root) ->
    Scribe.Normalizer.groupBlocks(root)
    _.each(root.querySelectorAll('br'), (node) =>
      Scribe.Normalizer.normalizeBreak(node, root)
    )
    _.each(root.childNodes, (childNode) =>
      Scribe.Normalizer.breakLine(childNode)
    )

  @breakLine: (lineNode) ->
    return if lineNode.childNodes.length == 1 and lineNode.firstChild.tagName == 'BR'
    Scribe.DOM.traversePostorder(lineNode, (node) =>
      if Scribe.Utils.isBlock(node)
        node = Scribe.DOM.switchTag(node, 'div') if node.tagName != 'DIV'
        if node.nextSibling?
          line = lineNode.ownerDocument.createElement('div')
          lineNode.parentNode.insertBefore(line, lineNode.nextSibling)
          while node.nextSibling?
            line.appendChild(node.nextSibling)
          Scribe.Normalizer.breakLine(line)
        return Scribe.DOM.unwrap(node)
      else
        return node
    )

  @groupBlocks: (root) ->
    curLine = root.firstChild
    while curLine?
      if Scribe.Utils.isBlock(curLine)
        curLine = curLine.nextSibling
      else
        line = root.ownerDocument.createElement('div')
        root.insertBefore(line, curLine)
        while curLine? and !Scribe.Utils.isBlock(curLine)
          nextLine = curLine.nextSibling
          line.appendChild(curLine)
          curLine = nextLine
        curLine = line

  @normalizeBreak: (node, root) ->
    return if node == root
    if node.previousSibling?
      if node.nextSibling?
        Scribe.DOM.splitAfter(node, root)
      node.parentNode.removeChild(node)
    else if node.nextSibling?
      if Scribe.DOM.splitAfter(node, root)
        Scribe.Normalizer.normalizeBreak(node, root)
    else if node.parentNode != root and node.parentNode.parentNode != root
      # Make sure

 is not unintentionally unwrapped
      Scribe.DOM.unwrap(node.parentNode)
      Scribe.Normalizer.normalizeBreak(node, root)

  @normalizeHtml: (html) ->
    # Remove leading and tailing whitespace
    html = html.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
    # Remove whitespace between tags
    html = html.replace(/\>\s+\</g, '><')
    # Standardize br
    html = html.replace(/
<\/br>/, '
')
    return html

  @removeNoBreak: (root) ->
    Scribe.DOM.traversePreorder(root, 0, (node) =>
      if node.nodeType == node.TEXT_NODE
        node.textContent = node.textContent.split(Scribe.DOM.NOBREAK_SPACE).join('')
      return node
    )

  @requireLeaf: (lineNode) ->
    unless lineNode.childNodes.length > 1
      if lineNode.tagName == 'OL' || lineNode.tagName == 'UL'
        lineNode.appendChild(lineNode.ownerDocument.createElement('li'))
        lineNode = lineNode.firstChild
      lineNode.appendChild(lineNode.ownerDocument.createElement('br'))

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

  constructor: (@container, @formatManager) ->

  mergeAdjacent: (lineNode) ->
    Scribe.DOM.traversePreorder(lineNode, 0, (node) =>
      if node.nodeType == node.ELEMENT_NODE and !Scribe.Line.isLineNode(node)
        next = node.nextSibling
        if next?.tagName == node.tagName and node.tagName != 'LI'
          [nodeFormat, nodeValue] = @formatManager.getFormat(node)
          [nextFormat, nextValue] = @formatManager.getFormat(next)
          if nodeFormat == nextFormat && nodeValue == nextValue
            node = Scribe.DOM.mergeNodes(node, next)
      return node
    )

  normalizeDoc: ->
    @container.appendChild(@container.ownerDocument.createElement('div')) unless @container.firstChild
    Scribe.Normalizer.applyRules(@container)
    Scribe.Normalizer.breakBlocks(@container)
    _.each(@container.childNodes, (lineNode) =>
      this.normalizeLine(lineNode)
      this.optimizeLine(lineNode)
    )

  normalizeLine: (lineNode) ->
    return if lineNode.childNodes.length == 1 and lineNode.childNodes[0].tagName == 'BR'
    Scribe.Normalizer.removeNoBreak(lineNode)
    this.normalizeTags(lineNode)
    Scribe.Normalizer.requireLeaf(lineNode)
    Scribe.Normalizer.wrapText(lineNode)

  normalizeTags: (lineNode) ->
    Scribe.DOM.traversePreorder(lineNode, 0, (node) =>
      [nodeFormat, nodeValue] = @formatManager.getFormat(node)
      if @formatManager.formats[nodeFormat]?
        @formatManager.formats[nodeFormat].clean(node)
      else
        Scribe.DOM.removeAttributes(node)
      return node
    )

  optimizeLine: (lineNode) ->
    return if lineNode.childNodes.length == 1 and lineNode.childNodes[0].tagName == 'BR'
    this.mergeAdjacent(lineNode)
    this.removeRedundant(lineNode)
    Scribe.Normalizer.wrapText(lineNode)

  removeRedundant: (lineNode) ->
    key = _.uniqueId('_Formats')
    lineNode[key] = {}
    isRedudant = (node) =>
      if node.nodeType == node.ELEMENT_NODE
        if Scribe.Utils.getNodeLength(node) == 0
          return node.tagName != 'BR' or node.parentNode.childNodes.length > 1
        [formatName, formatValue] = @formatManager.getFormat(node)
        if formatName?
          return node.parentNode[key][formatName]?     # Parent format value will overwrite child's so no need to check formatValue
        else if node.tagName == 'SPAN'
          # Check if childNodes need us
          if node.childNodes.length == 0 or !_.any(node.childNodes, (child) -> child.nodeType != child.ELEMENT_NODE)
            return true
          # Check if parent needs us
          if node.previousSibling == null && node.nextSibling == null and node.parentNode != lineNode and node.parentNode.tagName != 'LI'
            return true
      return false
    Scribe.DOM.traversePreorder(lineNode, 0, (node) =>
      if isRedudant(node)
        node = Scribe.DOM.unwrap(node)
      if node?
        node[key] = _.clone(node.parentNode[key])
        [formatName, formatValue] = @formatManager.getFormat(node)
        node[key][formatName] = formatValue if formatName?
      return node
    )
    delete lineNode[key]
    Scribe.DOM.traversePreorder(lineNode, 0, (node) ->
      delete node[key]
      return node
    )

module.exports = Scribe


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

class Scribe.Document
  @INDENT_PREFIX: 'indent-'

  constructor: (@root) ->
    @formatManager = new Scribe.FormatManager(@root)
    @normalizer = new Scribe.Normalizer(@root, @formatManager)
    @root.innerHTML = Scribe.Normalizer.normalizeHtml(@root.innerHTML)
    @lines = new LinkedList()
    @lineMap = {}
    @normalizer.normalizeDoc()
    _.each(@root.childNodes, (node) =>
      this.appendLine(node)
    )

  appendLine: (lineNode) ->
    return this.insertLineBefore(lineNode, null)

  findLeaf: (node) ->
    lineNode = node.parentNode
    while lineNode? && !Scribe.Line.isLineNode(lineNode)
      lineNode = lineNode.parentNode
    return null if !lineNode?
    line = this.findLine(lineNode)
    return line.findLeaf(node)

  findLine: (node) ->
    node = this.findLineNode(node)
    if node?
      return @lineMap[node.id]
    else
      return null

  findLineAtOffset: (offset) ->
    retLine = @lines.first
    _.all(@lines.toArray(), (line, index) =>
      retLine = line
      if offset < line.length
        return false
      else
        offset -= line.length if index < @lines.length - 1
        return true
    )
    return [retLine, offset]

  findLineNode: (node) ->
    while node? && !Scribe.Line.isLineNode(node)
      node = node.parentNode
    return node

  insertLineBefore: (newLineNode, refLine) ->
    line = new Scribe.Line(this, newLineNode)
    if refLine != null
      @lines.insertAfter(refLine.prev, line)
    else
      @lines.append(line)
    @lineMap[line.id] = line
    return line

  mergeLines: (line, lineToMerge) ->
    return unless line? and lineToMerge?
    _.each(_.clone(lineToMerge.node.childNodes), (child) ->
      line.node.appendChild(child)
    )
    Scribe.Utils.removeNode(lineToMerge.node)
    this.removeLine(lineToMerge)
    line.trailingNewline = lineToMerge.trailingNewline
    line.rebuild()

  removeLine: (line) ->
    delete @lineMap[line.id]
    @lines.remove(line)

  splitLine: (line, offset) ->
    [lineNode1, lineNode2] = Scribe.DOM.splitNode(line.node, offset, true)
    line.node = lineNode1
    this.updateLine(line)
    return this.insertLineBefore(lineNode2, line.next)

  toDelta: ->
    lines = @lines.toArray()
    ops = _.flatten(_.map(lines, (line, index) ->
      ops = Tandem.Delta.copy(line.delta).ops
      return ops
    ), true)
    delta = new Tandem.Delta(0, ops)
    return delta

  updateLine: (line) ->
    return line.rebuild()

module.exports = Scribe


)
@callbacks = {}
)
else if !doc
clearInterval(interval)


/**
 * DropKick
 *
 * Highly customizable <select> lists
 * https://github.com/JamieLottering/DropKick
 *
 * © 2011 Jamie Lottering <http://github.com/JamieLottering>
 *                        <http://twitter.com/JamieLottering>
 *
 */
(function ($, window, document) {

  var ie6 = false;

  document.documentElement.className = document.documentElement.className + ' dk_fouc';

  var
    // Public methods exposed to $.fn.dropkick()
    methods = {},

    // Cache every <select> element that gets dropkicked
    lists   = [],

    // Convenience keys for keyboard navigation
    keyMap = {
      'left'  : 37,
      'up'    : 38,
      'right' : 39,
      'down'  : 40,
      'enter' : 13
    },

    // HTML template for the dropdowns
    dropdownTemplate = [
      '
',
        '',
          '{{ label }}',
        '',
        '
',
          '
',
          '
',
        '
',
      '
'
    ].join(''),

    // HTML template for dropdown options
    optionTemplate = '
{{ text }}
',

    // Some nice default values
    defaults = {
      startSpeed : 1000,  // I recommend a high value here, I feel it makes the changes less noticeable to the user
      theme  : false,
      change : false
    },

    // Make sure we only bind keydown on the document once
    keysBound = false
  ;

  // Called by using $('foo').dropkick();
  methods.init = function (settings) {
    settings = $.extend({}, defaults, settings);

    return this.each(function () {
      var
        // The current <select> element
        $select = $(this),

        // Store a reference to the originally selected <option> element
        $original = $select.find(':selected').first(),

        // Save all of the <option> elements
        $options = $select.find('option'),

        // We store lots of great stuff using jQuery data
        data = $select.data('dropkick') || {},

        // This gets applied to the 'dk_container' element
        id = $select.attr('id') || $select.attr('name'),

        // This gets updated to be equal to the longest <option> element
        width  = settings.width || $select.outerWidth(),

        // Check if we have a tabindex set or not
        tabindex  = $select.attr('tabindex') ? $select.attr('tabindex') : '',

        // The completed dk_container element
        $dk = false,

        theme
      ;

      // Dont do anything if we've already setup dropkick on this element
      if (data.id) {
        return $select;
      } else {
        data.settings  = settings;
        data.tabindex  = tabindex;
        data.id        = id;
        data.$original = $original;
        data.$select   = $select;
        data.value     = _notBlank($select.val()) || _notBlank($original.attr('value'));
        data.label     = $original.text();
        data.options   = $options;
      }

      // Build the dropdown HTML
      $dk = _build(dropdownTemplate, data);

      // Make the dropdown fixed width if desired
      $dk.find('.dk_toggle').css({
        'width' : width + 'px'
      });

      // Hide the <select> list and place our new one in front of it
      $select.before($dk);

      // Update the reference to $dk
      $dk = $('#dk_container_' + id).fadeIn(settings.startSpeed);

      // Save the current theme
      theme = settings.theme ? settings.theme : 'default';
      $dk.addClass('dk_theme_' + theme);
      data.theme = theme;

      // Save the updated $dk reference into our data object
      data.$dk = $dk;

      // Save the dropkick data onto the <select> element
      $select.data('dropkick', data);

      // Do the same for the dropdown, but add a few helpers
      $dk.data('dropkick', data);

      lists[lists.length] = $select;

      // Focus events
      $dk.bind('focus.dropkick', function (e) {
        $dk.addClass('dk_focus');
      }).bind('blur.dropkick', function (e) {
        $dk.removeClass('dk_open dk_focus');
      });

      $select.bind('change', function (e) {
        text = $('option:selected', this).text();
        methods.set.call(this, text);
      });

      setTimeout(function () {
        $select.hide();
      }, 0);
    });
  };

  methods.set = function (text) {
    var
      $select   = $(this),
      list      = $select.data('dropkick'),
      $dk       = list.$dk;
    $('.dk_label', $dk).text(text);
    $(".dk_options_inner li", $dk).each(function(){
      $(this).removeAttr('class');
      if ($(this).text() == text){
        $(this).attr('class', 'dk_option_current');
      }
    });
  }

  // Allows dynamic theme changes
  methods.theme = function (newTheme) {
    var
      $select   = $(this),
      list      = $select.data('dropkick'),
      $dk       = list.$dk,
      oldtheme  = 'dk_theme_' + list.theme
    ;

    $dk.removeClass(oldtheme).addClass('dk_theme_' + newTheme);

    list.theme = newTheme;
  };

  // Reset all <selects and dropdowns in our lists array
  methods.reset = function () {
    for (var i = 0, l = lists.length; i < l; i++) {
      var
        listData  = lists[i].data('dropkick'),
        $dk       = listData.$dk,
        $current  = $dk.find('li').first()
      ;

      $dk.find('.dk_label').text(listData.label);
      $dk.find('.dk_options_inner').animate({ scrollTop: 0 }, 0);

      _setCurrent($current, $dk);
      _updateFields($current, $dk, true);
    }
  };

  // Expose the plugin
  $.fn.dropkick = function (method) {
    if (!ie6) {
      if (methods[method]) {
        return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
      } else if (typeof method === 'object' || ! method) {
        return methods.init.apply(this, arguments);
      }
    }
  };

  // private
  function _handleKeyBoardNav(e, $dk) {
    var
      code     = e.keyCode,
      data     = $dk.data('dropkick'),
      options  = $dk.find('.dk_options'),
      open     = $dk.hasClass('dk_open'),
      current  = $dk.find('.dk_option_current'),
      first    = options.find('li').first(),
      last     = options.find('li').last(),
      next,
      prev
    ;

    switch (code) {
      case keyMap.enter:
        if (open) {
          _updateFields(current.find('a'), $dk);
          _closeDropdown($dk);
        } else {
          _openDropdown($dk);
        }
        e.preventDefault();
      break;

      case keyMap.up:
        prev = current.prev('li');
        if (open) {
          if (prev.length) {
            _setCurrent(prev, $dk);
          } else {
            _setCurrent(last, $dk);
          }
        } else {
          _openDropdown($dk);
        }
        e.preventDefault();
      break;

      case keyMap.down:
        if (open) {
          next = current.next('li').first();
          if (next.length) {
            _setCurrent(next, $dk);
          } else {
            _setCurrent(first, $dk);
          }
        } else {
          _openDropdown($dk);
        }
        e.preventDefault();
      break;

      default:
      break;
    }
  }

  // Update the <select> value, and the dropdown label
  function _updateFields(option, $dk, reset) {
    var value, label, data;

    value = option.attr('data-dk-dropdown-value');
    label = option.text();
    data  = $dk.data('dropkick');

    $select = data.$select;
    $select.val(value);

    $dk.find('.dk_label').text(label);

    reset = reset || false;

    if (data.settings.change && !reset) {
      data.settings.change.call($select, value, label);
    }
  }

  // Set the currently selected option
  function _setCurrent($current, $dk) {
    $dk.find('.dk_option_current').removeClass('dk_option_current');
    $current.addClass('dk_option_current');

    _setScrollPos($dk, $current);
  }

  function _setScrollPos($dk, anchor) {
    var height = anchor.prevAll('li').outerHeight() * anchor.prevAll('li').length;
    $dk.find('.dk_options_inner').animate({ scrollTop: height + 'px' }, 0);
  }

  // Close a dropdown
  function _closeDropdown($dk) {
    $dk.removeClass('dk_open');
  }

  // Open a dropdown
  function _openDropdown($dk) {
    var data = $dk.data('dropkick');
    $dk.find('.dk_options').css({ top : $dk.find('.dk_toggle').outerHeight() - 1 });
    $dk.toggleClass('dk_open');

  }

  /**
   * Turn the dropdownTemplate into a jQuery object and fill in the variables.
   */
  function _build (tpl, view) {
    var
      // Template for the dropdown
      template  = tpl,
      // Holder of the dropdowns options
      options   = [],
      $dk
    ;

    template = template.replace('{{ id }}', view.id);
    template = template.replace('{{ label }}', view.label);
    template = template.replace('{{ tabindex }}', view.tabindex);

    if (view.options && view.options.length) {
      for (var i = 0, l = view.options.length; i < l; i++) {
        var
          $option   = $(view.options[i]),
          current   = 'dk_option_current',
          oTemplate = optionTemplate
        ;

        oTemplate = oTemplate.replace('{{ value }}', $option.val());
        oTemplate = oTemplate.replace('{{ current }}', (_notBlank($option.val()) === view.value) ? current : '');
        oTemplate = oTemplate.replace('{{ text }}', $option.text());

        options[options.length] = oTemplate;
      }
    }

    $dk = $(template);
    $dk.find('.dk_options_inner').html(options.join(''));

    return $dk;
  }

  function _notBlank(text) {
    return ($.trim(text).length > 0) ? text : false;
  }

  $(function () {

    // Handle click events on the dropdown toggler
    $(document).on('click', '.dk_toggle', function (e) {
      var $dk  = $(this).parents('.dk_container').first();

      _openDropdown($dk);

      if ("ontouchstart" in window) {
        $dk.addClass('dk_touch');
        $dk.find('.dk_options_inner').addClass('scrollable vertical');
      }

      e.preventDefault();
      return false;
    });

    // Handle click events on individual dropdown options
    $(document).on('click', '.dk_options a', function (e) {
      var
        $option = $(this),
        $dk     = $option.parents('.dk_container').first(),
        data    = $dk.data('dropkick')
      ;

      _closeDropdown($dk);
      _updateFields($option, $dk);
      _setCurrent($option.parent(), $dk);

      e.preventDefault();
      return false;
    });

    // Setup keyboard nav
    $(document).bind('keydown.dk_nav', function (e) {
      var
        // Look for an open dropdown...
        $open    = $('.dk_container.dk_open'),

        // Look for a focused dropdown
        $focused = $('.dk_container.dk_focus'),

        // Will be either $open, $focused, or null
        $dk = null
      ;

      // If we have an open dropdown, key events should get sent to that one
      if ($open.length) {
        $dk = $open;
      } else if ($focused.length && !$open.length) {
        // But if we have no open dropdowns, use the focused dropdown instead
        $dk = $focused;
      }

      if ($dk) {
        _handleKeyBoardNav(e, $dk);
      }
    });
  });
})(jQuery, window, document);