Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
15747 anikendra 1
/**
2
 * @license AngularJS v1.3.13
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
      // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
345
      html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
346
        function(all, text) {
347
          text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
348
 
349
          if (handler.chars) handler.chars(decodeEntities(text));
350
 
351
          return "";
352
      });
353
 
354
      parseEndTag("", stack.last());
355
    }
356
 
357
    if (html == last) {
358
      throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
359
                                        "of html: {0}", html);
360
    }
361
    last = html;
362
  }
363
 
364
  // Clean up any remaining tags
365
  parseEndTag();
366
 
367
  function parseStartTag(tag, tagName, rest, unary) {
368
    tagName = angular.lowercase(tagName);
369
    if (blockElements[tagName]) {
370
      while (stack.last() && inlineElements[stack.last()]) {
371
        parseEndTag("", stack.last());
372
      }
373
    }
374
 
375
    if (optionalEndTagElements[tagName] && stack.last() == tagName) {
376
      parseEndTag("", tagName);
377
    }
378
 
379
    unary = voidElements[tagName] || !!unary;
380
 
381
    if (!unary)
382
      stack.push(tagName);
383
 
384
    var attrs = {};
385
 
386
    rest.replace(ATTR_REGEXP,
387
      function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
388
        var value = doubleQuotedValue
389
          || singleQuotedValue
390
          || unquotedValue
391
          || '';
392
 
393
        attrs[name] = decodeEntities(value);
394
    });
395
    if (handler.start) handler.start(tagName, attrs, unary);
396
  }
397
 
398
  function parseEndTag(tag, tagName) {
399
    var pos = 0, i;
400
    tagName = angular.lowercase(tagName);
401
    if (tagName)
402
      // Find the closest opened tag of the same type
403
      for (pos = stack.length - 1; pos >= 0; pos--)
404
        if (stack[pos] == tagName)
405
          break;
406
 
407
    if (pos >= 0) {
408
      // Close all the open elements, up the stack
409
      for (i = stack.length - 1; i >= pos; i--)
410
        if (handler.end) handler.end(stack[i]);
411
 
412
      // Remove the open elements from the stack
413
      stack.length = pos;
414
    }
415
  }
416
}
417
 
418
var hiddenPre=document.createElement("pre");
419
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
420
/**
421
 * decodes all entities into regular string
422
 * @param value
423
 * @returns {string} A string with decoded entities.
424
 */
425
function decodeEntities(value) {
426
  if (!value) { return ''; }
427
 
428
  // Note: IE8 does not preserve spaces at the start/end of innerHTML
429
  // so we must capture them and reattach them afterward
430
  var parts = spaceRe.exec(value);
431
  var spaceBefore = parts[1];
432
  var spaceAfter = parts[3];
433
  var content = parts[2];
434
  if (content) {
435
    hiddenPre.innerHTML=content.replace(/</g,"&lt;");
436
    // innerText depends on styling as it doesn't display hidden elements.
437
    // Therefore, it's better to use textContent not to cause unnecessary
438
    // reflows. However, IE<9 don't support textContent so the innerText
439
    // fallback is necessary.
440
    content = 'textContent' in hiddenPre ?
441
      hiddenPre.textContent : hiddenPre.innerText;
442
  }
443
  return spaceBefore + content + spaceAfter;
444
}
445
 
446
/**
447
 * Escapes all potentially dangerous characters, so that the
448
 * resulting string can be safely inserted into attribute or
449
 * element text.
450
 * @param value
451
 * @returns {string} escaped text
452
 */
453
function encodeEntities(value) {
454
  return value.
455
    replace(/&/g, '&amp;').
456
    replace(SURROGATE_PAIR_REGEXP, function(value) {
457
      var hi = value.charCodeAt(0);
458
      var low = value.charCodeAt(1);
459
      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
460
    }).
461
    replace(NON_ALPHANUMERIC_REGEXP, function(value) {
462
      return '&#' + value.charCodeAt(0) + ';';
463
    }).
464
    replace(/</g, '&lt;').
465
    replace(/>/g, '&gt;');
466
}
467
 
468
/**
469
 * create an HTML/XML writer which writes to buffer
470
 * @param {Array} buf use buf.jain('') to get out sanitized html string
471
 * @returns {object} in the form of {
472
 *     start: function(tag, attrs, unary) {},
473
 *     end: function(tag) {},
474
 *     chars: function(text) {},
475
 *     comment: function(text) {}
476
 * }
477
 */
478
function htmlSanitizeWriter(buf, uriValidator) {
479
  var ignore = false;
480
  var out = angular.bind(buf, buf.push);
481
  return {
482
    start: function(tag, attrs, unary) {
483
      tag = angular.lowercase(tag);
484
      if (!ignore && specialElements[tag]) {
485
        ignore = tag;
486
      }
487
      if (!ignore && validElements[tag] === true) {
488
        out('<');
489
        out(tag);
490
        angular.forEach(attrs, function(value, key) {
491
          var lkey=angular.lowercase(key);
492
          var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
493
          if (validAttrs[lkey] === true &&
494
            (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
495
            out(' ');
496
            out(key);
497
            out('="');
498
            out(encodeEntities(value));
499
            out('"');
500
          }
501
        });
502
        out(unary ? '/>' : '>');
503
      }
504
    },
505
    end: function(tag) {
506
        tag = angular.lowercase(tag);
507
        if (!ignore && validElements[tag] === true) {
508
          out('</');
509
          out(tag);
510
          out('>');
511
        }
512
        if (tag == ignore) {
513
          ignore = false;
514
        }
515
      },
516
    chars: function(chars) {
517
        if (!ignore) {
518
          out(encodeEntities(chars));
519
        }
520
      }
521
  };
522
}
523
 
524
 
525
// define ngSanitize module and register $sanitize service
526
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
527
 
528
/* global sanitizeText: false */
529
 
530
/**
531
 * @ngdoc filter
532
 * @name linky
533
 * @kind function
534
 *
535
 * @description
536
 * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
537
 * plain email address links.
538
 *
539
 * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
540
 *
541
 * @param {string} text Input text.
542
 * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
543
 * @returns {string} Html-linkified text.
544
 *
545
 * @usage
546
   <span ng-bind-html="linky_expression | linky"></span>
547
 *
548
 * @example
549
   <example module="linkyExample" deps="angular-sanitize.js">
550
     <file name="index.html">
551
       <script>
552
         angular.module('linkyExample', ['ngSanitize'])
553
           .controller('ExampleController', ['$scope', function($scope) {
554
             $scope.snippet =
555
               'Pretty text with some links:\n'+
556
               'http://angularjs.org/,\n'+
557
               'mailto:us@somewhere.org,\n'+
558
               'another@somewhere.org,\n'+
559
               'and one more: ftp://127.0.0.1/.';
560
             $scope.snippetWithTarget = 'http://angularjs.org/';
561
           }]);
562
       </script>
563
       <div ng-controller="ExampleController">
564
       Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
565
       <table>
566
         <tr>
567
           <td>Filter</td>
568
           <td>Source</td>
569
           <td>Rendered</td>
570
         </tr>
571
         <tr id="linky-filter">
572
           <td>linky filter</td>
573
           <td>
574
             <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
575
           </td>
576
           <td>
577
             <div ng-bind-html="snippet | linky"></div>
578
           </td>
579
         </tr>
580
         <tr id="linky-target">
581
          <td>linky target</td>
582
          <td>
583
            <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
584
          </td>
585
          <td>
586
            <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
587
          </td>
588
         </tr>
589
         <tr id="escaped-html">
590
           <td>no filter</td>
591
           <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
592
           <td><div ng-bind="snippet"></div></td>
593
         </tr>
594
       </table>
595
     </file>
596
     <file name="protractor.js" type="protractor">
597
       it('should linkify the snippet with urls', function() {
598
         expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
599
             toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
600
                  'another@somewhere.org, and one more: ftp://127.0.0.1/.');
601
         expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
602
       });
603
 
604
       it('should not linkify snippet without the linky filter', function() {
605
         expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
606
             toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
607
                  'another@somewhere.org, and one more: ftp://127.0.0.1/.');
608
         expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
609
       });
610
 
611
       it('should update', function() {
612
         element(by.model('snippet')).clear();
613
         element(by.model('snippet')).sendKeys('new http://link.');
614
         expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
615
             toBe('new http://link.');
616
         expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
617
         expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
618
             .toBe('new http://link.');
619
       });
620
 
621
       it('should work with the target property', function() {
622
        expect(element(by.id('linky-target')).
623
            element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
624
            toBe('http://angularjs.org/');
625
        expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
626
       });
627
     </file>
628
   </example>
629
 */
630
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
631
  var LINKY_URL_REGEXP =
632
        /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,
633
      MAILTO_REGEXP = /^mailto:/;
634
 
635
  return function(text, target) {
636
    if (!text) return text;
637
    var match;
638
    var raw = text;
639
    var html = [];
640
    var url;
641
    var i;
642
    while ((match = raw.match(LINKY_URL_REGEXP))) {
643
      // We can not end in these as they are sometimes found at the end of the sentence
644
      url = match[0];
645
      // if we did not match ftp/http/www/mailto then assume mailto
646
      if (!match[2] && !match[4]) {
647
        url = (match[3] ? 'http://' : 'mailto:') + url;
648
      }
649
      i = match.index;
650
      addText(raw.substr(0, i));
651
      addLink(url, match[0].replace(MAILTO_REGEXP, ''));
652
      raw = raw.substring(i + match[0].length);
653
    }
654
    addText(raw);
655
    return $sanitize(html.join(''));
656
 
657
    function addText(text) {
658
      if (!text) {
659
        return;
660
      }
661
      html.push(sanitizeText(text));
662
    }
663
 
664
    function addLink(url, text) {
665
      html.push('<a ');
666
      if (angular.isDefined(target)) {
667
        html.push('target="',
668
                  target,
669
                  '" ');
670
      }
671
      html.push('href="',
672
                url.replace(/"/g, '&quot;'),
673
                '">');
674
      addText(text);
675
      html.push('</a>');
676
    }
677
  };
678
}]);
679
 
680
 
681
})(window, window.angular);