From 36e519a798a0ed210f75499523569efb72b388d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Ho=CC=88=C3=9Fl?= Date: Thu, 27 Oct 2016 16:49:38 +0200 Subject: [PATCH] Caching results of insertLineNumbers and extractRangeByLineNumbers --- openslides/motions/static/js/motions/diff.js | 1169 +++++++++-------- .../static/js/motions/linenumbering.js | 671 +++++----- 2 files changed, 942 insertions(+), 898 deletions(-) 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 += ''; - } - - 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 += ''; + } - /** - * 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 += ''; - } - 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
  • 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) { - 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 = 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 = '' + 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 = 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 += ''; + } + 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 = 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 = '' + 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 = 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; + }; + } +]); }());