vendor/assets/javascripts/unstable/angular-sanitize.js in angularjs-rails-1.2.14 vs vendor/assets/javascripts/unstable/angular-sanitize.js in angularjs-rails-1.2.15

- old
+ new

@@ -1,17 +1,27 @@ /** - * @license AngularJS v1.1.5 - * (c) 2010-2012 Google, Inc. http://angularjs.org + * @license AngularJS v1.3.0-beta.3 + * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) { -'use strict'; +(function(window, angular, undefined) {'use strict'; +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + /** - * @ngdoc overview + * @ngdoc module * @name ngSanitize * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + * <div doc-module-components="ngSanitize"></div> + * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. */ /* * HTML Parser By Misko Hevery (misko@hevery.com) * based on: HTML Parser By John Resig (ejohn.org) @@ -29,110 +39,137 @@ */ /** * @ngdoc service - * @name ngSanitize.$sanitize + * @name $sanitize * @function * * @description * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are * then serialized back to properly escaped html string. This means that no unsafe input can make * it into the returned string, however, since our parser is more strict than a typical browser * parser, it's possible that some obscure input, which would be recognized as valid HTML by a * browser, won't make it through the sanitizer. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. * * @param {string} html Html input. * @returns {string} Sanitized html. * * @example - <doc:example module="ngSanitize"> - <doc:source> - <script> - function Ctrl($scope) { - $scope.snippet = - '<p style="color:blue">an html\n' + - '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + - 'snippet</p>'; - } - </script> - <div ng-controller="Ctrl"> - Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> - <table> - <tr> - <td>Filter</td> - <td>Source</td> - <td>Rendered</td> - </tr> - <tr id="html-filter"> - <td>html filter</td> - <td> - <pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre> - </td> - <td> - <div ng-bind-html="snippet"></div> - </td> - </tr> - <tr id="escaped-html"> - <td>no filter</td> - <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td> - <td><div ng-bind="snippet"></div></td> - </tr> - <tr id="html-unsafe-filter"> - <td>unsafe html filter</td> - <td><pre>&lt;div ng-bind-html-unsafe="snippet"&gt;<br/>&lt;/div&gt;</pre></td> - <td><div ng-bind-html-unsafe="snippet"></div></td> - </tr> - </table> - </div> - </doc:source> - <doc:scenario> - it('should sanitize the html snippet ', function() { - expect(using('#html-filter').element('div').html()). - toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); - }); + <example module="ngSanitize" deps="angular-sanitize.js"> + <file name="index.html"> + <script> + function Ctrl($scope, $sce) { + $scope.snippet = + '<p style="color:blue">an html\n' + + '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + + 'snippet</p>'; + $scope.deliberatelyTrustDangerousSnippet = function() { + return $sce.trustAsHtml($scope.snippet); + }; + } + </script> + <div ng-controller="Ctrl"> + Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> + <table> + <tr> + <td>Directive</td> + <td>How</td> + <td>Source</td> + <td>Rendered</td> + </tr> + <tr id="bind-html-with-sanitize"> + <td>ng-bind-html</td> + <td>Automatically uses $sanitize</td> + <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td> + <td><div ng-bind-html="snippet"></div></td> + </tr> + <tr id="bind-html-with-trust"> + <td>ng-bind-html</td> + <td>Bypass $sanitize by explicitly trusting the dangerous value</td> + <td> + <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt; +&lt;/div&gt;</pre> + </td> + <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td> + </tr> + <tr id="bind-default"> + <td>ng-bind</td> + <td>Automatically escapes</td> + <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td> + <td><div ng-bind="snippet"></div></td> + </tr> + </table> + </div> + </file> + <file name="protractor.js" type="protractor"> + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); + }); - it('should escape snippet without any filter', function() { - expect(using('#escaped-html').element('div').html()). - toBe("&lt;p style=\"color:blue\"&gt;an html\n" + - "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" + - "snippet&lt;/p&gt;"); - }); + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); - it('should inline raw snippet if filtered as unsafe', function() { - expect(using('#html-unsafe-filter').element("div").html()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("&lt;p style=\"color:blue\"&gt;an html\n" + + "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" + + "snippet&lt;/p&gt;"); + }); - it('should update', function() { - input('snippet').enter('new <b>text</b>'); - expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>'); - expect(using('#escaped-html').element('div').html()).toBe("new &lt;b&gt;text&lt;/b&gt;"); - expect(using('#html-unsafe-filter').binding("snippet")).toBe('new <b>text</b>'); - }); - </doc:scenario> - </doc:example> + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new <b>text</b>'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new <b onclick="alert(1)">text</b>'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;"); + }); + </file> + </example> */ -var $sanitize = function(html) { +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { var buf = []; - htmlParser(html, htmlSanitizeWriter(buf)); - return buf.join(''); -}; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} // Regular Expressions for parsing tags and attributes -var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, +var START_TAG_REGEXP = + /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, BEGIN_TAG_REGEXP = /^</, BEGING_END_TAGE_REGEXP = /^<\s*\//, COMMENT_REGEXP = /<!--(.*?)-->/g, + DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/, - NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character) + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Good source of info about elements and attributes // http://dev.w3.org/html5/spec/Overview.html#semantics // http://simon.html5.org/html-elements @@ -143,35 +180,41 @@ // Elements that you can, intentionally, leave open (and which close themselves) // http://dev.w3.org/html5/spec/Overview.html#optional-tags var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), optionalEndTagInlineElements = makeMap("rp,rt"), - optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements); + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); // Safe Block Elements - HTML5 -var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," + - "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," + - "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); +var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")); // Inline Elements - HTML5 -var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," + - "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," + - "span,strike,strong,sub,sup,time,tt,u,var")); +var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); // Special Elements (can contain anything) var specialElements = makeMap("script,style"); -var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements); +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements); //Attributes that have href and hence need to be sanitized var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); var validAttrs = angular.extend({}, uriAttrs, makeMap( 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+ - 'scope,scrolling,shape,span,start,summary,target,title,type,'+ + 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+ 'valign,value,vspace,width')); function makeMap(str) { var obj = {}, items = str.split(','), i; for (i = 0; i < items.length; i++) obj[items[i]] = true; @@ -201,18 +244,26 @@ // Make sure we're not in a script or style element if ( !stack.last() || !specialElements[ stack.last() ] ) { // Comment if ( html.indexOf("<!--") === 0 ) { - index = html.indexOf("-->"); + // comments containing -- are not allowed unless they terminate the comment + index = html.indexOf("--", 4); - if ( index >= 0 ) { + if ( index >= 0 && html.lastIndexOf("-->", index) === index) { if (handler.comment) handler.comment( html.substring( 4, index ) ); html = html.substring( index + 3 ); chars = false; } + // DOCTYPE + } else if ( DOCTYPE_REGEXP.test(html) ) { + match = html.match( DOCTYPE_REGEXP ); + if ( match ) { + html = html.replace( match[0], ''); + chars = false; + } // end tag } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { match = html.match( END_TAG_REGEXP ); if ( match ) { @@ -240,25 +291,25 @@ if (handler.chars) handler.chars( decodeEntities(text) ); } } else { - html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){ - text = text. - replace(COMMENT_REGEXP, "$1"). - replace(CDATA_REGEXP, "$1"); + html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), + function(all, text){ + text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - if (handler.chars) handler.chars( decodeEntities(text) ); + if (handler.chars) handler.chars( decodeEntities(text) ); - return ""; + return ""; }); parseEndTag( "", stack.last() ); } if ( html == last ) { - throw "Parse Error: " + html; + throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + + "of html: {0}", html); } last = html; } // Clean up any remaining tags @@ -281,17 +332,18 @@ if ( !unary ) stack.push( tagName ); var attrs = {}; - rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) { - var value = doubleQuotedValue - || singleQoutedValue - || unqoutedValue - || ''; + rest.replace(ATTR_REGEXP, + function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) { + var value = doubleQuotedValue + || singleQuotedValue + || unquotedValue + || ''; - attrs[name] = decodeEntities(value); + attrs[name] = decodeEntities(value); }); if (handler.start) handler.start( tagName, attrs, unary ); } function parseEndTag( tag, tagName ) { @@ -312,27 +364,44 @@ stack.length = pos; } } } +var hiddenPre=document.createElement("pre"); +var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; /** * decodes all entities into regular string * @param value * @returns {string} A string with decoded entities. */ -var hiddenPre=document.createElement("pre"); function decodeEntities(value) { - hiddenPre.innerHTML=value.replace(/</g,"&lt;"); - return hiddenPre.innerText || hiddenPre.textContent || ''; + if (!value) { return ''; } + + // Note: IE8 does not preserve spaces at the start/end of innerHTML + // so we must capture them and reattach them afterward + var parts = spaceRe.exec(value); + var spaceBefore = parts[1]; + var spaceAfter = parts[3]; + var content = parts[2]; + if (content) { + hiddenPre.innerHTML=content.replace(/</g,"&lt;"); + // innerText depends on styling as it doesn't display hidden elements. + // Therefore, it's better to use textContent not to cause unnecessary + // reflows. However, IE<9 don't support textContent so the innerText + // fallback is necessary. + content = 'textContent' in hiddenPre ? + hiddenPre.textContent : hiddenPre.innerText; + } + return spaceBefore + content + spaceAfter; } /** * Escapes all potentially dangerous characters, so that the * resulting string can be safely inserted into attribute or * element text. * @param value - * @returns escaped text + * @returns {string} escaped text */ function encodeEntities(value) { return value. replace(/&/g, '&amp;'). replace(NON_ALPHANUMERIC_REGEXP, function(value){ @@ -350,25 +419,27 @@ * end: function(tag) {}, * chars: function(text) {}, * comment: function(text) {} * } */ -function htmlSanitizeWriter(buf){ +function htmlSanitizeWriter(buf, uriValidator){ var ignore = false; var out = angular.bind(buf, buf.push); return { start: function(tag, attrs, unary){ tag = angular.lowercase(tag); if (!ignore && specialElements[tag]) { ignore = tag; } - if (!ignore && validElements[tag] == true) { + if (!ignore && validElements[tag] === true) { out('<'); out(tag); angular.forEach(attrs, function(value, key){ var lkey=angular.lowercase(key); - if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { out(' '); out(key); out('="'); out(encodeEntities(value)); out('"'); @@ -377,11 +448,11 @@ out(unary ? '/>' : '>'); } }, end: function(tag){ tag = angular.lowercase(tag); - if (!ignore && validElements[tag] == true) { + if (!ignore && validElements[tag] === true) { out('</'); out(tag); out('>'); } if (tag == ignore) { @@ -396,54 +467,35 @@ }; } // define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).value('$sanitize', $sanitize); +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); -/** - * @ngdoc directive - * @name ngSanitize.directive:ngBindHtml - * - * @description - * Creates a binding that will sanitize the result of evaluating the `expression` with the - * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element. - * - * See {@link ngSanitize.$sanitize $sanitize} docs for examples. - * - * @element ANY - * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. - */ -angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); - scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) { - value = $sanitize(value); - element.html(value || ''); - }); - }; -}]); +/* global sanitizeText: false */ /** * @ngdoc filter - * @name ngSanitize.filter:linky + * @name linky * @function * * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * * @param {string} text Input text. * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. * @returns {string} Html-linkified text. * * @usage <span ng-bind-html="linky_expression | linky"></span> * * @example - <doc:example module="ngSanitize"> - <doc:source> + <example module="ngSanitize" deps="angular-sanitize.js"> + <file name="index.html"> <script> function Ctrl($scope) { $scope.snippet = 'Pretty text with some links:\n'+ 'http://angularjs.org/,\n'+ @@ -483,76 +535,90 @@ <td>no filter</td> <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td> <td><div ng-bind="snippet"></div></td> </tr> </table> - </doc:source> - <doc:scenario> + </file> + <file name="protractor.js" type="protractor"> it('should linkify the snippet with urls', function() { - expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links:&#10;' + - '<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' + - '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' + - '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' + - 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); }); - it ('should not linkify snippet without the linky filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("Pretty text with some links:\n" + - "http://angularjs.org/,\n" + - "mailto:us@somewhere.org,\n" + - "another@somewhere.org,\n" + - "and one more: ftp://127.0.0.1/."); + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); }); it('should update', function() { - input('snippet').enter('new http://link.'); - expect(using('#linky-filter').binding('snippet | linky')). - toBe('new <a href="http://link">http://link</a>.'); - expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); }); it('should work with the target property', function() { - expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")). - toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>'); + expect(element(by.id('linky-target')). + element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); }); - </doc:scenario> - </doc:example> + </file> + </example> */ -angular.module('ngSanitize').filter('linky', function() { - var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, MAILTO_REGEXP = /^mailto:/; return function(text, target) { if (!text) return text; var match; var raw = text; var html = []; - // TODO(vojta): use $sanitize instead - var writer = htmlSanitizeWriter(html); var url; var i; - var properties = {}; - if (angular.isDefined(target)) { - properties.target = target; - } while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; // if we did not match ftp/http/mailto then assume mailto if (match[2] == match[3]) url = 'mailto:' + url; i = match.index; - writer.chars(raw.substr(0, i)); - properties.href = url; - writer.start('a', properties); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); raw = raw.substring(i + match[0].length); } - writer.chars(raw); - return html.join(''); + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push('<a '); + if (angular.isDefined(target)) { + html.push('target="'); + html.push(target); + html.push('" '); + } + html.push('href="'); + html.push(url); + html.push('">'); + addText(text); + html.push('</a>'); + } }; -}); +}]); })(window, window.angular);