Skip to content

Commit

Permalink
Merge pull request #895 from JoelParke/master
Browse files Browse the repository at this point in the history
feat(textAngular): Added support to maintain html comments and most white space!
  • Loading branch information
JoelParke committed Sep 18, 2015
2 parents 8677c63 + 33aecd1 commit aba8265
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 43 deletions.
151 changes: 118 additions & 33 deletions src/taBind.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,35 +168,55 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'

var _blankTest = _taBlankTest(_defaultTest);

var _ensureContentWrapped = function(value){
if(_blankTest(value)) return value;
var _ensureContentWrapped = function(value) {
if (_blankTest(value)) return value;
var domTest = angular.element("<div>" + value + "</div>");
if(domTest.children().length === 0){
//console.log('domTest.children().length():', domTest.children().length);
if (domTest.children().length === 0) {
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
}else{
} else {
var _children = domTest[0].childNodes;
var i;
var _foundBlockElement = false;
for(i = 0; i < _children.length; i++){
if(_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
for (i = 0; i < _children.length; i++) {
if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
}
if(!_foundBlockElement){
if (!_foundBlockElement) {
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
}else{
}
else{
value = "";
for(i = 0; i < _children.length; i++){
if(!_children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)){
var _subVal = (_children[i].outerHTML || _children[i].nodeValue);
var node = _children[i];
var nodeName = node.nodeName.toLowerCase();
//console.log(nodeName);
if(nodeName === '#comment') {
value += '<!--' + node.nodeValue + '-->';
} else if(nodeName === '#text') {
// determine if this is all whitespace, if so, we will leave it as it is.
// otherwise, we will wrap it as it is
var text = node.textContent;
if (!text.trim()) {
// just whitespace
value += text;
} else {
// not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
}
} else if(!nodeName.match(BLOCKELEMENTS)){
/* istanbul ignore next: Doesn't seem to trigger on tests */
var _subVal = (node.outerHTML || node.nodeValue);
/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
if(_subVal.trim() !== '')
value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
else value += _subVal;
}else{
value += _children[i].outerHTML;
} else {
value += node.outerHTML;
}
}
}
}
//console.log(value);
return value;
};

Expand Down Expand Up @@ -372,36 +392,99 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
return result;
};

// add a forEach function that will work on a NodeList, etc..
var forEach = function (array, callback, scope) {
for (var i= 0; i<array.length; i++) {
callback.call(scope, i, array[i]);
}
};

// handle <ul> or <ol> nodes
var recursiveListFormat = function(listNode, tablevel){
var _html = '', _children = listNode.childNodes;
var _html = '';
var _subnodes = listNode.childNodes;
tablevel++;
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, listNode.outerHTML.indexOf('<li'));
for(var _i = 0; _i < _children.length; _i++){
// tab out and add the <ul> or <ol> html piece
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
forEach(_subnodes, function (index, node) {
/* istanbul ignore next: browser catch */
if(!_children[_i].outerHTML) continue;
if(_children[_i].nodeName.toLowerCase() === 'ul' || _children[_i].nodeName.toLowerCase() === 'ol')
_html += '\n' + recursiveListFormat(_children[_i], tablevel);
else
_html += '\n' + _repeat('\t', tablevel) + _children[_i].outerHTML;
}
var nodeName = node.nodeName.toLowerCase();
if (nodeName === '#comment') {
_html += '<!--' + node.nodeValue + '-->';
return;
}
if (nodeName === '#text') {
_html += node.textContent;
return;
}
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
if(!node.outerHTML) {
// no html to add
return;
}
if(nodeName === 'ul' || nodeName === 'ol') {
_html += '\n' + recursiveListFormat(node, tablevel);
}
else {
// no reformatting within this subnode, so just do the tabing...
_html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
}
});
// now add on the </ol> or </ul> piece
_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
return _html;
};
// handle formating of something like:
// <ol><!--First comment-->
// <li>Test Line 1<!--comment test list 1--></li>
// <ul><!--comment ul-->
// <li>Nested Line 1</li>
// <!--comment between nested lines--><li>Nested Line 2</li>
// </ul>
// <li>Test Line 3</li>
// </ol>
ngModel.$formatters.unshift(function(htmlValue){
// tabulate the HTML so it looks nicer
var _children = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
if(_children.length > 0){
//
// first get a list of the nodes...
// we do this by using the element parser...
//
// doing this -- which is simpiler -- breaks our tests...
//var _nodes=angular.element(htmlValue);
var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
if(_nodes.length > 0){
// do the reformatting of the layout...
htmlValue = '';
for(var i = 0; i < _children.length; i++){
/* istanbul ignore next: browser catch */
if(!_children[i].outerHTML) continue;
if(htmlValue.length > 0) htmlValue += '\n';
if(_children[i].nodeName.toLowerCase() === 'ul' || _children[i].nodeName.toLowerCase() === 'ol')
htmlValue += '' + recursiveListFormat(_children[i], 0);
else htmlValue += '' + _children[i].outerHTML;
}
forEach(_nodes, function (index, node) {
var nodeName = node.nodeName.toLowerCase();
if (nodeName === '#comment') {
htmlValue += '<!--' + node.nodeValue + '-->';
return;
}
if (nodeName === '#text') {
htmlValue += node.textContent;
return;
}
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
if(!node.outerHTML)
{
// nothing to format!
return;
}
if(htmlValue.length > 0) {
// we aready have some content, so drop to a new line
htmlValue += '\n';
}
if(nodeName === 'ul' || nodeName === 'ol') {
// okay a set of list stuff we want to reformat in a nested way
htmlValue += '' + recursiveListFormat(node, 0);
}
else {
// just use the original without any additional formating
htmlValue += '' + node.outerHTML;
}
});
}

return htmlValue;
});
}else{
Expand Down Expand Up @@ -534,7 +617,7 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
// insert missing parent of li element
text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
}

// parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
text = text.replace(/^[ |\u00A0]+/gm, function (match) {
var result = '';
Expand Down Expand Up @@ -713,11 +796,13 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
_setInnerHTML(_defaultVal);
taSelection.setSelectionToElementStart(element.children()[0]);
}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
/* we no longer do this, since there can be comments here and white space
var _savedSelection = $window.rangy.saveSelection();
val = _compileHtml();
val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
_setInnerHTML(val);
$window.rangy.restoreSelection(_savedSelection);
*/
}
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
Expand Down
33 changes: 25 additions & 8 deletions src/textAngular-sanitize.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,13 @@ var START_TAG_REGEXP =
BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
SINGLE_COMMENT_REGEXP = /(^<!--.*?-->)/,
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
// Match everything outside of normal chars and " (quote character)
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g,
WHITE_SPACE_REGEXP = /^(\s+)/;


// Good source of info about elements and attributes
Expand Down Expand Up @@ -288,14 +290,23 @@ function htmlParser(html, handler) {
// Make sure we're not in a script or style element
if (!stack.last() || !specialElements[ stack.last() ]) {

// Comment
if (html.indexOf("<!--") === 0) {
// comments containing -- are not allowed unless they terminate the comment
index = html.indexOf("--", 4);
// White space
if (WHITE_SPACE_REGEXP.test(html)) {
match = html.match(WHITE_SPACE_REGEXP);

if (index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment(html.substring(4, index));
html = html.substring(index + 3);
if (match) {
var mat = match[0];
if (handler.whitespace) handler.whitespace(match[0]);
html = html.replace(match[0], '');
chars = false;
}
//Comment
} else if (SINGLE_COMMENT_REGEXP.test(html)) {
match = html.match(SINGLE_COMMENT_REGEXP);

if (match) {
if (handler.comment) handler.comment(match[1]);
html = html.replace(match[0], '');
chars = false;
}
// DOCTYPE
Expand Down Expand Up @@ -587,6 +598,12 @@ function htmlSanitizeWriter(buf, uriValidator) {
out(unary ? '/>' : '>');
}
},
comment: function (com) {
out(com);
},
whitespace: function (ws) {
out(encodeEntities(ws));
},
end: function(tag) {
tag = angular.lowercase(tag);
if (!ignore && validElements[tag] === true) {
Expand Down
19 changes: 17 additions & 2 deletions test/taBind/taBind.$formatters.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('taBind.$formatters', function () {
afterEach(inject(function($document){
$document.find('body').html('');
}));

describe('should format textarea html for readability', function(){
it('adding newlines after immediate child tags', function(){
$rootScope.html = '<p>Test Line 1</p><div>Test Line 2</div><span>Test Line 3</span>';
Expand All @@ -32,10 +32,25 @@ describe('taBind.$formatters', function () {
$rootScope.$digest();
expect(element.val()).toBe('<ol>\n\t<li>Test Line 1</li>\n\t<ul>\n\t\t<li>Nested Line 1</li>\n\t\t<li>Nested Line 2</li>\n\t</ul>\n\t<li>Test Line 3</li>\n</ol>');
});
it('handle nested lists with comments', function(){
$rootScope.html = '<ol><!--This is line 1--><li>Test Line 1</li><ul><!--Nested line 1--> <li>Nested Line 1</li><li>Nested Line 2</li></ul><li>Test Line 3</li></ol>';
$rootScope.$digest();
expect(element.val()).toBe('<ol><!--This is line 1-->\n\t<li>Test Line 1</li>\n\t<ul><!--Nested line 1--> \n\t\t<li>Nested Line 1</li>\n\t\t<li>Nested Line 2</li>\n\t</ul>\n\t<li>Test Line 3</li>\n</ol>');
});
it('handles no tags (should wrap)', function(){
$rootScope.html = 'Test Line 1';
$rootScope.$digest();
expect(element.val()).toBe('<p>Test Line 1</p>');
});
it('handles html comments', function(){
$rootScope.html = '<!--This is a comment--><p>Test Line 1</p>';
$rootScope.$digest();
expect(element.val()).toBe('<!--This is a comment-->\n<p>Test Line 1</p>');
});
it('handles html comments with whitespace', function(){
$rootScope.html = '<!--This is a comment--> <p>Test Line 1</p>';
$rootScope.$digest();
expect(element.val()).toBe('<!--This is a comment--> \n<p>Test Line 1</p>');
});
});
});
});

0 comments on commit aba8265

Please sign in to comment.