// THIS FILE HAS BEEN MODIFIED for use with the mediawiki wikiEditor // It no longer requires etherpad.collab.ace.easysync2.Changeset // THIS FILE WAS ORIGINALLY AN APPJET MODULE: etherpad.collab.ace.contentcollector /** * 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. */ var _MAX_LIST_LEVEL = 8; function sanitizeUnicode(s) { return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?'); } function makeContentCollector( browser, domInterface ) { browser = browser || {}; var dom = domInterface || { isNodeText : function(n) { return (n.nodeType == 3); }, nodeTagName : function(n) { return n.tagName; }, nodeValue : function(n) { try { return n.nodeValue; } catch ( err ) { return ''; } }, nodeName : function(n) { return n.nodeName; }, 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 self = { length : function() { return textArray.length; }, atColumnZero : function() { return textArray[textArray.length - 1] === ""; }, startNew : function() { textArray.push(""); self.flush(true); }, textOfLine : function(i) { return textArray[i]; }, appendText : function(txt, attrString) { textArray[textArray.length - 1] += txt; // dmesg(txt+" / "+attrString); }, textLines : function() { return textArray.slice(); }, // call flush only when you're done flush : function(withNewline) { } }; 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) == " " && !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 _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; return oldListType; } function _exitList(state, oldListType) { state.listLevel--; if (state.listType != 'none') { state.listNesting--; } state.listType = oldListType; } function _produceListMarker(state) { } 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 */} }; } 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 (!browser.firefox || 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)); x += consumed; txt = rest; if (txt.length > 0) { _startNewLine(state); } } } else { var cls = dom.nodeProp(node, "className"); 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 isPre = (tname == "pre"); if ((!isPre) && browser.safari) { isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); } if (isPre) _incrementFlag(state, 'preMode'); var oldListTypeOrNull = null; var nc = dom.nodeNumChildren(node); for ( var i = 0; i < nc; i++) { var c = dom.nodeChild(node, i); //very specific IE case where it inserts which we want to ginore. //to reproduce copy content from wordpad andpaste into the middle of a line in IE if ( browser.msie && cls.indexOf('wikiEditor') >= 0 && dom.nodeName(c) == 'SPAN' && dom.nodeAttr(c, 'lang') == "" ) { continue; } cc.collectContent(c, state); } if (isPre) _decrementFlag(state, 'preMode'); if (oldListTypeOrNull) { _exitList(state, oldListTypeOrNull); } } } 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 lineStrings = cc.getLines(); if ( lineStrings.length > 0 && !lineStrings[lineStrings.length - 1] ) { lineStrings.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]; if (oldString.length > lineLimit + buffer) { var newStrings = []; 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); } if (oldString.length > 0) { newStrings.push(oldString); } 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); } } return { linesWrapped : linesWrapped, numLinesAfter : numLinesAfter }; } var wrapData = fixLongLines(); return { selStart : ss, selEnd : se, linesWrapped : wrapData.linesWrapped, numLinesAfter : wrapData.numLinesAfter, lines : lineStrings }; } return cc; }