mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 16:32:18 +02:00
Merge branch 'MDL-52724-master' of git://github.com/merrill-oakland/moodle
This commit is contained in:
commit
e5e5a06190
@ -51,6 +51,8 @@ class behat_form_editor extends behat_form_textarea {
|
||||
$editorid = $this->field->getAttribute('id');
|
||||
if ($this->running_javascript()) {
|
||||
$value = addslashes($value);
|
||||
// This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
|
||||
$value = str_replace("\n", "\\n", $value);
|
||||
$js = '
|
||||
(function() {
|
||||
var editor = Y.one(document.getElementById("'.$editorid.'editable"));
|
||||
|
169
lib/editor/atto/tests/behat/clean.feature
Normal file
169
lib/editor/atto/tests/behat/clean.feature
Normal file
@ -0,0 +1,169 @@
|
||||
@editor @editor_atto @atto @editor_moodleform
|
||||
Feature: Atto HTML cleanup.
|
||||
In order to test html cleaning functionality, I write in a HTML atto text field.
|
||||
|
||||
@javascript
|
||||
Scenario: Extra UL close and orphan LI items
|
||||
Given I log in as "admin"
|
||||
When I open my profile in edit mode
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
And I set the field "Description" to multiline:
|
||||
"""
|
||||
<li>A</li>
|
||||
<li>B</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>C</li>
|
||||
</ul></ul>
|
||||
<li class="someclass ul UL">D</li>
|
||||
<li>E</li>
|
||||
"""
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches multiline:
|
||||
"""
|
||||
<ol><li>A</li>
|
||||
<li>B</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>C</li>
|
||||
</ul>
|
||||
<ul><li class="someclass ul UL">D</li>
|
||||
<li>E</li></ul>
|
||||
"""
|
||||
|
||||
@javascript
|
||||
Scenario: Missing LI close tags, extra closing OL, missing closing UL tag
|
||||
Given I log in as "admin"
|
||||
When I open my profile in edit mode
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
And I set the field "Description" to multiline:
|
||||
"""
|
||||
<div class="ol"><ol>
|
||||
<li>A</li>
|
||||
<li>B
|
||||
</ol></div>
|
||||
<ul>
|
||||
<li>C
|
||||
<li>D</li>
|
||||
</ol>
|
||||
"""
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches multiline:
|
||||
"""
|
||||
<div class="ol"><ol>
|
||||
<li>A</li>
|
||||
<li>B
|
||||
</li></ol></div>
|
||||
<ul>
|
||||
<li>C
|
||||
</li><li>D</li></ul>
|
||||
|
||||
"""
|
||||
|
||||
@javascript
|
||||
Scenario: Missing beginning OL tag, empty LI close tag
|
||||
Given I log in as "admin"
|
||||
When I open my profile in edit mode
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
And I set the field "Description" to multiline:
|
||||
"""
|
||||
<p>Before</p>
|
||||
<li>A</li></li>
|
||||
<li>B</li>
|
||||
</ol>
|
||||
<p>After</p>
|
||||
<ul data-info="UL ul OL ol">
|
||||
<ul>
|
||||
C</li>
|
||||
<li>D</li>
|
||||
<li>E
|
||||
</ul>
|
||||
</ul><ul>
|
||||
<p>After 2</p>
|
||||
"""
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches multiline:
|
||||
"""
|
||||
<p>Before</p>
|
||||
<ol><li>A</li>
|
||||
<li>B</li>
|
||||
</ol>
|
||||
<p>After</p>
|
||||
<ul data-info="UL ul OL ol">
|
||||
<ul><li>
|
||||
C</li>
|
||||
<li>D</li>
|
||||
<li>E
|
||||
</li></ul>
|
||||
</ul>
|
||||
<p>After 2</p>
|
||||
"""
|
||||
|
||||
@javascript
|
||||
Scenario: Random close LI tag, extra LI open tag, missing OL tag
|
||||
Given I log in as "admin"
|
||||
When I open my profile in edit mode
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
And I set the field "Description" to multiline:
|
||||
"""
|
||||
<p>Before</p></li><ul>
|
||||
<ul>
|
||||
<li>A</li>
|
||||
B</li>
|
||||
<li>C</li>
|
||||
<ol>
|
||||
<li>D</li>
|
||||
<li>E
|
||||
<p>After</p>
|
||||
"""
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches multiline:
|
||||
"""
|
||||
<p>Before</p>
|
||||
<ul>
|
||||
<li>A</li><li>
|
||||
B</li>
|
||||
<li>C</li></ul>
|
||||
<ol>
|
||||
<li>D</li></ol>
|
||||
E
|
||||
<p>After</p>
|
||||
"""
|
||||
|
||||
@javascript
|
||||
Scenario: Missing opening LI tags, missing closing UL tag
|
||||
Given I log in as "admin"
|
||||
When I open my profile in edit mode
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
And I set the field "Description" to multiline:
|
||||
"""
|
||||
<li>Before</li>
|
||||
<ul>
|
||||
<li>A</li>
|
||||
B</li>
|
||||
<ol>
|
||||
1</li>
|
||||
</ol>
|
||||
<li>C
|
||||
<li>D</li>
|
||||
<p>After</p>
|
||||
"""
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches multiline:
|
||||
"""
|
||||
<ul><li>Before</li></ul>
|
||||
<ul>
|
||||
<li>A</li><li>
|
||||
B</li>
|
||||
<ol><li>
|
||||
1</li>
|
||||
</ol>
|
||||
<li>C
|
||||
</li><li>D</li></ul>
|
||||
<p>After</p>
|
||||
"""
|
@ -718,7 +718,7 @@ EditorTextArea.prototype = {
|
||||
this.editor.setHTML('');
|
||||
|
||||
// Copy cleaned HTML to editable div.
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value')));
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value'), true));
|
||||
|
||||
// Insert a paragraph in the empty contenteditable div.
|
||||
if (this.editor.getHTML() === '') {
|
||||
@ -1393,9 +1393,10 @@ EditorClean.prototype = {
|
||||
* @method _cleanHTML
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @param {Boolean} deepClean If true, do a more in depth (and resource intensive) cleaning of the HTML.
|
||||
* @return {String} The cleaned HTML
|
||||
*/
|
||||
_cleanHTML: function(content) {
|
||||
_cleanHTML: function(content, deepClean) {
|
||||
// Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
|
||||
|
||||
var rules = [
|
||||
@ -1412,7 +1413,13 @@ EditorClean.prototype = {
|
||||
{regex: /<\/?(?:title|meta|style|st\d|head\b|font|html|body|link)[^>]*?>/gi, replace: ""}
|
||||
];
|
||||
|
||||
return this._filterContentWithRules(content, rules);
|
||||
content = this._filterContentWithRules(content, rules);
|
||||
|
||||
if (deepClean) {
|
||||
content = this._cleanHTMLLists(content);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1443,6 +1450,8 @@ EditorClean.prototype = {
|
||||
pasteCleanup: function(sourceEvent) {
|
||||
// We only expect paste events, but we will check anyways.
|
||||
if (sourceEvent.type === 'paste') {
|
||||
// Register the delayed paste cleanup. We will cancel it if we register the fallback cleanup.
|
||||
var delayedCleanup = this.postPasteCleanupDelayed();
|
||||
// The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
|
||||
var event = sourceEvent._event;
|
||||
// Check if we have a valid clipboardData object in the event.
|
||||
@ -1465,6 +1474,7 @@ EditorClean.prototype = {
|
||||
content = event.clipboardData.getData('text/html');
|
||||
} catch (error) {
|
||||
// Something went wrong. Fallback.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -1497,6 +1507,7 @@ EditorClean.prototype = {
|
||||
// Something went wrong. Fallback.
|
||||
// Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
|
||||
// Wait for the clipboard event to finish then fallback clean the entire editor.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -1515,10 +1526,50 @@ EditorClean.prototype = {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls postPasteCleanup on a short timer to allow the paste event handlers to complete, then deep clean the content.
|
||||
*
|
||||
* @method postPasteCleanupDelayed
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanupDelayed: function() {
|
||||
Y.soon(Y.bind(this.postPasteCleanup, this));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do additional cleanup after the paste is complete.
|
||||
*
|
||||
* @method postPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanup: function() {
|
||||
Y.log('Executing delayed post paste cleanup', 'debug', LOGNAME);
|
||||
|
||||
// Save the current selection (cursor position).
|
||||
var selection = window.rangy.saveSelection();
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanHTML(content, true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
|
||||
// Restore the selection (cursor position).
|
||||
window.rangy.restoreSelection(selection);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup code after a paste event if we couldn't intercept the paste content.
|
||||
*
|
||||
* @method fallbackPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
fallbackPasteCleanup: function() {
|
||||
@ -1529,7 +1580,7 @@ EditorClean.prototype = {
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanPasteHTML(content));
|
||||
this.editor.set('innerHTML', this._cleanHTML(this._cleanPasteHTML(content), true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
@ -1709,6 +1760,198 @@ EditorClean.prototype = {
|
||||
});
|
||||
|
||||
return holder.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a function that searches for, and attempts to correct certain issues with ul/ol html lists.
|
||||
* This is needed because these lists are used heavily in page layout, and content with bad tags can
|
||||
* lead to broke course pages.
|
||||
*
|
||||
* The theory of operation here is to linearly process the incoming content, counting the opening and closing
|
||||
* of list tags, and determining when there is a mismatch.
|
||||
*
|
||||
* The specific issues this should be able to correct are:
|
||||
* - Orphaned li elements will be wrapped in a set of ul tags.
|
||||
* - li elements inside li elements.
|
||||
* - An extra closing ul, or ol tag will be discarded.
|
||||
* - An extra closing li tag will have an opening tag added if appropriate, or will be discarded.
|
||||
* - If there is an unmatched list open tag, a matching close tag will be inserted.
|
||||
*
|
||||
* It does it's best to match the case of corrected tags. Even though not required by html spec,
|
||||
* it seems like the safer route.
|
||||
*
|
||||
* A note on parent elements of li. This code assumes that li must have a ol or ul parent.
|
||||
* There are two other potential other parents of li. They are menu and dir. The dir tag was deprecated in
|
||||
* HTML4, and removed in HTML5. The menu tag is experimental as of this writing, and basically doesn't work
|
||||
* in any browsers, even Firefox, which theoretically has limited support for it. If other parents of li
|
||||
* become viable, they will need to be added to this code.
|
||||
*
|
||||
* @method _cleanHTMLLists
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @return {String} The cleaned content
|
||||
*/
|
||||
_cleanHTMLLists: function(content) {
|
||||
var output = '',
|
||||
toProcess = content,
|
||||
match = null,
|
||||
openTags = [],
|
||||
currentTag = null,
|
||||
previousTag = null;
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
fullTag: match[0],
|
||||
isOpen: (match[1].length == 1) ? false : true
|
||||
};
|
||||
|
||||
// Get the most recent open tag.
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
|
||||
// Slice up the content based on the match and add content before the match to output.
|
||||
output += toProcess.slice(0, match.index);
|
||||
toProcess = toProcess.slice(match.index + match[0].length);
|
||||
|
||||
// Now the full content is in output + currentTag.fullTag + toProcess. When making fixes, it is best to push the fix and
|
||||
// fullTag back onto the front or toProcess, then restart the loop. This allows processing to follow the normal path
|
||||
// most often. But sometimes we will need to modify output to insert or remove tags in the already complete code.
|
||||
|
||||
if (currentTag.isOpen) {
|
||||
// We are at the opening phase of a tag.
|
||||
// We have to do special processing for list items, as they can only be children of ul and ol tags.
|
||||
if (currentTag.tagLowerCase === 'li') {
|
||||
if (!previousTag) {
|
||||
// This means we have are opening a li, but aren't in a list. This is not allowed!
|
||||
|
||||
// We are going to check for the count of open and close ol tags ahead to decide what to do.
|
||||
var closeCount = (toProcess.match(/<\/(ol)[ >]/ig) || []).length;
|
||||
var openCount = (toProcess.match(/<(ol)[ >]/ig) || []).length;
|
||||
|
||||
if (closeCount > openCount) {
|
||||
// There are more close ol's ahead than opens ahead. So open the ol and try again.
|
||||
Y.log('Adding an opening ol for orphan li', 'debug', LOGNAME);
|
||||
toProcess = '<ol>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For the other cases, just open a ul and try again. Later the closing ul will get matched if it exists,
|
||||
// or if it doesn't one will automatically get inserted.
|
||||
Y.log('Adding an opening ul for orphan li', 'debug', LOGNAME);
|
||||
toProcess = '<ul>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === 'li') {
|
||||
// You aren't allowed to nest li tags. Close the current one before starting the new one.
|
||||
Y.log('Adding a closing ' + previousTag.tag + ' before opening a new one.', 'debug', LOGNAME);
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Previous tag must be a list at this point, so we can continue.
|
||||
}
|
||||
|
||||
// If we made it this far, record the tag to the open tags list.
|
||||
openTags.push({
|
||||
tag: currentTag.tag,
|
||||
tagLowerCase: currentTag.tagLowerCase,
|
||||
position: output.length,
|
||||
length: currentTag.fullTag.length
|
||||
});
|
||||
} else {
|
||||
// We are processing a closing tag.
|
||||
|
||||
if (openTags.length == 0) {
|
||||
// We are closing a tag that isn't open. That's a problem. Just discarding should be safe.
|
||||
Y.log('Discarding extra ' + currentTag.fullTag + ' tag.', 'debug', LOGNAME);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === currentTag.tagLowerCase) {
|
||||
// Closing a tag that matches the open tag. This is the nominal case. Pop it off, and update previousTag.
|
||||
if (currentTag.tag != previousTag.tag) {
|
||||
// This would mean cases don't match between the opening and closing tag.
|
||||
// We are going to swap them to match, even though not required.
|
||||
currentTag.fullTag = currentTag.fullTag.replace(currentTag.tag, previousTag.tag);
|
||||
}
|
||||
|
||||
openTags.pop();
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
} else {
|
||||
// We are closing a tag that isn't the most recent open one open, so we have a mismatch.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag.liEnd && (previousTag.liEnd < output.length)) {
|
||||
// We are closing an unopened li, but the parent list has complete li tags more than 0 chars ago.
|
||||
// Assume we are missing an open li at the end of the previous li, and insert there.
|
||||
Y.log('Inserting opening ' + currentTag.tag + ' after previous li.', 'debug', LOGNAME);
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.liEnd);
|
||||
} else if (currentTag.tagLowerCase === 'li' && !previousTag.liEnd &&
|
||||
((previousTag.position + previousTag.length) < output.length)) {
|
||||
// We are closing an unopened li, and the parent has no previous lis in it, but opened more than 0
|
||||
// chars ago. Assume we are missing a starting li, and insert it right after the list opened.
|
||||
Y.log('Inserting opening ' + currentTag.tag + ' at start of parent.', 'debug', LOGNAME);
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.position + previousTag.length);
|
||||
} else if (previousTag.tagLowerCase === 'li') {
|
||||
// We must be trying to close a ul/ol while in a li. Just assume we are missing a closing li.
|
||||
Y.log('Adding a closing ' + previousTag.tag + ' before closing ' + currentTag.tag + '.', 'debug', LOGNAME);
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
} else {
|
||||
// Here we must be trying to close a tag that isn't open, or is open higher up. Just discard.
|
||||
// If there ends up being a missing close tag later on, that will get fixed separately.
|
||||
Y.log('Discarding incorrect ' + currentTag.fullTag + '.', 'debug', LOGNAME);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a valid closing li tag, and a list, record where the li ended.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag) {
|
||||
previousTag.liEnd = output.length + currentTag.fullTag.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now we can add the tag to the output.
|
||||
output += currentTag.fullTag;
|
||||
}
|
||||
|
||||
// Add anything left in toProcess to the output.
|
||||
output += toProcess;
|
||||
|
||||
// Anything still in the openTags list are extra and need to be dealt with.
|
||||
if (openTags.length) {
|
||||
// Work on the list in reverse order so positions stay correct.
|
||||
while ((currentTag = openTags.pop())) {
|
||||
if (currentTag.liEnd) {
|
||||
// We have a position for the last list item in this element. Insert the closing it after that.
|
||||
output = this._insertString(output, '</' + currentTag.tag + '>', currentTag.liEnd);
|
||||
Y.log('Adding closing ' + currentTag.tag + ' based on last li location.', 'debug', LOGNAME);
|
||||
} else {
|
||||
// If there weren't any children list items, then we should just remove the tag where it started.
|
||||
// This will also remote an open li tag that runs to the end of the content, since it has no children lis.
|
||||
output = output.slice(0, currentTag.position) + output.slice(currentTag.position + currentTag.length);
|
||||
Y.log('Removing opening ' + currentTag.fullTag + ' because it was missing closing.', 'debug', LOGNAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert a string in the middle of an existing string at the specified location.
|
||||
*
|
||||
* @method _insertString
|
||||
* @param {String} content The subject of the insertion.
|
||||
* @param {String} insert The string that will be inserted.
|
||||
* @param {Number} position The location to make the insertion.
|
||||
* @return {String} The string with the new content inserted.
|
||||
*/
|
||||
_insertString: function(content, insert, position) {
|
||||
return content.slice(0, position) + insert + content.slice(position);
|
||||
}
|
||||
};
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -713,7 +713,7 @@ EditorTextArea.prototype = {
|
||||
this.editor.setHTML('');
|
||||
|
||||
// Copy cleaned HTML to editable div.
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value')));
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value'), true));
|
||||
|
||||
// Insert a paragraph in the empty contenteditable div.
|
||||
if (this.editor.getHTML() === '') {
|
||||
@ -1382,9 +1382,10 @@ EditorClean.prototype = {
|
||||
* @method _cleanHTML
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @param {Boolean} deepClean If true, do a more in depth (and resource intensive) cleaning of the HTML.
|
||||
* @return {String} The cleaned HTML
|
||||
*/
|
||||
_cleanHTML: function(content) {
|
||||
_cleanHTML: function(content, deepClean) {
|
||||
// Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
|
||||
|
||||
var rules = [
|
||||
@ -1401,7 +1402,13 @@ EditorClean.prototype = {
|
||||
{regex: /<\/?(?:title|meta|style|st\d|head\b|font|html|body|link)[^>]*?>/gi, replace: ""}
|
||||
];
|
||||
|
||||
return this._filterContentWithRules(content, rules);
|
||||
content = this._filterContentWithRules(content, rules);
|
||||
|
||||
if (deepClean) {
|
||||
content = this._cleanHTMLLists(content);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1432,6 +1439,8 @@ EditorClean.prototype = {
|
||||
pasteCleanup: function(sourceEvent) {
|
||||
// We only expect paste events, but we will check anyways.
|
||||
if (sourceEvent.type === 'paste') {
|
||||
// Register the delayed paste cleanup. We will cancel it if we register the fallback cleanup.
|
||||
var delayedCleanup = this.postPasteCleanupDelayed();
|
||||
// The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
|
||||
var event = sourceEvent._event;
|
||||
// Check if we have a valid clipboardData object in the event.
|
||||
@ -1454,6 +1463,7 @@ EditorClean.prototype = {
|
||||
content = event.clipboardData.getData('text/html');
|
||||
} catch (error) {
|
||||
// Something went wrong. Fallback.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -1486,6 +1496,7 @@ EditorClean.prototype = {
|
||||
// Something went wrong. Fallback.
|
||||
// Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
|
||||
// Wait for the clipboard event to finish then fallback clean the entire editor.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -1504,10 +1515,49 @@ EditorClean.prototype = {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls postPasteCleanup on a short timer to allow the paste event handlers to complete, then deep clean the content.
|
||||
*
|
||||
* @method postPasteCleanupDelayed
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanupDelayed: function() {
|
||||
Y.soon(Y.bind(this.postPasteCleanup, this));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do additional cleanup after the paste is complete.
|
||||
*
|
||||
* @method postPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanup: function() {
|
||||
|
||||
// Save the current selection (cursor position).
|
||||
var selection = window.rangy.saveSelection();
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanHTML(content, true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
|
||||
// Restore the selection (cursor position).
|
||||
window.rangy.restoreSelection(selection);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup code after a paste event if we couldn't intercept the paste content.
|
||||
*
|
||||
* @method fallbackPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
fallbackPasteCleanup: function() {
|
||||
@ -1517,7 +1567,7 @@ EditorClean.prototype = {
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanPasteHTML(content));
|
||||
this.editor.set('innerHTML', this._cleanHTML(this._cleanPasteHTML(content), true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
@ -1697,6 +1747,188 @@ EditorClean.prototype = {
|
||||
});
|
||||
|
||||
return holder.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a function that searches for, and attempts to correct certain issues with ul/ol html lists.
|
||||
* This is needed because these lists are used heavily in page layout, and content with bad tags can
|
||||
* lead to broke course pages.
|
||||
*
|
||||
* The theory of operation here is to linearly process the incoming content, counting the opening and closing
|
||||
* of list tags, and determining when there is a mismatch.
|
||||
*
|
||||
* The specific issues this should be able to correct are:
|
||||
* - Orphaned li elements will be wrapped in a set of ul tags.
|
||||
* - li elements inside li elements.
|
||||
* - An extra closing ul, or ol tag will be discarded.
|
||||
* - An extra closing li tag will have an opening tag added if appropriate, or will be discarded.
|
||||
* - If there is an unmatched list open tag, a matching close tag will be inserted.
|
||||
*
|
||||
* It does it's best to match the case of corrected tags. Even though not required by html spec,
|
||||
* it seems like the safer route.
|
||||
*
|
||||
* A note on parent elements of li. This code assumes that li must have a ol or ul parent.
|
||||
* There are two other potential other parents of li. They are menu and dir. The dir tag was deprecated in
|
||||
* HTML4, and removed in HTML5. The menu tag is experimental as of this writing, and basically doesn't work
|
||||
* in any browsers, even Firefox, which theoretically has limited support for it. If other parents of li
|
||||
* become viable, they will need to be added to this code.
|
||||
*
|
||||
* @method _cleanHTMLLists
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @return {String} The cleaned content
|
||||
*/
|
||||
_cleanHTMLLists: function(content) {
|
||||
var output = '',
|
||||
toProcess = content,
|
||||
match = null,
|
||||
openTags = [],
|
||||
currentTag = null,
|
||||
previousTag = null;
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
fullTag: match[0],
|
||||
isOpen: (match[1].length == 1) ? false : true
|
||||
};
|
||||
|
||||
// Get the most recent open tag.
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
|
||||
// Slice up the content based on the match and add content before the match to output.
|
||||
output += toProcess.slice(0, match.index);
|
||||
toProcess = toProcess.slice(match.index + match[0].length);
|
||||
|
||||
// Now the full content is in output + currentTag.fullTag + toProcess. When making fixes, it is best to push the fix and
|
||||
// fullTag back onto the front or toProcess, then restart the loop. This allows processing to follow the normal path
|
||||
// most often. But sometimes we will need to modify output to insert or remove tags in the already complete code.
|
||||
|
||||
if (currentTag.isOpen) {
|
||||
// We are at the opening phase of a tag.
|
||||
// We have to do special processing for list items, as they can only be children of ul and ol tags.
|
||||
if (currentTag.tagLowerCase === 'li') {
|
||||
if (!previousTag) {
|
||||
// This means we have are opening a li, but aren't in a list. This is not allowed!
|
||||
|
||||
// We are going to check for the count of open and close ol tags ahead to decide what to do.
|
||||
var closeCount = (toProcess.match(/<\/(ol)[ >]/ig) || []).length;
|
||||
var openCount = (toProcess.match(/<(ol)[ >]/ig) || []).length;
|
||||
|
||||
if (closeCount > openCount) {
|
||||
// There are more close ol's ahead than opens ahead. So open the ol and try again.
|
||||
toProcess = '<ol>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For the other cases, just open a ul and try again. Later the closing ul will get matched if it exists,
|
||||
// or if it doesn't one will automatically get inserted.
|
||||
toProcess = '<ul>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === 'li') {
|
||||
// You aren't allowed to nest li tags. Close the current one before starting the new one.
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Previous tag must be a list at this point, so we can continue.
|
||||
}
|
||||
|
||||
// If we made it this far, record the tag to the open tags list.
|
||||
openTags.push({
|
||||
tag: currentTag.tag,
|
||||
tagLowerCase: currentTag.tagLowerCase,
|
||||
position: output.length,
|
||||
length: currentTag.fullTag.length
|
||||
});
|
||||
} else {
|
||||
// We are processing a closing tag.
|
||||
|
||||
if (openTags.length == 0) {
|
||||
// We are closing a tag that isn't open. That's a problem. Just discarding should be safe.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === currentTag.tagLowerCase) {
|
||||
// Closing a tag that matches the open tag. This is the nominal case. Pop it off, and update previousTag.
|
||||
if (currentTag.tag != previousTag.tag) {
|
||||
// This would mean cases don't match between the opening and closing tag.
|
||||
// We are going to swap them to match, even though not required.
|
||||
currentTag.fullTag = currentTag.fullTag.replace(currentTag.tag, previousTag.tag);
|
||||
}
|
||||
|
||||
openTags.pop();
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
} else {
|
||||
// We are closing a tag that isn't the most recent open one open, so we have a mismatch.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag.liEnd && (previousTag.liEnd < output.length)) {
|
||||
// We are closing an unopened li, but the parent list has complete li tags more than 0 chars ago.
|
||||
// Assume we are missing an open li at the end of the previous li, and insert there.
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.liEnd);
|
||||
} else if (currentTag.tagLowerCase === 'li' && !previousTag.liEnd &&
|
||||
((previousTag.position + previousTag.length) < output.length)) {
|
||||
// We are closing an unopened li, and the parent has no previous lis in it, but opened more than 0
|
||||
// chars ago. Assume we are missing a starting li, and insert it right after the list opened.
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.position + previousTag.length);
|
||||
} else if (previousTag.tagLowerCase === 'li') {
|
||||
// We must be trying to close a ul/ol while in a li. Just assume we are missing a closing li.
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
} else {
|
||||
// Here we must be trying to close a tag that isn't open, or is open higher up. Just discard.
|
||||
// If there ends up being a missing close tag later on, that will get fixed separately.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a valid closing li tag, and a list, record where the li ended.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag) {
|
||||
previousTag.liEnd = output.length + currentTag.fullTag.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now we can add the tag to the output.
|
||||
output += currentTag.fullTag;
|
||||
}
|
||||
|
||||
// Add anything left in toProcess to the output.
|
||||
output += toProcess;
|
||||
|
||||
// Anything still in the openTags list are extra and need to be dealt with.
|
||||
if (openTags.length) {
|
||||
// Work on the list in reverse order so positions stay correct.
|
||||
while ((currentTag = openTags.pop())) {
|
||||
if (currentTag.liEnd) {
|
||||
// We have a position for the last list item in this element. Insert the closing it after that.
|
||||
output = this._insertString(output, '</' + currentTag.tag + '>', currentTag.liEnd);
|
||||
} else {
|
||||
// If there weren't any children list items, then we should just remove the tag where it started.
|
||||
// This will also remote an open li tag that runs to the end of the content, since it has no children lis.
|
||||
output = output.slice(0, currentTag.position) + output.slice(currentTag.position + currentTag.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert a string in the middle of an existing string at the specified location.
|
||||
*
|
||||
* @method _insertString
|
||||
* @param {String} content The subject of the insertion.
|
||||
* @param {String} insert The string that will be inserted.
|
||||
* @param {Number} position The location to make the insertion.
|
||||
* @return {String} The string with the new content inserted.
|
||||
*/
|
||||
_insertString: function(content, insert, position) {
|
||||
return content.slice(0, position) + insert + content.slice(position);
|
||||
}
|
||||
};
|
||||
|
||||
|
249
lib/editor/atto/yui/src/editor/js/clean.js
vendored
249
lib/editor/atto/yui/src/editor/js/clean.js
vendored
@ -98,9 +98,10 @@ EditorClean.prototype = {
|
||||
* @method _cleanHTML
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @param {Boolean} deepClean If true, do a more in depth (and resource intensive) cleaning of the HTML.
|
||||
* @return {String} The cleaned HTML
|
||||
*/
|
||||
_cleanHTML: function(content) {
|
||||
_cleanHTML: function(content, deepClean) {
|
||||
// Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
|
||||
|
||||
var rules = [
|
||||
@ -117,7 +118,13 @@ EditorClean.prototype = {
|
||||
{regex: /<\/?(?:title|meta|style|st\d|head\b|font|html|body|link)[^>]*?>/gi, replace: ""}
|
||||
];
|
||||
|
||||
return this._filterContentWithRules(content, rules);
|
||||
content = this._filterContentWithRules(content, rules);
|
||||
|
||||
if (deepClean) {
|
||||
content = this._cleanHTMLLists(content);
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -148,6 +155,8 @@ EditorClean.prototype = {
|
||||
pasteCleanup: function(sourceEvent) {
|
||||
// We only expect paste events, but we will check anyways.
|
||||
if (sourceEvent.type === 'paste') {
|
||||
// Register the delayed paste cleanup. We will cancel it if we register the fallback cleanup.
|
||||
var delayedCleanup = this.postPasteCleanupDelayed();
|
||||
// The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
|
||||
var event = sourceEvent._event;
|
||||
// Check if we have a valid clipboardData object in the event.
|
||||
@ -170,6 +179,7 @@ EditorClean.prototype = {
|
||||
content = event.clipboardData.getData('text/html');
|
||||
} catch (error) {
|
||||
// Something went wrong. Fallback.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -202,6 +212,7 @@ EditorClean.prototype = {
|
||||
// Something went wrong. Fallback.
|
||||
// Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
|
||||
// Wait for the clipboard event to finish then fallback clean the entire editor.
|
||||
delayedCleanup.cancel();
|
||||
this.fallbackPasteCleanupDelayed();
|
||||
return true;
|
||||
}
|
||||
@ -220,10 +231,50 @@ EditorClean.prototype = {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls postPasteCleanup on a short timer to allow the paste event handlers to complete, then deep clean the content.
|
||||
*
|
||||
* @method postPasteCleanupDelayed
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanupDelayed: function() {
|
||||
Y.soon(Y.bind(this.postPasteCleanup, this));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do additional cleanup after the paste is complete.
|
||||
*
|
||||
* @method postPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
postPasteCleanup: function() {
|
||||
Y.log('Executing delayed post paste cleanup', 'debug', LOGNAME);
|
||||
|
||||
// Save the current selection (cursor position).
|
||||
var selection = window.rangy.saveSelection();
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanHTML(content, true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
|
||||
// Restore the selection (cursor position).
|
||||
window.rangy.restoreSelection(selection);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup code after a paste event if we couldn't intercept the paste content.
|
||||
*
|
||||
* @method fallbackPasteCleanup
|
||||
* @return {object}
|
||||
* @chainable
|
||||
*/
|
||||
fallbackPasteCleanup: function() {
|
||||
@ -234,7 +285,7 @@ EditorClean.prototype = {
|
||||
|
||||
// Get, clean, and replace the content in the editable.
|
||||
var content = this.editor.get('innerHTML');
|
||||
this.editor.set('innerHTML', this._cleanPasteHTML(content));
|
||||
this.editor.set('innerHTML', this._cleanHTML(this._cleanPasteHTML(content), true));
|
||||
|
||||
// Update the textarea.
|
||||
this.updateOriginal();
|
||||
@ -414,6 +465,198 @@ EditorClean.prototype = {
|
||||
});
|
||||
|
||||
return holder.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is a function that searches for, and attempts to correct certain issues with ul/ol html lists.
|
||||
* This is needed because these lists are used heavily in page layout, and content with bad tags can
|
||||
* lead to broke course pages.
|
||||
*
|
||||
* The theory of operation here is to linearly process the incoming content, counting the opening and closing
|
||||
* of list tags, and determining when there is a mismatch.
|
||||
*
|
||||
* The specific issues this should be able to correct are:
|
||||
* - Orphaned li elements will be wrapped in a set of ul tags.
|
||||
* - li elements inside li elements.
|
||||
* - An extra closing ul, or ol tag will be discarded.
|
||||
* - An extra closing li tag will have an opening tag added if appropriate, or will be discarded.
|
||||
* - If there is an unmatched list open tag, a matching close tag will be inserted.
|
||||
*
|
||||
* It does it's best to match the case of corrected tags. Even though not required by html spec,
|
||||
* it seems like the safer route.
|
||||
*
|
||||
* A note on parent elements of li. This code assumes that li must have a ol or ul parent.
|
||||
* There are two other potential other parents of li. They are menu and dir. The dir tag was deprecated in
|
||||
* HTML4, and removed in HTML5. The menu tag is experimental as of this writing, and basically doesn't work
|
||||
* in any browsers, even Firefox, which theoretically has limited support for it. If other parents of li
|
||||
* become viable, they will need to be added to this code.
|
||||
*
|
||||
* @method _cleanHTMLLists
|
||||
* @private
|
||||
* @param {String} content The content to clean
|
||||
* @return {String} The cleaned content
|
||||
*/
|
||||
_cleanHTMLLists: function(content) {
|
||||
var output = '',
|
||||
toProcess = content,
|
||||
match = null,
|
||||
openTags = [],
|
||||
currentTag = null,
|
||||
previousTag = null;
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
fullTag: match[0],
|
||||
isOpen: (match[1].length == 1) ? false : true
|
||||
};
|
||||
|
||||
// Get the most recent open tag.
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
|
||||
// Slice up the content based on the match and add content before the match to output.
|
||||
output += toProcess.slice(0, match.index);
|
||||
toProcess = toProcess.slice(match.index + match[0].length);
|
||||
|
||||
// Now the full content is in output + currentTag.fullTag + toProcess. When making fixes, it is best to push the fix and
|
||||
// fullTag back onto the front or toProcess, then restart the loop. This allows processing to follow the normal path
|
||||
// most often. But sometimes we will need to modify output to insert or remove tags in the already complete code.
|
||||
|
||||
if (currentTag.isOpen) {
|
||||
// We are at the opening phase of a tag.
|
||||
// We have to do special processing for list items, as they can only be children of ul and ol tags.
|
||||
if (currentTag.tagLowerCase === 'li') {
|
||||
if (!previousTag) {
|
||||
// This means we have are opening a li, but aren't in a list. This is not allowed!
|
||||
|
||||
// We are going to check for the count of open and close ol tags ahead to decide what to do.
|
||||
var closeCount = (toProcess.match(/<\/(ol)[ >]/ig) || []).length;
|
||||
var openCount = (toProcess.match(/<(ol)[ >]/ig) || []).length;
|
||||
|
||||
if (closeCount > openCount) {
|
||||
// There are more close ol's ahead than opens ahead. So open the ol and try again.
|
||||
Y.log('Adding an opening ol for orphan li', 'debug', LOGNAME);
|
||||
toProcess = '<ol>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For the other cases, just open a ul and try again. Later the closing ul will get matched if it exists,
|
||||
// or if it doesn't one will automatically get inserted.
|
||||
Y.log('Adding an opening ul for orphan li', 'debug', LOGNAME);
|
||||
toProcess = '<ul>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === 'li') {
|
||||
// You aren't allowed to nest li tags. Close the current one before starting the new one.
|
||||
Y.log('Adding a closing ' + previousTag.tag + ' before opening a new one.', 'debug', LOGNAME);
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Previous tag must be a list at this point, so we can continue.
|
||||
}
|
||||
|
||||
// If we made it this far, record the tag to the open tags list.
|
||||
openTags.push({
|
||||
tag: currentTag.tag,
|
||||
tagLowerCase: currentTag.tagLowerCase,
|
||||
position: output.length,
|
||||
length: currentTag.fullTag.length
|
||||
});
|
||||
} else {
|
||||
// We are processing a closing tag.
|
||||
|
||||
if (openTags.length == 0) {
|
||||
// We are closing a tag that isn't open. That's a problem. Just discarding should be safe.
|
||||
Y.log('Discarding extra ' + currentTag.fullTag + ' tag.', 'debug', LOGNAME);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousTag.tagLowerCase === currentTag.tagLowerCase) {
|
||||
// Closing a tag that matches the open tag. This is the nominal case. Pop it off, and update previousTag.
|
||||
if (currentTag.tag != previousTag.tag) {
|
||||
// This would mean cases don't match between the opening and closing tag.
|
||||
// We are going to swap them to match, even though not required.
|
||||
currentTag.fullTag = currentTag.fullTag.replace(currentTag.tag, previousTag.tag);
|
||||
}
|
||||
|
||||
openTags.pop();
|
||||
previousTag = (openTags.length) ? openTags[openTags.length - 1] : null;
|
||||
} else {
|
||||
// We are closing a tag that isn't the most recent open one open, so we have a mismatch.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag.liEnd && (previousTag.liEnd < output.length)) {
|
||||
// We are closing an unopened li, but the parent list has complete li tags more than 0 chars ago.
|
||||
// Assume we are missing an open li at the end of the previous li, and insert there.
|
||||
Y.log('Inserting opening ' + currentTag.tag + ' after previous li.', 'debug', LOGNAME);
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.liEnd);
|
||||
} else if (currentTag.tagLowerCase === 'li' && !previousTag.liEnd &&
|
||||
((previousTag.position + previousTag.length) < output.length)) {
|
||||
// We are closing an unopened li, and the parent has no previous lis in it, but opened more than 0
|
||||
// chars ago. Assume we are missing a starting li, and insert it right after the list opened.
|
||||
Y.log('Inserting opening ' + currentTag.tag + ' at start of parent.', 'debug', LOGNAME);
|
||||
output = this._insertString(output, '<' + currentTag.tag + '>', previousTag.position + previousTag.length);
|
||||
} else if (previousTag.tagLowerCase === 'li') {
|
||||
// We must be trying to close a ul/ol while in a li. Just assume we are missing a closing li.
|
||||
Y.log('Adding a closing ' + previousTag.tag + ' before closing ' + currentTag.tag + '.', 'debug', LOGNAME);
|
||||
toProcess = '</' + previousTag.tag + '>' + currentTag.fullTag + toProcess;
|
||||
continue;
|
||||
} else {
|
||||
// Here we must be trying to close a tag that isn't open, or is open higher up. Just discard.
|
||||
// If there ends up being a missing close tag later on, that will get fixed separately.
|
||||
Y.log('Discarding incorrect ' + currentTag.fullTag + '.', 'debug', LOGNAME);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a valid closing li tag, and a list, record where the li ended.
|
||||
if (currentTag.tagLowerCase === 'li' && previousTag) {
|
||||
previousTag.liEnd = output.length + currentTag.fullTag.length;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now we can add the tag to the output.
|
||||
output += currentTag.fullTag;
|
||||
}
|
||||
|
||||
// Add anything left in toProcess to the output.
|
||||
output += toProcess;
|
||||
|
||||
// Anything still in the openTags list are extra and need to be dealt with.
|
||||
if (openTags.length) {
|
||||
// Work on the list in reverse order so positions stay correct.
|
||||
while ((currentTag = openTags.pop())) {
|
||||
if (currentTag.liEnd) {
|
||||
// We have a position for the last list item in this element. Insert the closing it after that.
|
||||
output = this._insertString(output, '</' + currentTag.tag + '>', currentTag.liEnd);
|
||||
Y.log('Adding closing ' + currentTag.tag + ' based on last li location.', 'debug', LOGNAME);
|
||||
} else {
|
||||
// If there weren't any children list items, then we should just remove the tag where it started.
|
||||
// This will also remote an open li tag that runs to the end of the content, since it has no children lis.
|
||||
output = output.slice(0, currentTag.position) + output.slice(currentTag.position + currentTag.length);
|
||||
Y.log('Removing opening ' + currentTag.fullTag + ' because it was missing closing.', 'debug', LOGNAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert a string in the middle of an existing string at the specified location.
|
||||
*
|
||||
* @method _insertString
|
||||
* @param {String} content The subject of the insertion.
|
||||
* @param {String} insert The string that will be inserted.
|
||||
* @param {Number} position The location to make the insertion.
|
||||
* @return {String} The string with the new content inserted.
|
||||
*/
|
||||
_insertString: function(content, insert, position) {
|
||||
return content.slice(0, position) + insert + content.slice(position);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -71,7 +71,7 @@ EditorTextArea.prototype = {
|
||||
this.editor.setHTML('');
|
||||
|
||||
// Copy cleaned HTML to editable div.
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value')));
|
||||
this.editor.append(this._cleanHTML(this.textarea.get('value'), true));
|
||||
|
||||
// Insert a paragraph in the empty contenteditable div.
|
||||
if (this.editor.getHTML() === '') {
|
||||
|
@ -314,6 +314,19 @@ class behat_forms extends behat_base {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the field matches the value.
|
||||
*
|
||||
* @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" matches multiline:$/
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $field
|
||||
* @param PyStringNode $value
|
||||
* @return void
|
||||
*/
|
||||
public function the_field_matches_multiline($field, PyStringNode $value) {
|
||||
$this->the_field_matches_value($field, (string)$value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, the field does not match the value.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user