Skip to content

Commit aba8265

Browse files
committed
Merge pull request #895 from JoelParke/master
feat(textAngular): Added support to maintain html comments and most white space!
2 parents 8677c63 + 33aecd1 commit aba8265

File tree

3 files changed

+160
-43
lines changed

3 files changed

+160
-43
lines changed

src/taBind.js

+118-33
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,55 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
168168

169169
var _blankTest = _taBlankTest(_defaultTest);
170170

171-
var _ensureContentWrapped = function(value){
172-
if(_blankTest(value)) return value;
171+
var _ensureContentWrapped = function(value) {
172+
if (_blankTest(value)) return value;
173173
var domTest = angular.element("<div>" + value + "</div>");
174-
if(domTest.children().length === 0){
174+
//console.log('domTest.children().length():', domTest.children().length);
175+
if (domTest.children().length === 0) {
175176
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
176-
}else{
177+
} else {
177178
var _children = domTest[0].childNodes;
178179
var i;
179180
var _foundBlockElement = false;
180-
for(i = 0; i < _children.length; i++){
181-
if(_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
181+
for (i = 0; i < _children.length; i++) {
182+
if (_foundBlockElement = _children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)) break;
182183
}
183-
if(!_foundBlockElement){
184+
if (!_foundBlockElement) {
184185
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
185-
}else{
186+
}
187+
else{
186188
value = "";
187189
for(i = 0; i < _children.length; i++){
188-
if(!_children[i].nodeName.toLowerCase().match(BLOCKELEMENTS)){
189-
var _subVal = (_children[i].outerHTML || _children[i].nodeValue);
190+
var node = _children[i];
191+
var nodeName = node.nodeName.toLowerCase();
192+
//console.log(nodeName);
193+
if(nodeName === '#comment') {
194+
value += '<!--' + node.nodeValue + '-->';
195+
} else if(nodeName === '#text') {
196+
// determine if this is all whitespace, if so, we will leave it as it is.
197+
// otherwise, we will wrap it as it is
198+
var text = node.textContent;
199+
if (!text.trim()) {
200+
// just whitespace
201+
value += text;
202+
} else {
203+
// not pure white space so wrap in <p>...</p> or whatever attrs.taDefaultWrap is set to.
204+
value += "<" + attrs.taDefaultWrap + ">" + text + "</" + attrs.taDefaultWrap + ">";
205+
}
206+
} else if(!nodeName.match(BLOCKELEMENTS)){
207+
/* istanbul ignore next: Doesn't seem to trigger on tests */
208+
var _subVal = (node.outerHTML || node.nodeValue);
190209
/* istanbul ignore else: Doesn't seem to trigger on tests, is tested though */
191210
if(_subVal.trim() !== '')
192211
value += "<" + attrs.taDefaultWrap + ">" + _subVal + "</" + attrs.taDefaultWrap + ">";
193212
else value += _subVal;
194-
}else{
195-
value += _children[i].outerHTML;
213+
} else {
214+
value += node.outerHTML;
196215
}
197216
}
198217
}
199218
}
219+
//console.log(value);
200220
return value;
201221
};
202222

@@ -372,36 +392,99 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
372392
return result;
373393
};
374394

395+
// add a forEach function that will work on a NodeList, etc..
396+
var forEach = function (array, callback, scope) {
397+
for (var i= 0; i<array.length; i++) {
398+
callback.call(scope, i, array[i]);
399+
}
400+
};
401+
402+
// handle <ul> or <ol> nodes
375403
var recursiveListFormat = function(listNode, tablevel){
376-
var _html = '', _children = listNode.childNodes;
404+
var _html = '';
405+
var _subnodes = listNode.childNodes;
377406
tablevel++;
378-
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, listNode.outerHTML.indexOf('<li'));
379-
for(var _i = 0; _i < _children.length; _i++){
407+
// tab out and add the <ul> or <ol> html piece
408+
_html += _repeat('\t', tablevel-1) + listNode.outerHTML.substring(0, 4);
409+
forEach(_subnodes, function (index, node) {
380410
/* istanbul ignore next: browser catch */
381-
if(!_children[_i].outerHTML) continue;
382-
if(_children[_i].nodeName.toLowerCase() === 'ul' || _children[_i].nodeName.toLowerCase() === 'ol')
383-
_html += '\n' + recursiveListFormat(_children[_i], tablevel);
384-
else
385-
_html += '\n' + _repeat('\t', tablevel) + _children[_i].outerHTML;
386-
}
411+
var nodeName = node.nodeName.toLowerCase();
412+
if (nodeName === '#comment') {
413+
_html += '<!--' + node.nodeValue + '-->';
414+
return;
415+
}
416+
if (nodeName === '#text') {
417+
_html += node.textContent;
418+
return;
419+
}
420+
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
421+
if(!node.outerHTML) {
422+
// no html to add
423+
return;
424+
}
425+
if(nodeName === 'ul' || nodeName === 'ol') {
426+
_html += '\n' + recursiveListFormat(node, tablevel);
427+
}
428+
else {
429+
// no reformatting within this subnode, so just do the tabing...
430+
_html += '\n' + _repeat('\t', tablevel) + node.outerHTML;
431+
}
432+
});
433+
// now add on the </ol> or </ul> piece
387434
_html += '\n' + _repeat('\t', tablevel-1) + listNode.outerHTML.substring(listNode.outerHTML.lastIndexOf('<'));
388435
return _html;
389436
};
437+
// handle formating of something like:
438+
// <ol><!--First comment-->
439+
// <li>Test Line 1<!--comment test list 1--></li>
440+
// <ul><!--comment ul-->
441+
// <li>Nested Line 1</li>
442+
// <!--comment between nested lines--><li>Nested Line 2</li>
443+
// </ul>
444+
// <li>Test Line 3</li>
445+
// </ol>
390446
ngModel.$formatters.unshift(function(htmlValue){
391447
// tabulate the HTML so it looks nicer
392-
var _children = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
393-
if(_children.length > 0){
448+
//
449+
// first get a list of the nodes...
450+
// we do this by using the element parser...
451+
//
452+
// doing this -- which is simpiler -- breaks our tests...
453+
//var _nodes=angular.element(htmlValue);
454+
var _nodes = angular.element('<div>' + htmlValue + '</div>')[0].childNodes;
455+
if(_nodes.length > 0){
456+
// do the reformatting of the layout...
394457
htmlValue = '';
395-
for(var i = 0; i < _children.length; i++){
396-
/* istanbul ignore next: browser catch */
397-
if(!_children[i].outerHTML) continue;
398-
if(htmlValue.length > 0) htmlValue += '\n';
399-
if(_children[i].nodeName.toLowerCase() === 'ul' || _children[i].nodeName.toLowerCase() === 'ol')
400-
htmlValue += '' + recursiveListFormat(_children[i], 0);
401-
else htmlValue += '' + _children[i].outerHTML;
402-
}
458+
forEach(_nodes, function (index, node) {
459+
var nodeName = node.nodeName.toLowerCase();
460+
if (nodeName === '#comment') {
461+
htmlValue += '<!--' + node.nodeValue + '-->';
462+
return;
463+
}
464+
if (nodeName === '#text') {
465+
htmlValue += node.textContent;
466+
return;
467+
}
468+
/* istanbul ignore next: not tested, and this was original code -- so not wanting to possibly cause an issue, leaving it... */
469+
if(!node.outerHTML)
470+
{
471+
// nothing to format!
472+
return;
473+
}
474+
if(htmlValue.length > 0) {
475+
// we aready have some content, so drop to a new line
476+
htmlValue += '\n';
477+
}
478+
if(nodeName === 'ul' || nodeName === 'ol') {
479+
// okay a set of list stuff we want to reformat in a nested way
480+
htmlValue += '' + recursiveListFormat(node, 0);
481+
}
482+
else {
483+
// just use the original without any additional formating
484+
htmlValue += '' + node.outerHTML;
485+
}
486+
});
403487
}
404-
405488
return htmlValue;
406489
});
407490
}else{
@@ -534,7 +617,7 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
534617
// insert missing parent of li element
535618
text = text.replace(/<li(\s.*)?>.*<\/li(\s.*)?>/i, '<ul>$&</ul>');
536619
}
537-
620+
538621
// parse whitespace from plaintext input, starting with preceding spaces that get stripped on paste
539622
text = text.replace(/^[ |\u00A0]+/gm, function (match) {
540623
var result = '';
@@ -713,11 +796,13 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
713796
_setInnerHTML(_defaultVal);
714797
taSelection.setSelectionToElementStart(element.children()[0]);
715798
}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
799+
/* we no longer do this, since there can be comments here and white space
716800
var _savedSelection = $window.rangy.saveSelection();
717801
val = _compileHtml();
718802
val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
719803
_setInnerHTML(val);
720804
$window.rangy.restoreSelection(_savedSelection);
805+
*/
721806
}
722807
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
723808
if(_keyupTimeout) $timeout.cancel(_keyupTimeout);

src/textAngular-sanitize.js

+25-8
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,13 @@ var START_TAG_REGEXP =
168168
BEGIN_TAG_REGEXP = /^</,
169169
BEGING_END_TAGE_REGEXP = /^<\//,
170170
COMMENT_REGEXP = /<!--(.*?)-->/g,
171+
SINGLE_COMMENT_REGEXP = /(^<!--.*?-->)/,
171172
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
172173
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
173174
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
174175
// Match everything outside of normal chars and " (quote character)
175-
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
176+
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g,
177+
WHITE_SPACE_REGEXP = /^(\s+)/;
176178

177179

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

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

296-
if (index >= 0 && html.lastIndexOf("-->", index) === index) {
297-
if (handler.comment) handler.comment(html.substring(4, index));
298-
html = html.substring(index + 3);
297+
if (match) {
298+
var mat = match[0];
299+
if (handler.whitespace) handler.whitespace(match[0]);
300+
html = html.replace(match[0], '');
301+
chars = false;
302+
}
303+
//Comment
304+
} else if (SINGLE_COMMENT_REGEXP.test(html)) {
305+
match = html.match(SINGLE_COMMENT_REGEXP);
306+
307+
if (match) {
308+
if (handler.comment) handler.comment(match[1]);
309+
html = html.replace(match[0], '');
299310
chars = false;
300311
}
301312
// DOCTYPE
@@ -587,6 +598,12 @@ function htmlSanitizeWriter(buf, uriValidator) {
587598
out(unary ? '/>' : '>');
588599
}
589600
},
601+
comment: function (com) {
602+
out(com);
603+
},
604+
whitespace: function (ws) {
605+
out(encodeEntities(ws));
606+
},
590607
end: function(tag) {
591608
tag = angular.lowercase(tag);
592609
if (!ignore && validElements[tag] === true) {

test/taBind/taBind.$formatters.spec.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('taBind.$formatters', function () {
1010
afterEach(inject(function($document){
1111
$document.find('body').html('');
1212
}));
13-
13+
1414
describe('should format textarea html for readability', function(){
1515
it('adding newlines after immediate child tags', function(){
1616
$rootScope.html = '<p>Test Line 1</p><div>Test Line 2</div><span>Test Line 3</span>';
@@ -32,10 +32,25 @@ describe('taBind.$formatters', function () {
3232
$rootScope.$digest();
3333
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>');
3434
});
35+
it('handle nested lists with comments', function(){
36+
$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>';
37+
$rootScope.$digest();
38+
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>');
39+
});
3540
it('handles no tags (should wrap)', function(){
3641
$rootScope.html = 'Test Line 1';
3742
$rootScope.$digest();
3843
expect(element.val()).toBe('<p>Test Line 1</p>');
3944
});
45+
it('handles html comments', function(){
46+
$rootScope.html = '<!--This is a comment--><p>Test Line 1</p>';
47+
$rootScope.$digest();
48+
expect(element.val()).toBe('<!--This is a comment-->\n<p>Test Line 1</p>');
49+
});
50+
it('handles html comments with whitespace', function(){
51+
$rootScope.html = '<!--This is a comment--> <p>Test Line 1</p>';
52+
$rootScope.$digest();
53+
expect(element.val()).toBe('<!--This is a comment--> \n<p>Test Line 1</p>');
54+
});
4055
});
41-
});
56+
});

0 commit comments

Comments
 (0)