angularjs-rails 1.4.8 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @license AngularJS v1.4.8
3
- * (c) 2010-2015 Google, Inc. http://angularjs.org
2
+ * @license AngularJS v1.5.0
3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
6
6
  (function(window, angular, undefined) {'use strict';
@@ -68,6 +68,9 @@ function shallowClearAndCopy(src, dst) {
68
68
  * @ngdoc service
69
69
  * @name $resource
70
70
  * @requires $http
71
+ * @requires ng.$log
72
+ * @requires $q
73
+ * @requires ng.$timeout
71
74
  *
72
75
  * @description
73
76
  * A factory which creates a resource object that lets you interact with
@@ -102,7 +105,7 @@ function shallowClearAndCopy(src, dst) {
102
105
  * can escape it with `/\.`.
103
106
  *
104
107
  * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
105
- * `actions` methods. If any of the parameter value is a function, it will be executed every time
108
+ * `actions` methods. If a parameter value is a function, it will be executed every time
106
109
  * when a param value needs to be obtained for a request (unless the param was overridden).
107
110
  *
108
111
  * Each key value in the parameter object is first bound to url template if present and then any
@@ -112,9 +115,9 @@ function shallowClearAndCopy(src, dst) {
112
115
  * URL `/path/greet?salutation=Hello`.
113
116
  *
114
117
  * If the parameter value is prefixed with `@` then the value for that parameter will be extracted
115
- * from the corresponding property on the `data` object (provided when calling an action method). For
116
- * example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of `someParam`
117
- * will be `data.someProp`.
118
+ * from the corresponding property on the `data` object (provided when calling an action method).
119
+ * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
120
+ * `someParam` will be `data.someProp`.
118
121
  *
119
122
  * @param {Object.<Object>=} actions Hash with declaration of custom actions that should extend
120
123
  * the default set of resource actions. The declaration should be created in the format of {@link
@@ -148,15 +151,21 @@ function shallowClearAndCopy(src, dst) {
148
151
  * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` –
149
152
  * transform function or an array of such functions. The transform function takes the http
150
153
  * response body and headers and returns its transformed (typically deserialized) version.
151
- * By default, transformResponse will contain one function that checks if the response looks like
152
- * a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, set
153
- * `transformResponse` to an empty array: `transformResponse: []`
154
+ * By default, transformResponse will contain one function that checks if the response looks
155
+ * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior,
156
+ * set `transformResponse` to an empty array: `transformResponse: []`
154
157
  * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
155
158
  * GET request, otherwise if a cache instance built with
156
159
  * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
157
160
  * caching.
158
- * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that
159
- * should abort the request when resolved.
161
+ * - **`timeout`** – `{number}` – timeout in milliseconds.<br />
162
+ * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are
163
+ * **not** supported in $resource, because the same value would be used for multiple requests.
164
+ * If you are looking for a way to cancel requests, you should use the `cancellable` option.
165
+ * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call
166
+ * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's
167
+ * return value. Calling `$cancelRequest()` for a non-cancellable or an already
168
+ * completed/cancelled request will have no effect.<br />
160
169
  * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the
161
170
  * XHR object. See
162
171
  * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5)
@@ -168,12 +177,13 @@ function shallowClearAndCopy(src, dst) {
168
177
  * with `http response` object. See {@link ng.$http $http interceptors}.
169
178
  *
170
179
  * @param {Object} options Hash with custom settings that should extend the
171
- * default `$resourceProvider` behavior. The only supported option is
172
- *
173
- * Where:
180
+ * default `$resourceProvider` behavior. The supported options are:
174
181
  *
175
182
  * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing
176
183
  * slashes from any calculated URL will be stripped. (Defaults to true.)
184
+ * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be
185
+ * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value.
186
+ * This can be overwritten per action. (Defaults to false.)
177
187
  *
178
188
  * @returns {Object} A resource "class" object with methods for the default set of resource actions
179
189
  * optionally extended with custom `actions`. The default set contains these actions:
@@ -221,7 +231,7 @@ function shallowClearAndCopy(src, dst) {
221
231
  * Class actions return empty instance (with additional properties below).
222
232
  * Instance actions return promise of the action.
223
233
  *
224
- * The Resource instances and collection have these additional properties:
234
+ * The Resource instances and collections have these additional properties:
225
235
  *
226
236
  * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this
227
237
  * instance or collection.
@@ -231,7 +241,7 @@ function shallowClearAndCopy(src, dst) {
231
241
  * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
232
242
  * rendering until the resource(s) are loaded.
233
243
  *
234
- * On failure, the promise is resolved with the {@link ng.$http http response} object, without
244
+ * On failure, the promise is rejected with the {@link ng.$http http response} object, without
235
245
  * the `resource` property.
236
246
  *
237
247
  * If an interceptor object was provided, the promise will instead be resolved with the value
@@ -241,6 +251,11 @@ function shallowClearAndCopy(src, dst) {
241
251
  * rejection), `false` before that. Knowing if the Resource has been resolved is useful in
242
252
  * data-binding.
243
253
  *
254
+ * The Resource instances and collections have these additional methods:
255
+ *
256
+ * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or
257
+ * collection, calling this method will abort the request.
258
+ *
244
259
  * @example
245
260
  *
246
261
  * # Credit card resource
@@ -285,6 +300,11 @@ function shallowClearAndCopy(src, dst) {
285
300
  *
286
301
  * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and
287
302
  * `headers`.
303
+ *
304
+ * @example
305
+ *
306
+ * # User resource
307
+ *
288
308
  * When the data is returned from the server then the object is an instance of the resource type and
289
309
  * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
290
310
  * operations (create, read, update, delete) on server-side data.
@@ -303,10 +323,10 @@ function shallowClearAndCopy(src, dst) {
303
323
  *
304
324
  ```js
305
325
  var User = $resource('/user/:userId', {userId:'@id'});
306
- User.get({userId:123}, function(u, getResponseHeaders){
307
- u.abc = true;
308
- u.$save(function(u, putResponseHeaders) {
309
- //u => saved user object
326
+ User.get({userId:123}, function(user, getResponseHeaders){
327
+ user.abc = true;
328
+ user.$save(function(user, putResponseHeaders) {
329
+ //user => saved user object
310
330
  //putResponseHeaders => $http header getter
311
331
  });
312
332
  });
@@ -321,8 +341,11 @@ function shallowClearAndCopy(src, dst) {
321
341
  $scope.user = user;
322
342
  });
323
343
  ```
324
-
344
+ *
345
+ * @example
346
+ *
325
347
  * # Creating a custom 'PUT' request
348
+ *
326
349
  * In this example we create a custom method on our resource to make a PUT request
327
350
  * ```js
328
351
  * var app = angular.module('app', ['ngResource', 'ngRoute']);
@@ -350,6 +373,34 @@ function shallowClearAndCopy(src, dst) {
350
373
  * // This will PUT /notes/ID with the note object in the request payload
351
374
  * }]);
352
375
  * ```
376
+ *
377
+ * @example
378
+ *
379
+ * # Cancelling requests
380
+ *
381
+ * If an action's configuration specifies that it is cancellable, you can cancel the request related
382
+ * to an instance or collection (as long as it is a result of a "non-instance" call):
383
+ *
384
+ ```js
385
+ // ...defining the `Hotel` resource...
386
+ var Hotel = $resource('/api/hotel/:id', {id: '@id'}, {
387
+ // Let's make the `query()` method cancellable
388
+ query: {method: 'get', isArray: true, cancellable: true}
389
+ });
390
+
391
+ // ...somewhere in the PlanVacationController...
392
+ ...
393
+ this.onDestinationChanged = function onDestinationChanged(destination) {
394
+ // We don't care about any pending request for hotels
395
+ // in a different destination any more
396
+ this.availableHotels.$cancelRequest();
397
+
398
+ // Let's query for hotels in '<destination>'
399
+ // (calls: /api/hotel?location=<destination>)
400
+ this.availableHotels = Hotel.query({location: destination});
401
+ };
402
+ ```
403
+ *
353
404
  */
354
405
  angular.module('ngResource', ['ng']).
355
406
  provider('$resource', function() {
@@ -370,7 +421,7 @@ angular.module('ngResource', ['ng']).
370
421
  }
371
422
  };
372
423
 
373
- this.$get = ['$http', '$q', function($http, $q) {
424
+ this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) {
374
425
 
375
426
  var noop = angular.noop,
376
427
  forEach = angular.forEach,
@@ -438,7 +489,9 @@ angular.module('ngResource', ['ng']).
438
489
  }
439
490
  if (!(new RegExp("^\\d+$").test(param)) && param &&
440
491
  (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) {
441
- urlParams[param] = true;
492
+ urlParams[param] = {
493
+ isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url)
494
+ };
442
495
  }
443
496
  });
444
497
  url = url.replace(/\\:/g, ':');
@@ -448,10 +501,14 @@ angular.module('ngResource', ['ng']).
448
501
  });
449
502
 
450
503
  params = params || {};
451
- forEach(self.urlParams, function(_, urlParam) {
504
+ forEach(self.urlParams, function(paramInfo, urlParam) {
452
505
  val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
453
506
  if (angular.isDefined(val) && val !== null) {
454
- encodedVal = encodeUriSegment(val);
507
+ if (paramInfo.isQueryParamValue) {
508
+ encodedVal = encodeUriQuery(val, true);
509
+ } else {
510
+ encodedVal = encodeUriSegment(val);
511
+ }
455
512
  url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) {
456
513
  return encodedVal + p1;
457
514
  });
@@ -523,6 +580,20 @@ angular.module('ngResource', ['ng']).
523
580
 
524
581
  forEach(actions, function(action, name) {
525
582
  var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method);
583
+ var numericTimeout = action.timeout;
584
+ var cancellable = angular.isDefined(action.cancellable) ? action.cancellable :
585
+ (options && angular.isDefined(options.cancellable)) ? options.cancellable :
586
+ provider.defaults.cancellable;
587
+
588
+ if (numericTimeout && !angular.isNumber(numericTimeout)) {
589
+ $log.debug('ngResource:\n' +
590
+ ' Only numeric values are allowed as `timeout`.\n' +
591
+ ' Promises are not supported in $resource, because the same value would ' +
592
+ 'be used for multiple requests. If you are looking for a way to cancel ' +
593
+ 'requests, you should use the `cancellable` option.');
594
+ delete action.timeout;
595
+ numericTimeout = null;
596
+ }
526
597
 
527
598
  Resource[name] = function(a1, a2, a3, a4) {
528
599
  var params = {}, data, success, error;
@@ -571,6 +642,8 @@ angular.module('ngResource', ['ng']).
571
642
  defaultResponseInterceptor;
572
643
  var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
573
644
  undefined;
645
+ var timeoutDeferred;
646
+ var numericTimeoutPromise;
574
647
 
575
648
  forEach(action, function(value, key) {
576
649
  switch (key) {
@@ -580,21 +653,27 @@ angular.module('ngResource', ['ng']).
580
653
  case 'params':
581
654
  case 'isArray':
582
655
  case 'interceptor':
583
- break;
584
- case 'timeout':
585
- httpConfig[key] = value;
656
+ case 'cancellable':
586
657
  break;
587
658
  }
588
659
  });
589
660
 
661
+ if (!isInstanceCall && cancellable) {
662
+ timeoutDeferred = $q.defer();
663
+ httpConfig.timeout = timeoutDeferred.promise;
664
+
665
+ if (numericTimeout) {
666
+ numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout);
667
+ }
668
+ }
669
+
590
670
  if (hasBody) httpConfig.data = data;
591
671
  route.setUrlParams(httpConfig,
592
672
  extend({}, extractParams(data, action.params || {}), params),
593
673
  action.url);
594
674
 
595
675
  var promise = $http(httpConfig).then(function(response) {
596
- var data = response.data,
597
- promise = value.$promise;
676
+ var data = response.data;
598
677
 
599
678
  if (data) {
600
679
  // Need to convert action.isArray to boolean in case it is undefined
@@ -619,24 +698,28 @@ angular.module('ngResource', ['ng']).
619
698
  }
620
699
  });
621
700
  } else {
701
+ var promise = value.$promise; // Save the promise
622
702
  shallowClearAndCopy(data, value);
623
- value.$promise = promise;
703
+ value.$promise = promise; // Restore the promise
624
704
  }
625
705
  }
626
-
627
- value.$resolved = true;
628
-
629
706
  response.resource = value;
630
707
 
631
708
  return response;
632
709
  }, function(response) {
633
- value.$resolved = true;
634
-
635
710
  (error || noop)(response);
636
-
637
711
  return $q.reject(response);
638
712
  });
639
713
 
714
+ promise.finally(function() {
715
+ value.$resolved = true;
716
+ if (!isInstanceCall && cancellable) {
717
+ value.$cancelRequest = angular.noop;
718
+ $timeout.cancel(numericTimeoutPromise);
719
+ timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null;
720
+ }
721
+ });
722
+
640
723
  promise = promise.then(
641
724
  function(response) {
642
725
  var value = responseInterceptor(response);
@@ -651,6 +734,7 @@ angular.module('ngResource', ['ng']).
651
734
  // - return the instance / collection
652
735
  value.$promise = promise;
653
736
  value.$resolved = false;
737
+ if (cancellable) value.$cancelRequest = timeoutDeferred.resolve;
654
738
 
655
739
  return value;
656
740
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @license AngularJS v1.4.8
3
- * (c) 2010-2015 Google, Inc. http://angularjs.org
2
+ * @license AngularJS v1.5.0
3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
6
6
  (function(window, angular, undefined) {'use strict';
@@ -105,8 +105,17 @@ function $RouteProvider() {
105
105
  * If all the promises are resolved successfully, the values of the resolved promises are
106
106
  * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is
107
107
  * fired. If any of the promises are rejected the
108
- * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object
109
- * is:
108
+ * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired.
109
+ * For easier access to the resolved dependencies from the template, the `resolve` map will
110
+ * be available on the scope of the route, under `$resolve` (by default) or a custom name
111
+ * specified by the `resolveAs` property (see below). This can be particularly useful, when
112
+ * working with {@link angular.Module#component components} as route templates.<br />
113
+ * <div class="alert alert-warning">
114
+ * **Note:** If your scope already contains a property with this name, it will be hidden
115
+ * or overwritten. Make sure, you specify an appropriate name for this property, that
116
+ * does not collide with other properties on the scope.
117
+ * </div>
118
+ * The map object is:
110
119
  *
111
120
  * - `key` – `{string}`: a name of a dependency to be injected into the controller.
112
121
  * - `factory` - `{string|function}`: If `string` then it is an alias for a service.
@@ -116,7 +125,10 @@ function $RouteProvider() {
116
125
  * `ngRoute.$routeParams` will still refer to the previous route within these resolve
117
126
  * functions. Use `$route.current.params` to access the new route parameters, instead.
118
127
  *
119
- * - `redirectTo` {(string|function())=} value to update
128
+ * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on
129
+ * the scope of the route. If omitted, defaults to `$resolve`.
130
+ *
131
+ * - `redirectTo` – `{(string|function())=}` – value to update
120
132
  * {@link ng.$location $location} path with and trigger route redirection.
121
133
  *
122
134
  * If `redirectTo` is a function, it will be called with the following parameters:
@@ -129,13 +141,13 @@ function $RouteProvider() {
129
141
  * The custom `redirectTo` function is expected to return a string which will be used
130
142
  * to update `$location.path()` and `$location.search()`.
131
143
  *
132
- * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()`
144
+ * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()`
133
145
  * or `$location.hash()` changes.
134
146
  *
135
147
  * If the option is set to `false` and url in the browser changes, then
136
148
  * `$routeUpdate` event is broadcasted on the root scope.
137
149
  *
138
- * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive
150
+ * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
139
151
  *
140
152
  * If the option is set to `true`, then the particular route can be matched without being
141
153
  * case sensitive
@@ -265,7 +277,7 @@ function $RouteProvider() {
265
277
  * @property {Object} current Reference to the current route definition.
266
278
  * The route definition contains:
267
279
  *
268
- * - `controller`: The controller constructor as define in route definition.
280
+ * - `controller`: The controller constructor as defined in the route definition.
269
281
  * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for
270
282
  * controller instantiation. The `locals` contain
271
283
  * the resolved values of the `resolve` map. Additionally the `locals` also contain:
@@ -273,6 +285,10 @@ function $RouteProvider() {
273
285
  * - `$scope` - The current route scope.
274
286
  * - `$template` - The current route template HTML.
275
287
  *
288
+ * The `locals` will be assigned to the route scope's `$resolve` property. You can override
289
+ * the property name, using `resolveAs` in the route definition. See
290
+ * {@link ngRoute.$routeProvider $routeProvider} for more info.
291
+ *
276
292
  * @property {Object} routes Object with all route configuration Objects as its properties.
277
293
  *
278
294
  * @description
@@ -468,10 +484,18 @@ function $RouteProvider() {
468
484
  */
469
485
  reload: function() {
470
486
  forceReload = true;
487
+
488
+ var fakeLocationEvent = {
489
+ defaultPrevented: false,
490
+ preventDefault: function fakePreventDefault() {
491
+ this.defaultPrevented = true;
492
+ forceReload = false;
493
+ }
494
+ };
495
+
471
496
  $rootScope.$evalAsync(function() {
472
- // Don't support cancellation of a reload for now...
473
- prepareRoute();
474
- commitRoute();
497
+ prepareRoute(fakeLocationEvent);
498
+ if (!fakeLocationEvent.defaultPrevented) commitRoute();
475
499
  });
476
500
  },
477
501
 
@@ -981,6 +1005,7 @@ function ngViewFillContentFactory($compile, $controller, $route) {
981
1005
  $element.data('$ngControllerController', controller);
982
1006
  $element.children().data('$ngControllerController', controller);
983
1007
  }
1008
+ scope[current.resolveAs || '$resolve'] = locals;
984
1009
 
985
1010
  link(scope);
986
1011
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @license AngularJS v1.4.8
3
- * (c) 2010-2015 Google, Inc. http://angularjs.org
2
+ * @license AngularJS v1.5.0
3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
6
6
  (function(window, angular, undefined) {'use strict';
@@ -33,36 +33,23 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
33
33
  * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
34
34
  */
35
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
36
  /**
54
37
  * @ngdoc service
55
38
  * @name $sanitize
56
39
  * @kind function
57
40
  *
58
41
  * @description
42
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
43
+ *
59
44
  * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
60
45
  * 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`}.
46
+ * it into the returned string.
47
+ *
48
+ * The whitelist for URL sanitization of attribute values is configured using the functions
49
+ * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
50
+ * `$compileProvider`}.
51
+ *
52
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
66
53
  *
67
54
  * @param {string} html HTML input.
68
55
  * @returns {string} Sanitized HTML.
@@ -148,16 +135,70 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
148
135
  </file>
149
136
  </example>
150
137
  */
138
+
139
+
140
+ /**
141
+ * @ngdoc provider
142
+ * @name $sanitizeProvider
143
+ *
144
+ * @description
145
+ * Creates and configures {@link $sanitize} instance.
146
+ */
151
147
  function $SanitizeProvider() {
148
+ var svgEnabled = false;
149
+
152
150
  this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
151
+ if (svgEnabled) {
152
+ angular.extend(validElements, svgElements);
153
+ }
153
154
  return function(html) {
154
155
  var buf = [];
155
156
  htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
156
- return !/^unsafe/.test($$sanitizeUri(uri, isImage));
157
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
157
158
  }));
158
159
  return buf.join('');
159
160
  };
160
161
  }];
162
+
163
+
164
+ /**
165
+ * @ngdoc method
166
+ * @name $sanitizeProvider#enableSvg
167
+ * @kind function
168
+ *
169
+ * @description
170
+ * Enables a subset of svg to be supported by the sanitizer.
171
+ *
172
+ * <div class="alert alert-warning">
173
+ * <p>By enabling this setting without taking other precautions, you might expose your
174
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
175
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
176
+ * link). Such behavior can then result in phishing incidents.</p>
177
+ *
178
+ * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
179
+ * tags within the sanitized content:</p>
180
+ *
181
+ * <br>
182
+ *
183
+ * <pre><code>
184
+ * .rootOfTheIncludedContent svg {
185
+ * overflow: hidden !important;
186
+ * }
187
+ * </code></pre>
188
+ * </div>
189
+ *
190
+ * @param {boolean=} regexp New regexp to whitelist urls with.
191
+ * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
192
+ * without an argument or self for chaining otherwise.
193
+ */
194
+ this.enableSvg = function(enableSvg) {
195
+ if (angular.isDefined(enableSvg)) {
196
+ svgEnabled = enableSvg;
197
+ return this;
198
+ } else {
199
+ return svgEnabled;
200
+ }
201
+ };
161
202
  }
162
203
 
163
204
  function sanitizeText(chars) {
@@ -169,18 +210,9 @@ function sanitizeText(chars) {
169
210
 
170
211
 
171
212
  // 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,
213
+ var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
182
214
  // Match everything outside of normal chars and " (quote character)
183
- NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
215
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
184
216
 
185
217
 
186
218
  // Good source of info about elements and attributes
@@ -189,23 +221,23 @@ var START_TAG_REGEXP =
189
221
 
190
222
  // Safe Void Elements - HTML5
191
223
  // http://dev.w3.org/html5/spec/Overview.html#void-elements
192
- var voidElements = makeMap("area,br,col,hr,img,wbr");
224
+ var voidElements = toMap("area,br,col,hr,img,wbr");
193
225
 
194
226
  // Elements that you can, intentionally, leave open (and which close themselves)
195
227
  // 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"),
228
+ var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
229
+ optionalEndTagInlineElements = toMap("rp,rt"),
198
230
  optionalEndTagElements = angular.extend({},
199
231
  optionalEndTagInlineElements,
200
232
  optionalEndTagBlockElements);
201
233
 
202
234
  // Safe Block Elements - HTML5
203
- var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
235
+ var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
204
236
  "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"));
237
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
206
238
 
207
239
  // Inline Elements - HTML5
208
- var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
240
+ var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
209
241
  "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
210
242
  "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
211
243
 
@@ -213,24 +245,23 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a
213
245
  // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
214
246
  // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
215
247
  // 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," +
248
+ var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
217
249
  "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
218
- "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
250
+ "radialGradient,rect,stop,svg,switch,text,title,tspan");
219
251
 
220
- // Special Elements (can contain anything)
221
- var specialElements = makeMap("script,style");
252
+ // Blocked Elements (will be stripped)
253
+ var blockedElements = toMap("script,style");
222
254
 
223
255
  var validElements = angular.extend({},
224
256
  voidElements,
225
257
  blockElements,
226
258
  inlineElements,
227
- optionalEndTagElements,
228
- svgElements);
259
+ optionalEndTagElements);
229
260
 
230
261
  //Attributes that have href and hence need to be sanitized
231
- var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
262
+ var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
232
263
 
233
- var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
264
+ var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
234
265
  'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
235
266
  'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
236
267
  'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
@@ -238,7 +269,7 @@ var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspac
238
269
 
239
270
  // SVG attributes (without "id" and "name" attributes)
240
271
  // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
241
- var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
272
+ var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
242
273
  'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
243
274
  'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
244
275
  'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
@@ -259,7 +290,7 @@ var validAttrs = angular.extend({},
259
290
  svgAttrs,
260
291
  htmlAttrs);
261
292
 
262
- function makeMap(str, lowercaseKeys) {
293
+ function toMap(str, lowercaseKeys) {
263
294
  var obj = {}, items = str.split(','), i;
264
295
  for (i = 0; i < items.length; i++) {
265
296
  obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
@@ -267,11 +298,32 @@ function makeMap(str, lowercaseKeys) {
267
298
  return obj;
268
299
  }
269
300
 
301
+ var inertBodyElement;
302
+ (function(window) {
303
+ var doc;
304
+ if (window.document && window.document.implementation) {
305
+ doc = window.document.implementation.createHTMLDocument("inert");
306
+ } else {
307
+ throw $sanitizeMinErr('noinert', "Can't create an inert html document");
308
+ }
309
+ var docElement = doc.documentElement || doc.getDocumentElement();
310
+ var bodyElements = docElement.getElementsByTagName('body');
311
+
312
+ // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
313
+ if (bodyElements.length === 1) {
314
+ inertBodyElement = bodyElements[0];
315
+ } else {
316
+ var html = doc.createElement('html');
317
+ inertBodyElement = doc.createElement('body');
318
+ html.appendChild(inertBodyElement);
319
+ doc.appendChild(html);
320
+ }
321
+ })(window);
270
322
 
271
323
  /**
272
324
  * @example
273
325
  * htmlParser(htmlString, {
274
- * start: function(tag, attrs, unary) {},
326
+ * start: function(tag, attrs) {},
275
327
  * end: function(tag) {},
276
328
  * chars: function(text) {},
277
329
  * comment: function(text) {}
@@ -281,169 +333,74 @@ function makeMap(str, lowercaseKeys) {
281
333
  * @param {object} handler
282
334
  */
283
335
  function htmlParser(html, handler) {
284
- if (typeof html !== 'string') {
285
- if (html === null || typeof html === 'undefined') {
286
- html = '';
287
- } else {
288
- html = '' + html;
289
- }
336
+ if (html === null || html === undefined) {
337
+ html = '';
338
+ } else if (typeof html !== 'string') {
339
+ html = '' + html;
290
340
  }
291
- var index, chars, match, stack = [], last = html, text;
292
- stack.last = function() { return stack[stack.length - 1]; };
341
+ inertBodyElement.innerHTML = html;
293
342
 
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
- }
343
+ //mXSS protection
344
+ var mXSSAttempts = 5;
345
+ do {
346
+ if (mXSSAttempts === 0) {
347
+ throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
348
+ }
349
+ mXSSAttempts--;
328
350
 
329
- // start tag
330
- } else if (BEGIN_TAG_REGEXP.test(html)) {
331
- match = html.match(START_TAG_REGEXP);
351
+ // strip custom-namespaced attributes on IE<=11
352
+ if (document.documentMode <= 11) {
353
+ stripCustomNsAttrs(inertBodyElement);
354
+ }
355
+ html = inertBodyElement.innerHTML; //trigger mXSS
356
+ inertBodyElement.innerHTML = html;
357
+ } while (html !== inertBodyElement.innerHTML);
358
+
359
+ var node = inertBodyElement.firstChild;
360
+ while (node) {
361
+ switch (node.nodeType) {
362
+ case 1: // ELEMENT_NODE
363
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
364
+ break;
365
+ case 3: // TEXT NODE
366
+ handler.chars(node.textContent);
367
+ break;
368
+ }
332
369
 
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);
370
+ var nextNode;
371
+ if (!(nextNode = node.firstChild)) {
372
+ if (node.nodeType == 1) {
373
+ handler.end(node.nodeName.toLowerCase());
374
+ }
375
+ nextNode = node.nextSibling;
376
+ if (!nextNode) {
377
+ while (nextNode == null) {
378
+ node = node.parentNode;
379
+ if (node === inertBodyElement) break;
380
+ nextNode = node.nextSibling;
381
+ if (node.nodeType == 1) {
382
+ handler.end(node.nodeName.toLowerCase());
338
383
  }
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
384
  }
345
385
  }
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
386
  }
374
- last = html;
387
+ node = nextNode;
375
388
  }
376
389
 
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);
390
+ while (node = inertBodyElement.firstChild) {
391
+ inertBodyElement.removeChild(node);
410
392
  }
393
+ }
411
394
 
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
- }
395
+ function attrToMap(attrs) {
396
+ var map = {};
397
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
398
+ var attr = attrs[i];
399
+ map[attr.name] = attr.value;
430
400
  }
401
+ return map;
431
402
  }
432
403
 
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
404
 
448
405
  /**
449
406
  * Escapes all potentially dangerous characters, so that the
@@ -469,24 +426,24 @@ function encodeEntities(value) {
469
426
 
470
427
  /**
471
428
  * create an HTML/XML writer which writes to buffer
472
- * @param {Array} buf use buf.jain('') to get out sanitized html string
429
+ * @param {Array} buf use buf.join('') to get out sanitized html string
473
430
  * @returns {object} in the form of {
474
- * start: function(tag, attrs, unary) {},
431
+ * start: function(tag, attrs) {},
475
432
  * end: function(tag) {},
476
433
  * chars: function(text) {},
477
434
  * comment: function(text) {}
478
435
  * }
479
436
  */
480
437
  function htmlSanitizeWriter(buf, uriValidator) {
481
- var ignore = false;
438
+ var ignoreCurrentElement = false;
482
439
  var out = angular.bind(buf, buf.push);
483
440
  return {
484
- start: function(tag, attrs, unary) {
441
+ start: function(tag, attrs) {
485
442
  tag = angular.lowercase(tag);
486
- if (!ignore && specialElements[tag]) {
487
- ignore = tag;
443
+ if (!ignoreCurrentElement && blockedElements[tag]) {
444
+ ignoreCurrentElement = tag;
488
445
  }
489
- if (!ignore && validElements[tag] === true) {
446
+ if (!ignoreCurrentElement && validElements[tag] === true) {
490
447
  out('<');
491
448
  out(tag);
492
449
  angular.forEach(attrs, function(value, key) {
@@ -501,29 +458,63 @@ function htmlSanitizeWriter(buf, uriValidator) {
501
458
  out('"');
502
459
  }
503
460
  });
504
- out(unary ? '/>' : '>');
461
+ out('>');
505
462
  }
506
463
  },
507
464
  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
- },
465
+ tag = angular.lowercase(tag);
466
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
467
+ out('</');
468
+ out(tag);
469
+ out('>');
470
+ }
471
+ if (tag == ignoreCurrentElement) {
472
+ ignoreCurrentElement = false;
473
+ }
474
+ },
518
475
  chars: function(chars) {
519
- if (!ignore) {
520
- out(encodeEntities(chars));
521
- }
476
+ if (!ignoreCurrentElement) {
477
+ out(encodeEntities(chars));
522
478
  }
479
+ }
523
480
  };
524
481
  }
525
482
 
526
483
 
484
+ /**
485
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
486
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
487
+ * to allow any of these custom attributes. This method strips them all.
488
+ *
489
+ * @param node Root element to process
490
+ */
491
+ function stripCustomNsAttrs(node) {
492
+ if (node.nodeType === Node.ELEMENT_NODE) {
493
+ var attrs = node.attributes;
494
+ for (var i = 0, l = attrs.length; i < l; i++) {
495
+ var attrNode = attrs[i];
496
+ var attrName = attrNode.name.toLowerCase();
497
+ if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
498
+ node.removeAttributeNode(attrNode);
499
+ i--;
500
+ l--;
501
+ }
502
+ }
503
+ }
504
+
505
+ var nextNode = node.firstChild;
506
+ if (nextNode) {
507
+ stripCustomNsAttrs(nextNode);
508
+ }
509
+
510
+ nextNode = node.nextSibling;
511
+ if (nextNode) {
512
+ stripCustomNsAttrs(nextNode);
513
+ }
514
+ }
515
+
516
+
517
+
527
518
  // define ngSanitize module and register $sanitize service
528
519
  angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
529
520
 
@@ -535,14 +526,25 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
535
526
  * @kind function
536
527
  *
537
528
  * @description
538
- * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
529
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
539
530
  * plain email address links.
540
531
  *
541
532
  * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
542
533
  *
543
534
  * @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.
535
+ * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
536
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
537
+ *
538
+ * Can be one of:
539
+ *
540
+ * - `object`: A map of attributes
541
+ * - `function`: Takes the url as a parameter and returns a map of attributes
542
+ *
543
+ * If the map of attributes contains a value for `target`, it overrides the value of
544
+ * the target parameter.
545
+ *
546
+ *
547
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
546
548
  *
547
549
  * @usage
548
550
  <span ng-bind-html="linky_expression | linky"></span>
@@ -550,25 +552,13 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
550
552
  * @example
551
553
  <example module="linkyExample" deps="angular-sanitize.js">
552
554
  <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
555
  <div ng-controller="ExampleController">
566
556
  Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
567
557
  <table>
568
558
  <tr>
569
- <td>Filter</td>
570
- <td>Source</td>
571
- <td>Rendered</td>
559
+ <th>Filter</th>
560
+ <th>Source</th>
561
+ <th>Rendered</th>
572
562
  </tr>
573
563
  <tr id="linky-filter">
574
564
  <td>linky filter</td>
@@ -582,10 +572,19 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
582
572
  <tr id="linky-target">
583
573
  <td>linky target</td>
584
574
  <td>
585
- <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
575
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
576
+ </td>
577
+ <td>
578
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
579
+ </td>
580
+ </tr>
581
+ <tr id="linky-custom-attributes">
582
+ <td>linky custom attributes</td>
583
+ <td>
584
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</pre>
586
585
  </td>
587
586
  <td>
588
- <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
587
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
589
588
  </td>
590
589
  </tr>
591
590
  <tr id="escaped-html">
@@ -595,6 +594,18 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
595
594
  </tr>
596
595
  </table>
597
596
  </file>
597
+ <file name="script.js">
598
+ angular.module('linkyExample', ['ngSanitize'])
599
+ .controller('ExampleController', ['$scope', function($scope) {
600
+ $scope.snippet =
601
+ 'Pretty text with some links:\n'+
602
+ 'http://angularjs.org/,\n'+
603
+ 'mailto:us@somewhere.org,\n'+
604
+ 'another@somewhere.org,\n'+
605
+ 'and one more: ftp://127.0.0.1/.';
606
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
607
+ }]);
608
+ </file>
598
609
  <file name="protractor.js" type="protractor">
599
610
  it('should linkify the snippet with urls', function() {
600
611
  expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
@@ -622,10 +633,17 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
622
633
 
623
634
  it('should work with the target property', function() {
624
635
  expect(element(by.id('linky-target')).
625
- element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
636
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
626
637
  toBe('http://angularjs.org/');
627
638
  expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
628
639
  });
640
+
641
+ it('should optionally add custom attributes', function() {
642
+ expect(element(by.id('linky-custom-attributes')).
643
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
644
+ toBe('http://angularjs.org/');
645
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
646
+ });
629
647
  </file>
630
648
  </example>
631
649
  */
@@ -634,8 +652,13 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
634
652
  /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
635
653
  MAILTO_REGEXP = /^mailto:/i;
636
654
 
637
- return function(text, target) {
638
- if (!text) return text;
655
+ var linkyMinErr = angular.$$minErr('linky');
656
+ var isString = angular.isString;
657
+
658
+ return function(text, target, attributes) {
659
+ if (text == null || text === '') return text;
660
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
661
+
639
662
  var match;
640
663
  var raw = text;
641
664
  var html = [];
@@ -664,8 +687,19 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
664
687
  }
665
688
 
666
689
  function addLink(url, text) {
690
+ var key;
667
691
  html.push('<a ');
668
- if (angular.isDefined(target)) {
692
+ if (angular.isFunction(attributes)) {
693
+ attributes = attributes(url);
694
+ }
695
+ if (angular.isObject(attributes)) {
696
+ for (key in attributes) {
697
+ html.push(key + '="' + attributes[key] + '" ');
698
+ }
699
+ } else {
700
+ attributes = {};
701
+ }
702
+ if (angular.isDefined(target) && !('target' in attributes)) {
669
703
  html.push('target="',
670
704
  target,
671
705
  '" ');