diff options
author | Peter 'Pita' Martischka <petermartischka@googlemail.com> | 2011-11-25 16:56:08 -0800 |
---|---|---|
committer | Peter 'Pita' Martischka <petermartischka@googlemail.com> | 2011-11-25 16:56:08 -0800 |
commit | 7296913cb0f347f4ea036427eb4e0dcdbaec9814 (patch) | |
tree | 2ca6d79f237d9a3aa0abf346cd6ca7e51c685660 | |
parent | 54b912f352bec8c3c6dced5c5bc9161ba4899366 (diff) | |
parent | 943f8c4682296c8b5424f1c5cec40a3fbea24195 (diff) | |
download | etherpad-lite-7296913cb0f347f4ea036427eb4e0dcdbaec9814.zip |
fixed merge confilicts
-rw-r--r-- | node/db/API.js | 22 | ||||
-rw-r--r-- | node/handler/APIHandler.js | 1 | ||||
-rw-r--r-- | node/utils/ImportHtml.js | 92 | ||||
-rw-r--r-- | node/utils/contentcollector.js | 692 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | static/js/ace2_common.js | 12 |
6 files changed, 820 insertions, 2 deletions
diff --git a/node/db/API.js b/node/db/API.js index 2069ce68..c40c49ca 100644 --- a/node/db/API.js +++ b/node/db/API.js @@ -26,6 +26,8 @@ var authorManager = require("./AuthorManager"); var sessionManager = require("./SessionManager"); var async = require("async"); var exportHtml = require("../utils/ExportHtml"); +var importHtml = require("../utils/ImportHtml"); +var cleanText = require("./Pad").cleanText; /**********************/ /**GROUP FUNCTIONS*****/ @@ -254,6 +256,26 @@ exports.getHTML = function(padID, rev, callback) }); } +exports.setHTML = function(padID, html, callback) +{ + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(err) + { + callback(err); + return; + } + + // add a new changeset with the new html to the pad + importHtml.setPadHTML(pad, cleanText(html)); + + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + + }); +} + /*****************/ /**PAD FUNCTIONS */ /*****************/ diff --git a/node/handler/APIHandler.js b/node/handler/APIHandler.js index 04464b08..2159cc40 100644 --- a/node/handler/APIHandler.js +++ b/node/handler/APIHandler.js @@ -51,6 +51,7 @@ var functions = { "getText" : ["padID", "rev"], "setText" : ["padID", "text"], "getHTML" : ["padID", "rev"], + "setHTML" : ["padID", "html"], "getRevisionsCount" : ["padID"], "deletePad" : ["padID"], "getReadOnlyID" : ["padID"], diff --git a/node/utils/ImportHtml.js b/node/utils/ImportHtml.js new file mode 100644 index 00000000..6441708e --- /dev/null +++ b/node/utils/ImportHtml.js @@ -0,0 +1,92 @@ +/** + * Copyright Yaco Sistemas S.L. 2011. + * + * 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 jsdom = require('jsdom').jsdom; +var log4js = require('log4js'); + +var Changeset = require("./Changeset"); +var contentcollector = require("./contentcollector"); +var map = require("../../static/js/ace2_common.js").map; + +function setPadHTML(pad, html, callback) +{ + var apiLogger = log4js.getLogger("ImportHtml"); + + // Clean the pad. This makes the rest of the code easier + // by several orders of magnitude. + pad.setText(""); + var padText = pad.text(); + + // Parse the incoming HTML with jsdom + var doc = jsdom(html.replace(/>\n+</g, '><')); + apiLogger.debug('html:'); + apiLogger.debug(html); + + // Convert a dom tree into a list of lines and attribute liens + // using the content collector object + var cc = contentcollector.makeContentCollector(true, null, pad.pool); + cc.collectContent(doc.childNodes[0]); + var result = cc.finish(); + apiLogger.debug('Lines:'); + var i; + for (i = 0; i < result.lines.length; i += 1) + { + apiLogger.debug('Line ' + (i + 1) + ' text: ' + result.lines[i]); + apiLogger.debug('Line ' + (i + 1) + ' attributes: ' + result.lineAttribs[i]); + } + + // Get the new plain text and its attributes + var newText = map(result.lines, function (e) { + return e + '\n'; + }).join(''); + apiLogger.debug('newText:'); + apiLogger.debug(newText); + var newAttribs = result.lineAttribs.join('|1+1') + '|1+1'; + + function eachAttribRun(attribs, func /*(startInNewText, endInNewText, attribs)*/ ) + { + var attribsIter = Changeset.opIterator(attribs); + var textIndex = 0; + var newTextStart = 0; + var newTextEnd = newText.length - 1; + while (attribsIter.hasNext()) + { + var op = attribsIter.next(); + var nextIndex = textIndex + op.chars; + if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) + { + func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); + } + textIndex = nextIndex; + } + } + + // create a new changeset with a helper builder object + var builder = Changeset.builder(1); + + // assemble each line into the builder + eachAttribRun(newAttribs, function(start, end, attribs) + { + builder.insert(newText.substring(start, end), attribs); + }); + + // the changeset is ready! + var theChangeset = builder.toString(); + apiLogger.debug('The changeset: ' + theChangeset); + pad.appendRevision(theChangeset); +} + +exports.setPadHTML = setPadHTML; diff --git a/node/utils/contentcollector.js b/node/utils/contentcollector.js new file mode 100644 index 00000000..60bd0a6e --- /dev/null +++ b/node/utils/contentcollector.js @@ -0,0 +1,692 @@ +// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector +// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); +// %APPJET%: import("etherpad.admin.plugins"); +/** + * 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 Changeset = require("../utils/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 plugins_; + if (typeof(plugins) != 'undefined') + { + plugins_ = plugins; + } + else + { + plugins_ = {callHook: function () {}}; + } + + 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()) + { + cc.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); + } + } + cc.incrementFlag = function(state, flagName) + { + state.flags[flagName] = (state.flags[flagName] || 0) + 1; + } + cc.decrementFlag = function(state, flagName) + { + state.flags[flagName]--; + } + cc.incrementAttrib = function(state, attribName) + { + if (!state.attribs[attribName]) + { + state.attribs[attribName] = 1; + } + else + { + state.attribs[attribName]++; + } + _recalcAttribString(state); + } + cc.decrementAttrib = function(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)); + } + cc.startNewLine = function(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.doAttrib = function(state, na) + { + state.localAttribs = (state.localAttribs || []); + state.localAttribs.push(na); + cc.incrementAttrib(state, na); + }; + cc.collectContent = function(node, state) + { + if (!state) + { + state = { + flags: { /*name -> nesting counter*/ + }, + localAttribs: null, + attribs: { /*name -> nesting counter*/ + }, + attribString: '' + }; + } + var localAttribs = state.localAttribs; + state.localAttribs = null; + 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) + { + cc.startNewLine(state); + } + } + } + else + { + var tname = (dom.nodeTagName(node) || "").toLowerCase(); + if (tname == "br") + { + cc.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) cc.incrementFlag(state, 'preMode'); + var oldListTypeOrNull = null; + var oldAuthorOrNull = null; + if (collectStyles) + { + plugins_.callHook('collectContentPre', { + cc: cc, + state: state, + tname: tname, + styl: styl, + cls: cls + }); + if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == "strong") + { + cc.doAttrib(state, "bold"); + } + if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == "em") + { + cc.doAttrib(state, "italic"); + } + if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == "ins") + { + cc.doAttrib(state, "underline"); + } + if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == "del") + { + cc.doAttrib(state, "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 (collectStyles) + { + plugins_.callHook('collectContentPost', { + cc: cc, + state: state, + tname: tname, + styl: styl, + cls: cls + }); + } + + if (isPre) cc.decrementFlag(state, 'preMode'); + if (state.localAttribs) + { + for (var i = 0; i < state.localAttribs.length; i++) + { + cc.decrementAttrib(state, state.localAttribs[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) + { + cc.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); + } + + state.localAttribs = localAttribs; + }; + // 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.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; +} + +exports.makeContentCollector = makeContentCollector; diff --git a/package.json b/package.json index f1c30caa..78648f0a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "uglify-js" : "1.1.1", "gzip" : "0.1.0", "formidable" : "1.0.7", - "log4js" : "0.3.9" + "log4js" : "0.3.9", + "jsdom" : "0.2.9" }, "version" : "1.0.0" } diff --git a/static/js/ace2_common.js b/static/js/ace2_common.js index 02dc350f..18478586 100644 --- a/static/js/ace2_common.js +++ b/static/js/ace2_common.js @@ -74,8 +74,12 @@ function isArray(testObject) return testObject && typeof testObject === 'object' && !(testObject.propertyIsEnumerable('length')) && typeof testObject.length === 'number'; } +if (typeof exports !== "undefined") +{ + var navigator = {userAgent: "node-js"}; +} // Figure out what browser is being used (stolen from jquery 1.2.1) -var userAgent = navigator.userAgent.toLowerCase(); +userAgent = navigator.userAgent.toLowerCase(); var browser = { version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1], safari: /webkit/.test(userAgent), @@ -85,6 +89,7 @@ var browser = { windows: /windows/.test(userAgent) // dgreensp }; + function getAssoc(obj, name) { return obj["_magicdom_" + name]; @@ -130,3 +135,8 @@ function htmlPrettyEscape(str) { return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\r?\n/g, '\\n'); } + +if (typeof exports !== "undefined") +{ + exports.map = map; +}
\ No newline at end of file |