From 2958a401adfee3f2320b682722ad1a5ecde747e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Ho=CC=88=C3=9Fl?= Date: Sun, 6 Nov 2016 14:58:22 +0100 Subject: [PATCH] Showing Diff inline --- openslides/core/static/css/app.css | 8 + openslides/core/static/js/core/pdf.js | 6 +- openslides/motions/static/js/motions/base.js | 31 +- openslides/motions/static/js/motions/diff.js | 317 +++++++++++++++++- .../static/js/motions/linenumbering.js | 20 +- .../motions/motion-detail/view-diff.html | 2 +- tests/karma/motions/diff.service.test.js | 49 +++ .../motions/linenumbering.service.test.js | 23 ++ 8 files changed, 414 insertions(+), 42 deletions(-) diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index a8e30f1b4..b63f89f8c 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -399,6 +399,14 @@ img { .motion-text li { margin-left: 30px; } +.motion-text ins { + color: green; + text-decoration: underline; +} +.motion-text del { + color: red; + text-decoration: line-through; +} .motion-text.line-numbers-outside { padding-left: 40px; position: relative; diff --git a/openslides/core/static/js/core/pdf.js b/openslides/core/static/js/core/pdf.js index ea5aad6e1..b5a8c4ec4 100644 --- a/openslides/core/static/js/core/pdf.js +++ b/openslides/core/static/js/core/pdf.js @@ -394,7 +394,9 @@ angular.module('OpenSlidesApp.core.pdf', []) "h4": ["font-size:24"], "h5": ["font-size:22"], "h6": ["font-size:20"], - "a": ["color:blue", "text-decoration:underline"] + "a": ["color:blue", "text-decoration:underline"], + "del": ["color:red", "text-decoration:line-through"], + "ins": ["color:green", "text-decoration:underline"] }, classStyles = { "delete": ["color:red", "text-decoration:line-through"], @@ -541,6 +543,8 @@ angular.module('OpenSlidesApp.core.pdf', []) case "u": case "em": case "i": + case "ins": + case "del": parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode); break; case "table": diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index 66ad23ae3..451eee99a 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -280,7 +280,7 @@ angular.module('OpenSlidesApp.motions', [ text = ''; for (var i = 0; i < changes.length; i++) { text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight); - text += changes[i].format(this, versionId, highlight); + text += changes[i].getDiff(this, versionId); } text += this.getTextRemainderAfterLastChangeRecommendation(versionId, changes); break; @@ -584,37 +584,16 @@ angular.module('OpenSlidesApp.motions', [ saveStatus: function() { this.DSSave(); }, - format: function(motion, version, highlight) { + getDiff: function(motion, version, highlight) { var lineLength = Config.get('motions_line_length').value, html = lineNumberingService.insertLineNumbers(motion.getVersion(version).text, lineLength); var data = diffService.extractRangeByLineNumbers(html, this.line_from, this.line_to), oldText = data.outerContextStart + data.innerContextStart + - data.html + data.innerContextEnd + data.outerContextEnd, - oldTextWithBreaks = lineNumberingService.insertLineNumbersNode(oldText, lineLength, highlight, this.line_from), - newTextWithBreaks = lineNumberingService.insertLineNumbersNode(this.text, lineLength, null, this.line_from); + data.html + data.innerContextEnd + data.outerContextEnd; - for (var i = 0; i < oldTextWithBreaks.childNodes.length; i++) { - diffService.addCSSClass(oldTextWithBreaks.childNodes[i], 'delete'); - } - for (i = 0; i < newTextWithBreaks.childNodes.length; i++) { - diffService.addCSSClass(newTextWithBreaks.childNodes[i], 'insert'); - } - - var mergedFragment = document.createDocumentFragment(), - el; - while (oldTextWithBreaks.firstChild) { - el = oldTextWithBreaks.firstChild; - oldTextWithBreaks.removeChild(el); - mergedFragment.appendChild(el); - } - while (newTextWithBreaks.firstChild) { - el = newTextWithBreaks.firstChild; - newTextWithBreaks.removeChild(el); - mergedFragment.appendChild(el); - } - - return diffService._serializeDom(mergedFragment); + var diff = diffService.diff(oldText, this.text, lineLength, this.line_from); + return lineNumberingService.insertLineNumbers(diff, lineLength, highlight, null, this.line_from); }, getType: function(original_full_html) { var lineLength = Config.get('motions_line_length').value, diff --git a/openslides/motions/static/js/motions/diff.js b/openslides/motions/static/js/motions/diff.js index e9b4bfc5b..21403b479 100644 --- a/openslides/motions/static/js/motions/diff.js +++ b/openslides/motions/static/js/motions/diff.js @@ -524,22 +524,49 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi return out; }; + /** + * + * @param {string} html + * @returns {string} + * @private + */ + this._normalizeHtmlForDiff = function (html) { + // Convert all HTML tags to uppercase, strip trailing whitespaces + html = html.replace(/<[^>]+>/g, function (tag) { + return tag.toUpperCase(); + }); + + var entities = { + ' ': ' ', + '–': '-', + 'ä': 'ä', + 'ö': 'ö', + 'ü': 'ü', + 'Ä': 'Ä', + 'Ö': 'Ö', + 'Ü': 'Ü', + 'ß': 'ß' + }; + + 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(/\u00A0/g, ' '); + html = html.replace(/\u2013/g, '-'); + for (var ent in entities) { + html = html.replace(new RegExp(ent, 'g'), entities[ent]); + } + + return html; + }; + /** * @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); + htmlOld = this._normalizeHtmlForDiff(htmlOld); + htmlNew = this._normalizeHtmlForDiff(htmlNew); if (htmlOld == htmlNew) { return this.TYPE_REPLACEMENT; @@ -609,7 +636,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi if (classes.indexOf(className) == -1) { classes.push(className); } - node.setAttribute('class', classes); + node.setAttribute('class', classes.join(' ')); }; this.addDiffMarkup = function (originalHTML, newHTML, fromLine, toLine, diffFormatterCb) { @@ -651,6 +678,274 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi return this._serializeDom(mergedFragment, true); }; + + /** + * Adapted from http://ejohn.org/projects/javascript-diff-algorithm/ + * by John Resig, MIT License + * @param {array} oldArr + * @param {array} newArr + * @returns {object} + */ + this._diff = function (oldArr, newArr) { + var ns = {}, + os = {}, + i; + + for (i = 0; i < newArr.length; i++) { + if (ns[newArr[i]] === undefined) + ns[newArr[i]] = {rows: [], o: null}; + ns[newArr[i]].rows.push(i); + } + + for (i = 0; i < oldArr.length; i++) { + if (os[oldArr[i]] === undefined) + os[oldArr[i]] = {rows: [], n: null}; + os[oldArr[i]].rows.push(i); + } + + for (i in ns) { + if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { + newArr[ns[i].rows[0]] = {text: newArr[ns[i].rows[0]], row: os[i].rows[0]}; + oldArr[os[i].rows[0]] = {text: oldArr[os[i].rows[0]], row: ns[i].rows[0]}; + } + } + + for (i = 0; i < newArr.length - 1; i++) { + if (newArr[i].text !== null && newArr[i + 1].text === undefined && newArr[i].row + 1 < oldArr.length && + oldArr[newArr[i].row + 1].text === undefined && newArr[i + 1] == oldArr[newArr[i].row + 1]) { + newArr[i + 1] = {text: newArr[i + 1], row: newArr[i].row + 1}; + oldArr[newArr[i].row + 1] = {text: oldArr[newArr[i].row + 1], row: i + 1}; + } + } + + for (i = newArr.length - 1; i > 0; i--) { + if (newArr[i].text !== null && newArr[i - 1].text === undefined && newArr[i].row > 0 && + oldArr[newArr[i].row - 1].text === undefined && newArr[i - 1] == oldArr[newArr[i].row - 1]) { + newArr[i - 1] = {text: newArr[i - 1], row: newArr[i].row - 1}; + oldArr[newArr[i].row - 1] = {text: oldArr[newArr[i].row - 1], row: i - 1}; + } + } + + return {o: oldArr, n: newArr}; + }; + + this._tokenizeHtml = function (str) { + var splitArrayEntries = function (arr, by, prepend) { + var newArr = []; + for (var i = 0; i < arr.length; i++) { + if (arr[i][0] == '<' && (by == " " || by == "\n")) { + // Don't split HTML tags + newArr.push(arr[i]); + continue; + } + + var parts = arr[i].split(by); + if (parts.length == 1) { + newArr.push(arr[i]); + } else { + var j; + if (prepend) { + if (parts[0] !== '') { + newArr.push(parts[0]); + } + for (j = 1; j < parts.length; j++) { + newArr.push(by + parts[j]); + } + } else { + for (j = 0; j < parts.length - 1; j++) { + newArr.push(parts[j] + by); + } + if (parts[parts.length - 1] !== '') { + newArr.push(parts[parts.length - 1]); + } + } + } + } + return newArr; + }; + var arr = splitArrayEntries([str], '<', true); + arr = splitArrayEntries(arr, '>', false); + arr = splitArrayEntries(arr, " ", false); + arr = splitArrayEntries(arr, "\n", false); + return arr; + }; + + this._outputcharcode = function (pre, str) { + var arr = []; + for (var i = 0; i < str.length; i++) { + arr.push(str.charCodeAt(i)); + } + console.log(str, pre, arr); + }; + + /** + * @param {string} oldStr + * @param {string} newStr + * @returns {string} + */ + this._diffString = function (oldStr, newStr) { + oldStr = this._normalizeHtmlForDiff(oldStr.replace(/\s+$/, '').replace(/^\s+/, '')); + newStr = this._normalizeHtmlForDiff(newStr.replace(/\s+$/, '').replace(/^\s+/, '')); + + var out = this._diff(this._tokenizeHtml(oldStr), this._tokenizeHtml(newStr)); + var str = ""; + var i; + + if (out.n.length === 0) { + for (i = 0; i < out.o.length; i++) { + //this._outputcharcode('del', out.o[i]); + str += '' + out.o[i] + ""; + } + } else { + if (out.n[0].text === undefined) { + for (var k = 0; k < out.o.length && out.o[k].text === undefined; k++) { + //this._outputcharcode('del', out.o[k]); + str += '' + out.o[k] + ""; + } + } + + for (i = 0; i < out.n.length; i++) { + if (out.n[i].text === undefined) { + //this._outputcharcode('ins', out.n[i]); + str += '' + out.n[i] + ""; + } else { + var pre = ""; + + for (var j = out.n[i].row + 1; j < out.o.length && out.o[j].text === undefined; j++) { + //this._outputcharcode('del', out.o[j]); + pre += '' + out.o[j] + ""; + } + str += out.n[i].text + pre; + } + } + } + + return str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' '); + }; + + /** + * + * @param {string} html + * @returns {boolean} + * @private + */ + this._diffDetectBrokenDiffHtml = function(html) { + var match = html.match(/<(ins|del)><[^>]*><\/(ins|del)>/gi); + return (match !== null && match.length > 0); + }; + + this._diffParagraphs = function(oldText, newText, lineLength, firstLineNumber) { + var oldTextWithBreaks, newTextWithBreaks; + + if (lineLength !== undefined) { + oldTextWithBreaks = lineNumberingService.insertLineNumbersNode(oldText, lineLength, null, firstLineNumber); + newTextWithBreaks = lineNumberingService.insertLineNumbersNode(newText, lineLength, null, firstLineNumber); + } else { + oldTextWithBreaks = document.createElement('div'); + oldTextWithBreaks.innerHTML = oldText; + newTextWithBreaks = document.createElement('div'); + newTextWithBreaks.innerHTML = newText; + } + + for (var i = 0; i < oldTextWithBreaks.childNodes.length; i++) { + this.addCSSClass(oldTextWithBreaks.childNodes[i], 'delete'); + } + for (i = 0; i < newTextWithBreaks.childNodes.length; i++) { + this.addCSSClass(newTextWithBreaks.childNodes[i], 'insert'); + } + + var mergedFragment = document.createDocumentFragment(), + el; + while (oldTextWithBreaks.firstChild) { + el = oldTextWithBreaks.firstChild; + oldTextWithBreaks.removeChild(el); + mergedFragment.appendChild(el); + } + while (newTextWithBreaks.firstChild) { + el = newTextWithBreaks.firstChild; + newTextWithBreaks.removeChild(el); + mergedFragment.appendChild(el); + } + + return this._serializeDom(mergedFragment); + }; + + /** + * This function calculates the diff between two strings and tries to fix problems with the resulting HTML. + * If lineLength and firstLineNumber is given, line numbers will be returned es well + * + * @param {number} lineLength + * @param {number} firstLineNumber + * @param {string} htmlOld + * @param {string} htmlNew + * @returns {string} + */ + this.diff = function (htmlOld, htmlNew, lineLength, firstLineNumber) { + var cacheKey = lineLength + ' ' + firstLineNumber + ' ' + + lineNumberingService.djb2hash(htmlOld) + lineNumberingService.djb2hash(htmlNew), + cached = diffCache.get(cacheKey); + if (!angular.isUndefined(cached)) { + return cached; + } + + var str = this._diffString(htmlOld, htmlNew), + diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ') + .replace(/<\/ins>/gi, '').replace(/<\/del>/gi, ''); + + diffUnnormalized = diffUnnormalized.replace(/([a-z0-9,_-]* ?)<\/del>([a-z0-9,_-]* ?)<\/ins>/gi, function (found, oldText, newText) { + var foundDiff = false, commonStart = '', commonEnd = '', + remainderOld = oldText, remainderNew = newText; + + while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) { + if (remainderOld[0] == remainderNew[0]) { + commonStart += remainderOld[0]; + remainderOld = remainderOld.substr(1); + remainderNew = remainderNew.substr(1); + } else { + foundDiff = true; + } + } + + foundDiff = false; + while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) { + if (remainderOld[remainderOld.length - 1] == remainderNew[remainderNew.length - 1]) { + commonEnd = remainderOld[remainderOld.length - 1] + commonEnd; + remainderNew = remainderNew.substr(0, remainderNew.length - 1); + remainderOld = remainderOld.substr(0, remainderOld.length - 1); + } else { + foundDiff = true; + } + } + + var out = commonStart; + if (remainderOld !== '') { + out += '' + remainderOld + ''; + } + if (remainderNew !== '') { + out += '' + remainderNew + ''; + } + out += commonEnd; + + return out; + }); + + var diff; + if (this._diffDetectBrokenDiffHtml(diffUnnormalized)) { + diff = this._diffParagraphs(htmlOld, htmlNew, lineLength, firstLineNumber); + } else { + var node = document.createElement('div'); + node.innerHTML = diffUnnormalized; + diff = node.innerHTML; + + if (lineLength !== undefined && firstLineNumber !== undefined) { + node = lineNumberingService.insertLineNumbersNode(diff, lineLength, null, firstLineNumber); + diff = node.innerHTML; + } + } + + diffCache.put(cacheKey, diff); + return diff; + }; } ]); diff --git a/openslides/motions/static/js/motions/linenumbering.js b/openslides/motions/static/js/motions/linenumbering.js index 01b3fc359..2b7d91c03 100644 --- a/openslides/motions/static/js/motions/linenumbering.js +++ b/openslides/motions/static/js/motions/linenumbering.js @@ -9,6 +9,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', []) * * Only the following inline elements are supported: * - 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT' + * - 'INS' and 'DEL' are supported, but line numbering does not affect the content of 'INS'-elements * * Only other inline elements are allowed within inline elements. * No constructs like
    are allowed. CSS-attributes like 'display: block' are ignored. @@ -37,11 +38,15 @@ angular.module('OpenSlidesApp.motions.lineNumbering', []) this._isInlineElement = function (node) { var inlineElements = [ - 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT' + 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT', 'INS', 'DEL' ]; return (inlineElements.indexOf(node.nodeName) > -1); }; + this._isIgnoredByLineNumbering = function (node) { + return (node.nodeName == 'INS'); + }; + this._isOsLineBreakNode = function (node) { var isLineBreak = false; if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) { @@ -304,7 +309,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', []) } 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])) { + if (overlength && this._isInlineElement(oldChildren[i]) && !this._isIgnoredByLineNumbering(oldChildren[i])) { this._currentInlineOffset = 0; node.appendChild(this._createLineBreak()); node.appendChild(this._createLineNumber()); @@ -327,7 +332,9 @@ angular.module('OpenSlidesApp.motions.lineNumbering', []) if (node.nodeType !== ELEMENT_NODE) { throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue; } - if (this._isInlineElement(node)) { + if (this._isIgnoredByLineNumbering(node)) { + return node; + } else if (this._isInlineElement(node)) { return this._insertLineNumbersToInlineNode(node, length, highlight); } else { var newLength = this._calcBlockNodeLength(node, length); @@ -357,6 +364,13 @@ angular.module('OpenSlidesApp.motions.lineNumbering', []) return root.innerHTML; }; + /** + * + * @param {string} html + * @param {number} lineLength + * @param {number} highlight - optional + * @param {number} firstLine + */ this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) { var root = document.createElement('div'); root.innerHTML = html; diff --git a/openslides/motions/static/templates/motions/motion-detail/view-diff.html b/openslides/motions/static/templates/motions/motion-detail/view-diff.html index 3b5c4f6ca..46dc0bd52 100644 --- a/openslides/motions/static/templates/motions/motion-detail/view-diff.html +++ b/openslides/motions/static/templates/motions/motion-detail/view-diff.html @@ -36,7 +36,7 @@
    + ng-bind-html="change.getDiff(motion, version, highlight) | trusted"> diff --git a/tests/karma/motions/diff.service.test.js b/tests/karma/motions/diff.service.test.js index 1d7acc4cc..d3598f45c 100644 --- a/tests/karma/motions/diff.service.test.js +++ b/tests/karma/motions/diff.service.test.js @@ -287,4 +287,53 @@ describe('linenumbering', function () { expect(calculatedType).toBe(diffService.TYPE_REPLACEMENT); }); }); + + describe('the core diff algorithm', function() { + it('acts as documented by the official documentation', function () { + var before = "The red brown fox jumped over the rolling log.", + after = "The brown spotted fox leaped over the rolling log."; + var diff = diffService.diff(before, after); + expect(diff).toBe('The red brown spotted fox jumleaped over the rolling log.'); + }); + + it('ignores changing cases in HTML tags', function () { + var before = "The brown spotted fox jumped over the rolling log.", + after = "The brown spotted fox leaped over the rolling log."; + var diff = diffService.diff(before, after); + + expect(diff).toBe('The brown spotted fox jumleaped over the rolling log.'); + }); + + it('merges multiple inserts and deletes', function () { + var before = "Test1 Test2 Test3 Test4 Test5 Test9", + after = "Test1 Test6 Test7 Test8 Test9"; + var diff = diffService.diff(before, after); + + expect(diff).toBe('Test1 Test2 Test3 Test4 Test5 Test6 Test7 Test8 Test9'); + }); + + it('detects insertions and deletions in a word (1)', function () { + var before = "Test1 Test2 Test3 Test4 Test5 Test6 Test7", + after = "Test1 Test Test3 Test4addon Test5 Test6 Test7"; + var diff = diffService.diff(before, after); + + expect(diff).toBe('Test1 Test2 Test3 Test4addon Test5 Test6 Test7'); + }); + + it('detects insertions and deletions in a word (2)', function () { + var before = "Test Test", + after = "Test Testappend"; + var diff = diffService.diff(before, after); + + expect(diff).toBe('Test Testappend'); + }); + + it('cannot handle changing CSS-classes', function () { + var before = "

    Test1 Test2

    ", + after = "

    Test1 Test2

    "; + var diff = diffService.diff(before, after); + + expect(diff).toBe("

    Test1 Test2

    Test1 Test2

    "); + }); + }); }); diff --git a/tests/karma/motions/linenumbering.service.test.js b/tests/karma/motions/linenumbering.service.test.js index 0f627ef99..bfa747577 100644 --- a/tests/karma/motions/linenumbering.service.test.js +++ b/tests/karma/motions/linenumbering.service.test.js @@ -126,6 +126,13 @@ describe('linenumbering', function () { expect(outHtml).toBe(noMarkup(1) + 'Lorem ' + brMarkup(2) + 'ipsum ' + brMarkup(3) + 'dolor' + brMarkup(4) + 'sit ' + brMarkup(5) + 'amet'); expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml); }); + + it('counts within DEL nodes', function () { + var inHtml = "1234 1234 1234 1234"; + var outHtml = lineNumberingService.insertLineNumbers(inHtml, 10); + expect(outHtml).toBe(noMarkup(1) + '1234 1234 ' + brMarkup(2) + '1234 1234'); + expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml); + }); }); @@ -228,4 +235,20 @@ describe('linenumbering', function () { expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml); }); }); + + describe('line numbering in regard to the inline diff', function() { + it('does not count within INS nodes', function () { + var inHtml = "1234 1234 1234 1234"; + var outHtml = lineNumberingService.insertLineNumbers(inHtml, 10); + expect(outHtml).toBe(noMarkup(1) + '1234 1234 1234 ' + brMarkup(2) + '1234'); + expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml); + }); + + it('does not create a new line for a trailing INS', function () { + var inHtml = "

    et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, conseteturdsfsdf23

    "; + var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80); + expect(outHtml).toBe('

    ' + noMarkup(1) + 'et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata ' + brMarkup(2) + 'sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, conseteturdsfsdf23

    '); + expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml); + }); + }); });