Merge pull request #3288 from CatoTH/Issue3269-Issue3232-Split-Elements2

Issue3269 issue3232 split elements2
This commit is contained in:
Emanuel Schütze 2017-07-27 15:12:44 +02:00 committed by GitHub
commit 2dc7db2b8c
7 changed files with 274 additions and 58 deletions

View File

@ -34,6 +34,8 @@ Motions:
- Added config value for customize sorting of category list in
pdf/docx export [#3329].
- Added config value for pagenumber alignment in PDF [#3327].
- Bugfix: Several bugfixes regarding splitting list items in
change recommendations [#3288]
Users:
- User without permission to see users can now see agenda item speakers,

View File

@ -109,6 +109,18 @@ h4 {
p {
padding-bottom: 14px;
}
p.os-split-after {
padding-bottom: 0;
margin-bottom: 0;
}
p.os-split-before {
padding-top: 0;
margin-top: 0;
}
ul.os-split-after, ol.os-split-after {
margin-bottom: 0;
}
a {
text-decoration: none;
@ -607,6 +619,10 @@ img {
/** Diff view */
.motion-text-holder li.os-split-before {
list-style-type: none;
}
.change-recommendation-overview {
background-color: #eee;
border: solid 1px #ddd;
@ -657,19 +673,24 @@ img {
border: solid 1px #eee;
border-radius: 3px;
margin-bottom: 0;
margin-top: -24px;
padding-top: 0;
padding-right: 155px;
}
.diff-box-transparent {
margin-top: -24px;
}
.motion-text-with-diffs li.os-split-before {
list-style-type: none;
}
.motion-text-with-diffs .original-text {
min-height: 30px; /* Spacer between .diff-box, in case .original-text is empty */
}
.motion-text-with-diffs .original-text ul:last-child {
.motion-text-with-diffs .original-text ul:last-child, .motion-text-with-diffs .original-text ol:last-child {
padding-bottom: 16px;
}
.motion-text-with-diffs .original-text ul.os-split-after:last-child, .motion-text-with-diffs .original-text ol.os-split-after:last-child {
padding-bottom: 0;
}
.motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box {
margin-right: -220px;
}
@ -713,9 +734,6 @@ img {
text-decoration: underline;
}
.motion-text-diff p {
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
.motion-text-diff.line-numbers-outside .insert .os-line-number {
display: none;

View File

@ -538,7 +538,9 @@ tr.elected td {
.diff-box {
margin-left: 25px;
padding-top: 0;
margin-top: -10px;
}
.motion-text-with-diffs li.os-split-before {
list-style-type: none;
}
.motion-text-with-diffs .original-text {
min-height: 30px; // Spacer between .diff-box, in case .original-text is empty
@ -546,6 +548,16 @@ tr.elected td {
.motion-text-with-diffs .original-text ul:last-child {
padding-bottom: 16px;
}
.motion-text-with-diffs .original-text ul.os-split-after:last-child {
padding-bottom: 0;
}
ol.os-split-after, ul.os-split-after {
margin-bottom: 0;
padding-bottom: 0;
}
p.os-split-after {
margin-bottom: 0;
}
.motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box {
margin-right: -220px;
}
@ -560,8 +572,11 @@ tr.elected td {
color: green;
text-decoration: underline;
}
.motion-text-diff p {
padding-bottom: 0;
.motion-text-diff p.os-split-before {
padding-top: 0;
margin-top: 0;
}
.motion-text-diff p.os-split-after {
margin-top: 0;
margin-bottom: 0;
}

View File

@ -920,13 +920,13 @@ angular.module('OpenSlidesApp.core', [
allowedContent:
'h1 h2 h3 b i u strike sup sub strong em;' +
'blockquote p pre table' +
'(text-align-left,text-align-center,text-align-right,text-align-justify){text-align};' +
'(text-align-left,text-align-center,text-align-right,text-align-justify,os-split-before,os-split-after){text-align};' +
'a[!href];' +
'img[!src,alt]{width,height,float};' +
'tr th td caption;' +
'li; ol[start]{list-style-type};' +
'ul{list-style};' +
'span[!*]{color,background-color}(os-line-number,line-number-*);' +
'li(os-split-before,os-split-after); ol(os-split-before,os-split-after)[start]{list-style-type};' +
'ul(os-split-before,os-split-after){list-style};' +
'span[!*]{color,background-color}(os-split-before,os-split-after,os-line-number,line-number-*);' +
'br(os-line-break);',
// there seems to be an error in CKeditor that parses spaces in extraPlugins as part of the plugin name.

View File

@ -308,6 +308,25 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
return fragment;
};
/**
* When a <li> with a os-split-before-class (set by extractRangeByLineNumbers) is edited when creating a
* change recommendation and is split again in CKEditor, the second list items also gets that class.
* This is not correct however, as the second one actually is a new list item. So we need to remove it again.
*
* @param {string} html
* @returns {string}
*/
this.removeDuplicateClassesInsertedByCkeditor = function(html) {
var fragment = this.htmlToFragment(html);
var items = fragment.querySelectorAll('li.os-split-before');
for (var i = 0; i < items.length; i++) {
if (!this._isFirstNonemptyChild(items[i].parentNode, items[i])) {
this.removeCSSClass(items[i], 'os-split-before');
}
}
return this._serializeDom(fragment, false);
};
/**
* Returns the HTML snippet between two given line numbers.
*
@ -338,8 +357,18 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
* - followingHtml: The HTML after the selected area
* - followingHtmlStartSnippet: A HTML snippet that opens all HTML tags necessary to render "followingHtml"
*
*
* In some cases, the returned HTML tags receive additional CSS classes, providing information both for
* rendering it and for merging it again correctly.
* - os-split-*: These classes are set for all HTML Tags that have been split into two by this process,
* e.g. if the fromLine- or toLine-line-break was somewhere in the middle of this tag.
* If a tag is split, the first one receives "os-split-after", and the second one "os-split-before".
* For example, for the following string <p>Line 1<br>Line 2<br>Line 3</p>:
* - extracting line 1 to 2 results in <p class="os-split-after">Line 1</p>
* - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p>
* - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p>
*/
this.extractRangeByLineNumbers = function(htmlIn, fromLine, toLine, debug) {
this.extractRangeByLineNumbers = function(htmlIn, fromLine, toLine) {
if (typeof(htmlIn) !== 'string') {
throw 'Invalid call - extractRangeByLineNumbers expects a string as first argument';
}
@ -384,27 +413,51 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
toChildTraceAbs.shift();
var followingHtml = this._serializePartialDomFromChild(fragment, toChildTraceAbs, false);
var currNode = fromLineNode.parentNode;
var currNode = fromLineNode,
isSplit = false;
while (currNode.parentNode) {
previousHtmlEndSnippet += '</' + currNode.nodeName + '>';
if (!this._isFirstNonemptyChild(currNode.parentNode, currNode)) {
isSplit = true;
}
if (isSplit) {
this.addCSSClass(currNode.parentNode, 'os-split-before');
}
if (currNode.nodeName !== 'OS-LINEBREAK') {
previousHtmlEndSnippet += '</' + currNode.nodeName + '>';
}
currNode = currNode.parentNode;
}
currNode = toLineNode.parentNode;
currNode = toLineNode;
isSplit = false;
while (currNode.parentNode) {
followingHtmlStartSnippet = this._serializeTag(currNode) + followingHtmlStartSnippet;
if (!this._isFirstNonemptyChild(currNode.parentNode, currNode)) {
isSplit = true;
}
if (isSplit) {
this.addCSSClass(currNode.parentNode, 'os-split-after');
}
followingHtmlStartSnippet = this._serializeTag(currNode.parentNode) + followingHtmlStartSnippet;
currNode = currNode.parentNode;
}
var found = false;
isSplit = false;
for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
if (fromChildTraceRel[i].nodeName === 'OS-LINEBREAK') {
found = true;
} else {
if (!this._isFirstNonemptyChild(fromChildTraceRel[i], fromChildTraceRel[i + 1])) {
isSplit = true;
}
if (fromChildTraceRel[i].nodeName === 'OL') {
fakeOl = fromChildTraceRel[i].cloneNode(false);
fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode));
innerContextStart += this._serializeTag(fakeOl);
} else {
if (i < (fromChildTraceRel.length - 1) && isSplit) {
this.addCSSClass(fromChildTraceRel[i], 'os-split-before');
}
innerContextStart += this._serializeTag(fromChildTraceRel[i]);
}
}
@ -512,28 +565,28 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
var lastNode = nodes1[nodes1.length - 1],
firstNode = nodes2[0];
if (lastNode.nodeType == TEXT_NODE && firstNode.nodeType == TEXT_NODE) {
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) {
} 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
// Remove #text nodes inside of List elements (OL/UL), as they are confusing
var lastChildren, firstChildren;
if (lastNode.nodeName == 'OL' || lastNode.nodeName == 'UL') {
if (lastNode.nodeName === 'OL' || lastNode.nodeName === 'UL') {
lastChildren = [];
firstChildren = [];
for (i = 0; i < firstNode.childNodes.length; i++) {
if (firstNode.childNodes[i].nodeType == ELEMENT_NODE) {
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) {
if (lastNode.childNodes[i].nodeType === ELEMENT_NODE) {
lastChildren.push(lastNode.childNodes[i]);
}
}
@ -546,12 +599,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
for (i = 0; i < children.length; i++) {
newNode.appendChild(children[i]);
}
out.push(newNode);
} else {
if (lastNode.nodeName != 'TEMPLATE') {
if (lastNode.nodeName !== 'TEMPLATE') {
out.push(lastNode);
}
if (firstNode.nodeName != 'TEMPLATE') {
if (firstNode.nodeName !== 'TEMPLATE') {
out.push(firstNode);
}
}
@ -570,9 +624,23 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
* @private
*/
this._normalizeHtmlForDiff = function (html) {
// Convert all HTML tags to uppercase, strip trailing whitespaces
html = html.replace(/<[^>]+>/g, function (tag) {
return tag.toUpperCase();
// Convert all HTML tags to uppercase, but leave the values of attributes unchanged
html = html.replace(/<(\/?[a-z]*)( [^>]*)?>/ig, function (html, tag, attributes) {
var tagNormalized = tag.toUpperCase();
if (attributes === undefined) {
attributes = "";
}
attributes = attributes.replace(/( [^"'=]*)(= *((["'])(.*?)\4))?/gi, function (attr, attrName, attrRest, attrRest2, quot, attrValue) {
var attrNormalized = attrName.toUpperCase();
if (attrRest !== undefined) {
if (attrNormalized === ' CLASS') {
attrValue = attrValue.split(' ').sort().join(' ');
}
attrNormalized += "=" + quot + attrValue + quot;
}
return attrNormalized;
});
return "<" + tagNormalized + attributes + ">";
});
var entities = {
@ -649,7 +717,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
followingFragment = this.htmlToFragment(followingHtml),
newFragment = this.htmlToFragment(newHTML);
if (data.html.length > 0 && data.html.substr(-1) == ' ') {
if (data.html.length > 0 && data.html.substr(-1) === ' ') {
this._insertDanglingSpace(newFragment);
}
@ -667,21 +735,46 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
el.parentNode.removeChild(el);
}
var forgottenSplitClasses = mergedFragment.querySelectorAll(".os-split-before, .os-split-after");
for (i = 0; i < forgottenSplitClasses.length; i++) {
this.removeCSSClass(forgottenSplitClasses[i], 'os-split-before');
this.removeCSSClass(forgottenSplitClasses[i], 'os-split-after');
}
return this._serializeDom(mergedFragment, true);
};
this.addCSSClass = function (node, className) {
if (node.nodeType != ELEMENT_NODE) {
if (node.nodeType !== ELEMENT_NODE) {
return;
}
var classes = node.getAttribute('class');
classes = (classes ? classes.split(' ') : []);
if (classes.indexOf(className) == -1) {
if (classes.indexOf(className) === -1) {
classes.push(className);
}
node.setAttribute('class', classes.join(' '));
};
this.removeCSSClass = function (node, className) {
if (node.nodeType !== ELEMENT_NODE) {
return;
}
var classes = node.getAttribute('class'),
newClasses = [];
classes = (classes ? classes.split(' ') : []);
for (var i = 0; i < classes.length; i++) {
if (classes[i] !== className) {
newClasses.push(classes[i]);
}
}
if (newClasses.length === 0) {
node.removeAttribute('class');
} else {
node.setAttribute('class', newClasses.join(' '));
}
};
this.addDiffMarkup = function (originalHTML, newHTML, fromLine, toLine, diffFormatterCb) {
var data = this.extractRangeByLineNumbers(originalHTML, fromLine, toLine),
previousHtml = data.previousHtml + '<TEMPLATE></TEMPLATE>' + data.previousHtmlEndSnippet,
@ -1151,19 +1244,14 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
// Remove <del> tags that only delete line numbers
diffUnnormalized = diffUnnormalized.replace(
/<del>((<BR CLASS="OS-LINE-BREAK">)?<span[^>]+OS-LINE-NUMBER[^>]+?>\s*<\/span>)<\/del>/gi,
/<del>((<BR CLASS="os-line-break">)?<span[^>]+os-line-number[^>]+?>\s*<\/span>)<\/del>/gi,
function(found,tag) {
return tag;
}
);
// Lowercase line number markup
diffUnnormalized = diffUnnormalized.replace(
/<BR CLASS="OS-LINE-BREAK">/gi,
'<br class="os-line-break">'
);
diffUnnormalized = diffUnnormalized.replace(
/<span[^>]+OS-LINE-NUMBER[^>]+?>\s*<\/span>/gi,
/<span[^>]+os-line-number[^>]+?>\s*<\/span>/gi,
function(found) {
return found.toLowerCase().replace(/> <\/span/gi, ">&nbsp;</span");
}

View File

@ -1550,9 +1550,10 @@ angular.module('OpenSlidesApp.motions.site', [
'$scope',
'MotionChangeRecommendation',
'ChangeRecommendationForm',
'diffService',
'change',
'ErrorMessage',
function ($scope, MotionChangeRecommendation, ChangeRecommendationForm, change, ErrorMessage) {
function ($scope, MotionChangeRecommendation, ChangeRecommendationForm, diffService, change, ErrorMessage) {
$scope.alert = {};
$scope.model = angular.copy(change);
@ -1560,6 +1561,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.formFields = ChangeRecommendationForm.getFormFields(change.line_from, change.line_to);
// save motion
$scope.save = function (change) {
change.text = diffService.removeDuplicateClassesInsertedByCkeditor(change.text);
// inject the changed change recommendation (copy) object back into DS store
MotionChangeRecommendation.inject(change);
// save changed change recommendation object on server
@ -1607,6 +1609,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.formFields = ChangeRecommendationForm.getFormFields(lineFrom, lineTo);
// save motion
$scope.save = function (motion) {
motion.text = diffService.removeDuplicateClassesInsertedByCkeditor(motion.text);
MotionChangeRecommendation.create(motion).then(
function(success) {
$scope.closeThisDialog();

View File

@ -5,10 +5,10 @@ describe('linenumbering', function () {
var diffService, baseHtml1, baseHtmlDom1, baseHtml2, baseHtmlDom2, baseHtml3, baseHtmlDom3,
brMarkup = function (no) {
return '<br class="os-line-break">' +
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
'<span class="line-number-' + no + ' os-line-number" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
},
noMarkup = function (no) {
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
return '<span class="line-number-' + no + ' os-line-number" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
};
beforeEach(inject(function (_diffService_, _lineNumberingService_) {
@ -127,21 +127,52 @@ describe('linenumbering', function () {
it('extracts a single line', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtml1, 1, 2);
expect(diff.html).toBe('<P>Line 1 ');
expect(diff.html).toBe('<P class="os-split-after">Line 1 ');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
});
it('extracts lines from nested UL/LI-structures', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtml1, 7, 9);
expect(diff.html).toBe('Line 7</LI><LI class="li-class"><UL><LI>Level 2 LI 8</LI>');
expect(diff.html).toBe('Line 7</LI><LI class="li-class os-split-after"><UL class="os-split-after"><LI>Level 2 LI 8</LI>');
expect(diff.ancestor.nodeName).toBe('UL');
expect(diff.outerContextStart).toBe('<UL class="ul-class">');
expect(diff.outerContextStart).toBe('<UL class="ul-class os-split-before os-split-after">');
expect(diff.outerContextEnd).toBe('</UL>');
expect(diff.innerContextStart).toBe('<LI class="li-class">');
expect(diff.innerContextStart).toBe('<LI class="li-class os-split-before">');
expect(diff.innerContextEnd).toBe('</UL></LI>');
expect(diff.previousHtmlEndSnippet).toBe('</LI></UL>');
expect(diff.followingHtmlStartSnippet).toBe('<UL class="ul-class"><LI class="li-class"><UL>');
expect(diff.followingHtmlStartSnippet).toBe('<UL class="ul-class os-split-before os-split-after"><LI class="li-class os-split-after"><UL class="os-split-after">');
});
it('extracts lines from double-nested UL/LI-structures (1)', function () {
var html = '<p>' + noMarkup(1) + 'Line 1</p>' +
'<ul><li><p>' + noMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3' + brMarkup(4) + 'Line 5</p></li></ul>';
var diff = diffService.extractRangeByLineNumbers(html, 3, 4);
expect(diff.html).toBe('Line 3');
expect(diff.ancestor.nodeName).toBe('P');
expect(diff.outerContextStart).toBe('<UL class="os-split-before os-split-after"><LI class="os-split-before os-split-after"><P class="os-split-before os-split-after">');
expect(diff.outerContextEnd).toBe('</P></LI></UL>');
expect(diff.innerContextStart).toBe('');
expect(diff.innerContextEnd).toBe('');
expect(diff.previousHtmlEndSnippet).toBe('</P></LI></UL>');
expect(diff.followingHtmlStartSnippet).toBe('<UL class="os-split-before os-split-after"><LI class="os-split-before os-split-after"><P class="os-split-before os-split-after">');
});
it('extracts lines from double-nested UL/LI-structures (2)', function () {
var html = '<p>' + noMarkup(1) + 'Line 1</p>' +
'<ul><li><p>' + noMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3' + brMarkup(4) + '</p></li></ul>';
var diff = diffService.extractRangeByLineNumbers(html, 2, 3);
expect(diff.html).toBe('<UL class="os-split-after"><LI class="os-split-after"><P class="os-split-after">Line 2');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
expect(diff.innerContextStart).toBe('');
expect(diff.innerContextEnd).toBe('</P></LI></UL>');
expect(diff.previousHtmlEndSnippet).toBe('');
// @TODO in followingHtmlStartSnippet, os-split-li is not set yet in this case.
// This is not entirely correct, but as this field is never actually used, it's not bothering (yet)
// This comment remains to document a potential pitfall in the future
// expect(diff.followingHtmlStartSnippet).toBe('<UL><LI class="os-split-li"><P>');
});
it('extracts a single line right before a UL/LI', function () {
@ -155,14 +186,14 @@ describe('linenumbering', function () {
it('extracts lines from a more complex example', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtml2, 6, 11);
expect(diff.html).toBe('owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl pfenningguat schoo griasd eich midnand.</P><UL><LI>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?</LI><LI>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</LI>');
expect(diff.html).toBe('owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl pfenningguat schoo griasd eich midnand.</P><UL class="os-split-after"><LI>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?</LI><LI>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</LI>');
expect(diff.ancestor.nodeName).toBe('#document-fragment');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
expect(diff.innerContextStart).toBe('<P>');
expect(diff.innerContextStart).toBe('<P class="os-split-before">');
expect(diff.innerContextEnd).toBe('</UL>');
expect(diff.previousHtmlEndSnippet).toBe('</P>');
expect(diff.followingHtmlStartSnippet).toBe('<UL>');
expect(diff.followingHtmlStartSnippet).toBe('<UL class="os-split-after">');
});
it('extracts the end of a section', function () {
@ -172,7 +203,7 @@ describe('linenumbering', function () {
expect(diff.ancestor.nodeName).toBe('#document-fragment');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
expect(diff.innerContextStart).toBe('<P>');
expect(diff.innerContextStart).toBe('<P class="os-split-before">');
expect(diff.innerContextEnd).toBe('');
expect(diff.previousHtmlEndSnippet).toBe('</P>');
expect(diff.followingHtml).toBe('');
@ -184,7 +215,7 @@ describe('linenumbering', function () {
expect(diff.html).toBe('<LI>Line 3.3</LI></OL></LI><LI> Line 4</LI></OL>');
expect(diff.ancestor.nodeName).toBe('#document-fragment');
expect(diff.innerContextStart).toBe('<OL start="3"><LI><OL start="3">');
expect(diff.innerContextStart).toBe('<OL class="os-split-before" start="3"><LI class="os-split-before"><OL class="os-split-before" start="3">');
expect(diff.innerContextEnd).toBe('');
expect(diff.previousHtmlEndSnippet).toBe('</OL></LI></OL>');
});
@ -192,9 +223,9 @@ describe('linenumbering', function () {
it('preserves the numbering of OLs (2)', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtml3, 3, 5);
expect(diff.html).toBe('<LI><OL><LI>Line 3.1</LI><LI>Line 3.2</LI>');
expect(diff.html).toBe('<LI class="os-split-after"><OL class="os-split-after"><LI>Line 3.1</LI><LI>Line 3.2</LI>');
expect(diff.ancestor.nodeName).toBe('OL');
expect(diff.outerContextStart).toBe('<OL start="3">');
expect(diff.outerContextStart).toBe('<OL class="os-split-before os-split-after" start="3">');
expect(diff.outerContextEnd).toBe('</OL>');
});
@ -203,6 +234,23 @@ describe('linenumbering', function () {
var diff = diffService.extractRangeByLineNumbers(inHtml, 1, 2);
expect(diff.html).toBe('<H2>Looks like a &lt;p&gt; tag &lt;/p&gt;</H2>');
});
it('marks split list items', function () {
var html = '<ol><li>' + noMarkup(1) + 'Line 1' + brMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3</li></ol>'
var diff = diffService.extractRangeByLineNumbers(html, 2, 3);
expect(diff.outerContextStart.toLowerCase()).toBe('<ol class="os-split-before os-split-after" start="1"><li class="os-split-before os-split-after">');
diff = diffService.extractRangeByLineNumbers(html, 3, null);
expect(diff.innerContextStart.toLowerCase()).toBe('<ol class="os-split-before" start="1"><li class="os-split-before">');
});
it('does not mark the second list item as being split', function () {
var html = '<ol><li>' + noMarkup(1) + 'Line 1</li><li>' + noMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3</li></ol>';
var diff = diffService.extractRangeByLineNumbers(html, 2, 3);
expect(diff.outerContextStart.toLowerCase()).toBe('<ol class="os-split-before os-split-after" start="2">');
expect(diff.innerContextStart.toLowerCase()).toBe('');
expect(diff.html.toLowerCase()).toBe('<li class="os-split-after">line 2');
});
});
describe('merging two sections', function () {
@ -319,7 +367,41 @@ describe('linenumbering', function () {
});
});
describe('the core diff algorithm', function() {
describe('diff normalization', function () {
it('uppercases normal HTML tags', function () {
var unnormalized = 'The <strong>brown</strong> fox',
normalized = diffService._normalizeHtmlForDiff(unnormalized);
expect(normalized).toBe('The <STRONG>brown</STRONG> fox')
});
it('uppercases the names of html attributes, but not the values', function () {
var unnormalized = 'This is our cool <a href="https://www.openslides.de/">home page</a> - have a look! ' +
'<input type="checkbox" checked title=\'A title with "s\'>',
normalized = diffService._normalizeHtmlForDiff(unnormalized);
expect(normalized).toBe('This is our cool <A HREF="https://www.openslides.de/">home page</A> - have a look! ' +
'<INPUT TYPE="checkbox" CHECKED TITLE=\'A title with "s\'>')
});
it('strips unnecessary spaces', function () {
var unnormalized = "<ul> <li>Test</li>\n</ul>",
normalized = diffService._normalizeHtmlForDiff(unnormalized);
expect(normalized).toBe('<UL><LI>Test</LI></UL>');
});
it('normalizes html entities', function () {
var unnormalized = "German characters like &szlig; or &ouml;",
normalized = diffService._normalizeHtmlForDiff(unnormalized);
expect(normalized).toBe('German characters like ß or ö');
});
it('sorts css classes', function () {
var unnormalized = "<P class='os-split-before os-split-after'>Test</P>",
normalized = diffService._normalizeHtmlForDiff(unnormalized);
expect(normalized).toBe("<P CLASS='os-split-after os-split-before'>Test</P>");
});
});
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.";
@ -529,11 +611,11 @@ describe('linenumbering', function () {
});
it('detects broken HTML and lowercases class names', function () {
var before = "<p><span class=\"os-line-number line-number-3\" data-line-number=\"3\" contenteditable=\"false\">&nbsp;</span>holen, da rief sie alle sieben herbei und sprach:</p>\n\n<p><span class=\"os-line-number line-number-4\" data-line-number=\"4\" contenteditable=\"false\">&nbsp;</span><span style=\"color: #000000;\">\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er <br class=\"os-line-break\"><span class=\"os-line-number line-number-5\" data-line-number=\"5\" contenteditable=\"false\">&nbsp;</span>hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber <br class=\"os-line-break\"><span class=\"os-line-number line-number-6\" data-line-number=\"6\" contenteditable=\"false\">&nbsp;</span>an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"</span></p>\n\n<p><span class=\"os-line-number line-number-7\" data-line-number=\"7\" contenteditable=\"false\">&nbsp;</span>Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne </p>",
var before = "<p><span class=\"line-number-3 os-line-number\" data-line-number=\"3\" contenteditable=\"false\">&nbsp;</span>holen, da rief sie alle sieben herbei und sprach:</p>\n\n<p><span class=\"line-number-4 os-line-number\" data-line-number=\"4\" contenteditable=\"false\">&nbsp;</span><span style=\"color: #000000;\">\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er <br class=\"os-line-break\"><span class=\"line-number-5 os-line-number\" data-line-number=\"5\" contenteditable=\"false\">&nbsp;</span>hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber <br class=\"os-line-break\"><span class=\"line-number-6 os-line-number\" data-line-number=\"6\" contenteditable=\"false\">&nbsp;</span>an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"</span></p>\n\n<p><span class=\"line-number-7 os-line-number\" data-line-number=\"7\" contenteditable=\"false\">&nbsp;</span>Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne </p>",
after = "<p>holen, da rief sie alle sieben herbei und sprach:</p>\n\n<p><span style=\"color: #000000;\">Hello</span></p>\n\n<p><span style=\"color: #000000;\">World</span></p>\n\n<p><span style=\"color: #000000;\">Ya</span></p>\n\n<p>Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne</p>";
var diff = diffService.diff(before, after);
expect(diff).toBe("<P class=\"delete\"><SPAN class=\"os-line-number line-number-3\" data-line-number=\"3\" contenteditable=\"false\"> </SPAN>holen, da rief sie alle sieben herbei und sprach:</P><DEL>\n\n</DEL>" +
"<P class=\"delete\"><SPAN class=\"os-line-number line-number-4\" data-line-number=\"4\" contenteditable=\"false\"> </SPAN><SPAN>\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er <BR class=\"os-line-break\"><SPAN class=\"os-line-number line-number-5\" data-line-number=\"5\" contenteditable=\"false\"> </SPAN>hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber <BR class=\"os-line-break\"><SPAN class=\"os-line-number line-number-6\" data-line-number=\"6\" contenteditable=\"false\"> </SPAN>an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"</SPAN></P><DEL>\n\n</DEL><P class=\"delete\"><SPAN class=\"os-line-number line-number-7\" data-line-number=\"7\" contenteditable=\"false\"> </SPAN>Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne </P>" +
expect(diff).toBe("<P class=\"delete\"><SPAN class=\"line-number-3 os-line-number\" data-line-number=\"3\" contenteditable=\"false\"> </SPAN>holen, da rief sie alle sieben herbei und sprach:</P><DEL>\n\n</DEL>" +
"<P class=\"delete\"><SPAN class=\"line-number-4 os-line-number\" data-line-number=\"4\" contenteditable=\"false\"> </SPAN><SPAN>\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er <BR class=\"os-line-break\"><SPAN class=\"line-number-5 os-line-number\" data-line-number=\"5\" contenteditable=\"false\"> </SPAN>hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber <BR class=\"os-line-break\"><SPAN class=\"line-number-6 os-line-number\" data-line-number=\"6\" contenteditable=\"false\"> </SPAN>an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"</SPAN></P><DEL>\n\n</DEL><P class=\"delete\"><SPAN class=\"line-number-7 os-line-number\" data-line-number=\"7\" contenteditable=\"false\"> </SPAN>Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne </P>" +
"<P class=\"insert\">holen, da rief sie alle sieben herbei und sprach:</P><INS>\n\n</INS>" +
"<P class=\"insert\"><SPAN>Hello</SPAN></P><INS>\n\n</INS>" +
"<P class=\"insert\"><SPAN>World</SPAN></P><INS>\n\n</INS>" +
@ -554,5 +636,13 @@ describe('linenumbering', function () {
inserted = diffService.addCSSClassToFirstTag(strIn, "newClass");
expect(inserted).toBe("<ol start='2' class=\"my-old-class newClass\"><li>")
});
})
});
describe('removeDuplicateClassesInsertedByCkeditor', function () {
it('removes additional classes', function () {
var strIn = '<ul class="os-split-before os-split-after"><li class="os-split-before"><ul class="os-split-before os-split-after"><li class="os-split-before">...here it goes on</li><li class="os-split-before">This has been added</li></ul></li></ul>',
cleaned = diffService.removeDuplicateClassesInsertedByCkeditor(strIn);
expect(cleaned).toBe('<UL class="os-split-before os-split-after"><LI class="os-split-before"><UL class="os-split-before os-split-after"><LI class="os-split-before">...here it goes on</LI><LI>This has been added</LI></UL></LI></UL>');
});
});
});