diff --git a/openslides/motions/static/js/motions/diff.js b/openslides/motions/static/js/motions/diff.js
index 55ce25b52..b8a5411d8 100644
--- a/openslides/motions/static/js/motions/diff.js
+++ b/openslides/motions/static/js/motions/diff.js
@@ -4,261 +4,229 @@
angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumbering'])
-.service('diffService', function (lineNumberingService) {
- var ELEMENT_NODE = 1,
- TEXT_NODE = 3,
- DOCUMENT_FRAGMENT_NODE = 11;
+.service('diffService', [
+ 'lineNumberingService',
+ '$cacheFactory',
+ function (lineNumberingService, $cacheFactory) {
+ var ELEMENT_NODE = 1,
+ TEXT_NODE = 3,
+ DOCUMENT_FRAGMENT_NODE = 11;
- this.TYPE_REPLACEMENT = 0;
- this.TYPE_INSERTION = 1;
- this.TYPE_DELETION = 2;
+ var diffCache = $cacheFactory('diff.service');
- this.getLineNumberNode = function(fragment, lineNumber) {
- return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
- };
+ this.TYPE_REPLACEMENT = 0;
+ this.TYPE_INSERTION = 1;
+ this.TYPE_DELETION = 2;
- this._getNodeContextTrace = function(node) {
- var context = [],
- currNode = node;
- while (currNode) {
- context.unshift(currNode);
- currNode = currNode.parentNode;
- }
- return context;
- };
+ this.getLineNumberNode = function(fragment, lineNumber) {
+ return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
+ };
- // Adds elements like
- this._insertInternalLineMarkers = function(fragment) {
- if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
- // Prevent duplicate calls
- return;
- }
- var lineNumbers = fragment.querySelectorAll('span.os-line-number'),
- lineMarker, maxLineNumber;
-
- for (var i = 0; i < lineNumbers.length; i++) {
- var insertBefore = lineNumbers[i];
- while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
- insertBefore = insertBefore.parentNode;
+ this._getNodeContextTrace = function(node) {
+ var context = [],
+ currNode = node;
+ while (currNode) {
+ context.unshift(currNode);
+ currNode = currNode.parentNode;
}
+ return context;
+ };
+
+ // Adds elements like
+ this._insertInternalLineMarkers = function(fragment) {
+ if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
+ // Prevent duplicate calls
+ return;
+ }
+ var lineNumbers = fragment.querySelectorAll('span.os-line-number'),
+ lineMarker, maxLineNumber;
+
+ for (var i = 0; i < lineNumbers.length; i++) {
+ var insertBefore = lineNumbers[i];
+ while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
+ insertBefore = insertBefore.parentNode;
+ }
+ lineMarker = document.createElement('OS-LINEBREAK');
+ lineMarker.setAttribute('data-line-number', lineNumbers[i].getAttribute('data-line-number'));
+ lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
+ insertBefore.parentNode.insertBefore(lineMarker, insertBefore);
+ maxLineNumber = lineNumbers[i].getAttribute('data-line-number');
+ }
+
+ // Add one more "fake" line number at the end and beginning, so we can select the last line as well
lineMarker = document.createElement('OS-LINEBREAK');
- lineMarker.setAttribute('data-line-number', lineNumbers[i].getAttribute('data-line-number'));
- lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
- insertBefore.parentNode.insertBefore(lineMarker, insertBefore);
- maxLineNumber = lineNumbers[i].getAttribute('data-line-number');
- }
+ lineMarker.setAttribute('data-line-number', (parseInt(maxLineNumber) + 1));
+ lineMarker.setAttribute('class', 'os-line-number line-number-' + (parseInt(maxLineNumber) + 1));
+ fragment.appendChild(lineMarker);
- // Add one more "fake" line number at the end and beginning, so we can select the last line as well
- lineMarker = document.createElement('OS-LINEBREAK');
- lineMarker.setAttribute('data-line-number', (parseInt(maxLineNumber) + 1));
- lineMarker.setAttribute('class', 'os-line-number line-number-' + (parseInt(maxLineNumber) + 1));
- fragment.appendChild(lineMarker);
+ lineMarker = document.createElement('OS-LINEBREAK');
+ lineMarker.setAttribute('data-line-number', '0');
+ lineMarker.setAttribute('class', 'os-line-number line-number-0');
+ fragment.insertBefore(lineMarker, fragment.firstChild);
+ };
- lineMarker = document.createElement('OS-LINEBREAK');
- lineMarker.setAttribute('data-line-number', '0');
- lineMarker.setAttribute('class', 'os-line-number line-number-0');
- fragment.insertBefore(lineMarker, fragment.firstChild);
- };
-
- // @TODO Check if this is actually necessary
- this._insertInternalLiNumbers = function(fragment) {
- if (fragment.querySelectorAll('LI[os-li-number]').length > 0) {
- // Prevent duplicate calls
- return;
- }
- var ols = fragment.querySelectorAll('OL');
- for (var i = 0; i < ols.length; i++) {
- var ol = ols[i],
- liNo = 0;
- for (var j = 0; j < ol.childNodes.length; j++) {
- if (ol.childNodes[j].nodeName == 'LI') {
- liNo++;
- ol.childNodes[j].setAttribute('os-li-number', liNo);
- }
+ // @TODO Check if this is actually necessary
+ this._insertInternalLiNumbers = function(fragment) {
+ if (fragment.querySelectorAll('LI[os-li-number]').length > 0) {
+ // Prevent duplicate calls
+ return;
}
- }
- };
-
- this._addStartToOlIfNecessary = function(node) {
- var firstLiNo = null;
- for (var i = 0; i < node.childNodes.length && firstLiNo === null; i++) {
- if (node.childNode[i].nodeName == 'LI') {
- var lineNo = node.childNode[i].getAttribute('ol-li-number');
- if (lineNo) {
- firstLiNo = parseInt(lineNo);
- }
- }
- }
- if (firstLiNo > 1) {
- node.setAttribute('start', firstLiNo);
- }
- };
-
- this._isWithinNthLIOfOL = function(olNode, descendantNode) {
- var nthLIOfOL = null;
- while (descendantNode.parentNode) {
- if (descendantNode.parentNode == olNode) {
- var lisBeforeOl = 0,
- foundMe = false;
- for (var i = 0; i < olNode.childNodes.length && !foundMe; i++) {
- if (olNode.childNodes[i] == descendantNode) {
- foundMe = true;
- } else if (olNode.childNodes[i].nodeName == 'LI') {
- lisBeforeOl++;
+ var ols = fragment.querySelectorAll('OL');
+ for (var i = 0; i < ols.length; i++) {
+ var ol = ols[i],
+ liNo = 0;
+ for (var j = 0; j < ol.childNodes.length; j++) {
+ if (ol.childNodes[j].nodeName == 'LI') {
+ liNo++;
+ ol.childNodes[j].setAttribute('os-li-number', liNo);
}
}
- nthLIOfOL = lisBeforeOl + 1;
}
- descendantNode = descendantNode.parentNode;
- }
- return nthLIOfOL;
- };
-
- /*
- * Returns an array with the following values:
- * 0: the most specific DOM-node that contains both line numbers
- * 1: the context of node1 (an array of dom-elements; 0 is the document fragment)
- * 2: the context of node2 (an array of dom-elements; 0 is the document fragment)
- * 3: the index of [0] in the two arrays
- */
- this._getCommonAncestor = function(node1, node2) {
- var trace1 = this._getNodeContextTrace(node1),
- trace2 = this._getNodeContextTrace(node2),
- commonAncestor = null,
- commonIndex = null,
- childTrace1 = [],
- childTrace2 = [];
-
- for (var i = 0; i < trace1.length && i < trace2.length; i++) {
- if (trace1[i] == trace2[i]) {
- commonAncestor = trace1[i];
- commonIndex = i;
- }
- }
- for (i = commonIndex + 1; i < trace1.length; i++) {
- childTrace1.push(trace1[i]);
- }
- for (i = commonIndex + 1; i < trace2.length; i++) {
- childTrace2.push(trace2[i]);
- }
- return {
- 'commonAncestor': commonAncestor,
- 'trace1' : childTrace1,
- 'trace2' : childTrace2,
- 'index': commonIndex
};
- };
- this._serializeTag = function(node) {
- if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
- // Fragments are only placeholders and do not have an HTML representation
- return '';
- }
- var html = '<' + node.nodeName;
- for (var i = 0; i < node.attributes.length; i++) {
- var attr = node.attributes[i];
- if (attr.name != 'os-li-number') {
- html += ' ' + attr.name + '="' + attr.value + '"';
- }
- }
- html += '>';
- return html;
- };
-
- this._serializeDom = function(node, stripLineNumbers) {
- if (node.nodeType == TEXT_NODE) {
- return node.nodeValue;
- }
- if (stripLineNumbers && (
- lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node))) {
- return '';
- }
- if (node.nodeName == 'OS-LINEBREAK') {
- return '';
- }
- if (node.nodeName == 'BR') {
- var br = '
';
- }
-
- var html = this._serializeTag(node);
- for (var i = 0; i < node.childNodes.length; i++) {
- if (node.childNodes[i].nodeType == TEXT_NODE) {
- html += node.childNodes[i].nodeValue;
- } else if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) && !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
- html += this._serializeDom(node.childNodes[i], stripLineNumbers);
- }
- }
- if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
- html += '' + node.nodeName + '>';
- }
-
- return html;
- };
-
- /**
- * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
- */
- this._serializePartialDomToChild = function(node, toChildTrace, stripLineNumbers) {
- if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
- return '';
- }
- if (node.nodeName == 'OS-LINEBREAK') {
- return '';
- }
-
- var html = this._serializeTag(node);
-
- for (var i = 0, found = false; i < node.childNodes.length && !found; i++) {
- if (node.childNodes[i] == toChildTrace[0]) {
- found = true;
- var remainingTrace = toChildTrace;
- remainingTrace.shift();
- if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
- html += this._serializePartialDomToChild(node.childNodes[i], remainingTrace, stripLineNumbers);
+ this._addStartToOlIfNecessary = function(node) {
+ var firstLiNo = null;
+ for (var i = 0; i < node.childNodes.length && firstLiNo === null; i++) {
+ if (node.childNode[i].nodeName == 'LI') {
+ var lineNo = node.childNode[i].getAttribute('ol-li-number');
+ if (lineNo) {
+ firstLiNo = parseInt(lineNo);
+ }
}
- } else if (node.childNodes[i].nodeType == TEXT_NODE) {
- html += node.childNodes[i].nodeValue;
- } else {
- if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
- !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
+ }
+ if (firstLiNo > 1) {
+ node.setAttribute('start', firstLiNo);
+ }
+ };
+
+ this._isWithinNthLIOfOL = function(olNode, descendantNode) {
+ var nthLIOfOL = null;
+ while (descendantNode.parentNode) {
+ if (descendantNode.parentNode == olNode) {
+ var lisBeforeOl = 0,
+ foundMe = false;
+ for (var i = 0; i < olNode.childNodes.length && !foundMe; i++) {
+ if (olNode.childNodes[i] == descendantNode) {
+ foundMe = true;
+ } else if (olNode.childNodes[i].nodeName == 'LI') {
+ lisBeforeOl++;
+ }
+ }
+ nthLIOfOL = lisBeforeOl + 1;
+ }
+ descendantNode = descendantNode.parentNode;
+ }
+ return nthLIOfOL;
+ };
+
+ /*
+ * Returns an array with the following values:
+ * 0: the most specific DOM-node that contains both line numbers
+ * 1: the context of node1 (an array of dom-elements; 0 is the document fragment)
+ * 2: the context of node2 (an array of dom-elements; 0 is the document fragment)
+ * 3: the index of [0] in the two arrays
+ */
+ this._getCommonAncestor = function(node1, node2) {
+ var trace1 = this._getNodeContextTrace(node1),
+ trace2 = this._getNodeContextTrace(node2),
+ commonAncestor = null,
+ commonIndex = null,
+ childTrace1 = [],
+ childTrace2 = [];
+
+ for (var i = 0; i < trace1.length && i < trace2.length; i++) {
+ if (trace1[i] == trace2[i]) {
+ commonAncestor = trace1[i];
+ commonIndex = i;
+ }
+ }
+ for (i = commonIndex + 1; i < trace1.length; i++) {
+ childTrace1.push(trace1[i]);
+ }
+ for (i = commonIndex + 1; i < trace2.length; i++) {
+ childTrace2.push(trace2[i]);
+ }
+ return {
+ 'commonAncestor': commonAncestor,
+ 'trace1' : childTrace1,
+ 'trace2' : childTrace2,
+ 'index': commonIndex
+ };
+ };
+
+ this._serializeTag = function(node) {
+ if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
+ // Fragments are only placeholders and do not have an HTML representation
+ return '';
+ }
+ var html = '<' + node.nodeName;
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attr = node.attributes[i];
+ if (attr.name != 'os-li-number') {
+ html += ' ' + attr.name + '="' + attr.value + '"';
+ }
+ }
+ html += '>';
+ return html;
+ };
+
+ this._serializeDom = function(node, stripLineNumbers) {
+ if (node.nodeType == TEXT_NODE) {
+ return node.nodeValue;
+ }
+ if (stripLineNumbers && (
+ lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node))) {
+ return '';
+ }
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
+ }
+ if (node.nodeName == 'BR') {
+ var br = '
';
+ }
+
+ var html = this._serializeTag(node);
+ for (var i = 0; i < node.childNodes.length; i++) {
+ if (node.childNodes[i].nodeType == TEXT_NODE) {
+ html += node.childNodes[i].nodeValue;
+ } else if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) && !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
html += this._serializeDom(node.childNodes[i], stripLineNumbers);
}
}
- }
- if (!found) {
- console.trace();
- throw "Inconsistency or invalid call of this function detected (to)";
- }
- return html;
- };
+ if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
+ html += '' + node.nodeName + '>';
+ }
- /**
- * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
- */
- this._serializePartialDomFromChild = function(node, fromChildTrace, stripLineNumbers) {
- if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
- return '';
- }
- if (node.nodeName == 'OS-LINEBREAK') {
- return '';
- }
+ return html;
+ };
- var html = '';
- for (var i = 0, found = false; i < node.childNodes.length; i++) {
- if (node.childNodes[i] == fromChildTrace[0]) {
- found = true;
- var remainingTrace = fromChildTrace;
- remainingTrace.shift();
- if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
- html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers);
- }
- } else if (found) {
- if (node.childNodes[i].nodeType == TEXT_NODE) {
+ /**
+ * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
+ */
+ this._serializePartialDomToChild = function(node, toChildTrace, stripLineNumbers) {
+ if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
+ return '';
+ }
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
+ }
+
+ var html = this._serializeTag(node);
+
+ for (var i = 0, found = false; i < node.childNodes.length && !found; i++) {
+ if (node.childNodes[i] == toChildTrace[0]) {
+ found = true;
+ var remainingTrace = toChildTrace;
+ remainingTrace.shift();
+ if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
+ html += this._serializePartialDomToChild(node.childNodes[i], remainingTrace, stripLineNumbers);
+ }
+ } else if (node.childNodes[i].nodeType == TEXT_NODE) {
html += node.childNodes[i].nodeValue;
} else {
if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
@@ -267,367 +235,414 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
}
}
}
- }
- if (!found) {
- console.trace();
- throw "Inconsistency or invalid call of this function detected (from)";
- }
- if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
- html += '' + node.nodeName + '>';
- }
- return html;
- };
-
- this.htmlToFragment = function(html) {
- var fragment = document.createDocumentFragment(),
- div = document.createElement('DIV');
- div.innerHTML = html;
- while (div.childElementCount) {
- var child = div.childNodes[0];
- div.removeChild(child);
- fragment.appendChild(child);
- }
- return fragment;
- };
-
- /**
- * Returns the HTML snippet between two given line numbers.
- *
- * Hint:
- * - The last line (toLine) is not included anymore, as the number refers to the line breaking element
- * - if toLine === null, then everything from fromLine to the end of the fragment is returned
- *
- * In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
- * that contains the whole section specified by the line numbers (like a P-element if only one paragraph is selected
- * or the most outer DIV, if multiple sections selected).
- *
- * This additional information is meant to render the snippet correctly without producing broken HTML
- *
- * The return object has the following fields:
- * - html: The HTML between the two line numbers.
- * Line numbers and automatically set line breaks are stripped.
- * All HTML tags are converted to uppercase
- * (e.g. Line 2
Line3Line 4
)
- * - ancestor: the most specific DOM element that contains the HTML snippet (e.g. a UL, if several LIs are selected)
- * - outerContextStart: An HTML string that opens all necessary tags to get the browser into the rendering mode
- * of the ancestor element (e.g. in the case of the multiple LIs)
- * - outerContectEnd: An HTML string that closes all necessary tags from the ancestor element (e.g.
- * - innerContextStart: A string that opens all necessary tags between the ancestor
- * and the beginning of the selection (e.g. )
- * - innerContextEnd: A string that closes all tags after the end of the selection to the ancestor (e.g. )
- * - previousHtml: The HTML before the selected area begins (including line numbers)
- * - previousHtmlEndSnippet: A HTML snippet that closes all open tags from previousHtml
- * - followingHtml: The HTML after the selected area
- * - followingHtmlStartSnippet: A HTML snippet that opens all HTML tags necessary to render "followingHtml"
- *
- */
- this.extractRangeByLineNumbers = function(fragment, fromLine, toLine, debug) {
- if (typeof(fragment) == 'string') {
- fragment = this.htmlToFragment(fragment);
- }
- this._insertInternalLineMarkers(fragment);
- this._insertInternalLiNumbers(fragment);
- if (toLine === null) {
- var internalLineMarkers = fragment.querySelectorAll('OS-LINEBREAK');
- toLine = parseInt(internalLineMarkers[internalLineMarkers.length - 1].getAttribute("data-line-number"));
- }
-
- var fromLineNode = this.getLineNumberNode(fragment, fromLine),
- toLineNode = (toLine ? this.getLineNumberNode(fragment, toLine) : null),
- ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
-
- var fromChildTraceRel = ancestorData.trace1,
- fromChildTraceAbs = this._getNodeContextTrace(fromLineNode),
- toChildTraceRel = ancestorData.trace2,
- toChildTraceAbs = this._getNodeContextTrace(toLineNode),
- ancestor = ancestorData.commonAncestor,
- html = '',
- outerContextStart = '',
- outerContextEnd = '',
- innerContextStart = '',
- innerContextEnd = '',
- previousHtmlEndSnippet = '',
- followingHtmlStartSnippet = '',
- fakeOl;
-
-
- fromChildTraceAbs.shift();
- var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false);
- toChildTraceAbs.shift();
- var followingHtml = this._serializePartialDomFromChild(fragment, toChildTraceAbs, false);
-
- var currNode = fromLineNode.parentNode;
- while (currNode.parentNode) {
- previousHtmlEndSnippet += '' + currNode.nodeName + '>';
- currNode = currNode.parentNode;
- }
- currNode = toLineNode.parentNode;
- while (currNode.parentNode) {
- followingHtmlStartSnippet = this._serializeTag(currNode) + followingHtmlStartSnippet;
- currNode = currNode.parentNode;
- }
-
- var found = false;
- for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
- if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
- found = true;
- } else {
- if (fromChildTraceRel[i].nodeName == 'OL') {
- fakeOl = fromChildTraceRel[i].cloneNode(false);
- fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode));
- innerContextStart += this._serializeTag(fakeOl);
- } else {
- innerContextStart += this._serializeTag(fromChildTraceRel[i]);
- }
+ if (!found) {
+ console.trace();
+ throw "Inconsistency or invalid call of this function detected (to)";
}
- }
- found = false;
- for (i = 0; i < toChildTraceRel.length && !found; i++) {
- if (toChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
- found = true;
- } else {
- innerContextEnd = '' + toChildTraceRel[i].nodeName + '>' + innerContextEnd;
- }
- }
-
- found = false;
- for (i = 0; i < ancestor.childNodes.length; i++) {
- if (ancestor.childNodes[i] == fromChildTraceRel[0]) {
- found = true;
- fromChildTraceRel.shift();
- html += this._serializePartialDomFromChild(ancestor.childNodes[i], fromChildTraceRel, true);
- } else if (ancestor.childNodes[i] == toChildTraceRel[0]) {
- found = false;
- toChildTraceRel.shift();
- html += this._serializePartialDomToChild(ancestor.childNodes[i], toChildTraceRel, true);
- } else if (found === true) {
- html += this._serializeDom(ancestor.childNodes[i], true);
- }
- }
-
- currNode = ancestor;
- while (currNode.parentNode) {
- if (currNode.nodeName == 'OL') {
- fakeOl = currNode.cloneNode(false);
- fakeOl.setAttribute('start', this._isWithinNthLIOfOL(currNode, fromLineNode));
- outerContextStart = this._serializeTag(fakeOl) + outerContextStart;
- } else {
- outerContextStart = this._serializeTag(currNode) + outerContextStart;
- }
- outerContextEnd += '' + currNode.nodeName + '>';
- currNode = currNode.parentNode;
- }
-
- return {
- 'html': html,
- 'ancestor': ancestor,
- 'outerContextStart': outerContextStart,
- 'outerContextEnd': outerContextEnd,
- 'innerContextStart': innerContextStart,
- 'innerContextEnd': innerContextEnd,
- 'previousHtml': previousHtml,
- 'previousHtmlEndSnippet': previousHtmlEndSnippet,
- 'followingHtml': followingHtml,
- 'followingHtmlStartSnippet': followingHtmlStartSnippet
- };
- };
-
- /*
- * This functions merges to arrays of nodes. The last element of nodes1 and the first element of nodes2
- * are merged, if they are of the same type.
- *
- * This is done recursively until a TEMPLATE-Tag is is found, which was inserted in this.replaceLines.
- * Using a TEMPLATE-Tag is a rather dirty hack, as it is allowed inside of any other element, including .
- *
- */
- this._replaceLinesMergeNodeArrays = function(nodes1, nodes2) {
- if (nodes1.length === 0) {
- return nodes2;
- }
- if (nodes2.length === 0) {
- return nodes1;
- }
-
- var out = [];
- for (var i = 0; i < nodes1.length - 1; i++) {
- out.push(nodes1[i]);
- }
-
- var lastNode = nodes1[nodes1.length - 1],
- firstNode = nodes2[0];
- if (lastNode.nodeType == TEXT_NODE && firstNode.nodeType == TEXT_NODE) {
- var newTextNode = lastNode.ownerDocument.createTextNode(lastNode.nodeValue + firstNode.nodeValue);
- out.push(newTextNode);
- } else if (lastNode.nodeName == firstNode.nodeName) {
- var newNode = lastNode.ownerDocument.createElement(lastNode.nodeName);
- for (i = 0; i < lastNode.attributes.length; i++) {
- var attr = lastNode.attributes[i];
- newNode.setAttribute(attr.name, attr.value);
- }
-
- // Remove #text nodes inside of List elements, as they are confusing
- var lastChildren, firstChildren;
- if (lastNode.nodeName == 'OL' || lastNode.nodeName == 'UL') {
- lastChildren = [];
- firstChildren = [];
- for (i = 0; i < firstNode.childNodes.length; i++) {
- if (firstNode.childNodes[i].nodeType == ELEMENT_NODE) {
- firstChildren.push(firstNode.childNodes[i]);
- }
- }
- for (i = 0; i < lastNode.childNodes.length; i++) {
- if (lastNode.childNodes[i].nodeType == ELEMENT_NODE) {
- lastChildren.push(lastNode.childNodes[i]);
- }
- }
- } else {
- lastChildren = lastNode.childNodes;
- firstChildren = firstNode.childNodes;
- }
-
- var children = this._replaceLinesMergeNodeArrays(lastChildren, firstChildren);
- for (i = 0; i < children.length; i++) {
- newNode.appendChild(children[i]);
- }
- out.push(newNode);
- } else {
- if (lastNode.nodeName != 'TEMPLATE') {
- out.push(lastNode);
- }
- if (firstNode.nodeName != 'TEMPLATE') {
- out.push(firstNode);
- }
- }
-
- for (i = 1; i < nodes2.length; i++) {
- out.push(nodes2[i]);
- }
-
- return out;
- };
-
- /**
- * @param {string} htmlOld
- * @param {string} htmlNew
- * @returns {number}
- */
- this.detectReplacementType = function (htmlOld, htmlNew) {
- // Convert all HTML tags to uppercase, strip trailing whitespaces
- var normalizeHtml = function(html) {
- html = html.replace(/<[^>]+>/g, function(tag) { return tag.toUpperCase(); });
- html = html.replace(/\s+<\/P>/gi, '').replace(/\s+<\/DIV>/gi, '').replace(/\s+<\/LI>/gi, '');
- html = html.replace(/\s+- /gi, '
- ').replace(/<\/LI>\s+/gi, '
');
- html = html.replace(/ /gi, ' ').replace(/\u00A0/g, ' '); // non-breaking spaces
return html;
};
- htmlOld = normalizeHtml(htmlOld);
- htmlNew = normalizeHtml(htmlNew);
- if (htmlOld == htmlNew) {
- return this.TYPE_REPLACEMENT;
- }
-
- var i, foundDiff;
- for (i = 0, foundDiff = false; i < htmlOld.length && i < htmlNew.length && foundDiff === false; i++) {
- if (htmlOld[i] != htmlNew[i]) {
- foundDiff = true;
+ /**
+ * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
+ */
+ this._serializePartialDomFromChild = function(node, fromChildTrace, stripLineNumbers) {
+ if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
+ return '';
}
- }
-
- var remainderOld = htmlOld.substr(i - 1),
- remainderNew = htmlNew.substr(i - 1),
- type = this.TYPE_REPLACEMENT;
-
- if (remainderOld.length > remainderNew.length) {
- if (remainderOld.substr(remainderOld.length - remainderNew.length) == remainderNew) {
- type = this.TYPE_DELETION;
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
}
- } else if (remainderOld.length < remainderNew.length) {
- if (remainderNew.substr(remainderNew.length - remainderOld.length) == remainderOld) {
- type = this.TYPE_INSERTION;
+
+ var html = '';
+ for (var i = 0, found = false; i < node.childNodes.length; i++) {
+ if (node.childNodes[i] == fromChildTrace[0]) {
+ found = true;
+ var remainingTrace = fromChildTrace;
+ remainingTrace.shift();
+ if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
+ html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers);
+ }
+ } else if (found) {
+ if (node.childNodes[i].nodeType == TEXT_NODE) {
+ html += node.childNodes[i].nodeValue;
+ } else {
+ if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
+ !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
+ html += this._serializeDom(node.childNodes[i], stripLineNumbers);
+ }
+ }
+ }
}
- }
+ if (!found) {
+ console.trace();
+ throw "Inconsistency or invalid call of this function detected (from)";
+ }
+ if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
+ html += '' + node.nodeName + '>';
+ }
+ return html;
+ };
- return type;
- };
+ this.htmlToFragment = function(html) {
+ var fragment = document.createDocumentFragment(),
+ div = document.createElement('DIV');
+ div.innerHTML = html;
+ while (div.childElementCount) {
+ var child = div.childNodes[0];
+ div.removeChild(child);
+ fragment.appendChild(child);
+ }
+ return fragment;
+ };
- this.replaceLines = function (fragment, newHTML, fromLine, toLine) {
- var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
- previousHtml = data.previousHtml + '' + data.previousHtmlEndSnippet,
- previousFragment = this.htmlToFragment(previousHtml),
- followingHtml = data.followingHtmlStartSnippet + '' + data.followingHtml,
- followingFragment = this.htmlToFragment(followingHtml),
- newFragment = this.htmlToFragment(newHTML);
+ /**
+ * Returns the HTML snippet between two given line numbers.
+ *
+ * Hint:
+ * - The last line (toLine) is not included anymore, as the number refers to the line breaking element
+ * - if toLine === null, then everything from fromLine to the end of the fragment is returned
+ *
+ * In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
+ * that contains the whole section specified by the line numbers (like a P-element if only one paragraph is selected
+ * or the most outer DIV, if multiple sections selected).
+ *
+ * This additional information is meant to render the snippet correctly without producing broken HTML
+ *
+ * The return object has the following fields:
+ * - html: The HTML between the two line numbers.
+ * Line numbers and automatically set line breaks are stripped.
+ * All HTML tags are converted to uppercase
+ * (e.g. Line 2- Line3
- Line 4
)
+ * - ancestor: the most specific DOM element that contains the HTML snippet (e.g. a UL, if several LIs are selected)
+ * - outerContextStart: An HTML string that opens all necessary tags to get the browser into the rendering mode
+ * of the ancestor element (e.g. in the case of the multiple LIs)
+ * - outerContectEnd: An HTML string that closes all necessary tags from the ancestor element (e.g.
+ * - innerContextStart: A string that opens all necessary tags between the ancestor
+ * and the beginning of the selection (e.g. - )
+ * - innerContextEnd: A string that closes all tags after the end of the selection to the ancestor (e.g.
)
+ * - previousHtml: The HTML before the selected area begins (including line numbers)
+ * - previousHtmlEndSnippet: A HTML snippet that closes all open tags from previousHtml
+ * - followingHtml: The HTML after the selected area
+ * - followingHtmlStartSnippet: A HTML snippet that opens all HTML tags necessary to render "followingHtml"
+ *
+ */
+ this.extractRangeByLineNumbers = function(fragment, fromLine, toLine, debug) {
- var merged = this._replaceLinesMergeNodeArrays(previousFragment.childNodes, newFragment.childNodes);
- merged = this._replaceLinesMergeNodeArrays(merged, followingFragment.childNodes);
+ var cacheKey = fromLine + "-" + toLine + "-" + lineNumberingService.djb2hash(fragment),
+ cached = diffCache.get(cacheKey);
+ if (!angular.isUndefined(cached)) {
+ return cached;
+ }
- var mergedFragment = document.createDocumentFragment();
- for (var i = 0; i < merged.length; i++) {
- mergedFragment.appendChild(merged[i]);
- }
+ if (typeof(fragment) == 'string') {
+ fragment = this.htmlToFragment(fragment);
+ }
+ this._insertInternalLineMarkers(fragment);
+ this._insertInternalLiNumbers(fragment);
+ if (toLine === null) {
+ var internalLineMarkers = fragment.querySelectorAll('OS-LINEBREAK');
+ toLine = parseInt(internalLineMarkers[internalLineMarkers.length - 1].getAttribute("data-line-number"));
+ }
- var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
- for (i = 0; i < forgottenTemplates.length; i++) {
- var el = forgottenTemplates[i];
- el.parentNode.removeChild(el);
- }
+ var fromLineNode = this.getLineNumberNode(fragment, fromLine),
+ toLineNode = (toLine ? this.getLineNumberNode(fragment, toLine) : null),
+ ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
- return this._serializeDom(mergedFragment, true);
- };
+ var fromChildTraceRel = ancestorData.trace1,
+ fromChildTraceAbs = this._getNodeContextTrace(fromLineNode),
+ toChildTraceRel = ancestorData.trace2,
+ toChildTraceAbs = this._getNodeContextTrace(toLineNode),
+ ancestor = ancestorData.commonAncestor,
+ html = '',
+ outerContextStart = '',
+ outerContextEnd = '',
+ innerContextStart = '',
+ innerContextEnd = '',
+ previousHtmlEndSnippet = '',
+ followingHtmlStartSnippet = '',
+ fakeOl;
- this.addCSSClass = function (node, className) {
- if (node.nodeType != ELEMENT_NODE) {
- return;
- }
- var classes = node.getAttribute('class');
- classes = (classes ? classes.split(' ') : []);
- if (classes.indexOf(className) == -1) {
- classes.push(className);
- }
- node.setAttribute('class', classes);
- };
- this.addDiffMarkup = function (fragment, newHTML, fromLine, toLine, diffFormatterCb) {
- var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
- previousHtml = data.previousHtml + '' + data.previousHtmlEndSnippet,
- previousFragment = this.htmlToFragment(previousHtml),
- followingHtml = data.followingHtmlStartSnippet + '' + data.followingHtml,
- followingFragment = this.htmlToFragment(followingHtml),
- newFragment = this.htmlToFragment(newHTML),
- oldHTML = data.outerContextStart + data.innerContextStart + data.html +
- data.innerContextEnd + data.outerContextEnd,
- oldFragment = this.htmlToFragment(oldHTML),
- el;
+ fromChildTraceAbs.shift();
+ var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false);
+ toChildTraceAbs.shift();
+ var followingHtml = this._serializePartialDomFromChild(fragment, toChildTraceAbs, false);
- var diffFragment = diffFormatterCb(oldFragment, newFragment);
+ var currNode = fromLineNode.parentNode;
+ while (currNode.parentNode) {
+ previousHtmlEndSnippet += '' + currNode.nodeName + '>';
+ currNode = currNode.parentNode;
+ }
+ currNode = toLineNode.parentNode;
+ while (currNode.parentNode) {
+ followingHtmlStartSnippet = this._serializeTag(currNode) + followingHtmlStartSnippet;
+ currNode = currNode.parentNode;
+ }
- var mergedFragment = document.createDocumentFragment();
- while (previousFragment.firstChild) {
- el = previousFragment.firstChild;
- previousFragment.removeChild(el);
- mergedFragment.appendChild(el);
- }
- while (diffFragment.firstChild) {
- el = diffFragment.firstChild;
- diffFragment.removeChild(el);
- mergedFragment.appendChild(el);
- }
- while (followingFragment.firstChild) {
- el = followingFragment.firstChild;
- followingFragment.removeChild(el);
- mergedFragment.appendChild(el);
- }
+ var found = false;
+ for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
+ if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
+ found = true;
+ } else {
+ if (fromChildTraceRel[i].nodeName == 'OL') {
+ fakeOl = fromChildTraceRel[i].cloneNode(false);
+ fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode));
+ innerContextStart += this._serializeTag(fakeOl);
+ } else {
+ innerContextStart += this._serializeTag(fromChildTraceRel[i]);
+ }
+ }
+ }
+ found = false;
+ for (i = 0; i < toChildTraceRel.length && !found; i++) {
+ if (toChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
+ found = true;
+ } else {
+ innerContextEnd = '' + toChildTraceRel[i].nodeName + '>' + innerContextEnd;
+ }
+ }
- var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
- for (var i = 0; i < forgottenTemplates.length; i++) {
- el = forgottenTemplates[i];
- el.parentNode.removeChild(el);
- }
+ found = false;
+ for (i = 0; i < ancestor.childNodes.length; i++) {
+ if (ancestor.childNodes[i] == fromChildTraceRel[0]) {
+ found = true;
+ fromChildTraceRel.shift();
+ html += this._serializePartialDomFromChild(ancestor.childNodes[i], fromChildTraceRel, true);
+ } else if (ancestor.childNodes[i] == toChildTraceRel[0]) {
+ found = false;
+ toChildTraceRel.shift();
+ html += this._serializePartialDomToChild(ancestor.childNodes[i], toChildTraceRel, true);
+ } else if (found === true) {
+ html += this._serializeDom(ancestor.childNodes[i], true);
+ }
+ }
- return this._serializeDom(mergedFragment, true);
- };
-});
+ currNode = ancestor;
+ while (currNode.parentNode) {
+ if (currNode.nodeName == 'OL') {
+ fakeOl = currNode.cloneNode(false);
+ fakeOl.setAttribute('start', this._isWithinNthLIOfOL(currNode, fromLineNode));
+ outerContextStart = this._serializeTag(fakeOl) + outerContextStart;
+ } else {
+ outerContextStart = this._serializeTag(currNode) + outerContextStart;
+ }
+ outerContextEnd += '' + currNode.nodeName + '>';
+ currNode = currNode.parentNode;
+ }
+ var ret = {
+ 'html': html,
+ 'ancestor': ancestor,
+ 'outerContextStart': outerContextStart,
+ 'outerContextEnd': outerContextEnd,
+ 'innerContextStart': innerContextStart,
+ 'innerContextEnd': innerContextEnd,
+ 'previousHtml': previousHtml,
+ 'previousHtmlEndSnippet': previousHtmlEndSnippet,
+ 'followingHtml': followingHtml,
+ 'followingHtmlStartSnippet': followingHtmlStartSnippet
+ };
+
+ diffCache.put(cacheKey, ret);
+ return ret;
+ };
+
+ /*
+ * This functions merges to arrays of nodes. The last element of nodes1 and the first element of nodes2
+ * are merged, if they are of the same type.
+ *
+ * This is done recursively until a TEMPLATE-Tag is is found, which was inserted in this.replaceLines.
+ * Using a TEMPLATE-Tag is a rather dirty hack, as it is allowed inside of any other element, including .
+ *
+ */
+ this._replaceLinesMergeNodeArrays = function(nodes1, nodes2) {
+ if (nodes1.length === 0) {
+ return nodes2;
+ }
+ if (nodes2.length === 0) {
+ return nodes1;
+ }
+
+ var out = [];
+ for (var i = 0; i < nodes1.length - 1; i++) {
+ out.push(nodes1[i]);
+ }
+
+ var lastNode = nodes1[nodes1.length - 1],
+ firstNode = nodes2[0];
+ if (lastNode.nodeType == TEXT_NODE && firstNode.nodeType == TEXT_NODE) {
+ var newTextNode = lastNode.ownerDocument.createTextNode(lastNode.nodeValue + firstNode.nodeValue);
+ out.push(newTextNode);
+ } else if (lastNode.nodeName == firstNode.nodeName) {
+ var newNode = lastNode.ownerDocument.createElement(lastNode.nodeName);
+ for (i = 0; i < lastNode.attributes.length; i++) {
+ var attr = lastNode.attributes[i];
+ newNode.setAttribute(attr.name, attr.value);
+ }
+
+ // Remove #text nodes inside of List elements, as they are confusing
+ var lastChildren, firstChildren;
+ if (lastNode.nodeName == 'OL' || lastNode.nodeName == 'UL') {
+ lastChildren = [];
+ firstChildren = [];
+ for (i = 0; i < firstNode.childNodes.length; i++) {
+ if (firstNode.childNodes[i].nodeType == ELEMENT_NODE) {
+ firstChildren.push(firstNode.childNodes[i]);
+ }
+ }
+ for (i = 0; i < lastNode.childNodes.length; i++) {
+ if (lastNode.childNodes[i].nodeType == ELEMENT_NODE) {
+ lastChildren.push(lastNode.childNodes[i]);
+ }
+ }
+ } else {
+ lastChildren = lastNode.childNodes;
+ firstChildren = firstNode.childNodes;
+ }
+
+ var children = this._replaceLinesMergeNodeArrays(lastChildren, firstChildren);
+ for (i = 0; i < children.length; i++) {
+ newNode.appendChild(children[i]);
+ }
+ out.push(newNode);
+ } else {
+ if (lastNode.nodeName != 'TEMPLATE') {
+ out.push(lastNode);
+ }
+ if (firstNode.nodeName != 'TEMPLATE') {
+ out.push(firstNode);
+ }
+ }
+
+ for (i = 1; i < nodes2.length; i++) {
+ out.push(nodes2[i]);
+ }
+
+ return out;
+ };
+
+ /**
+ * @param {string} htmlOld
+ * @param {string} htmlNew
+ * @returns {number}
+ */
+ this.detectReplacementType = function (htmlOld, htmlNew) {
+ // Convert all HTML tags to uppercase, strip trailing whitespaces
+ var normalizeHtml = function(html) {
+ html = html.replace(/<[^>]+>/g, function(tag) { return tag.toUpperCase(); });
+ html = html.replace(/\s+<\/P>/gi, '').replace(/\s+<\/DIV>/gi, '').replace(/\s+<\/LI>/gi, '');
+ html = html.replace(/\s+- /gi, '
- ').replace(/<\/LI>\s+/gi, '
');
+ html = html.replace(/ /gi, ' ').replace(/\u00A0/g, ' '); // non-breaking spaces
+ return html;
+ };
+ htmlOld = normalizeHtml(htmlOld);
+ htmlNew = normalizeHtml(htmlNew);
+
+ if (htmlOld == htmlNew) {
+ return this.TYPE_REPLACEMENT;
+ }
+
+ var i, foundDiff;
+ for (i = 0, foundDiff = false; i < htmlOld.length && i < htmlNew.length && foundDiff === false; i++) {
+ if (htmlOld[i] != htmlNew[i]) {
+ foundDiff = true;
+ }
+ }
+
+ var remainderOld = htmlOld.substr(i - 1),
+ remainderNew = htmlNew.substr(i - 1),
+ type = this.TYPE_REPLACEMENT;
+
+ if (remainderOld.length > remainderNew.length) {
+ if (remainderOld.substr(remainderOld.length - remainderNew.length) == remainderNew) {
+ type = this.TYPE_DELETION;
+ }
+ } else if (remainderOld.length < remainderNew.length) {
+ if (remainderNew.substr(remainderNew.length - remainderOld.length) == remainderOld) {
+ type = this.TYPE_INSERTION;
+ }
+ }
+
+ return type;
+ };
+
+ this.replaceLines = function (fragment, newHTML, fromLine, toLine) {
+ var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
+ previousHtml = data.previousHtml + '' + data.previousHtmlEndSnippet,
+ previousFragment = this.htmlToFragment(previousHtml),
+ followingHtml = data.followingHtmlStartSnippet + '' + data.followingHtml,
+ followingFragment = this.htmlToFragment(followingHtml),
+ newFragment = this.htmlToFragment(newHTML);
+
+ var merged = this._replaceLinesMergeNodeArrays(previousFragment.childNodes, newFragment.childNodes);
+ merged = this._replaceLinesMergeNodeArrays(merged, followingFragment.childNodes);
+
+ var mergedFragment = document.createDocumentFragment();
+ for (var i = 0; i < merged.length; i++) {
+ mergedFragment.appendChild(merged[i]);
+ }
+
+ var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
+ for (i = 0; i < forgottenTemplates.length; i++) {
+ var el = forgottenTemplates[i];
+ el.parentNode.removeChild(el);
+ }
+
+ return this._serializeDom(mergedFragment, true);
+ };
+
+ this.addCSSClass = function (node, className) {
+ if (node.nodeType != ELEMENT_NODE) {
+ return;
+ }
+ var classes = node.getAttribute('class');
+ classes = (classes ? classes.split(' ') : []);
+ if (classes.indexOf(className) == -1) {
+ classes.push(className);
+ }
+ node.setAttribute('class', classes);
+ };
+
+ this.addDiffMarkup = function (fragment, newHTML, fromLine, toLine, diffFormatterCb) {
+ var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
+ previousHtml = data.previousHtml + '' + data.previousHtmlEndSnippet,
+ previousFragment = this.htmlToFragment(previousHtml),
+ followingHtml = data.followingHtmlStartSnippet + '' + data.followingHtml,
+ followingFragment = this.htmlToFragment(followingHtml),
+ newFragment = this.htmlToFragment(newHTML),
+ oldHTML = data.outerContextStart + data.innerContextStart + data.html +
+ data.innerContextEnd + data.outerContextEnd,
+ oldFragment = this.htmlToFragment(oldHTML),
+ el;
+
+ var diffFragment = diffFormatterCb(oldFragment, newFragment);
+
+ var mergedFragment = document.createDocumentFragment();
+ while (previousFragment.firstChild) {
+ el = previousFragment.firstChild;
+ previousFragment.removeChild(el);
+ mergedFragment.appendChild(el);
+ }
+ while (diffFragment.firstChild) {
+ el = diffFragment.firstChild;
+ diffFragment.removeChild(el);
+ mergedFragment.appendChild(el);
+ }
+ while (followingFragment.firstChild) {
+ el = followingFragment.firstChild;
+ followingFragment.removeChild(el);
+ mergedFragment.appendChild(el);
+ }
+
+ var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
+ for (var i = 0; i < forgottenTemplates.length; i++) {
+ el = forgottenTemplates[i];
+ el.parentNode.removeChild(el);
+ }
+
+ return this._serializeDom(mergedFragment, true);
+ };
+ }
+]);
}());
diff --git a/openslides/motions/static/js/motions/linenumbering.js b/openslides/motions/static/js/motions/linenumbering.js
index 36eb9da66..509c8e548 100644
--- a/openslides/motions/static/js/motions/linenumbering.js
+++ b/openslides/motions/static/js/motions/linenumbering.js
@@ -14,369 +14,398 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
* No constructs like are allowed. CSS-attributes like 'display: block' are ignored.
*/
-.service('lineNumberingService', function () {
- var ELEMENT_NODE = 1,
- TEXT_NODE = 3;
+.service('lineNumberingService', [
+ '$cacheFactory',
+ function ($cacheFactory) {
+ var ELEMENT_NODE = 1,
+ TEXT_NODE = 3;
- this._currentInlineOffset = null;
- this._currentLineNumber = null;
- this._prependLineNumberToFirstText = false;
+ this._currentInlineOffset = null;
+ this._currentLineNumber = null;
+ this._prependLineNumberToFirstText = false;
- this._isInlineElement = function (node) {
- var inlineElements = [
- 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT'
- ];
- return (inlineElements.indexOf(node.nodeName) > -1);
- };
+ var lineNumberCache = $cacheFactory('linenumbering.service');
- this._isOsLineBreakNode = function (node) {
- var isLineBreak = false;
- if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) {
- var classes = node.getAttribute('class').split(' ');
- if (classes.indexOf('os-line-break') > -1) {
- isLineBreak = true;
+ this.djb2hash = function(str) {
+ var hash = 5381, char;
+ for (var i = 0; i < str.length; i++) {
+ char = str.charCodeAt(i);
+ hash = ((hash << 5) + hash) + char;
}
- }
- return isLineBreak;
- };
-
- this._isOsLineNumberNode = function (node) {
- var isLineNumber = false;
- if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'SPAN' && node.hasAttribute('class')) {
- var classes = node.getAttribute('class').split(' ');
- if (classes.indexOf('os-line-number') > -1) {
- isLineNumber = true;
- }
- }
- return isLineNumber;
- };
-
- this._createLineBreak = function () {
- var br = document.createElement('br');
- br.setAttribute('class', 'os-line-break');
- return br;
- };
-
- this._createLineNumber = function () {
- var node = document.createElement('span');
- var lineNumber = this._currentLineNumber;
- this._currentLineNumber++;
- node.setAttribute('class', 'os-line-number line-number-' + lineNumber);
- node.setAttribute('data-line-number', lineNumber + '');
- node.setAttribute('name', 'L' + lineNumber);
- node.setAttribute('contenteditable', 'false');
- node.innerHTML = ' '; // Prevent tinymce from stripping out empty span's
- return node;
- };
-
- /**
- * Splits a TEXT_NODE into an array of TEXT_NODEs and BR-Elements separating them into lines.
- * Each line has a maximum length of 'length', with one exception: spaces are accepted to exceed the length.
- * Otherwise the string is split by the last space or dash in the line.
- *
- * @param node
- * @param length
- * @param highlight
- * @returns Array
- * @private
- */
- this._textNodeToLines = function (node, length, highlight) {
- var out = [],
- currLineStart = 0,
- i = 0,
- firstTextNode = true,
- lastBreakableIndex = null,
- service = this;
- var addLine = function (text, highlight) {
- var node;
- if (typeof highlight === 'undefined') {
- highlight = -1;
- }
- if (firstTextNode) {
- if (highlight == service._currentLineNumber - 1) {
- node = document.createElement('span');
- node.setAttribute('class', 'highlight');
- node.innerHTML = text;
- } else {
- node = document.createTextNode(text);
- }
- firstTextNode = false;
- } else {
- if (service._currentLineNumber == highlight) {
- node = document.createElement('span');
- node.setAttribute('class', 'highlight');
- node.innerHTML = text;
- } else {
- node = document.createTextNode(text);
- }
- out.push(service._createLineBreak());
- out.push(service._createLineNumber());
- }
- out.push(node);
+ return hash.toString();
};
- if (node.nodeValue == "\n") {
- out.push(node);
- } else {
+ this._isInlineElement = function (node) {
+ var inlineElements = [
+ 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT'
+ ];
+ return (inlineElements.indexOf(node.nodeName) > -1);
+ };
- // This happens if a previous inline element exactly stretches to the end of the line
- if (this._currentInlineOffset >= length) {
- out.push(service._createLineBreak());
- out.push(service._createLineNumber());
- this._currentInlineOffset = 0;
- } else if (this._prependLineNumberToFirstText) {
- out.push(service._createLineNumber());
+ this._isOsLineBreakNode = function (node) {
+ var isLineBreak = false;
+ if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) {
+ var classes = node.getAttribute('class').split(' ');
+ if (classes.indexOf('os-line-break') > -1) {
+ isLineBreak = true;
+ }
}
- this._prependLineNumberToFirstText = false;
+ return isLineBreak;
+ };
- while (i < node.nodeValue.length) {
- var lineBreakAt = null;
- if (this._currentInlineOffset >= length) {
- if (lastBreakableIndex !== null) {
- lineBreakAt = lastBreakableIndex;
+ this._isOsLineNumberNode = function (node) {
+ var isLineNumber = false;
+ if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'SPAN' && node.hasAttribute('class')) {
+ var classes = node.getAttribute('class').split(' ');
+ if (classes.indexOf('os-line-number') > -1) {
+ isLineNumber = true;
+ }
+ }
+ return isLineNumber;
+ };
+
+ this._createLineBreak = function () {
+ var br = document.createElement('br');
+ br.setAttribute('class', 'os-line-break');
+ return br;
+ };
+
+ this._createLineNumber = function () {
+ var node = document.createElement('span');
+ var lineNumber = this._currentLineNumber;
+ this._currentLineNumber++;
+ node.setAttribute('class', 'os-line-number line-number-' + lineNumber);
+ node.setAttribute('data-line-number', lineNumber + '');
+ node.setAttribute('name', 'L' + lineNumber);
+ node.setAttribute('contenteditable', 'false');
+ node.innerHTML = ' '; // Prevent tinymce from stripping out empty span's
+ return node;
+ };
+
+ /**
+ * Splits a TEXT_NODE into an array of TEXT_NODEs and BR-Elements separating them into lines.
+ * Each line has a maximum length of 'length', with one exception: spaces are accepted to exceed the length.
+ * Otherwise the string is split by the last space or dash in the line.
+ *
+ * @param node
+ * @param length
+ * @param highlight
+ * @returns Array
+ * @private
+ */
+ this._textNodeToLines = function (node, length, highlight) {
+ var out = [],
+ currLineStart = 0,
+ i = 0,
+ firstTextNode = true,
+ lastBreakableIndex = null,
+ service = this;
+ var addLine = function (text, highlight) {
+ var node;
+ if (typeof highlight === 'undefined') {
+ highlight = -1;
+ }
+ if (firstTextNode) {
+ if (highlight == service._currentLineNumber - 1) {
+ node = document.createElement('span');
+ node.setAttribute('class', 'highlight');
+ node.innerHTML = text;
} else {
- lineBreakAt = i - 1;
+ node = document.createTextNode(text);
}
+ firstTextNode = false;
+ } else {
+ if (service._currentLineNumber == highlight) {
+ node = document.createElement('span');
+ node.setAttribute('class', 'highlight');
+ node.innerHTML = text;
+ } else {
+ node = document.createTextNode(text);
+ }
+ out.push(service._createLineBreak());
+ out.push(service._createLineNumber());
}
- if (lineBreakAt !== null && node.nodeValue[i] != ' ') {
- var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
- addLine(currLine, highlight);
+ out.push(node);
+ };
- currLineStart = lineBreakAt + 1;
- this._currentInlineOffset = i - lineBreakAt - 1;
- lastBreakableIndex = null;
- }
+ if (node.nodeValue == "\n") {
+ out.push(node);
+ } else {
- if (node.nodeValue[i] == ' ' || node.nodeValue[i] == '-') {
- lastBreakableIndex = i;
- }
-
- this._currentInlineOffset++;
- i++;
-
- }
- addLine(node.nodeValue.substring(currLineStart), highlight);
- }
- return out;
- };
-
-
- /**
- * Moves line breaking and line numbering markup before inline elements
- *
- * @param innerNode
- * @param outerNode
- * @private
- */
- this._moveLeadingLineBreaksToOuterNode = function (innerNode, outerNode) {
- if (this._isInlineElement(innerNode)) {
- if (this._isOsLineBreakNode(innerNode.firstChild)) {
- var br = innerNode.firstChild;
- innerNode.removeChild(br);
- outerNode.appendChild(br);
- }
- if (this._isOsLineNumberNode(innerNode.firstChild)) {
- var span = innerNode.firstChild;
- innerNode.removeChild(span);
- outerNode.appendChild(span);
- }
- }
- };
-
- this._lengthOfFirstInlineWord = function (node) {
- if (!node.firstChild) {
- return 0;
- }
- if (node.firstChild.nodeType == TEXT_NODE) {
- var parts = node.firstChild.nodeValue.split(' ');
- return parts[0].length;
- } else {
- return this._lengthOfFirstInlineWord(node.firstChild);
- }
- };
-
- this._insertLineNumbersToInlineNode = function (node, length, highlight) {
- var oldChildren = [], i;
- for (i = 0; i < node.childNodes.length; i++) {
- oldChildren.push(node.childNodes[i]);
- }
-
- while (node.firstChild) {
- node.removeChild(node.firstChild);
- }
-
- for (i = 0; i < oldChildren.length; i++) {
- if (oldChildren[i].nodeType == TEXT_NODE) {
- var ret = this._textNodeToLines(oldChildren[i], length, highlight);
- for (var j = 0; j < ret.length; j++) {
- node.appendChild(ret[j]);
- }
- } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
- var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
- overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
- if (overlength && this._isInlineElement(oldChildren[i])) {
+ // This happens if a previous inline element exactly stretches to the end of the line
+ if (this._currentInlineOffset >= length) {
+ out.push(service._createLineBreak());
+ out.push(service._createLineNumber());
this._currentInlineOffset = 0;
- node.appendChild(this._createLineBreak());
- node.appendChild(this._createLineNumber());
+ } else if (this._prependLineNumberToFirstText) {
+ out.push(service._createLineNumber());
}
- var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
- this._moveLeadingLineBreaksToOuterNode(changedNode, node);
- node.appendChild(changedNode);
- } else {
- throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
- }
- }
+ this._prependLineNumberToFirstText = false;
- return node;
- };
-
- this._calcBlockNodeLength = function (node, oldLength) {
- var newLength = oldLength;
- switch (node.nodeName) {
- case 'LI':
- newLength -= 5;
- break;
- case 'BLOCKQUOTE':
- newLength -= 20;
- break;
- case 'DIV':
- case 'P':
- var styles = node.getAttribute("style"),
- padding = 0;
- if (styles) {
- var leftpad = styles.split("padding-left:");
- if (leftpad.length > 1) {
- leftpad = parseInt(leftpad[1]);
- padding += leftpad;
+ while (i < node.nodeValue.length) {
+ var lineBreakAt = null;
+ if (this._currentInlineOffset >= length) {
+ if (lastBreakableIndex !== null) {
+ lineBreakAt = lastBreakableIndex;
+ } else {
+ lineBreakAt = i - 1;
+ }
}
- var rightpad = styles.split("padding-right:");
- if (rightpad.length > 1) {
- rightpad = parseInt(rightpad[1]);
- padding += rightpad;
+ if (lineBreakAt !== null && node.nodeValue[i] != ' ') {
+ var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
+ addLine(currLine, highlight);
+
+ currLineStart = lineBreakAt + 1;
+ this._currentInlineOffset = i - lineBreakAt - 1;
+ lastBreakableIndex = null;
}
- newLength -= (padding / 5);
+
+ if (node.nodeValue[i] == ' ' || node.nodeValue[i] == '-') {
+ lastBreakableIndex = i;
+ }
+
+ this._currentInlineOffset++;
+ i++;
+
}
- break;
- case 'H1':
- newLength *= 0.5;
- break;
- case 'H2':
- newLength *= 0.66;
- break;
- case 'H3':
- newLength *= 0.66;
- break;
- }
- return Math.ceil(newLength);
- };
+ addLine(node.nodeValue.substring(currLineStart), highlight);
+ }
+ return out;
+ };
- this._insertLineNumbersToBlockNode = function (node, length, highlight) {
- this._currentInlineOffset = 0;
- this._prependLineNumberToFirstText = true;
- var oldChildren = [], i;
- for (i = 0; i < node.childNodes.length; i++) {
- oldChildren.push(node.childNodes[i]);
- }
-
- while (node.firstChild) {
- node.removeChild(node.firstChild);
- }
-
- for (i = 0; i < oldChildren.length; i++) {
- if (oldChildren[i].nodeType == TEXT_NODE) {
- var ret = this._textNodeToLines(oldChildren[i], length, highlight);
- for (var j = 0; j < ret.length; j++) {
- node.appendChild(ret[j]);
+ /**
+ * Moves line breaking and line numbering markup before inline elements
+ *
+ * @param innerNode
+ * @param outerNode
+ * @private
+ */
+ this._moveLeadingLineBreaksToOuterNode = function (innerNode, outerNode) {
+ if (this._isInlineElement(innerNode)) {
+ if (this._isOsLineBreakNode(innerNode.firstChild)) {
+ var br = innerNode.firstChild;
+ innerNode.removeChild(br);
+ outerNode.appendChild(br);
}
- } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
- var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
- overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
- if (overlength && this._isInlineElement(oldChildren[i])) {
- this._currentInlineOffset = 0;
- node.appendChild(this._createLineBreak());
- node.appendChild(this._createLineNumber());
+ if (this._isOsLineNumberNode(innerNode.firstChild)) {
+ var span = innerNode.firstChild;
+ innerNode.removeChild(span);
+ outerNode.appendChild(span);
}
- var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
- this._moveLeadingLineBreaksToOuterNode(changedNode, node);
- node.appendChild(changedNode);
+ }
+ };
+
+ this._lengthOfFirstInlineWord = function (node) {
+ if (!node.firstChild) {
+ return 0;
+ }
+ if (node.firstChild.nodeType == TEXT_NODE) {
+ var parts = node.firstChild.nodeValue.split(' ');
+ return parts[0].length;
} else {
- throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
+ return this._lengthOfFirstInlineWord(node.firstChild);
}
- }
+ };
- this._currentInlineOffset = 0;
- this._prependLineNumberToFirstText = true;
+ this._insertLineNumbersToInlineNode = function (node, length, highlight) {
+ var oldChildren = [], i;
+ for (i = 0; i < node.childNodes.length; i++) {
+ oldChildren.push(node.childNodes[i]);
+ }
- return node;
- };
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
- this._insertLineNumbersToNode = function (node, length, highlight) {
- if (node.nodeType !== ELEMENT_NODE) {
- throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue;
- }
- if (this._isInlineElement(node)) {
- return this._insertLineNumbersToInlineNode(node, length, highlight);
- } else {
- var newLength = this._calcBlockNodeLength(node, length);
- return this._insertLineNumbersToBlockNode(node, newLength, highlight);
- }
- };
+ for (i = 0; i < oldChildren.length; i++) {
+ if (oldChildren[i].nodeType == TEXT_NODE) {
+ var ret = this._textNodeToLines(oldChildren[i], length, highlight);
+ for (var j = 0; j < ret.length; j++) {
+ node.appendChild(ret[j]);
+ }
+ } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
+ var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
+ overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
+ if (overlength && this._isInlineElement(oldChildren[i])) {
+ this._currentInlineOffset = 0;
+ node.appendChild(this._createLineBreak());
+ node.appendChild(this._createLineNumber());
+ }
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
+ this._moveLeadingLineBreaksToOuterNode(changedNode, node);
+ node.appendChild(changedNode);
+ } else {
+ throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
+ }
+ }
- this._stripLineNumbers = function (node) {
+ return node;
+ };
- for (var i = 0; i < node.childNodes.length; i++) {
- if (this._isOsLineBreakNode(node.childNodes[i]) || this._isOsLineNumberNode(node.childNodes[i])) {
- node.removeChild(node.childNodes[i]);
- i--;
+ this._calcBlockNodeLength = function (node, oldLength) {
+ var newLength = oldLength;
+ switch (node.nodeName) {
+ case 'LI':
+ newLength -= 5;
+ break;
+ case 'BLOCKQUOTE':
+ newLength -= 20;
+ break;
+ case 'DIV':
+ case 'P':
+ var styles = node.getAttribute("style"),
+ padding = 0;
+ if (styles) {
+ var leftpad = styles.split("padding-left:");
+ if (leftpad.length > 1) {
+ leftpad = parseInt(leftpad[1]);
+ padding += leftpad;
+ }
+ var rightpad = styles.split("padding-right:");
+ if (rightpad.length > 1) {
+ rightpad = parseInt(rightpad[1]);
+ padding += rightpad;
+ }
+ newLength -= (padding / 5);
+ }
+ break;
+ case 'H1':
+ newLength *= 0.5;
+ break;
+ case 'H2':
+ newLength *= 0.66;
+ break;
+ case 'H3':
+ newLength *= 0.66;
+ break;
+ }
+ return Math.ceil(newLength);
+ };
+
+ this._insertLineNumbersToBlockNode = function (node, length, highlight) {
+ this._currentInlineOffset = 0;
+ this._prependLineNumberToFirstText = true;
+
+ var oldChildren = [], i;
+ for (i = 0; i < node.childNodes.length; i++) {
+ oldChildren.push(node.childNodes[i]);
+ }
+
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+
+ for (i = 0; i < oldChildren.length; i++) {
+ if (oldChildren[i].nodeType == TEXT_NODE) {
+ var ret = this._textNodeToLines(oldChildren[i], length, highlight);
+ for (var j = 0; j < ret.length; j++) {
+ node.appendChild(ret[j]);
+ }
+ } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
+ var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
+ overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
+ if (overlength && this._isInlineElement(oldChildren[i])) {
+ this._currentInlineOffset = 0;
+ node.appendChild(this._createLineBreak());
+ node.appendChild(this._createLineNumber());
+ }
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
+ this._moveLeadingLineBreaksToOuterNode(changedNode, node);
+ node.appendChild(changedNode);
+ } else {
+ throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
+ }
+ }
+
+ this._currentInlineOffset = 0;
+ this._prependLineNumberToFirstText = true;
+
+ return node;
+ };
+
+ this._insertLineNumbersToNode = function (node, length, highlight) {
+ if (node.nodeType !== ELEMENT_NODE) {
+ throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue;
+ }
+ if (this._isInlineElement(node)) {
+ return this._insertLineNumbersToInlineNode(node, length, highlight);
} else {
- this._stripLineNumbers(node.childNodes[i]);
+ var newLength = this._calcBlockNodeLength(node, length);
+ return this._insertLineNumbersToBlockNode(node, newLength, highlight);
}
- }
- };
+ };
- this._nodesToHtml = function (nodes) {
- var root = document.createElement('div');
- for (var i in nodes) {
- if (nodes.hasOwnProperty(i)) {
- root.appendChild(nodes[i]);
+ this._stripLineNumbers = function (node) {
+
+ for (var i = 0; i < node.childNodes.length; i++) {
+ if (this._isOsLineBreakNode(node.childNodes[i]) || this._isOsLineNumberNode(node.childNodes[i])) {
+ node.removeChild(node.childNodes[i]);
+ i--;
+ } else {
+ this._stripLineNumbers(node.childNodes[i]);
+ }
}
- }
- return root.innerHTML;
- };
+ };
- this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) {
- var root = document.createElement('div');
- root.innerHTML = html;
+ this._nodesToHtml = function (nodes) {
+ var root = document.createElement('div');
+ for (var i in nodes) {
+ if (nodes.hasOwnProperty(i)) {
+ root.appendChild(nodes[i]);
+ }
+ }
+ return root.innerHTML;
+ };
- this._currentInlineOffset = 0;
- if (firstLine) {
- this._currentLineNumber = firstLine;
- } else {
- this._currentLineNumber = 1;
- }
- this._prependLineNumberToFirstText = true;
+ this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) {
+ var root = document.createElement('div');
+ root.innerHTML = html;
- return this._insertLineNumbersToNode(root, lineLength, highlight);
- };
+ this._currentInlineOffset = 0;
+ if (firstLine) {
+ this._currentLineNumber = firstLine;
+ } else {
+ this._currentLineNumber = 1;
+ }
+ this._prependLineNumberToFirstText = true;
- this.insertLineNumbers = function (html, lineLength, highlight, callback, firstLine) {
- var newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
+ return this._insertLineNumbersToNode(root, lineLength, highlight);
+ };
- if (callback) {
- callback();
- }
+ this.insertLineNumbers = function (html, lineLength, highlight, callback, firstLine) {
+ var newHtml, newRoot;
- return newRoot.innerHTML;
- };
+ if (highlight > 0) {
+ // Caching versions with highlighted line numbers is probably not worth it
+ newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
+ newHtml = newRoot.innerHTML;
+ } else {
+ var cacheKey = this.djb2hash(html);
+ newHtml = lineNumberCache.get(cacheKey);
- this.stripLineNumbers = function (html) {
- var root = document.createElement('div');
- root.innerHTML = html;
- this._stripLineNumbers(root);
- return root.innerHTML;
- };
-});
+ if (angular.isUndefined(newHtml)) {
+ newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
+ newHtml = newRoot.innerHTML;
+ lineNumberCache.put(cacheKey, newHtml);
+ }
+ }
+
+ if (callback) {
+ callback();
+ }
+
+ return newHtml;
+ };
+
+ this.stripLineNumbers = function (html) {
+ var root = document.createElement('div');
+ root.innerHTML = html;
+ this._stripLineNumbers(root);
+ return root.innerHTML;
+ };
+ }
+]);
}());