Caching results of insertLineNumbers and extractRangeByLineNumbers

This commit is contained in:
Tobias Hößl 2016-10-27 16:49:38 +02:00
parent 4d48f2d8a5
commit 36e519a798
2 changed files with 942 additions and 898 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,369 +14,398 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
* No constructs like <a...><div></div></a> are allowed. CSS-attributes like 'display: block' are ignored. * No constructs like <a...><div></div></a> are allowed. CSS-attributes like 'display: block' are ignored.
*/ */
.service('lineNumberingService', function () { .service('lineNumberingService', [
var ELEMENT_NODE = 1, '$cacheFactory',
TEXT_NODE = 3; function ($cacheFactory) {
var ELEMENT_NODE = 1,
TEXT_NODE = 3;
this._currentInlineOffset = null; this._currentInlineOffset = null;
this._currentLineNumber = null; this._currentLineNumber = null;
this._prependLineNumberToFirstText = false; this._prependLineNumberToFirstText = false;
this._isInlineElement = function (node) { var lineNumberCache = $cacheFactory('linenumbering.service');
var inlineElements = [
'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT'
];
return (inlineElements.indexOf(node.nodeName) > -1);
};
this._isOsLineBreakNode = function (node) { this.djb2hash = function(str) {
var isLineBreak = false; var hash = 5381, char;
if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) { for (var i = 0; i < str.length; i++) {
var classes = node.getAttribute('class').split(' '); char = str.charCodeAt(i);
if (classes.indexOf('os-line-break') > -1) { hash = ((hash << 5) + hash) + char;
isLineBreak = true;
} }
} return hash.toString();
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 = '&nbsp;'; // 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);
}; };
if (node.nodeValue == "\n") { this._isInlineElement = function (node) {
out.push(node); var inlineElements = [
} else { '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 this._isOsLineBreakNode = function (node) {
if (this._currentInlineOffset >= length) { var isLineBreak = false;
out.push(service._createLineBreak()); if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) {
out.push(service._createLineNumber()); var classes = node.getAttribute('class').split(' ');
this._currentInlineOffset = 0; if (classes.indexOf('os-line-break') > -1) {
} else if (this._prependLineNumberToFirstText) { isLineBreak = true;
out.push(service._createLineNumber()); }
} }
this._prependLineNumberToFirstText = false; return isLineBreak;
};
while (i < node.nodeValue.length) { this._isOsLineNumberNode = function (node) {
var lineBreakAt = null; var isLineNumber = false;
if (this._currentInlineOffset >= length) { if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'SPAN' && node.hasAttribute('class')) {
if (lastBreakableIndex !== null) { var classes = node.getAttribute('class').split(' ');
lineBreakAt = lastBreakableIndex; 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 = '&nbsp;'; // 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 { } 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] != ' ') { out.push(node);
var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1); };
addLine(currLine, highlight);
currLineStart = lineBreakAt + 1; if (node.nodeValue == "\n") {
this._currentInlineOffset = i - lineBreakAt - 1; out.push(node);
lastBreakableIndex = null; } else {
}
if (node.nodeValue[i] == ' ' || node.nodeValue[i] == '-') { // This happens if a previous inline element exactly stretches to the end of the line
lastBreakableIndex = i; if (this._currentInlineOffset >= length) {
} out.push(service._createLineBreak());
out.push(service._createLineNumber());
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._currentInlineOffset = 0; this._currentInlineOffset = 0;
node.appendChild(this._createLineBreak()); } else if (this._prependLineNumberToFirstText) {
node.appendChild(this._createLineNumber()); out.push(service._createLineNumber());
} }
var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight); this._prependLineNumberToFirstText = false;
this._moveLeadingLineBreaksToOuterNode(changedNode, node);
node.appendChild(changedNode);
} else {
throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
}
}
return node; while (i < node.nodeValue.length) {
}; var lineBreakAt = null;
if (this._currentInlineOffset >= length) {
this._calcBlockNodeLength = function (node, oldLength) { if (lastBreakableIndex !== null) {
var newLength = oldLength; lineBreakAt = lastBreakableIndex;
switch (node.nodeName) { } else {
case 'LI': lineBreakAt = i - 1;
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 (lineBreakAt !== null && node.nodeValue[i] != ' ') {
if (rightpad.length > 1) { var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
rightpad = parseInt(rightpad[1]); addLine(currLine, highlight);
padding += rightpad;
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; addLine(node.nodeValue.substring(currLineStart), highlight);
case 'H1': }
newLength *= 0.5; return out;
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++) { * Moves line breaking and line numbering markup before inline elements
oldChildren.push(node.childNodes[i]); *
} * @param innerNode
* @param outerNode
while (node.firstChild) { * @private
node.removeChild(node.firstChild); */
} this._moveLeadingLineBreaksToOuterNode = function (innerNode, outerNode) {
if (this._isInlineElement(innerNode)) {
for (i = 0; i < oldChildren.length; i++) { if (this._isOsLineBreakNode(innerNode.firstChild)) {
if (oldChildren[i].nodeType == TEXT_NODE) { var br = innerNode.firstChild;
var ret = this._textNodeToLines(oldChildren[i], length, highlight); innerNode.removeChild(br);
for (var j = 0; j < ret.length; j++) { outerNode.appendChild(br);
node.appendChild(ret[j]);
} }
} else if (oldChildren[i].nodeType == ELEMENT_NODE) { if (this._isOsLineNumberNode(innerNode.firstChild)) {
var firstword = this._lengthOfFirstInlineWord(oldChildren[i]), var span = innerNode.firstChild;
overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0); innerNode.removeChild(span);
if (overlength && this._isInlineElement(oldChildren[i])) { outerNode.appendChild(span);
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);
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 { } else {
throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i]; return this._lengthOfFirstInlineWord(node.firstChild);
} }
} };
this._currentInlineOffset = 0; this._insertLineNumbersToInlineNode = function (node, length, highlight) {
this._prependLineNumberToFirstText = true; 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) { for (i = 0; i < oldChildren.length; i++) {
if (node.nodeType !== ELEMENT_NODE) { if (oldChildren[i].nodeType == TEXT_NODE) {
throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue; var ret = this._textNodeToLines(oldChildren[i], length, highlight);
} for (var j = 0; j < ret.length; j++) {
if (this._isInlineElement(node)) { node.appendChild(ret[j]);
return this._insertLineNumbersToInlineNode(node, length, highlight); }
} else { } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
var newLength = this._calcBlockNodeLength(node, length); var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
return this._insertLineNumbersToBlockNode(node, newLength, highlight); 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++) { this._calcBlockNodeLength = function (node, oldLength) {
if (this._isOsLineBreakNode(node.childNodes[i]) || this._isOsLineNumberNode(node.childNodes[i])) { var newLength = oldLength;
node.removeChild(node.childNodes[i]); switch (node.nodeName) {
i--; 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 { } else {
this._stripLineNumbers(node.childNodes[i]); var newLength = this._calcBlockNodeLength(node, length);
return this._insertLineNumbersToBlockNode(node, newLength, highlight);
} }
} };
};
this._nodesToHtml = function (nodes) { this._stripLineNumbers = function (node) {
var root = document.createElement('div');
for (var i in nodes) { for (var i = 0; i < node.childNodes.length; i++) {
if (nodes.hasOwnProperty(i)) { if (this._isOsLineBreakNode(node.childNodes[i]) || this._isOsLineNumberNode(node.childNodes[i])) {
root.appendChild(nodes[i]); node.removeChild(node.childNodes[i]);
i--;
} else {
this._stripLineNumbers(node.childNodes[i]);
}
} }
} };
return root.innerHTML;
};
this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) { this._nodesToHtml = function (nodes) {
var root = document.createElement('div'); var root = document.createElement('div');
root.innerHTML = html; for (var i in nodes) {
if (nodes.hasOwnProperty(i)) {
root.appendChild(nodes[i]);
}
}
return root.innerHTML;
};
this._currentInlineOffset = 0; this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) {
if (firstLine) { var root = document.createElement('div');
this._currentLineNumber = firstLine; root.innerHTML = html;
} else {
this._currentLineNumber = 1;
}
this._prependLineNumberToFirstText = true;
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) { return this._insertLineNumbersToNode(root, lineLength, highlight);
var newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine); };
if (callback) { this.insertLineNumbers = function (html, lineLength, highlight, callback, firstLine) {
callback(); 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) { if (angular.isUndefined(newHtml)) {
var root = document.createElement('div'); newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
root.innerHTML = html; newHtml = newRoot.innerHTML;
this._stripLineNumbers(root); lineNumberCache.put(cacheKey, newHtml);
return root.innerHTML; }
}; }
});
if (callback) {
callback();
}
return newHtml;
};
this.stripLineNumbers = function (html) {
var root = document.createElement('div');
root.innerHTML = html;
this._stripLineNumbers(root);
return root.innerHTML;
};
}
]);
}()); }());