text: /**
   * Copyright 2009 Google Inc.
   * 
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * 
   *      http://www.apache.org/licenses/LICENSE-2.0
   * 
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS-IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  // DO NOT EDIT THIS FILE, edit infrastructure/ace/www/contentcollector.js
  import("etherpad.collab.ace.easysync2.Changeset")
  
  var _MAX_LIST_LEVEL = 8;
  
  function sanitizeUnicode(s) {
    return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?');
  }
  
  function makeContentCollector(collectStyles, browser, apool, domInterface,
                                className2Author) {
    browser = browser || {};
  
    var dom = domInterface || {
      isNodeText: function(n) {
        return (n.nodeType == 3);
      },
      nodeTagName: function(n) {
        return n.tagName;
      },
      nodeValue: function(n) {
        return n.nodeValue;
      },
      nodeNumChildren: function(n) {
        return n.childNodes.length;
      },
      nodeChild: function(n, i) {
        return n.childNodes.item(i);
      },
      nodeProp: function(n, p) {
        return n[p];
      },
      nodeAttr: function(n, a) {
        return n.getAttribute(a);
      },
      optNodeInnerHTML: function(n) {
        return n.innerHTML;
      }
    };
  
    var _blockElems = { "div":1, "p":1, "pre":1, "li":1 };
    function isBlockElement(n) {
      return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()];
    }
    function textify(str) {
      return sanitizeUnicode(
        str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, '        '));
    }
    function getAssoc(node, name) {
      return dom.nodeProp(node, "_magicdom_"+name);
    }
  
    var lines = (function() {
      var textArray = [];
      var attribsArray = [];
      var attribsBuilder = null;
      var op = Changeset.newOp('+');
      var self = {
        length: function() { return textArray.length; },
        atColumnZero: function() {
          return textArray[textArray.length-1] === "";
        },
        startNew: function() {
          textArray.push("");
          self.flush(true);
          attribsBuilder = Changeset.smartOpAssembler();
        },
        textOfLine: function(i) { return textArray[i]; },
        appendText: function(txt, attrString) {
          textArray[textArray.length-1] += txt;
          //dmesg(txt+" / "+attrString);
          op.attribs = attrString;
          op.chars = txt.length;
          attribsBuilder.append(op);
        },
        textLines: function() { return textArray.slice(); },
        attribLines: function() { return attribsArray; },
        // call flush only when you're done
        flush: function(withNewline) {
          if (attribsBuilder) {
            attribsArray.push(attribsBuilder.toString());
            attribsBuilder = null;
          }
        }
      };
      self.startNew();
      return self;
    }());
    var cc = {};
    function _ensureColumnZero(state) {
      if (! lines.atColumnZero()) {
        _startNewLine(state);
      }
    }
    var selection, startPoint, endPoint;
    var selStart = [-1,-1], selEnd = [-1,-1];
    var blockElems = { "div":1, "p":1, "pre":1 };
    function _isEmpty(node, state) {
      // consider clean blank lines pasted in IE to be empty
      if (dom.nodeNumChildren(node) == 0) return true;
      if (dom.nodeNumChildren(node) == 1 &&
          getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == "&nbsp;"
          && ! getAssoc(node, "unpasted")) {
        if (state) {
          var child = dom.nodeChild(node, 0);
          _reachPoint(child, 0, state);
          _reachPoint(child, 1, state);
        }
        return true;
      }
      return false;
    }
    function _pointHere(charsAfter, state) {
      var ln = lines.length()-1;
      var chr = lines.textOfLine(ln).length;
      if (chr == 0 && state.listType && state.listType != 'none') {
        chr += 1; // listMarker
      }
      chr += charsAfter;
      return [ln, chr];
    }
    function _reachBlockPoint(nd, idx, state) {
      if (! dom.isNodeText(nd)) _reachPoint(nd, idx, state);
    }
    function _reachPoint(nd, idx, state) {
      if (startPoint && nd == startPoint.node && startPoint.index == idx) {
        selStart = _pointHere(0, state);
      }
      if (endPoint && nd == endPoint.node && endPoint.index == idx) {
        selEnd = _pointHere(0, state);
      }
    }
    function _incrementFlag(state, flagName) {
      state.flags[flagName] = (state.flags[flagName] || 0)+1;
    }
    function _decrementFlag(state, flagName) {
      state.flags[flagName]--;
    }
    function _incrementAttrib(state, attribName) {
      if (! state.attribs[attribName]) {
        state.attribs[attribName] = 1;
      }
      else {
        state.attribs[attribName]++;
      }
      _recalcAttribString(state);
    }
    function _decrementAttrib(state, attribName) {
      state.attribs[attribName]--;
      _recalcAttribString(state);
    }
    function _enterList(state, listType) {
      var oldListType = state.listType;
      state.listLevel = (state.listLevel || 0)+1;
      if (listType != 'none') {
        state.listNesting = (state.listNesting || 0)+1;
      }
      state.listType = listType;
      _recalcAttribString(state);
      return oldListType;
    }
    function _exitList(state, oldListType) {
      state.listLevel--;
      if (state.listType != 'none') {
        state.listNesting--;
      }
      state.listType = oldListType;
      _recalcAttribString(state);
    }
    function _enterAuthor(state, author) {
      var oldAuthor = state.author;
      state.authorLevel = (state.authorLevel || 0)+1;
      state.author = author;
      _recalcAttribString(state);
      return oldAuthor;
    }
    function _exitAuthor(state, oldAuthor) {
      state.authorLevel--;
      state.author = oldAuthor;
      _recalcAttribString(state);
    }
    function _recalcAttribString(state) {
      var lst = [];
      for(var a in state.attribs) {
        if (state.attribs[a]) {
          lst.push([a,'true']);
        }
      }
      if (state.authorLevel > 0) {
        var authorAttrib = ['author', state.author];
        if (apool.putAttrib(authorAttrib, true) >= 0) {
          // require that author already be in pool
          // (don't add authors from other documents, etc.)
          lst.push(authorAttrib);
        }
      }
      state.attribString = Changeset.makeAttribsString('+', lst, apool);
    }
    function _produceListMarker(state) {
      lines.appendText('*', Changeset.makeAttribsString(
        '+', [['list', state.listType],
              ['insertorder', 'first']],
        apool));
    }
    function _startNewLine(state) {
      if (state) {
        var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0;
        if (atBeginningOfLine && state.listType && state.listType != 'none') {
          _produceListMarker(state);
        }
      }
      lines.startNew();
    }
    cc.notifySelection = function (sel) {
      if (sel) {
        selection = sel;
        startPoint = selection.startPoint;
        endPoint = selection.endPoint;
      }
    };
    cc.collectContent = function (node, state) {
      if (! state) {
        state = {flags: {/*name -> nesting counter*/},
                 attribs: {/*name -> nesting counter*/},
                 attribString: ''};
      }
      var isBlock = isBlockElement(node);
      var isEmpty = _isEmpty(node, state);
      if (isBlock) _ensureColumnZero(state);
      var startLine = lines.length()-1;
      _reachBlockPoint(node, 0, state);
      if (dom.isNodeText(node)) {
        var txt = dom.nodeValue(node);
        var rest = '';
        var x = 0; // offset into original text
        if (txt.length == 0) {
          if (startPoint && node == startPoint.node) {
            selStart = _pointHere(0, state);
          }
          if (endPoint && node == endPoint.node) {
            selEnd = _pointHere(0, state);
          }
        }
        while (txt.length > 0) {
          var consumed = 0;
          if (state.flags.preMode) {
            var firstLine = txt.split('\n',1)[0];
            consumed = firstLine.length+1;
            rest = txt.substring(consumed);
            txt = firstLine;
          }
          else { /* will only run this loop body once */ }
          if (startPoint && node == startPoint.node &&
              startPoint.index-x <= txt.length) {
            selStart = _pointHere(startPoint.index-x, state);
          }
          if (endPoint && node == endPoint.node &&
              endPoint.index-x <= txt.length) {
            selEnd = _pointHere(endPoint.index-x, state);
          }
          var txt2 = txt;
          if ((! state.flags.preMode) && /^[\r\n]*$/.exec(txt)) {
            // prevents textnodes containing just "\n" from being significant
            // in safari when pasting text, now that we convert them to
            // spaces instead of removing them, because in other cases
            // removing "\n" from pasted HTML will collapse words together.
            txt2 = "";
          }
          var atBeginningOfLine = lines.textOfLine(lines.length()-1).length == 0;
          if (atBeginningOfLine) {
            // newlines in the source mustn't become spaces at beginning of line box
            txt2 = txt2.replace(/^\n*/, '');
          }
          if (atBeginningOfLine && state.listType && state.listType != 'none') {
            _produceListMarker(state);
          }
          lines.appendText(textify(txt2), state.attribString);
          x += consumed;
          txt = rest;
          if (txt.length > 0) {
            _startNewLine(state);
          }
        }
      }
      else {
        var tname = (dom.nodeTagName(node) || "").toLowerCase();
        if (tname == "br") {
          _startNewLine(state);
        }
        else if (tname == "script" || tname == "style") {
          // ignore
        }
        else if (! isEmpty) {
          var styl = dom.nodeAttr(node, "style");
          var cls = dom.nodeProp(node, "className");
  
          var isPre = (tname == "pre");
          if ((! isPre) && browser.safari) {
            isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
          }
          if (isPre) _incrementFlag(state, 'preMode');
          var attribs = null;
          var oldListTypeOrNull = null;
          var oldAuthorOrNull = null;
          if (collectStyles) {
            function doAttrib(na) {
              attribs = (attribs || []);
              attribs.push(na);
              _incrementAttrib(state, na);
            }
            if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) ||
                tname == "strong") {
              doAttrib("bold");
            }
            if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) ||
                tname == "em") {
              doAttrib("italic");
            }
            if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) ||
                tname == "ins") {
              doAttrib("underline");
            }
            if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) ||
                tname == "del") {
              doAttrib("strikethrough");
            }
            if (tname == "ul") {
              var type;
              var rr = cls &&  /(?:^| )list-(bullet[12345678])\b/.exec(cls);
              type = rr && rr[1] || "bullet"+
                String(Math.min(_MAX_LIST_LEVEL, (state.listNesting||0)+1));
              oldListTypeOrNull = (_enterList(state, type) || 'none');
            }
            else if ((tname == "div" || tname == "p") && cls &&
                cls.match(/(?:^| )ace-line\b/)) {
              oldListTypeOrNull = (_enterList(state, type) || 'none');
            }
            if (className2Author && cls) {
              var classes = cls.match(/\S+/g);
              if (classes && classes.length > 0) {
                for(var i=0;i<classes.length;i++) {
                  var c = classes[i];
                  var a = className2Author(c);
                  if (a) {
                    oldAuthorOrNull = (_enterAuthor(state, a) || 'none');
                    break;
                  }
                }
              }
            }
          }
  
          var nc = dom.nodeNumChildren(node);
          for(var i=0;i<nc;i++) {
            var c = dom.nodeChild(node, i);
            cc.collectContent(c, state);
          }
  
          if (isPre) _decrementFlag(state, 'preMode');
          if (attribs) {
            for(var i=0;i<attribs.length;i++) {
              _decrementAttrib(state, attribs[i]);
            }
          }
          if (oldListTypeOrNull) {
            _exitList(state, oldListTypeOrNull);
          }
          if (oldAuthorOrNull) {
            _exitAuthor(state, oldAuthorOrNull);
          }
        }
      }
      if (! browser.msie) {
        _reachBlockPoint(node, 1, state);
      }
      if (isBlock) {
        if (lines.length()-1 == startLine) {
          _startNewLine(state);
        }
        else {
          _ensureColumnZero(state);
        }
      }
  
      if (browser.msie) {
        // in IE, a point immediately after a DIV appears on the next line
        _reachBlockPoint(node, 1, state);
      }
    };
    // can pass a falsy value for end of doc
    cc.notifyNextNode = function (node) {
      // an "empty block" won't end a line; this addresses an issue in IE with
      // typing into a blank line at the end of the document.  typed text
      // goes into the body, and the empty line div still looks clean.
      // it is incorporated as dirty by the rule that a dirty region has
      // to end a line.
      if ((!node) || (isBlockElement(node) && !_isEmpty(node))) {
        _ensureColumnZero(null);
      }
    };
    // each returns [line, char] or [-1,-1]
    var getSelectionStart = function() { return selStart; };
    var getSelectionEnd = function() { return selEnd; };
  
    // returns array of strings for lines found, last entry will be "" if
    // last line is complete (i.e. if a following span should be on a new line).
    // can be called at any point
    cc.getLines = function() { return lines.textLines(); };
  
    //cc.applyHints = function(hints) {
    //if (hints.pastedLines) {
    //
    //}
    //}
  
    cc.finish = function() {
      lines.flush();
      var lineAttribs = lines.attribLines();
      var lineStrings = cc.getLines();
  
      lineStrings.length--;
      lineAttribs.length--;
  
      var ss = getSelectionStart();
      var se = getSelectionEnd();
  
      function fixLongLines() {
        // design mode does not deal with with really long lines!
        var lineLimit = 2000; // chars
        var buffer = 10; // chars allowed over before wrapping
        var linesWrapped = 0;
        var numLinesAfter = 0;
        for(var i=lineStrings.length-1; i>=0; i--) {
  	var oldString = lineStrings[i];
  	var oldAttribString = lineAttribs[i];
  	if (oldString.length > lineLimit+buffer) {
  	  var newStrings = [];
  	  var newAttribStrings = [];
  	  while (oldString.length > lineLimit) {
  	    //var semiloc = oldString.lastIndexOf(';', lineLimit-1);
  	    //var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit);
  	    lengthToTake = lineLimit;
  	    newStrings.push(oldString.substring(0, lengthToTake));
  	    oldString = oldString.substring(lengthToTake);
  	    newAttribStrings.push(Changeset.subattribution(oldAttribString,
  							   0, lengthToTake));
  	    oldAttribString = Changeset.subattribution(oldAttribString,
  						       lengthToTake);
  	  }
  	  if (oldString.length > 0) {
  	    newStrings.push(oldString);
  	    newAttribStrings.push(oldAttribString);
  	  }
  	  function fixLineNumber(lineChar) {
  	    if (lineChar[0] < 0) return;
  	    var n = lineChar[0];
  	    var c = lineChar[1];
  	    if (n > i) {
  	      n += (newStrings.length-1);
  	    }
  	    else if (n == i) {
  	      var a = 0;
  	      while (c > newStrings[a].length) {
  		c -= newStrings[a].length;
  		a++;
  	      }
  	      n += a;
  	    }
  	    lineChar[0] = n;
  	    lineChar[1] = c;
  	  }
  	  fixLineNumber(ss);
  	  fixLineNumber(se);
  	  linesWrapped++;
  	  numLinesAfter += newStrings.length;
  
  	  newStrings.unshift(i, 1);
  	  lineStrings.splice.apply(lineStrings, newStrings);
  	  newAttribStrings.unshift(i, 1);
  	  lineAttribs.splice.apply(lineAttribs, newAttribStrings);
  	}
        }
        return {linesWrapped:linesWrapped, numLinesAfter:numLinesAfter};
      }
      var wrapData = fixLongLines();
  
      return { selStart: ss, selEnd: se, linesWrapped: wrapData.linesWrapped,
               numLinesAfter: wrapData.numLinesAfter,
               lines: lineStrings, lineAttribs: lineAttribs };
    }
  
    return cc;
  }
  