Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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