OpenSlides/openslides/motions/static/js/motions/diff.js
2016-08-20 11:18:17 +02:00

408 lines
16 KiB
JavaScript

(function () {
"use strict";
angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumbering'])
.service('diffService', function (lineNumberingService) {
var ELEMENT_NODE = 1,
TEXT_NODE = 3,
DOCUMENT_FRAGMENT_NODE = 11;
this.getLineNumberNode = function(fragment, lineNumber) {
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
};
this._getNodeContextTrace = function(node) {
var context = [],
currNode = node;
while (currNode) {
context.unshift(currNode);
currNode = currNode.parentNode;
}
return context;
};
this._insertInternalLineMarkers = function(fragment) {
if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
// Prevent duplicate calls
return;
}
var lineNumbers = fragment.querySelectorAll('span.os-line-number');
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;
}
var 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);
}
};
/*
* 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];
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 = '<BR';
for (i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
br += " " + attr.name + "=\"" + attr.value + "\"";
}
return 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);
}
} 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]))) {
html += this._serializeDom(node.childNodes[i], stripLineNumbers);
}
}
}
if (!found) {
console.trace();
throw "Inconsistency or invalid call of this function detected";
}
return 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 '';
}
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";
}
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
*
* 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</LI><LI>Line3</LI><LI>Line 4 <br>)
* - 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. <DIV><UL> in the case of the multiple LIs)
* - outerContectEnd: An HTML string that closes all necessary tags from the ancestor element (e.g. </UL></DIV>
* - innerContextStart: A string that opens all necessary tags between the ancestor
* and the beginning of the selection (e.g. <LI>)
* - innerContextEnd: A string that closes all tags after the end of the selection to the ancestor (e.g. </LI>)
* - 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) {
this._insertInternalLineMarkers(fragment);
var fromLineNode = this.getLineNumberNode(fragment, fromLine),
toLineNode = this.getLineNumberNode(fragment, toLine),
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 = '';
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 {
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;
}
}
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) {
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._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]);
}
out.push(nodes1[nodes1.length - 1]);
out.push(nodes2[0]);
for (i = 1; i < nodes2.length; i++) {
out.push(nodes2[i]);
}
/*
if (node1.nodeName != node2.nodeName) {
return null;
}
var newNode = node1.ownerDocument.createElement(node1.nodeName);
for (var i = 0; i < node1.attributes.length; i++) {
var attr = node1.attributes[i];
newNode.setAttribute(attr.name, attr.value);
}
return newNode;
*/
return out;
};
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),
child;
var merged = document.createDocumentFragment();
while (previousFragment.children.length > 0) {
child = previousFragment.children[0];
previousFragment.removeChild(child);
merged.appendChild(child);
}
while (newFragment.children.length > 0) {
child = newFragment.children[0];
newFragment.removeChild(child);
merged.appendChild(child);
}
while (followingFragment.children.length > 0) {
child = followingFragment.children[0];
followingFragment.removeChild(child);
merged.appendChild(child);
}
//var merged = this._replaceLinesAttemptMerge(lastOfPrevious, firstOfReplaced);
return this._serializeDom(merged, true);
};
});
}());