Built motion from commit 7767ffc.|0.0.132
[motion.git] / public / bower_components / angular-sanitize / angular-sanitize.js
1 /**
2  * @license AngularJS v1.4.10
3  * (c) 2010-2015 Google, Inc. http://angularjs.org
4  * License: MIT
5  */
6 (function(window, angular, undefined) {'use strict';
7
8 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
9  *     Any commits to this file should be reviewed with security in mind.  *
10  *   Changes to this file can potentially create security vulnerabilities. *
11  *          An approval from 2 Core members with history of modifying      *
12  *                         this file is required.                          *
13  *                                                                         *
14  *  Does the change somehow allow for arbitrary javascript to be executed? *
15  *    Or allows for someone to change the prototype of built-in objects?   *
16  *     Or gives undesired access to variables likes document or window?    *
17  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
18
19 var $sanitizeMinErr = angular.$$minErr('$sanitize');
20
21 /**
22  * @ngdoc module
23  * @name ngSanitize
24  * @description
25  *
26  * # ngSanitize
27  *
28  * The `ngSanitize` module provides functionality to sanitize HTML.
29  *
30  *
31  * <div doc-module-components="ngSanitize"></div>
32  *
33  * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
34  */
35
36 /*
37  * HTML Parser By Misko Hevery (misko@hevery.com)
38  * based on:  HTML Parser By John Resig (ejohn.org)
39  * Original code by Erik Arvidsson, Mozilla Public License
40  * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
41  *
42  * // Use like so:
43  * htmlParser(htmlString, {
44  *     start: function(tag, attrs, unary) {},
45  *     end: function(tag) {},
46  *     chars: function(text) {},
47  *     comment: function(text) {}
48  * });
49  *
50  */
51
52
53 /**
54  * @ngdoc service
55  * @name $sanitize
56  * @kind function
57  *
58  * @description
59  *   The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
60  *   then serialized back to properly escaped html string. This means that no unsafe input can make
61  *   it into the returned string, however, since our parser is more strict than a typical browser
62  *   parser, it's possible that some obscure input, which would be recognized as valid HTML by a
63  *   browser, won't make it through the sanitizer. The input may also contain SVG markup.
64  *   The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
65  *   `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
66  *
67  * @param {string} html HTML input.
68  * @returns {string} Sanitized HTML.
69  *
70  * @example
71    <example module="sanitizeExample" deps="angular-sanitize.js">
72    <file name="index.html">
73      <script>
74          angular.module('sanitizeExample', ['ngSanitize'])
75            .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
76              $scope.snippet =
77                '<p style="color:blue">an html\n' +
78                '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
79                'snippet</p>';
80              $scope.deliberatelyTrustDangerousSnippet = function() {
81                return $sce.trustAsHtml($scope.snippet);
82              };
83            }]);
84      </script>
85      <div ng-controller="ExampleController">
86         Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
87        <table>
88          <tr>
89            <td>Directive</td>
90            <td>How</td>
91            <td>Source</td>
92            <td>Rendered</td>
93          </tr>
94          <tr id="bind-html-with-sanitize">
95            <td>ng-bind-html</td>
96            <td>Automatically uses $sanitize</td>
97            <td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
98            <td><div ng-bind-html="snippet"></div></td>
99          </tr>
100          <tr id="bind-html-with-trust">
101            <td>ng-bind-html</td>
102            <td>Bypass $sanitize by explicitly trusting the dangerous value</td>
103            <td>
104            <pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
105 &lt;/div&gt;</pre>
106            </td>
107            <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
108          </tr>
109          <tr id="bind-default">
110            <td>ng-bind</td>
111            <td>Automatically escapes</td>
112            <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
113            <td><div ng-bind="snippet"></div></td>
114          </tr>
115        </table>
116        </div>
117    </file>
118    <file name="protractor.js" type="protractor">
119      it('should sanitize the html snippet by default', function() {
120        expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
121          toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
122      });
123
124      it('should inline raw snippet if bound to a trusted value', function() {
125        expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
126          toBe("<p style=\"color:blue\">an html\n" +
127               "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
128               "snippet</p>");
129      });
130
131      it('should escape snippet without any filter', function() {
132        expect(element(by.css('#bind-default div')).getInnerHtml()).
133          toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
134               "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
135               "snippet&lt;/p&gt;");
136      });
137
138      it('should update', function() {
139        element(by.model('snippet')).clear();
140        element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
141        expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
142          toBe('new <b>text</b>');
143        expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
144          'new <b onclick="alert(1)">text</b>');
145        expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
146          "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
147      });
148    </file>
149    </example>
150  */
151 function $SanitizeProvider() {
152   this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
153     return function(html) {
154       var buf = [];
155       htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
156         return !/^unsafe/.test($$sanitizeUri(uri, isImage));
157       }));
158       return buf.join('');
159     };
160   }];
161 }
162
163 function sanitizeText(chars) {
164   var buf = [];
165   var writer = htmlSanitizeWriter(buf, angular.noop);
166   writer.chars(chars);
167   return buf.join('');
168 }
169
170
171 // Regular Expressions for parsing tags and attributes
172 var START_TAG_REGEXP =
173        /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
174   END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
175   ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
176   BEGIN_TAG_REGEXP = /^</,
177   BEGING_END_TAGE_REGEXP = /^<\//,
178   COMMENT_REGEXP = /<!--(.*?)-->/g,
179   DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
180   CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
181   SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
182   // Match everything outside of normal chars and " (quote character)
183   NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
184
185
186 // Good source of info about elements and attributes
187 // http://dev.w3.org/html5/spec/Overview.html#semantics
188 // http://simon.html5.org/html-elements
189
190 // Safe Void Elements - HTML5
191 // http://dev.w3.org/html5/spec/Overview.html#void-elements
192 var voidElements = makeMap("area,br,col,hr,img,wbr");
193
194 // Elements that you can, intentionally, leave open (and which close themselves)
195 // http://dev.w3.org/html5/spec/Overview.html#optional-tags
196 var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
197     optionalEndTagInlineElements = makeMap("rp,rt"),
198     optionalEndTagElements = angular.extend({},
199                                             optionalEndTagInlineElements,
200                                             optionalEndTagBlockElements);
201
202 // Safe Block Elements - HTML5
203 var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
204         "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
205         "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
206
207 // Inline Elements - HTML5
208 var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
209         "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
210         "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
211
212 // SVG Elements
213 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
214 // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
215 // They can potentially allow for arbitrary javascript to be executed. See #11290
216 var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
217         "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
218         "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
219
220 // Special Elements (can contain anything)
221 var specialElements = makeMap("script,style");
222
223 var validElements = angular.extend({},
224                                    voidElements,
225                                    blockElements,
226                                    inlineElements,
227                                    optionalEndTagElements,
228                                    svgElements);
229
230 //Attributes that have href and hence need to be sanitized
231 var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
232
233 var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
234     'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
235     'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
236     'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
237     'valign,value,vspace,width');
238
239 // SVG attributes (without "id" and "name" attributes)
240 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
241 var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
242     'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
243     'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
244     'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
245     'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
246     'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
247     'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
248     'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
249     'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
250     'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
251     'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
252     'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
253     'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
254     'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
255     'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
256
257 var validAttrs = angular.extend({},
258                                 uriAttrs,
259                                 svgAttrs,
260                                 htmlAttrs);
261
262 function makeMap(str, lowercaseKeys) {
263   var obj = {}, items = str.split(','), i;
264   for (i = 0; i < items.length; i++) {
265     obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
266   }
267   return obj;
268 }
269
270
271 /**
272  * @example
273  * htmlParser(htmlString, {
274  *     start: function(tag, attrs, unary) {},
275  *     end: function(tag) {},
276  *     chars: function(text) {},
277  *     comment: function(text) {}
278  * });
279  *
280  * @param {string} html string
281  * @param {object} handler
282  */
283 function htmlParser(html, handler) {
284   if (typeof html !== 'string') {
285     if (html === null || typeof html === 'undefined') {
286       html = '';
287     } else {
288       html = '' + html;
289     }
290   }
291   var index, chars, match, stack = [], last = html, text;
292   stack.last = function() { return stack[stack.length - 1]; };
293
294   while (html) {
295     text = '';
296     chars = true;
297
298     // Make sure we're not in a script or style element
299     if (!stack.last() || !specialElements[stack.last()]) {
300
301       // Comment
302       if (html.indexOf("<!--") === 0) {
303         // comments containing -- are not allowed unless they terminate the comment
304         index = html.indexOf("--", 4);
305
306         if (index >= 0 && html.lastIndexOf("-->", index) === index) {
307           if (handler.comment) handler.comment(html.substring(4, index));
308           html = html.substring(index + 3);
309           chars = false;
310         }
311       // DOCTYPE
312       } else if (DOCTYPE_REGEXP.test(html)) {
313         match = html.match(DOCTYPE_REGEXP);
314
315         if (match) {
316           html = html.replace(match[0], '');
317           chars = false;
318         }
319       // end tag
320       } else if (BEGING_END_TAGE_REGEXP.test(html)) {
321         match = html.match(END_TAG_REGEXP);
322
323         if (match) {
324           html = html.substring(match[0].length);
325           match[0].replace(END_TAG_REGEXP, parseEndTag);
326           chars = false;
327         }
328
329       // start tag
330       } else if (BEGIN_TAG_REGEXP.test(html)) {
331         match = html.match(START_TAG_REGEXP);
332
333         if (match) {
334           // We only have a valid start-tag if there is a '>'.
335           if (match[4]) {
336             html = html.substring(match[0].length);
337             match[0].replace(START_TAG_REGEXP, parseStartTag);
338           }
339           chars = false;
340         } else {
341           // no ending tag found --- this piece should be encoded as an entity.
342           text += '<';
343           html = html.substring(1);
344         }
345       }
346
347       if (chars) {
348         index = html.indexOf("<");
349
350         text += index < 0 ? html : html.substring(0, index);
351         html = index < 0 ? "" : html.substring(index);
352
353         if (handler.chars) handler.chars(decodeEntities(text));
354       }
355
356     } else {
357       // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
358       html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
359         function(all, text) {
360           text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
361
362           if (handler.chars) handler.chars(decodeEntities(text));
363
364           return "";
365       });
366
367       parseEndTag("", stack.last());
368     }
369
370     if (html == last) {
371       throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
372                                         "of html: {0}", html);
373     }
374     last = html;
375   }
376
377   // Clean up any remaining tags
378   parseEndTag();
379
380   function parseStartTag(tag, tagName, rest, unary) {
381     tagName = angular.lowercase(tagName);
382     if (blockElements[tagName]) {
383       while (stack.last() && inlineElements[stack.last()]) {
384         parseEndTag("", stack.last());
385       }
386     }
387
388     if (optionalEndTagElements[tagName] && stack.last() == tagName) {
389       parseEndTag("", tagName);
390     }
391
392     unary = voidElements[tagName] || !!unary;
393
394     if (!unary) {
395       stack.push(tagName);
396     }
397
398     var attrs = {};
399
400     rest.replace(ATTR_REGEXP,
401       function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
402         var value = doubleQuotedValue
403           || singleQuotedValue
404           || unquotedValue
405           || '';
406
407         attrs[name] = decodeEntities(value);
408     });
409     if (handler.start) handler.start(tagName, attrs, unary);
410   }
411
412   function parseEndTag(tag, tagName) {
413     var pos = 0, i;
414     tagName = angular.lowercase(tagName);
415     if (tagName) {
416       // Find the closest opened tag of the same type
417       for (pos = stack.length - 1; pos >= 0; pos--) {
418         if (stack[pos] == tagName) break;
419       }
420     }
421
422     if (pos >= 0) {
423       // Close all the open elements, up the stack
424       for (i = stack.length - 1; i >= pos; i--)
425         if (handler.end) handler.end(stack[i]);
426
427       // Remove the open elements from the stack
428       stack.length = pos;
429     }
430   }
431 }
432
433 var hiddenPre=document.createElement("pre");
434 /**
435  * decodes all entities into regular string
436  * @param value
437  * @returns {string} A string with decoded entities.
438  */
439 function decodeEntities(value) {
440   if (!value) { return ''; }
441
442   hiddenPre.innerHTML = value.replace(/</g,"&lt;");
443   // innerText depends on styling as it doesn't display hidden elements.
444   // Therefore, it's better to use textContent not to cause unnecessary reflows.
445   return hiddenPre.textContent;
446 }
447
448 /**
449  * Escapes all potentially dangerous characters, so that the
450  * resulting string can be safely inserted into attribute or
451  * element text.
452  * @param value
453  * @returns {string} escaped text
454  */
455 function encodeEntities(value) {
456   return value.
457     replace(/&/g, '&amp;').
458     replace(SURROGATE_PAIR_REGEXP, function(value) {
459       var hi = value.charCodeAt(0);
460       var low = value.charCodeAt(1);
461       return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
462     }).
463     replace(NON_ALPHANUMERIC_REGEXP, function(value) {
464       return '&#' + value.charCodeAt(0) + ';';
465     }).
466     replace(/</g, '&lt;').
467     replace(/>/g, '&gt;');
468 }
469
470 /**
471  * create an HTML/XML writer which writes to buffer
472  * @param {Array} buf use buf.jain('') to get out sanitized html string
473  * @returns {object} in the form of {
474  *     start: function(tag, attrs, unary) {},
475  *     end: function(tag) {},
476  *     chars: function(text) {},
477  *     comment: function(text) {}
478  * }
479  */
480 function htmlSanitizeWriter(buf, uriValidator) {
481   var ignore = false;
482   var out = angular.bind(buf, buf.push);
483   return {
484     start: function(tag, attrs, unary) {
485       tag = angular.lowercase(tag);
486       if (!ignore && specialElements[tag]) {
487         ignore = tag;
488       }
489       if (!ignore && validElements[tag] === true) {
490         out('<');
491         out(tag);
492         angular.forEach(attrs, function(value, key) {
493           var lkey=angular.lowercase(key);
494           var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
495           if (validAttrs[lkey] === true &&
496             (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
497             out(' ');
498             out(key);
499             out('="');
500             out(encodeEntities(value));
501             out('"');
502           }
503         });
504         out(unary ? '/>' : '>');
505       }
506     },
507     end: function(tag) {
508         tag = angular.lowercase(tag);
509         if (!ignore && validElements[tag] === true) {
510           out('</');
511           out(tag);
512           out('>');
513         }
514         if (tag == ignore) {
515           ignore = false;
516         }
517       },
518     chars: function(chars) {
519         if (!ignore) {
520           out(encodeEntities(chars));
521         }
522       }
523   };
524 }
525
526
527 // define ngSanitize module and register $sanitize service
528 angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
529
530 /* global sanitizeText: false */
531
532 /**
533  * @ngdoc filter
534  * @name linky
535  * @kind function
536  *
537  * @description
538  * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
539  * plain email address links.
540  *
541  * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
542  *
543  * @param {string} text Input text.
544  * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
545  * @returns {string} Html-linkified text.
546  *
547  * @usage
548    <span ng-bind-html="linky_expression | linky"></span>
549  *
550  * @example
551    <example module="linkyExample" deps="angular-sanitize.js">
552      <file name="index.html">
553        <script>
554          angular.module('linkyExample', ['ngSanitize'])
555            .controller('ExampleController', ['$scope', function($scope) {
556              $scope.snippet =
557                'Pretty text with some links:\n'+
558                'http://angularjs.org/,\n'+
559                'mailto:us@somewhere.org,\n'+
560                'another@somewhere.org,\n'+
561                'and one more: ftp://127.0.0.1/.';
562              $scope.snippetWithTarget = 'http://angularjs.org/';
563            }]);
564        </script>
565        <div ng-controller="ExampleController">
566        Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
567        <table>
568          <tr>
569            <td>Filter</td>
570            <td>Source</td>
571            <td>Rendered</td>
572          </tr>
573          <tr id="linky-filter">
574            <td>linky filter</td>
575            <td>
576              <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
577            </td>
578            <td>
579              <div ng-bind-html="snippet | linky"></div>
580            </td>
581          </tr>
582          <tr id="linky-target">
583           <td>linky target</td>
584           <td>
585             <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
586           </td>
587           <td>
588             <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
589           </td>
590          </tr>
591          <tr id="escaped-html">
592            <td>no filter</td>
593            <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
594            <td><div ng-bind="snippet"></div></td>
595          </tr>
596        </table>
597      </file>
598      <file name="protractor.js" type="protractor">
599        it('should linkify the snippet with urls', function() {
600          expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
601              toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
602                   'another@somewhere.org, and one more: ftp://127.0.0.1/.');
603          expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
604        });
605
606        it('should not linkify snippet without the linky filter', function() {
607          expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
608              toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
609                   'another@somewhere.org, and one more: ftp://127.0.0.1/.');
610          expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
611        });
612
613        it('should update', function() {
614          element(by.model('snippet')).clear();
615          element(by.model('snippet')).sendKeys('new http://link.');
616          expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
617              toBe('new http://link.');
618          expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
619          expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
620              .toBe('new http://link.');
621        });
622
623        it('should work with the target property', function() {
624         expect(element(by.id('linky-target')).
625             element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
626             toBe('http://angularjs.org/');
627         expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
628        });
629      </file>
630    </example>
631  */
632 angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
633   var LINKY_URL_REGEXP =
634         /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
635       MAILTO_REGEXP = /^mailto:/i;
636
637   return function(text, target) {
638     if (!text) return text;
639     var match;
640     var raw = text;
641     var html = [];
642     var url;
643     var i;
644     while ((match = raw.match(LINKY_URL_REGEXP))) {
645       // We can not end in these as they are sometimes found at the end of the sentence
646       url = match[0];
647       // if we did not match ftp/http/www/mailto then assume mailto
648       if (!match[2] && !match[4]) {
649         url = (match[3] ? 'http://' : 'mailto:') + url;
650       }
651       i = match.index;
652       addText(raw.substr(0, i));
653       addLink(url, match[0].replace(MAILTO_REGEXP, ''));
654       raw = raw.substring(i + match[0].length);
655     }
656     addText(raw);
657     return $sanitize(html.join(''));
658
659     function addText(text) {
660       if (!text) {
661         return;
662       }
663       html.push(sanitizeText(text));
664     }
665
666     function addLink(url, text) {
667       html.push('<a ');
668       if (angular.isDefined(target)) {
669         html.push('target="',
670                   target,
671                   '" ');
672       }
673       html.push('href="',
674                 url.replace(/"/g, '&quot;'),
675                 '">');
676       addText(text);
677       html.push('</a>');
678     }
679   };
680 }]);
681
682
683 })(window, window.angular);