angular-on-rails 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * @license AngularJS v1.0.5
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ /**
10
+ * @ngdoc overview
11
+ * @name ngResource
12
+ * @description
13
+ */
14
+
15
+ /**
16
+ * @ngdoc object
17
+ * @name ngResource.$resource
18
+ * @requires $http
19
+ *
20
+ * @description
21
+ * A factory which creates a resource object that lets you interact with
22
+ * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
23
+ *
24
+ * The returned resource object has action methods which provide high-level behaviors without
25
+ * the need to interact with the low level {@link ng.$http $http} service.
26
+ *
27
+ * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
28
+ * `/user/:username`. If you are using a URL with a port number (e.g.
29
+ * `http://example.com:8080/api`), you'll need to escape the colon character before the port
30
+ * number, like this: `$resource('http://example.com\\:8080/api')`.
31
+ *
32
+ * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
33
+ * `actions` methods.
34
+ *
35
+ * Each key value in the parameter object is first bound to url template if present and then any
36
+ * excess keys are appended to the url search query after the `?`.
37
+ *
38
+ * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
39
+ * URL `/path/greet?salutation=Hello`.
40
+ *
41
+ * If the parameter value is prefixed with `@` then the value of that parameter is extracted from
42
+ * the data object (useful for non-GET operations).
43
+ *
44
+ * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
45
+ * default set of resource actions. The declaration should be created in the following format:
46
+ *
47
+ * {action1: {method:?, params:?, isArray:?},
48
+ * action2: {method:?, params:?, isArray:?},
49
+ * ...}
50
+ *
51
+ * Where:
52
+ *
53
+ * - `action` – {string} – The name of action. This name becomes the name of the method on your
54
+ * resource object.
55
+ * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
56
+ * and `JSONP`
57
+ * - `params` – {object=} – Optional set of pre-bound parameters for this action.
58
+ * - isArray – {boolean=} – If true then the returned object for this action is an array, see
59
+ * `returns` section.
60
+ *
61
+ * @returns {Object} A resource "class" object with methods for the default set of resource actions
62
+ * optionally extended with custom `actions`. The default set contains these actions:
63
+ *
64
+ * { 'get': {method:'GET'},
65
+ * 'save': {method:'POST'},
66
+ * 'query': {method:'GET', isArray:true},
67
+ * 'remove': {method:'DELETE'},
68
+ * 'delete': {method:'DELETE'} };
69
+ *
70
+ * Calling these methods invoke an {@link ng.$http} with the specified http method,
71
+ * destination and parameters. When the data is returned from the server then the object is an
72
+ * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
73
+ * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
74
+ * read, update, delete) on server-side data like this:
75
+ * <pre>
76
+ var User = $resource('/user/:userId', {userId:'@id'});
77
+ var user = User.get({userId:123}, function() {
78
+ user.abc = true;
79
+ user.$save();
80
+ });
81
+ </pre>
82
+ *
83
+ * It is important to realize that invoking a $resource object method immediately returns an
84
+ * empty reference (object or array depending on `isArray`). Once the data is returned from the
85
+ * server the existing reference is populated with the actual data. This is a useful trick since
86
+ * usually the resource is assigned to a model which is then rendered by the view. Having an empty
87
+ * object results in no rendering, once the data arrives from the server then the object is
88
+ * populated with the data and the view automatically re-renders itself showing the new data. This
89
+ * means that in most case one never has to write a callback function for the action methods.
90
+ *
91
+ * The action methods on the class object or instance object can be invoked with the following
92
+ * parameters:
93
+ *
94
+ * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
95
+ * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
96
+ * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
97
+ *
98
+ *
99
+ * @example
100
+ *
101
+ * # Credit card resource
102
+ *
103
+ * <pre>
104
+ // Define CreditCard class
105
+ var CreditCard = $resource('/user/:userId/card/:cardId',
106
+ {userId:123, cardId:'@id'}, {
107
+ charge: {method:'POST', params:{charge:true}}
108
+ });
109
+
110
+ // We can retrieve a collection from the server
111
+ var cards = CreditCard.query(function() {
112
+ // GET: /user/123/card
113
+ // server returns: [ {id:456, number:'1234', name:'Smith'} ];
114
+
115
+ var card = cards[0];
116
+ // each item is an instance of CreditCard
117
+ expect(card instanceof CreditCard).toEqual(true);
118
+ card.name = "J. Smith";
119
+ // non GET methods are mapped onto the instances
120
+ card.$save();
121
+ // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
122
+ // server returns: {id:456, number:'1234', name: 'J. Smith'};
123
+
124
+ // our custom method is mapped as well.
125
+ card.$charge({amount:9.99});
126
+ // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
127
+ });
128
+
129
+ // we can create an instance as well
130
+ var newCard = new CreditCard({number:'0123'});
131
+ newCard.name = "Mike Smith";
132
+ newCard.$save();
133
+ // POST: /user/123/card {number:'0123', name:'Mike Smith'}
134
+ // server returns: {id:789, number:'01234', name: 'Mike Smith'};
135
+ expect(newCard.id).toEqual(789);
136
+ * </pre>
137
+ *
138
+ * The object returned from this function execution is a resource "class" which has "static" method
139
+ * for each action in the definition.
140
+ *
141
+ * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`.
142
+ * When the data is returned from the server then the object is an instance of the resource type and
143
+ * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
144
+ * operations (create, read, update, delete) on server-side data.
145
+
146
+ <pre>
147
+ var User = $resource('/user/:userId', {userId:'@id'});
148
+ var user = User.get({userId:123}, function() {
149
+ user.abc = true;
150
+ user.$save();
151
+ });
152
+ </pre>
153
+ *
154
+ * It's worth noting that the success callback for `get`, `query` and other method gets passed
155
+ * in the response that came from the server as well as $http header getter function, so one
156
+ * could rewrite the above example and get access to http headers as:
157
+ *
158
+ <pre>
159
+ var User = $resource('/user/:userId', {userId:'@id'});
160
+ User.get({userId:123}, function(u, getResponseHeaders){
161
+ u.abc = true;
162
+ u.$save(function(u, putResponseHeaders) {
163
+ //u => saved user object
164
+ //putResponseHeaders => $http header getter
165
+ });
166
+ });
167
+ </pre>
168
+
169
+ * # Buzz client
170
+
171
+ Let's look at what a buzz client created with the `$resource` service looks like:
172
+ <doc:example>
173
+ <doc:source jsfiddle="false">
174
+ <script>
175
+ function BuzzController($resource) {
176
+ this.userId = 'googlebuzz';
177
+ this.Activity = $resource(
178
+ 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
179
+ {alt:'json', callback:'JSON_CALLBACK'},
180
+ {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}}
181
+ );
182
+ }
183
+
184
+ BuzzController.prototype = {
185
+ fetch: function() {
186
+ this.activities = this.Activity.get({userId:this.userId});
187
+ },
188
+ expandReplies: function(activity) {
189
+ activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
190
+ }
191
+ };
192
+ BuzzController.$inject = ['$resource'];
193
+ </script>
194
+
195
+ <div ng-controller="BuzzController">
196
+ <input ng-model="userId"/>
197
+ <button ng-click="fetch()">fetch</button>
198
+ <hr/>
199
+ <div ng-repeat="item in activities.data.items">
200
+ <h1 style="font-size: 15px;">
201
+ <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
202
+ <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
203
+ <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
204
+ </h1>
205
+ {{item.object.content | html}}
206
+ <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
207
+ <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
208
+ <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </doc:source>
213
+ <doc:scenario>
214
+ </doc:scenario>
215
+ </doc:example>
216
+ */
217
+ angular.module('ngResource', ['ng']).
218
+ factory('$resource', ['$http', '$parse', function($http, $parse) {
219
+ var DEFAULT_ACTIONS = {
220
+ 'get': {method:'GET'},
221
+ 'save': {method:'POST'},
222
+ 'query': {method:'GET', isArray:true},
223
+ 'remove': {method:'DELETE'},
224
+ 'delete': {method:'DELETE'}
225
+ };
226
+ var noop = angular.noop,
227
+ forEach = angular.forEach,
228
+ extend = angular.extend,
229
+ copy = angular.copy,
230
+ isFunction = angular.isFunction,
231
+ getter = function(obj, path) {
232
+ return $parse(path)(obj);
233
+ };
234
+
235
+ /**
236
+ * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
237
+ * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
238
+ * segments:
239
+ * segment = *pchar
240
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
241
+ * pct-encoded = "%" HEXDIG HEXDIG
242
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
243
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
244
+ * / "*" / "+" / "," / ";" / "="
245
+ */
246
+ function encodeUriSegment(val) {
247
+ return encodeUriQuery(val, true).
248
+ replace(/%26/gi, '&').
249
+ replace(/%3D/gi, '=').
250
+ replace(/%2B/gi, '+');
251
+ }
252
+
253
+
254
+ /**
255
+ * This method is intended for encoding *key* or *value* parts of query component. We need a custom
256
+ * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be
257
+ * encoded per http://tools.ietf.org/html/rfc3986:
258
+ * query = *( pchar / "/" / "?" )
259
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
260
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
261
+ * pct-encoded = "%" HEXDIG HEXDIG
262
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
263
+ * / "*" / "+" / "," / ";" / "="
264
+ */
265
+ function encodeUriQuery(val, pctEncodeSpaces) {
266
+ return encodeURIComponent(val).
267
+ replace(/%40/gi, '@').
268
+ replace(/%3A/gi, ':').
269
+ replace(/%24/g, '$').
270
+ replace(/%2C/gi, ',').
271
+ replace((pctEncodeSpaces ? null : /%20/g), '+');
272
+ }
273
+
274
+ function Route(template, defaults) {
275
+ this.template = template = template + '#';
276
+ this.defaults = defaults || {};
277
+ var urlParams = this.urlParams = {};
278
+ forEach(template.split(/\W/), function(param){
279
+ if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) {
280
+ urlParams[param] = true;
281
+ }
282
+ });
283
+ this.template = template.replace(/\\:/g, ':');
284
+ }
285
+
286
+ Route.prototype = {
287
+ url: function(params) {
288
+ var self = this,
289
+ url = this.template,
290
+ val,
291
+ encodedVal;
292
+
293
+ params = params || {};
294
+ forEach(this.urlParams, function(_, urlParam){
295
+ val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
296
+ if (angular.isDefined(val) && val !== null) {
297
+ encodedVal = encodeUriSegment(val);
298
+ url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1");
299
+ } else {
300
+ url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match,
301
+ leadingSlashes, tail) {
302
+ if (tail.charAt(0) == '/') {
303
+ return tail;
304
+ } else {
305
+ return leadingSlashes + tail;
306
+ }
307
+ });
308
+ }
309
+ });
310
+ url = url.replace(/\/?#$/, '');
311
+ var query = [];
312
+ forEach(params, function(value, key){
313
+ if (!self.urlParams[key]) {
314
+ query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
315
+ }
316
+ });
317
+ query.sort();
318
+ url = url.replace(/\/*$/, '');
319
+ return url + (query.length ? '?' + query.join('&') : '');
320
+ }
321
+ };
322
+
323
+
324
+ function ResourceFactory(url, paramDefaults, actions) {
325
+ var route = new Route(url);
326
+
327
+ actions = extend({}, DEFAULT_ACTIONS, actions);
328
+
329
+ function extractParams(data, actionParams){
330
+ var ids = {};
331
+ actionParams = extend({}, paramDefaults, actionParams);
332
+ forEach(actionParams, function(value, key){
333
+ ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
334
+ });
335
+ return ids;
336
+ }
337
+
338
+ function Resource(value){
339
+ copy(value || {}, this);
340
+ }
341
+
342
+ forEach(actions, function(action, name) {
343
+ action.method = angular.uppercase(action.method);
344
+ var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
345
+ Resource[name] = function(a1, a2, a3, a4) {
346
+ var params = {};
347
+ var data;
348
+ var success = noop;
349
+ var error = null;
350
+ switch(arguments.length) {
351
+ case 4:
352
+ error = a4;
353
+ success = a3;
354
+ //fallthrough
355
+ case 3:
356
+ case 2:
357
+ if (isFunction(a2)) {
358
+ if (isFunction(a1)) {
359
+ success = a1;
360
+ error = a2;
361
+ break;
362
+ }
363
+
364
+ success = a2;
365
+ error = a3;
366
+ //fallthrough
367
+ } else {
368
+ params = a1;
369
+ data = a2;
370
+ success = a3;
371
+ break;
372
+ }
373
+ case 1:
374
+ if (isFunction(a1)) success = a1;
375
+ else if (hasBody) data = a1;
376
+ else params = a1;
377
+ break;
378
+ case 0: break;
379
+ default:
380
+ throw "Expected between 0-4 arguments [params, data, success, error], got " +
381
+ arguments.length + " arguments.";
382
+ }
383
+
384
+ var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
385
+ $http({
386
+ method: action.method,
387
+ url: route.url(extend({}, extractParams(data, action.params || {}), params)),
388
+ data: data
389
+ }).then(function(response) {
390
+ var data = response.data;
391
+
392
+ if (data) {
393
+ if (action.isArray) {
394
+ value.length = 0;
395
+ forEach(data, function(item) {
396
+ value.push(new Resource(item));
397
+ });
398
+ } else {
399
+ copy(data, value);
400
+ }
401
+ }
402
+ (success||noop)(value, response.headers);
403
+ }, error);
404
+
405
+ return value;
406
+ };
407
+
408
+
409
+ Resource.prototype['$' + name] = function(a1, a2, a3) {
410
+ var params = extractParams(this),
411
+ success = noop,
412
+ error;
413
+
414
+ switch(arguments.length) {
415
+ case 3: params = a1; success = a2; error = a3; break;
416
+ case 2:
417
+ case 1:
418
+ if (isFunction(a1)) {
419
+ success = a1;
420
+ error = a2;
421
+ } else {
422
+ params = a1;
423
+ success = a2 || noop;
424
+ }
425
+ case 0: break;
426
+ default:
427
+ throw "Expected between 1-3 arguments [params, success, error], got " +
428
+ arguments.length + " arguments.";
429
+ }
430
+ var data = hasBody ? this : undefined;
431
+ Resource[name].call(this, params, data, success, error);
432
+ };
433
+ });
434
+
435
+ Resource.bind = function(additionalParamDefaults){
436
+ return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
437
+ };
438
+
439
+ return Resource;
440
+ }
441
+
442
+ return ResourceFactory;
443
+ }]);
444
+
445
+ })(window, window.angular);
@@ -0,0 +1,535 @@
1
+ /**
2
+ * @license AngularJS v1.0.5
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ /**
10
+ * @ngdoc overview
11
+ * @name ngSanitize
12
+ * @description
13
+ */
14
+
15
+ /*
16
+ * HTML Parser By Misko Hevery (misko@hevery.com)
17
+ * based on: HTML Parser By John Resig (ejohn.org)
18
+ * Original code by Erik Arvidsson, Mozilla Public License
19
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
20
+ *
21
+ * // Use like so:
22
+ * htmlParser(htmlString, {
23
+ * start: function(tag, attrs, unary) {},
24
+ * end: function(tag) {},
25
+ * chars: function(text) {},
26
+ * comment: function(text) {}
27
+ * });
28
+ *
29
+ */
30
+
31
+
32
+ /**
33
+ * @ngdoc service
34
+ * @name ngSanitize.$sanitize
35
+ * @function
36
+ *
37
+ * @description
38
+ * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
39
+ * then serialized back to properly escaped html string. This means that no unsafe input can make
40
+ * it into the returned string, however, since our parser is more strict than a typical browser
41
+ * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
42
+ * browser, won't make it through the sanitizer.
43
+ *
44
+ * @param {string} html Html input.
45
+ * @returns {string} Sanitized html.
46
+ *
47
+ * @example
48
+ <doc:example module="ngSanitize">
49
+ <doc:source>
50
+ <script>
51
+ function Ctrl($scope) {
52
+ $scope.snippet =
53
+ '<p style="color:blue">an html\n' +
54
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
55
+ 'snippet</p>';
56
+ }
57
+ </script>
58
+ <div ng-controller="Ctrl">
59
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
60
+ <table>
61
+ <tr>
62
+ <td>Filter</td>
63
+ <td>Source</td>
64
+ <td>Rendered</td>
65
+ </tr>
66
+ <tr id="html-filter">
67
+ <td>html filter</td>
68
+ <td>
69
+ <pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre>
70
+ </td>
71
+ <td>
72
+ <div ng-bind-html="snippet"></div>
73
+ </td>
74
+ </tr>
75
+ <tr id="escaped-html">
76
+ <td>no filter</td>
77
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
78
+ <td><div ng-bind="snippet"></div></td>
79
+ </tr>
80
+ <tr id="html-unsafe-filter">
81
+ <td>unsafe html filter</td>
82
+ <td><pre>&lt;div ng-bind-html-unsafe="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
83
+ <td><div ng-bind-html-unsafe="snippet"></div></td>
84
+ </tr>
85
+ </table>
86
+ </div>
87
+ </doc:source>
88
+ <doc:scenario>
89
+ it('should sanitize the html snippet ', function() {
90
+ expect(using('#html-filter').element('div').html()).
91
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
92
+ });
93
+
94
+ it('should escape snippet without any filter', function() {
95
+ expect(using('#escaped-html').element('div').html()).
96
+ toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
97
+ "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
98
+ "snippet&lt;/p&gt;");
99
+ });
100
+
101
+ it('should inline raw snippet if filtered as unsafe', function() {
102
+ expect(using('#html-unsafe-filter').element("div").html()).
103
+ toBe("<p style=\"color:blue\">an html\n" +
104
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
105
+ "snippet</p>");
106
+ });
107
+
108
+ it('should update', function() {
109
+ input('snippet').enter('new <b>text</b>');
110
+ expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>');
111
+ expect(using('#escaped-html').element('div').html()).toBe("new &lt;b&gt;text&lt;/b&gt;");
112
+ expect(using('#html-unsafe-filter').binding("snippet")).toBe('new <b>text</b>');
113
+ });
114
+ </doc:scenario>
115
+ </doc:example>
116
+ */
117
+ var $sanitize = function(html) {
118
+ var buf = [];
119
+ htmlParser(html, htmlSanitizeWriter(buf));
120
+ return buf.join('');
121
+ };
122
+
123
+
124
+ // Regular Expressions for parsing tags and attributes
125
+ var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
126
+ END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
127
+ ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
128
+ BEGIN_TAG_REGEXP = /^</,
129
+ BEGING_END_TAGE_REGEXP = /^<\s*\//,
130
+ COMMENT_REGEXP = /<!--(.*?)-->/g,
131
+ CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
132
+ URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/,
133
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character)
134
+
135
+
136
+ // Good source of info about elements and attributes
137
+ // http://dev.w3.org/html5/spec/Overview.html#semantics
138
+ // http://simon.html5.org/html-elements
139
+
140
+ // Safe Void Elements - HTML5
141
+ // http://dev.w3.org/html5/spec/Overview.html#void-elements
142
+ var voidElements = makeMap("area,br,col,hr,img,wbr");
143
+
144
+ // Elements that you can, intentionally, leave open (and which close themselves)
145
+ // http://dev.w3.org/html5/spec/Overview.html#optional-tags
146
+ var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
147
+ optionalEndTagInlineElements = makeMap("rp,rt"),
148
+ optionalEndTagElements = angular.extend({}, optionalEndTagInlineElements, optionalEndTagBlockElements);
149
+
150
+ // Safe Block Elements - HTML5
151
+ var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article,aside," +
152
+ "blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6," +
153
+ "header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
154
+
155
+ // Inline Elements - HTML5
156
+ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b,bdi,bdo," +
157
+ "big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small," +
158
+ "span,strike,strong,sub,sup,time,tt,u,var"));
159
+
160
+
161
+ // Special Elements (can contain anything)
162
+ var specialElements = makeMap("script,style");
163
+
164
+ var validElements = angular.extend({}, voidElements, blockElements, inlineElements, optionalEndTagElements);
165
+
166
+ //Attributes that have href and hence need to be sanitized
167
+ var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
168
+ var validAttrs = angular.extend({}, uriAttrs, makeMap(
169
+ 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
170
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
171
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
172
+ 'scope,scrolling,shape,span,start,summary,target,title,type,'+
173
+ 'valign,value,vspace,width'));
174
+
175
+ function makeMap(str) {
176
+ var obj = {}, items = str.split(','), i;
177
+ for (i = 0; i < items.length; i++) obj[items[i]] = true;
178
+ return obj;
179
+ }
180
+
181
+
182
+ /**
183
+ * @example
184
+ * htmlParser(htmlString, {
185
+ * start: function(tag, attrs, unary) {},
186
+ * end: function(tag) {},
187
+ * chars: function(text) {},
188
+ * comment: function(text) {}
189
+ * });
190
+ *
191
+ * @param {string} html string
192
+ * @param {object} handler
193
+ */
194
+ function htmlParser( html, handler ) {
195
+ var index, chars, match, stack = [], last = html;
196
+ stack.last = function() { return stack[ stack.length - 1 ]; };
197
+
198
+ while ( html ) {
199
+ chars = true;
200
+
201
+ // Make sure we're not in a script or style element
202
+ if ( !stack.last() || !specialElements[ stack.last() ] ) {
203
+
204
+ // Comment
205
+ if ( html.indexOf("<!--") === 0 ) {
206
+ index = html.indexOf("-->");
207
+
208
+ if ( index >= 0 ) {
209
+ if (handler.comment) handler.comment( html.substring( 4, index ) );
210
+ html = html.substring( index + 3 );
211
+ chars = false;
212
+ }
213
+
214
+ // end tag
215
+ } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
216
+ match = html.match( END_TAG_REGEXP );
217
+
218
+ if ( match ) {
219
+ html = html.substring( match[0].length );
220
+ match[0].replace( END_TAG_REGEXP, parseEndTag );
221
+ chars = false;
222
+ }
223
+
224
+ // start tag
225
+ } else if ( BEGIN_TAG_REGEXP.test(html) ) {
226
+ match = html.match( START_TAG_REGEXP );
227
+
228
+ if ( match ) {
229
+ html = html.substring( match[0].length );
230
+ match[0].replace( START_TAG_REGEXP, parseStartTag );
231
+ chars = false;
232
+ }
233
+ }
234
+
235
+ if ( chars ) {
236
+ index = html.indexOf("<");
237
+
238
+ var text = index < 0 ? html : html.substring( 0, index );
239
+ html = index < 0 ? "" : html.substring( index );
240
+
241
+ if (handler.chars) handler.chars( decodeEntities(text) );
242
+ }
243
+
244
+ } else {
245
+ html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){
246
+ text = text.
247
+ replace(COMMENT_REGEXP, "$1").
248
+ replace(CDATA_REGEXP, "$1");
249
+
250
+ if (handler.chars) handler.chars( decodeEntities(text) );
251
+
252
+ return "";
253
+ });
254
+
255
+ parseEndTag( "", stack.last() );
256
+ }
257
+
258
+ if ( html == last ) {
259
+ throw "Parse Error: " + html;
260
+ }
261
+ last = html;
262
+ }
263
+
264
+ // Clean up any remaining tags
265
+ parseEndTag();
266
+
267
+ function parseStartTag( tag, tagName, rest, unary ) {
268
+ tagName = angular.lowercase(tagName);
269
+ if ( blockElements[ tagName ] ) {
270
+ while ( stack.last() && inlineElements[ stack.last() ] ) {
271
+ parseEndTag( "", stack.last() );
272
+ }
273
+ }
274
+
275
+ if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
276
+ parseEndTag( "", tagName );
277
+ }
278
+
279
+ unary = voidElements[ tagName ] || !!unary;
280
+
281
+ if ( !unary )
282
+ stack.push( tagName );
283
+
284
+ var attrs = {};
285
+
286
+ rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) {
287
+ var value = doubleQuotedValue
288
+ || singleQoutedValue
289
+ || unqoutedValue
290
+ || '';
291
+
292
+ attrs[name] = decodeEntities(value);
293
+ });
294
+ if (handler.start) handler.start( tagName, attrs, unary );
295
+ }
296
+
297
+ function parseEndTag( tag, tagName ) {
298
+ var pos = 0, i;
299
+ tagName = angular.lowercase(tagName);
300
+ if ( tagName )
301
+ // Find the closest opened tag of the same type
302
+ for ( pos = stack.length - 1; pos >= 0; pos-- )
303
+ if ( stack[ pos ] == tagName )
304
+ break;
305
+
306
+ if ( pos >= 0 ) {
307
+ // Close all the open elements, up the stack
308
+ for ( i = stack.length - 1; i >= pos; i-- )
309
+ if (handler.end) handler.end( stack[ i ] );
310
+
311
+ // Remove the open elements from the stack
312
+ stack.length = pos;
313
+ }
314
+ }
315
+ }
316
+
317
+ /**
318
+ * decodes all entities into regular string
319
+ * @param value
320
+ * @returns {string} A string with decoded entities.
321
+ */
322
+ var hiddenPre=document.createElement("pre");
323
+ function decodeEntities(value) {
324
+ hiddenPre.innerHTML=value.replace(/</g,"&lt;");
325
+ return hiddenPre.innerText || hiddenPre.textContent || '';
326
+ }
327
+
328
+ /**
329
+ * Escapes all potentially dangerous characters, so that the
330
+ * resulting string can be safely inserted into attribute or
331
+ * element text.
332
+ * @param value
333
+ * @returns escaped text
334
+ */
335
+ function encodeEntities(value) {
336
+ return value.
337
+ replace(/&/g, '&amp;').
338
+ replace(NON_ALPHANUMERIC_REGEXP, function(value){
339
+ return '&#' + value.charCodeAt(0) + ';';
340
+ }).
341
+ replace(/</g, '&lt;').
342
+ replace(/>/g, '&gt;');
343
+ }
344
+
345
+ /**
346
+ * create an HTML/XML writer which writes to buffer
347
+ * @param {Array} buf use buf.jain('') to get out sanitized html string
348
+ * @returns {object} in the form of {
349
+ * start: function(tag, attrs, unary) {},
350
+ * end: function(tag) {},
351
+ * chars: function(text) {},
352
+ * comment: function(text) {}
353
+ * }
354
+ */
355
+ function htmlSanitizeWriter(buf){
356
+ var ignore = false;
357
+ var out = angular.bind(buf, buf.push);
358
+ return {
359
+ start: function(tag, attrs, unary){
360
+ tag = angular.lowercase(tag);
361
+ if (!ignore && specialElements[tag]) {
362
+ ignore = tag;
363
+ }
364
+ if (!ignore && validElements[tag] == true) {
365
+ out('<');
366
+ out(tag);
367
+ angular.forEach(attrs, function(value, key){
368
+ var lkey=angular.lowercase(key);
369
+ if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) {
370
+ out(' ');
371
+ out(key);
372
+ out('="');
373
+ out(encodeEntities(value));
374
+ out('"');
375
+ }
376
+ });
377
+ out(unary ? '/>' : '>');
378
+ }
379
+ },
380
+ end: function(tag){
381
+ tag = angular.lowercase(tag);
382
+ if (!ignore && validElements[tag] == true) {
383
+ out('</');
384
+ out(tag);
385
+ out('>');
386
+ }
387
+ if (tag == ignore) {
388
+ ignore = false;
389
+ }
390
+ },
391
+ chars: function(chars){
392
+ if (!ignore) {
393
+ out(encodeEntities(chars));
394
+ }
395
+ }
396
+ };
397
+ }
398
+
399
+
400
+ // define ngSanitize module and register $sanitize service
401
+ angular.module('ngSanitize', []).value('$sanitize', $sanitize);
402
+
403
+ /**
404
+ * @ngdoc directive
405
+ * @name ngSanitize.directive:ngBindHtml
406
+ *
407
+ * @description
408
+ * Creates a binding that will sanitize the result of evaluating the `expression` with the
409
+ * {@link ngSanitize.$sanitize $sanitize} service and innerHTML the result into the current element.
410
+ *
411
+ * See {@link ngSanitize.$sanitize $sanitize} docs for examples.
412
+ *
413
+ * @element ANY
414
+ * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
415
+ */
416
+ angular.module('ngSanitize').directive('ngBindHtml', ['$sanitize', function($sanitize) {
417
+ return function(scope, element, attr) {
418
+ element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
419
+ scope.$watch(attr.ngBindHtml, function ngBindHtmlWatchAction(value) {
420
+ value = $sanitize(value);
421
+ element.html(value || '');
422
+ });
423
+ };
424
+ }]);
425
+ /**
426
+ * @ngdoc filter
427
+ * @name ngSanitize.filter:linky
428
+ * @function
429
+ *
430
+ * @description
431
+ * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
432
+ * plain email address links.
433
+ *
434
+ * @param {string} text Input text.
435
+ * @returns {string} Html-linkified text.
436
+ *
437
+ * @usage
438
+ <span ng-bind-html="linky_expression | linky"></span>
439
+ *
440
+ * @example
441
+ <doc:example module="ngSanitize">
442
+ <doc:source>
443
+ <script>
444
+ function Ctrl($scope) {
445
+ $scope.snippet =
446
+ 'Pretty text with some links:\n'+
447
+ 'http://angularjs.org/,\n'+
448
+ 'mailto:us@somewhere.org,\n'+
449
+ 'another@somewhere.org,\n'+
450
+ 'and one more: ftp://127.0.0.1/.';
451
+ }
452
+ </script>
453
+ <div ng-controller="Ctrl">
454
+ Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
455
+ <table>
456
+ <tr>
457
+ <td>Filter</td>
458
+ <td>Source</td>
459
+ <td>Rendered</td>
460
+ </tr>
461
+ <tr id="linky-filter">
462
+ <td>linky filter</td>
463
+ <td>
464
+ <pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
465
+ </td>
466
+ <td>
467
+ <div ng-bind-html="snippet | linky"></div>
468
+ </td>
469
+ </tr>
470
+ <tr id="escaped-html">
471
+ <td>no filter</td>
472
+ <td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
473
+ <td><div ng-bind="snippet"></div></td>
474
+ </tr>
475
+ </table>
476
+ </doc:source>
477
+ <doc:scenario>
478
+ it('should linkify the snippet with urls', function() {
479
+ expect(using('#linky-filter').binding('snippet | linky')).
480
+ toBe('Pretty text with some links:&#10;' +
481
+ '<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' +
482
+ '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' +
483
+ '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' +
484
+ 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
485
+ });
486
+
487
+ it ('should not linkify snippet without the linky filter', function() {
488
+ expect(using('#escaped-html').binding('snippet')).
489
+ toBe("Pretty text with some links:\n" +
490
+ "http://angularjs.org/,\n" +
491
+ "mailto:us@somewhere.org,\n" +
492
+ "another@somewhere.org,\n" +
493
+ "and one more: ftp://127.0.0.1/.");
494
+ });
495
+
496
+ it('should update', function() {
497
+ input('snippet').enter('new http://link.');
498
+ expect(using('#linky-filter').binding('snippet | linky')).
499
+ toBe('new <a href="http://link">http://link</a>.');
500
+ expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
501
+ });
502
+ </doc:scenario>
503
+ </doc:example>
504
+ */
505
+ angular.module('ngSanitize').filter('linky', function() {
506
+ var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/,
507
+ MAILTO_REGEXP = /^mailto:/;
508
+
509
+ return function(text) {
510
+ if (!text) return text;
511
+ var match;
512
+ var raw = text;
513
+ var html = [];
514
+ // TODO(vojta): use $sanitize instead
515
+ var writer = htmlSanitizeWriter(html);
516
+ var url;
517
+ var i;
518
+ while ((match = raw.match(LINKY_URL_REGEXP))) {
519
+ // We can not end in these as they are sometimes found at the end of the sentence
520
+ url = match[0];
521
+ // if we did not match ftp/http/mailto then assume mailto
522
+ if (match[2] == match[3]) url = 'mailto:' + url;
523
+ i = match.index;
524
+ writer.chars(raw.substr(0, i));
525
+ writer.start('a', {href:url});
526
+ writer.chars(match[0].replace(MAILTO_REGEXP, ''));
527
+ writer.end('a');
528
+ raw = raw.substring(i + match[0].length);
529
+ }
530
+ writer.chars(raw);
531
+ return html.join('');
532
+ };
533
+ });
534
+
535
+ })(window, window.angular);