mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/WikiEditor
synced 2024-12-24 13:54:22 +00:00
440 lines
12 KiB
JavaScript
440 lines
12 KiB
JavaScript
// 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 <span lang="en"> 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;
|
|
}
|
|
|
|
|