1 | /** |
||
2 | * @license AngularJS v1.8.2 |
||
3 | * (c) 2010-2020 Google LLC. http://angularjs.org |
||
4 | * License: MIT |
||
5 | */ |
||
6 | (function(window, angular) {'use strict'; |
||
7 | |||
8 | // NOTE: ADVANCED_OPTIMIZATIONS mode. |
||
9 | // |
||
10 | // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using |
||
11 | // constructs incompatible with that mode. |
||
12 | |||
13 | /* global isFunction: false */ |
||
14 | /* global noop: false */ |
||
15 | /* global toJson: false */ |
||
16 | /* global $$stringify: false */ |
||
17 | |||
18 | // Convert an index into the string into line/column for use in error messages |
||
19 | // As such, this doesn't have to be efficient. |
||
20 | function indexToLineAndColumn(text, index) { |
||
21 | var lines = text.split(/\n/g); |
||
22 | for (var i = 0; i < lines.length; i++) { |
||
23 | var line = lines[i]; |
||
24 | if (index >= line.length) { |
||
25 | index -= line.length; |
||
26 | } else { |
||
27 | return { line: i + 1, column: index + 1 }; |
||
28 | } |
||
29 | } |
||
0 ignored issues
–
show
Best Practice
introduced
by
Loading history...
|
|||
30 | } |
||
31 | var PARSE_CACHE_FOR_TEXT_LITERALS = Object.create(null); |
||
32 | |||
33 | function parseTextLiteral(text) { |
||
34 | var cachedFn = PARSE_CACHE_FOR_TEXT_LITERALS[text]; |
||
35 | if (cachedFn != null) { |
||
36 | return cachedFn; |
||
37 | } |
||
38 | function parsedFn(context) { return text; } |
||
39 | parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { |
||
40 | var unwatch = scope['$watch'](noop, |
||
41 | function textLiteralWatcher() { |
||
42 | listener(text, text, scope); |
||
43 | unwatch(); |
||
44 | }, |
||
45 | objectEquality); |
||
46 | return unwatch; |
||
47 | }; |
||
48 | PARSE_CACHE_FOR_TEXT_LITERALS[text] = parsedFn; |
||
49 | parsedFn['exp'] = text; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
50 | parsedFn['expressions'] = []; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. |
||
51 | return parsedFn; |
||
52 | } |
||
53 | |||
54 | function subtractOffset(expressionFn, offset) { |
||
55 | if (offset === 0) { |
||
56 | return expressionFn; |
||
57 | } |
||
58 | function minusOffset(value) { |
||
59 | return (value == null) ? value : value - offset; |
||
60 | } |
||
61 | function parsedFn(context) { return minusOffset(expressionFn(context)); } |
||
62 | var unwatch; |
||
63 | parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { |
||
64 | unwatch = scope['$watch'](expressionFn, |
||
65 | function pluralExpressionWatchListener(newValue, oldValue) { |
||
66 | listener(minusOffset(newValue), minusOffset(oldValue), scope); |
||
67 | }, |
||
68 | objectEquality); |
||
69 | return unwatch; |
||
70 | }; |
||
71 | return parsedFn; |
||
72 | } |
||
73 | |||
74 | // NOTE: ADVANCED_OPTIMIZATIONS mode. |
||
75 | // |
||
76 | // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using |
||
77 | // constructs incompatible with that mode. |
||
78 | |||
79 | /* global $interpolateMinErr: false */ |
||
80 | /* global isFunction: false */ |
||
81 | /* global noop: false */ |
||
82 | |||
83 | /** |
||
84 | * @constructor |
||
85 | * @private |
||
86 | */ |
||
87 | function MessageSelectorBase(expressionFn, choices) { |
||
88 | var self = this; |
||
89 | this.expressionFn = expressionFn; |
||
90 | this.choices = choices; |
||
91 | if (choices['other'] === undefined) { |
||
92 | throw $interpolateMinErr('reqother', '“other” is a required option.'); |
||
93 | } |
||
94 | this.parsedFn = function(context) { return self.getResult(context); }; |
||
95 | this.parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { |
||
96 | return self.watchDelegate(scope, listener, objectEquality); |
||
97 | }; |
||
98 | this.parsedFn['exp'] = expressionFn['exp']; |
||
99 | this.parsedFn['expressions'] = expressionFn['expressions']; |
||
100 | } |
||
101 | |||
102 | MessageSelectorBase.prototype.getMessageFn = function getMessageFn(value) { |
||
103 | return this.choices[this.categorizeValue(value)]; |
||
104 | }; |
||
105 | |||
106 | MessageSelectorBase.prototype.getResult = function getResult(context) { |
||
107 | return this.getMessageFn(this.expressionFn(context))(context); |
||
108 | }; |
||
109 | |||
110 | MessageSelectorBase.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { |
||
111 | var watchers = new MessageSelectorWatchers(this, scope, listener, objectEquality); |
||
112 | return function() { watchers.cancelWatch(); }; |
||
113 | }; |
||
114 | |||
115 | /** |
||
116 | * @constructor |
||
117 | * @private |
||
118 | */ |
||
119 | function MessageSelectorWatchers(msgSelector, scope, listener, objectEquality) { |
||
120 | var self = this; |
||
121 | this.scope = scope; |
||
122 | this.msgSelector = msgSelector; |
||
123 | this.listener = listener; |
||
124 | this.objectEquality = objectEquality; |
||
125 | this.lastMessage = undefined; |
||
126 | this.messageFnWatcher = noop; |
||
127 | var expressionFnListener = function(newValue, oldValue) { return self.expressionFnListener(newValue, oldValue); }; |
||
128 | this.expressionFnWatcher = scope['$watch'](msgSelector.expressionFn, expressionFnListener, objectEquality); |
||
129 | } |
||
130 | |||
131 | MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnListener(newValue, oldValue) { |
||
132 | var self = this; |
||
133 | this.messageFnWatcher(); |
||
134 | var messageFnListener = function(newMessage, oldMessage) { return self.messageFnListener(newMessage, oldMessage); }; |
||
135 | var messageFn = this.msgSelector.getMessageFn(newValue); |
||
136 | this.messageFnWatcher = this.scope['$watch'](messageFn, messageFnListener, this.objectEquality); |
||
137 | }; |
||
138 | |||
139 | MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) { |
||
140 | this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope); |
||
141 | this.lastMessage = newMessage; |
||
142 | }; |
||
143 | |||
144 | MessageSelectorWatchers.prototype.cancelWatch = function cancelWatch() { |
||
145 | this.expressionFnWatcher(); |
||
146 | this.messageFnWatcher(); |
||
147 | }; |
||
148 | |||
149 | /** |
||
150 | * @constructor |
||
151 | * @extends MessageSelectorBase |
||
152 | * @private |
||
153 | */ |
||
154 | function SelectMessage(expressionFn, choices) { |
||
155 | MessageSelectorBase.call(this, expressionFn, choices); |
||
156 | } |
||
157 | |||
158 | function SelectMessageProto() {} |
||
159 | SelectMessageProto.prototype = MessageSelectorBase.prototype; |
||
160 | |||
161 | SelectMessage.prototype = new SelectMessageProto(); |
||
162 | SelectMessage.prototype.categorizeValue = function categorizeSelectValue(value) { |
||
163 | return (this.choices[value] !== undefined) ? value : 'other'; |
||
164 | }; |
||
165 | |||
166 | /** |
||
167 | * @constructor |
||
168 | * @extends MessageSelectorBase |
||
169 | * @private |
||
170 | */ |
||
171 | function PluralMessage(expressionFn, choices, offset, pluralCat) { |
||
172 | MessageSelectorBase.call(this, expressionFn, choices); |
||
173 | this.offset = offset; |
||
174 | this.pluralCat = pluralCat; |
||
175 | } |
||
176 | |||
177 | function PluralMessageProto() {} |
||
178 | PluralMessageProto.prototype = MessageSelectorBase.prototype; |
||
179 | |||
180 | PluralMessage.prototype = new PluralMessageProto(); |
||
181 | PluralMessage.prototype.categorizeValue = function categorizePluralValue(value) { |
||
182 | if (isNaN(value)) { |
||
183 | return 'other'; |
||
184 | } else if (this.choices[value] !== undefined) { |
||
185 | return value; |
||
186 | } else { |
||
187 | var category = this.pluralCat(value - this.offset); |
||
188 | return (this.choices[category] !== undefined) ? category : 'other'; |
||
189 | } |
||
190 | }; |
||
191 | |||
192 | // NOTE: ADVANCED_OPTIMIZATIONS mode. |
||
193 | // |
||
194 | // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using |
||
195 | // constructs incompatible with that mode. |
||
196 | |||
197 | /* global $interpolateMinErr: false */ |
||
198 | /* global isFunction: false */ |
||
199 | /* global parseTextLiteral: false */ |
||
200 | |||
201 | /** |
||
202 | * @constructor |
||
203 | * @private |
||
204 | */ |
||
205 | function InterpolationParts(trustedContext, allOrNothing) { |
||
206 | this.trustedContext = trustedContext; |
||
207 | this.allOrNothing = allOrNothing; |
||
208 | this.textParts = []; |
||
209 | this.expressionFns = []; |
||
210 | this.expressionIndices = []; |
||
211 | this.partialText = ''; |
||
212 | this.concatParts = null; |
||
213 | } |
||
214 | |||
215 | InterpolationParts.prototype.flushPartialText = function flushPartialText() { |
||
216 | if (this.partialText) { |
||
217 | if (this.concatParts == null) { |
||
218 | this.textParts.push(this.partialText); |
||
219 | } else { |
||
220 | this.textParts.push(this.concatParts.join('')); |
||
221 | this.concatParts = null; |
||
222 | } |
||
223 | this.partialText = ''; |
||
224 | } |
||
225 | }; |
||
226 | |||
227 | InterpolationParts.prototype.addText = function addText(text) { |
||
228 | if (text.length) { |
||
229 | if (!this.partialText) { |
||
230 | this.partialText = text; |
||
231 | } else if (this.concatParts) { |
||
232 | this.concatParts.push(text); |
||
233 | } else { |
||
234 | this.concatParts = [this.partialText, text]; |
||
235 | } |
||
236 | } |
||
237 | }; |
||
238 | |||
239 | InterpolationParts.prototype.addExpressionFn = function addExpressionFn(expressionFn) { |
||
240 | this.flushPartialText(); |
||
241 | this.expressionIndices.push(this.textParts.length); |
||
242 | this.expressionFns.push(expressionFn); |
||
243 | this.textParts.push(''); |
||
244 | }; |
||
245 | |||
246 | InterpolationParts.prototype.getExpressionValues = function getExpressionValues(context) { |
||
247 | var expressionValues = new Array(this.expressionFns.length); |
||
248 | for (var i = 0; i < this.expressionFns.length; i++) { |
||
249 | expressionValues[i] = this.expressionFns[i](context); |
||
250 | } |
||
251 | return expressionValues; |
||
252 | }; |
||
253 | |||
254 | InterpolationParts.prototype.getResult = function getResult(expressionValues) { |
||
255 | for (var i = 0; i < this.expressionIndices.length; i++) { |
||
256 | var expressionValue = expressionValues[i]; |
||
257 | if (this.allOrNothing && expressionValue === undefined) return; |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
258 | this.textParts[this.expressionIndices[i]] = expressionValue; |
||
259 | } |
||
260 | return this.textParts.join(''); |
||
261 | }; |
||
262 | |||
263 | |||
264 | InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression, originalText) { |
||
265 | var self = this; |
||
266 | this.flushPartialText(); |
||
267 | if (mustHaveExpression && this.expressionFns.length === 0) { |
||
268 | return undefined; |
||
269 | } |
||
270 | if (this.textParts.length === 0) { |
||
271 | return parseTextLiteral(''); |
||
272 | } |
||
273 | if (this.trustedContext && this.textParts.length > 1) { |
||
274 | $interpolateMinErr['throwNoconcat'](originalText); |
||
275 | } |
||
276 | if (this.expressionFns.length === 0) { |
||
277 | if (this.textParts.length !== 1) { this.errorInParseLogic(); } |
||
278 | return parseTextLiteral(this.textParts[0]); |
||
279 | } |
||
280 | var parsedFn = function(context) { |
||
281 | return self.getResult(self.getExpressionValues(context)); |
||
282 | }; |
||
283 | parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { |
||
284 | return self.watchDelegate(scope, listener, objectEquality); |
||
285 | }; |
||
286 | |||
287 | parsedFn['exp'] = originalText; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
288 | parsedFn['expressions'] = new Array(this.expressionFns.length); // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. |
||
289 | for (var i = 0; i < this.expressionFns.length; i++) { |
||
290 | parsedFn['expressions'][i] = this.expressionFns[i]['exp']; |
||
291 | } |
||
292 | |||
293 | return parsedFn; |
||
294 | }; |
||
295 | |||
296 | InterpolationParts.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { |
||
297 | var watcher = new InterpolationPartsWatcher(this, scope, listener, objectEquality); |
||
298 | return function() { watcher.cancelWatch(); }; |
||
299 | }; |
||
300 | |||
301 | function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEquality) { |
||
302 | this.interpolationParts = interpolationParts; |
||
303 | this.scope = scope; |
||
304 | this.previousResult = (undefined); |
||
305 | this.listener = listener; |
||
306 | var self = this; |
||
307 | this.expressionFnsWatcher = scope['$watchGroup'](interpolationParts.expressionFns, function(newExpressionValues, oldExpressionValues) { |
||
308 | self.watchListener(newExpressionValues, oldExpressionValues); |
||
309 | }); |
||
310 | } |
||
311 | |||
312 | InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) { |
||
313 | var result = this.interpolationParts.getResult(newExpressionValues); |
||
314 | this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope); |
||
315 | this.previousResult = result; |
||
316 | }; |
||
317 | |||
318 | InterpolationPartsWatcher.prototype.cancelWatch = function cancelWatch() { |
||
319 | this.expressionFnsWatcher(); |
||
320 | }; |
||
321 | |||
322 | // NOTE: ADVANCED_OPTIMIZATIONS mode. |
||
323 | // |
||
324 | // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using |
||
325 | // constructs incompatible with that mode. |
||
326 | |||
327 | /* global $interpolateMinErr: false */ |
||
328 | /* global indexToLineAndColumn: false */ |
||
329 | /* global InterpolationParts: false */ |
||
330 | /* global PluralMessage: false */ |
||
331 | /* global SelectMessage: false */ |
||
332 | /* global subtractOffset: false */ |
||
333 | |||
334 | // The params src and dst are exactly one of two types: NestedParserState or MessageFormatParser. |
||
335 | // This function is fully optimized by V8. (inspect via IRHydra or --trace-deopt.) |
||
336 | // The idea behind writing it this way is to avoid repeating oneself. This is the ONE place where |
||
337 | // the parser state that is saved/restored when parsing nested mustaches is specified. |
||
338 | function copyNestedParserState(src, dst) { |
||
339 | dst.expressionFn = src.expressionFn; |
||
340 | dst.expressionMinusOffsetFn = src.expressionMinusOffsetFn; |
||
341 | dst.pluralOffset = src.pluralOffset; |
||
342 | dst.choices = src.choices; |
||
343 | dst.choiceKey = src.choiceKey; |
||
344 | dst.interpolationParts = src.interpolationParts; |
||
345 | dst.ruleChoiceKeyword = src.ruleChoiceKeyword; |
||
346 | dst.msgStartIndex = src.msgStartIndex; |
||
347 | dst.expressionStartIndex = src.expressionStartIndex; |
||
348 | } |
||
349 | |||
350 | function NestedParserState(parser) { |
||
351 | copyNestedParserState(parser, this); |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * @constructor |
||
356 | * @private |
||
357 | */ |
||
358 | function MessageFormatParser(text, startIndex, $parse, pluralCat, stringifier, |
||
359 | mustHaveExpression, trustedContext, allOrNothing) { |
||
360 | this.text = text; |
||
361 | this.index = startIndex || 0; |
||
362 | this.$parse = $parse; |
||
363 | this.pluralCat = pluralCat; |
||
364 | this.stringifier = stringifier; |
||
365 | this.mustHaveExpression = !!mustHaveExpression; |
||
366 | this.trustedContext = trustedContext; |
||
367 | this.allOrNothing = !!allOrNothing; |
||
368 | this.expressionFn = null; |
||
369 | this.expressionMinusOffsetFn = null; |
||
370 | this.pluralOffset = null; |
||
371 | this.choices = null; |
||
372 | this.choiceKey = null; |
||
373 | this.interpolationParts = null; |
||
374 | this.msgStartIndex = null; |
||
375 | this.nestedStateStack = []; |
||
376 | this.parsedFn = null; |
||
377 | this.rule = null; |
||
378 | this.ruleStack = null; |
||
379 | this.ruleChoiceKeyword = null; |
||
380 | this.interpNestLevel = null; |
||
381 | this.expressionStartIndex = null; |
||
382 | this.stringStartIndex = null; |
||
383 | this.stringQuote = null; |
||
384 | this.stringInterestsRe = null; |
||
385 | this.angularOperatorStack = null; |
||
386 | this.textPart = null; |
||
387 | } |
||
388 | |||
389 | // preserve v8 optimization. |
||
390 | var EMPTY_STATE = new NestedParserState(new MessageFormatParser( |
||
391 | /* text= */ '', /* startIndex= */ 0, /* $parse= */ null, /* pluralCat= */ null, /* stringifier= */ null, |
||
392 | /* mustHaveExpression= */ false, /* trustedContext= */ null, /* allOrNothing */ false)); |
||
393 | |||
394 | MessageFormatParser.prototype.pushState = function pushState() { |
||
395 | this.nestedStateStack.push(new NestedParserState(this)); |
||
396 | copyNestedParserState(EMPTY_STATE, this); |
||
397 | }; |
||
398 | |||
399 | MessageFormatParser.prototype.popState = function popState() { |
||
400 | if (this.nestedStateStack.length === 0) { |
||
401 | this.errorInParseLogic(); |
||
402 | } |
||
403 | var previousState = this.nestedStateStack.pop(); |
||
404 | copyNestedParserState(previousState, this); |
||
405 | }; |
||
406 | |||
407 | // Oh my JavaScript! Who knew you couldn't match a regex at a specific |
||
408 | // location in a string but will always search forward?! |
||
409 | // Apparently you'll be growing this ability via the sticky flag (y) in |
||
410 | // ES6. I'll just to work around you for now. |
||
411 | MessageFormatParser.prototype.matchRe = function matchRe(re, search) { |
||
412 | re.lastIndex = this.index; |
||
413 | var match = re.exec(this.text); |
||
414 | if (match != null && (search === true || (match.index === this.index))) { |
||
415 | this.index = re.lastIndex; |
||
416 | return match; |
||
417 | } |
||
418 | return null; |
||
419 | }; |
||
420 | |||
421 | MessageFormatParser.prototype.searchRe = function searchRe(re) { |
||
422 | return this.matchRe(re, true); |
||
423 | }; |
||
424 | |||
425 | |||
426 | MessageFormatParser.prototype.consumeRe = function consumeRe(re) { |
||
427 | // Without the sticky flag, we can't use the .test() method to consume a |
||
428 | // match at the current index. Instead, we'll use the slower .exec() method |
||
429 | // and verify match.index. |
||
430 | return !!this.matchRe(re); |
||
431 | }; |
||
432 | |||
433 | // Run through our grammar avoiding deeply nested function call chains. |
||
434 | MessageFormatParser.prototype.run = function run(initialRule) { |
||
435 | this.ruleStack = [initialRule]; |
||
436 | do { |
||
437 | this.rule = this.ruleStack.pop(); |
||
438 | while (this.rule) { |
||
439 | this.rule(); |
||
440 | } |
||
441 | this.assertRuleOrNull(this.rule); |
||
442 | } while (this.ruleStack.length > 0); |
||
443 | }; |
||
444 | |||
445 | MessageFormatParser.prototype.errorInParseLogic = function errorInParseLogic() { |
||
446 | throw $interpolateMinErr('logicbug', |
||
447 | 'The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”', |
||
448 | this.text); |
||
449 | }; |
||
450 | |||
451 | MessageFormatParser.prototype.assertRuleOrNull = function assertRuleOrNull(rule) { |
||
452 | if (rule === undefined) { |
||
453 | this.errorInParseLogic(); |
||
454 | } |
||
455 | }; |
||
456 | |||
457 | var NEXT_WORD_RE = /\s*(\w+)\s*/g; |
||
458 | MessageFormatParser.prototype.errorExpecting = function errorExpecting() { |
||
459 | // What was wrong with the syntax? Unsupported type, missing comma, or something else? |
||
460 | var match = this.matchRe(NEXT_WORD_RE), position; |
||
461 | if (match == null) { |
||
462 | position = indexToLineAndColumn(this.text, this.index); |
||
463 | throw $interpolateMinErr('reqarg', |
||
464 | 'Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”', |
||
465 | position.line, position.column, this.text); |
||
466 | } |
||
467 | var word = match[1]; |
||
468 | if (word === 'select' || word === 'plural') { |
||
469 | position = indexToLineAndColumn(this.text, this.index); |
||
470 | throw $interpolateMinErr('reqcomma', |
||
471 | 'Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”', |
||
472 | word, position.line, position.column, this.text); |
||
473 | } else { |
||
474 | position = indexToLineAndColumn(this.text, this.index); |
||
475 | throw $interpolateMinErr('unknarg', |
||
476 | 'Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”', |
||
477 | word, position.line, position.column, this.text); |
||
478 | } |
||
479 | }; |
||
480 | |||
481 | var STRING_START_RE = /['"]/g; |
||
482 | MessageFormatParser.prototype.ruleString = function ruleString() { |
||
483 | var match = this.matchRe(STRING_START_RE); |
||
484 | if (match == null) { |
||
485 | var position = indexToLineAndColumn(this.text, this.index); |
||
486 | throw $interpolateMinErr('wantstring', |
||
487 | 'Expected the beginning of a string at line {0}, column {1} in text “{2}”', |
||
488 | position.line, position.column, this.text); |
||
489 | } |
||
490 | this.startStringAtMatch(match); |
||
491 | }; |
||
492 | |||
493 | MessageFormatParser.prototype.startStringAtMatch = function startStringAtMatch(match) { |
||
494 | this.stringStartIndex = match.index; |
||
495 | this.stringQuote = match[0]; |
||
496 | this.stringInterestsRe = this.stringQuote === '\'' ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE; |
||
497 | this.rule = this.ruleInsideString; |
||
498 | }; |
||
499 | |||
500 | var SQUOTED_STRING_INTEREST_RE = /\\(?:\\|'|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|'/g; |
||
501 | var DQUOTED_STRING_INTEREST_RE = /\\(?:\\|"|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|"/g; |
||
502 | MessageFormatParser.prototype.ruleInsideString = function ruleInsideString() { |
||
503 | var match = this.searchRe(this.stringInterestsRe); |
||
504 | if (match == null) { |
||
505 | var position = indexToLineAndColumn(this.text, this.stringStartIndex); |
||
506 | throw $interpolateMinErr('untermstr', |
||
507 | 'The string beginning at line {0}, column {1} is unterminated in text “{2}”', |
||
508 | position.line, position.column, this.text); |
||
509 | } |
||
510 | if (match[0] === this.stringQuote) { |
||
511 | this.rule = null; |
||
512 | } |
||
513 | }; |
||
514 | |||
515 | var PLURAL_OR_SELECT_ARG_TYPE_RE = /\s*(plural|select)\s*,\s*/g; |
||
516 | MessageFormatParser.prototype.rulePluralOrSelect = function rulePluralOrSelect() { |
||
517 | var match = this.searchRe(PLURAL_OR_SELECT_ARG_TYPE_RE); |
||
518 | if (match == null) { |
||
519 | this.errorExpecting(); |
||
520 | } |
||
521 | var argType = match[1]; |
||
522 | switch (argType) { |
||
523 | case 'plural': this.rule = this.rulePluralStyle; break; |
||
524 | case 'select': this.rule = this.ruleSelectStyle; break; |
||
525 | default: this.errorInParseLogic(); |
||
526 | } |
||
527 | }; |
||
528 | |||
529 | MessageFormatParser.prototype.rulePluralStyle = function rulePluralStyle() { |
||
530 | this.choices = Object.create(null); |
||
531 | this.ruleChoiceKeyword = this.rulePluralValueOrKeyword; |
||
532 | this.rule = this.rulePluralOffset; |
||
533 | }; |
||
534 | |||
535 | MessageFormatParser.prototype.ruleSelectStyle = function ruleSelectStyle() { |
||
536 | this.choices = Object.create(null); |
||
537 | this.ruleChoiceKeyword = this.ruleSelectKeyword; |
||
538 | this.rule = this.ruleSelectKeyword; |
||
539 | }; |
||
540 | |||
541 | var NUMBER_RE = /[0]|(?:[1-9][0-9]*)/g; |
||
542 | var PLURAL_OFFSET_RE = new RegExp('\\s*offset\\s*:\\s*(' + NUMBER_RE.source + ')', 'g'); |
||
543 | |||
544 | MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() { |
||
545 | var match = this.matchRe(PLURAL_OFFSET_RE); |
||
546 | this.pluralOffset = (match == null) ? 0 : parseInt(match[1], 10); |
||
547 | this.expressionMinusOffsetFn = subtractOffset(this.expressionFn, this.pluralOffset); |
||
548 | this.rule = this.rulePluralValueOrKeyword; |
||
549 | }; |
||
550 | |||
551 | MessageFormatParser.prototype.assertChoiceKeyIsNew = function assertChoiceKeyIsNew(choiceKey, index) { |
||
552 | if (this.choices[choiceKey] !== undefined) { |
||
553 | var position = indexToLineAndColumn(this.text, index); |
||
554 | throw $interpolateMinErr('dupvalue', |
||
555 | 'The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”', |
||
556 | choiceKey, position.line, position.column, this.text); |
||
557 | } |
||
558 | }; |
||
559 | |||
560 | var SELECT_KEYWORD = /\s*(\w+)/g; |
||
561 | MessageFormatParser.prototype.ruleSelectKeyword = function ruleSelectKeyword() { |
||
562 | var match = this.matchRe(SELECT_KEYWORD); |
||
563 | if (match == null) { |
||
564 | this.parsedFn = new SelectMessage(this.expressionFn, this.choices).parsedFn; |
||
565 | this.rule = null; |
||
566 | return; |
||
567 | } |
||
568 | this.choiceKey = match[1]; |
||
569 | this.assertChoiceKeyIsNew(this.choiceKey, match.index); |
||
570 | this.rule = this.ruleMessageText; |
||
571 | }; |
||
572 | |||
573 | var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp('\\s*(?:(?:=(' + NUMBER_RE.source + '))|(\\w+))', 'g'); |
||
574 | MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValueOrKeyword() { |
||
575 | var match = this.matchRe(EXPLICIT_VALUE_OR_KEYWORD_RE); |
||
576 | if (match == null) { |
||
577 | this.parsedFn = new PluralMessage(this.expressionFn, this.choices, this.pluralOffset, this.pluralCat).parsedFn; |
||
578 | this.rule = null; |
||
579 | return; |
||
580 | } |
||
581 | if (match[1] != null) { |
||
582 | this.choiceKey = parseInt(match[1], 10); |
||
583 | } else { |
||
584 | this.choiceKey = match[2]; |
||
585 | } |
||
586 | this.assertChoiceKeyIsNew(this.choiceKey, match.index); |
||
587 | this.rule = this.ruleMessageText; |
||
588 | }; |
||
589 | |||
590 | var BRACE_OPEN_RE = /\s*\{/g; |
||
591 | var BRACE_CLOSE_RE = /}/g; |
||
592 | MessageFormatParser.prototype.ruleMessageText = function ruleMessageText() { |
||
593 | if (!this.consumeRe(BRACE_OPEN_RE)) { |
||
594 | var position = indexToLineAndColumn(this.text, this.index); |
||
595 | throw $interpolateMinErr('reqopenbrace', |
||
596 | 'The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”', |
||
597 | this.choiceKey, position.line, position.column, this.text); |
||
598 | } |
||
599 | this.msgStartIndex = this.index; |
||
600 | this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); |
||
601 | this.rule = this.ruleInInterpolationOrMessageText; |
||
602 | }; |
||
603 | |||
604 | // Note: Since "\" is used as an escape character, don't allow it to be part of the |
||
605 | // startSymbol/endSymbol when I add the feature to allow them to be redefined. |
||
606 | var INTERP_OR_END_MESSAGE_RE = /\\.|{{|}/g; |
||
607 | var INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE = /\\.|{{|#|}/g; |
||
608 | var ESCAPE_OR_MUSTACHE_BEGIN_RE = /\\.|{{/g; |
||
609 | MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function advanceInInterpolationOrMessageText() { |
||
610 | var currentIndex = this.index, match; |
||
611 | if (this.ruleChoiceKeyword == null) { // interpolation |
||
612 | match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); |
||
613 | if (match == null) { // End of interpolation text. Nothing more to process. |
||
614 | this.textPart = this.text.substring(currentIndex); |
||
615 | this.index = this.text.length; |
||
616 | return null; |
||
617 | } |
||
618 | } else { |
||
619 | match = this.searchRe(this.ruleChoiceKeyword === this.rulePluralValueOrKeyword ? |
||
620 | INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE : INTERP_OR_END_MESSAGE_RE); |
||
621 | if (match == null) { |
||
622 | var position = indexToLineAndColumn(this.text, this.msgStartIndex); |
||
623 | throw $interpolateMinErr('reqendbrace', |
||
624 | 'The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”', |
||
625 | this.choiceKey, position.line, position.column, this.text); |
||
626 | } |
||
627 | } |
||
628 | // match is non-null. |
||
629 | var token = match[0]; |
||
630 | this.textPart = this.text.substring(currentIndex, match.index); |
||
631 | return token; |
||
632 | }; |
||
633 | |||
634 | MessageFormatParser.prototype.ruleInInterpolationOrMessageText = function ruleInInterpolationOrMessageText() { |
||
635 | var currentIndex = this.index; |
||
636 | var token = this.advanceInInterpolationOrMessageText(); |
||
637 | if (token == null) { |
||
638 | // End of interpolation text. Nothing more to process. |
||
639 | this.index = this.text.length; |
||
640 | this.interpolationParts.addText(this.text.substring(currentIndex)); |
||
641 | this.rule = null; |
||
642 | return; |
||
643 | } |
||
644 | if (token[0] === '\\') { |
||
645 | // unescape next character and continue |
||
646 | this.interpolationParts.addText(this.textPart + token[1]); |
||
647 | return; |
||
648 | } |
||
649 | this.interpolationParts.addText(this.textPart); |
||
650 | if (token === '{{') { |
||
651 | this.pushState(); |
||
652 | this.ruleStack.push(this.ruleEndMustacheInInterpolationOrMessage); |
||
653 | this.rule = this.ruleEnteredMustache; |
||
654 | } else if (token === '}') { |
||
655 | this.choices[this.choiceKey] = this.interpolationParts.toParsedFn(/*mustHaveExpression=*/false, this.text); |
||
656 | this.rule = this.ruleChoiceKeyword; |
||
657 | } else if (token === '#') { |
||
658 | this.interpolationParts.addExpressionFn(this.expressionMinusOffsetFn); |
||
659 | } else { |
||
660 | this.errorInParseLogic(); |
||
661 | } |
||
662 | }; |
||
663 | |||
664 | MessageFormatParser.prototype.ruleInterpolate = function ruleInterpolate() { |
||
665 | this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); |
||
666 | this.rule = this.ruleInInterpolation; |
||
667 | }; |
||
668 | |||
669 | MessageFormatParser.prototype.ruleInInterpolation = function ruleInInterpolation() { |
||
670 | var currentIndex = this.index; |
||
671 | var match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); |
||
672 | if (match == null) { |
||
673 | // End of interpolation text. Nothing more to process. |
||
674 | this.index = this.text.length; |
||
675 | this.interpolationParts.addText(this.text.substring(currentIndex)); |
||
676 | this.parsedFn = this.interpolationParts.toParsedFn(this.mustHaveExpression, this.text); |
||
677 | this.rule = null; |
||
678 | return; |
||
679 | } |
||
680 | var token = match[0]; |
||
681 | if (token[0] === '\\') { |
||
682 | // unescape next character and continue |
||
683 | this.interpolationParts.addText(this.text.substring(currentIndex, match.index) + token[1]); |
||
684 | return; |
||
685 | } |
||
686 | this.interpolationParts.addText(this.text.substring(currentIndex, match.index)); |
||
687 | this.pushState(); |
||
688 | this.ruleStack.push(this.ruleInterpolationEndMustache); |
||
689 | this.rule = this.ruleEnteredMustache; |
||
690 | }; |
||
691 | |||
692 | MessageFormatParser.prototype.ruleInterpolationEndMustache = function ruleInterpolationEndMustache() { |
||
693 | var expressionFn = this.parsedFn; |
||
694 | this.popState(); |
||
695 | this.interpolationParts.addExpressionFn(expressionFn); |
||
696 | this.rule = this.ruleInInterpolation; |
||
697 | }; |
||
698 | |||
699 | MessageFormatParser.prototype.ruleEnteredMustache = function ruleEnteredMustache() { |
||
700 | this.parsedFn = null; |
||
701 | this.ruleStack.push(this.ruleEndMustache); |
||
702 | this.rule = this.ruleAngularExpression; |
||
703 | }; |
||
704 | |||
705 | MessageFormatParser.prototype.ruleEndMustacheInInterpolationOrMessage = function ruleEndMustacheInInterpolationOrMessage() { |
||
706 | var expressionFn = this.parsedFn; |
||
707 | this.popState(); |
||
708 | this.interpolationParts.addExpressionFn(expressionFn); |
||
709 | this.rule = this.ruleInInterpolationOrMessageText; |
||
710 | }; |
||
711 | |||
712 | |||
713 | |||
714 | var INTERP_END_RE = /\s*}}/g; |
||
715 | MessageFormatParser.prototype.ruleEndMustache = function ruleEndMustache() { |
||
716 | var match = this.matchRe(INTERP_END_RE); |
||
717 | if (match == null) { |
||
718 | var position = indexToLineAndColumn(this.text, this.index); |
||
719 | throw $interpolateMinErr('reqendinterp', |
||
720 | 'Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”', |
||
721 | '}}', position.line, position.column, this.text); |
||
722 | } |
||
723 | if (this.parsedFn == null) { |
||
724 | // If we parsed a MessageFormat extension, (e.g. select/plural today, maybe more some other |
||
725 | // day), then the result *has* to be a string and those rules would have already set |
||
726 | // this.parsedFn. If there was no MessageFormat extension, then there is no requirement to |
||
727 | // stringify the result and parsedFn isn't set. We set it here. While we could have set it |
||
728 | // unconditionally when exiting the AngularJS expression, I intend for us to not just replace |
||
729 | // $interpolate, but also to replace $parse in a future version (so ng-bind can work), and in |
||
730 | // such a case we do not want to unnecessarily stringify something if it's not going to be used |
||
731 | // in a string context. |
||
732 | this.parsedFn = this.$parse(this.expressionFn, this.stringifier); |
||
733 | this.parsedFn['exp'] = this.expressionFn['exp']; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
734 | this.parsedFn['expressions'] = this.expressionFn['expressions']; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. |
||
735 | } |
||
736 | this.rule = null; |
||
737 | }; |
||
738 | |||
739 | MessageFormatParser.prototype.ruleAngularExpression = function ruleAngularExpression() { |
||
740 | this.angularOperatorStack = []; |
||
741 | this.expressionStartIndex = this.index; |
||
742 | this.rule = this.ruleInAngularExpression; |
||
743 | }; |
||
744 | |||
745 | function getEndOperator(opBegin) { |
||
746 | switch (opBegin) { |
||
747 | case '{': return '}'; |
||
748 | case '[': return ']'; |
||
749 | case '(': return ')'; |
||
750 | default: return null; |
||
751 | } |
||
752 | } |
||
753 | |||
754 | function getBeginOperator(opEnd) { |
||
755 | switch (opEnd) { |
||
756 | case '}': return '{'; |
||
757 | case ']': return '['; |
||
758 | case ')': return '('; |
||
759 | default: return null; |
||
760 | } |
||
761 | } |
||
762 | |||
763 | // TODO(chirayu): The interpolation endSymbol must also be accounted for. It |
||
764 | // just so happens that "}" is an operator so it's in the list below. But we |
||
765 | // should support any other type of start/end interpolation symbol. |
||
766 | var INTERESTING_OPERATORS_RE = /[[\]{}()'",]/g; |
||
767 | MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularExpression() { |
||
768 | var match = this.searchRe(INTERESTING_OPERATORS_RE); |
||
769 | var position; |
||
770 | if (match == null) { |
||
771 | if (this.angularOperatorStack.length === 0) { |
||
772 | // This is the end of the AngularJS expression so this is actually a |
||
773 | // success. Note that when inside an interpolation, this means we even |
||
774 | // consumed the closing interpolation symbols if they were curlies. This |
||
775 | // is NOT an error at this point but will become an error further up the |
||
776 | // stack when the part that saw the opening curlies is unable to find the |
||
777 | // closing ones. |
||
778 | this.index = this.text.length; |
||
779 | this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); |
||
780 | // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
781 | this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); |
||
782 | this.expressionFn['expressions'] = this.expressionFn['expressions']; |
||
783 | this.rule = null; |
||
784 | return; |
||
785 | } |
||
786 | var innermostOperator = this.angularOperatorStack[0]; |
||
787 | throw $interpolateMinErr('badexpr', |
||
788 | 'Unexpected end of AngularJS expression. Expecting operator “{0}” at the end of the text “{1}”', |
||
789 | this.getEndOperator(innermostOperator), this.text); |
||
790 | } |
||
791 | var operator = match[0]; |
||
792 | if (operator === '\'' || operator === '"') { |
||
793 | this.ruleStack.push(this.ruleInAngularExpression); |
||
794 | this.startStringAtMatch(match); |
||
795 | return; |
||
796 | } |
||
797 | if (operator === ',') { |
||
798 | if (this.trustedContext) { |
||
799 | position = indexToLineAndColumn(this.text, this.index); |
||
800 | throw $interpolateMinErr('unsafe', |
||
801 | 'Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”', |
||
802 | this.trustedContext, position.line, position.column, this.text); |
||
803 | } |
||
804 | // only the top level comma has relevance. |
||
805 | if (this.angularOperatorStack.length === 0) { |
||
806 | // todo: does this need to be trimmed? |
||
807 | this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, match.index)); |
||
808 | // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
809 | this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, match.index); |
||
810 | this.expressionFn['expressions'] = this.expressionFn['expressions']; |
||
811 | this.rule = null; |
||
812 | this.rule = this.rulePluralOrSelect; |
||
813 | } |
||
814 | return; |
||
815 | } |
||
816 | if (getEndOperator(operator) != null) { |
||
817 | this.angularOperatorStack.unshift(operator); |
||
818 | return; |
||
819 | } |
||
820 | var beginOperator = getBeginOperator(operator); |
||
821 | if (beginOperator == null) { |
||
822 | this.errorInParseLogic(); |
||
823 | } |
||
824 | if (this.angularOperatorStack.length > 0) { |
||
825 | if (beginOperator === this.angularOperatorStack[0]) { |
||
826 | this.angularOperatorStack.shift(); |
||
827 | return; |
||
828 | } |
||
829 | position = indexToLineAndColumn(this.text, this.index); |
||
830 | throw $interpolateMinErr('badexpr', |
||
831 | 'Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”', |
||
832 | operator, position.line, position.column, getEndOperator(this.angularOperatorStack[0]), this.text); |
||
833 | } |
||
834 | // We are trying to pop off the operator stack but there really isn't anything to pop off. |
||
835 | this.index = match.index; |
||
836 | this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); |
||
837 | // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js |
||
838 | this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); |
||
839 | this.expressionFn['expressions'] = this.expressionFn['expressions']; |
||
840 | this.rule = null; |
||
841 | }; |
||
842 | |||
843 | // NOTE: ADVANCED_OPTIMIZATIONS mode. |
||
844 | // |
||
845 | // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using |
||
846 | // constructs incompatible with that mode. |
||
847 | |||
848 | /* global $interpolateMinErr: true */ |
||
849 | /* global isFunction: true */ |
||
850 | /* global noop: true */ |
||
851 | /* global toJson: true */ |
||
852 | /* global MessageFormatParser: false */ |
||
853 | |||
854 | /** |
||
855 | * @ngdoc module |
||
856 | * @name ngMessageFormat |
||
857 | * @packageName angular-message-format |
||
858 | * |
||
859 | * @description |
||
860 | * |
||
861 | * ## What is ngMessageFormat? |
||
862 | * |
||
863 | * The ngMessageFormat module extends the AngularJS {@link ng.$interpolate `$interpolate`} service |
||
864 | * with a syntax for handling pluralization and gender specific messages, which is based on the |
||
865 | * [ICU MessageFormat syntax][ICU]. |
||
866 | * |
||
867 | * See [the design doc][ngMessageFormat doc] for more information. |
||
868 | * |
||
869 | * [ICU]: http://userguide.icu-project.org/formatparse/messages#TOC-MessageFormat |
||
870 | * [ngMessageFormat doc]: https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit |
||
871 | * |
||
872 | * ## Examples |
||
873 | * |
||
874 | * ### Gender |
||
875 | * |
||
876 | * This example uses the "select" keyword to specify the message based on gender. |
||
877 | * |
||
878 | * <example name="ngMessageFormat-example-gender" module="msgFmtExample" deps="angular-message-format.js"> |
||
879 | * <file name="index.html"> |
||
880 | * <div ng-controller="AppController"> |
||
881 | * Select Recipient:<br> |
||
882 | <select ng-model="recipient" ng-options="person as person.name for person in recipients"> |
||
883 | </select> |
||
884 | <p>{{recipient.gender, select, |
||
885 | male {{{recipient.name}} unwrapped his gift. } |
||
886 | female {{{recipient.name}} unwrapped her gift. } |
||
887 | other {{{recipient.name}} unwrapped their gift. } |
||
888 | }}</p> |
||
889 | * </div> |
||
890 | * </file> |
||
891 | * <file name="script.js"> |
||
892 | * function Person(name, gender) { |
||
893 | * this.name = name; |
||
894 | * this.gender = gender; |
||
895 | * } |
||
896 | * |
||
897 | * var alice = new Person('Alice', 'female'), |
||
898 | * bob = new Person('Bob', 'male'), |
||
899 | * ashley = new Person('Ashley', ''); |
||
900 | * |
||
901 | * angular.module('msgFmtExample', ['ngMessageFormat']) |
||
902 | * .controller('AppController', ['$scope', function($scope) { |
||
903 | * $scope.recipients = [alice, bob, ashley]; |
||
904 | * $scope.recipient = $scope.recipients[0]; |
||
905 | * }]); |
||
906 | * </file> |
||
907 | * </example> |
||
908 | * |
||
909 | * ### Plural |
||
910 | * |
||
911 | * This example shows how the "plural" keyword is used to account for a variable number of entities. |
||
912 | * The "#" variable holds the current number and can be embedded in the message. |
||
913 | * |
||
914 | * Note that "=1" takes precedence over "one". |
||
915 | * |
||
916 | * The example also shows the "offset" keyword, which allows you to offset the value of the "#" variable. |
||
917 | * |
||
918 | * <example name="ngMessageFormat-example-plural" module="msgFmtExample" deps="angular-message-format.js"> |
||
919 | * <file name="index.html"> |
||
920 | * <div ng-controller="AppController"> |
||
921 | * <button ng-click="recipients.pop()" id="decreaseRecipients">decreaseRecipients</button><br> |
||
922 | * Select recipients:<br> |
||
923 | * <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people"> |
||
924 | * </select><br> |
||
925 | * <p>{{recipients.length, plural, offset:1 |
||
926 | * =0 {{{sender.name}} gave no gifts (\#=#)} |
||
927 | * =1 {{{sender.name}} gave a gift to {{recipients[0].name}} (\#=#)} |
||
928 | * one {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)} |
||
929 | * other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)} |
||
930 | * }}</p> |
||
931 | * </div> |
||
932 | * </file> |
||
933 | * |
||
934 | * <file name="script.js"> |
||
935 | * function Person(name, gender) { |
||
936 | * this.name = name; |
||
937 | * this.gender = gender; |
||
938 | * } |
||
939 | * |
||
940 | * var alice = new Person('Alice', 'female'), |
||
941 | * bob = new Person('Bob', 'male'), |
||
942 | * sarah = new Person('Sarah', 'female'), |
||
943 | * harry = new Person('Harry Potter', 'male'), |
||
944 | * ashley = new Person('Ashley', ''); |
||
945 | * |
||
946 | * angular.module('msgFmtExample', ['ngMessageFormat']) |
||
947 | * .controller('AppController', ['$scope', function($scope) { |
||
948 | * $scope.people = [alice, bob, sarah, ashley]; |
||
949 | * $scope.recipients = [alice, bob, sarah]; |
||
950 | * $scope.sender = harry; |
||
951 | * }]); |
||
952 | * </file> |
||
953 | * |
||
954 | * <file name="protractor.js" type="protractor"> |
||
955 | * describe('MessageFormat plural', function() { |
||
956 | * |
||
957 | * it('should pluralize initial values', function() { |
||
958 | * var messageElem = element(by.binding('recipients.length')), |
||
959 | * decreaseRecipientsBtn = element(by.id('decreaseRecipients')); |
||
960 | * |
||
961 | * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)'); |
||
962 | * decreaseRecipientsBtn.click(); |
||
963 | * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)'); |
||
964 | * decreaseRecipientsBtn.click(); |
||
965 | * expect(messageElem.getText()).toEqual('Harry Potter gave a gift to Alice (#=0)'); |
||
966 | * decreaseRecipientsBtn.click(); |
||
967 | * expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)'); |
||
968 | * }); |
||
969 | * }); |
||
970 | * </file> |
||
971 | * </example> |
||
972 | * |
||
973 | * ### Plural and Gender together |
||
974 | * |
||
975 | * This example shows how you can specify gender rules for specific plural matches - in this case, |
||
976 | * =1 is special cased for gender. |
||
977 | * <example name="ngMessageFormat-example-plural-gender" module="msgFmtExample" deps="angular-message-format.js"> |
||
978 | * <file name="index.html"> |
||
979 | * <div ng-controller="AppController"> |
||
980 | Select recipients:<br> |
||
981 | <select multiple size=5 ng-model="recipients" ng-options="person as person.name for person in people"> |
||
982 | </select><br> |
||
983 | <p>{{recipients.length, plural, |
||
984 | =0 {{{sender.name}} has not given any gifts to anyone.} |
||
985 | =1 { {{recipients[0].gender, select, |
||
986 | female { {{sender.name}} gave {{recipients[0].name}} her gift.} |
||
987 | male { {{sender.name}} gave {{recipients[0].name}} his gift.} |
||
988 | other { {{sender.name}} gave {{recipients[0].name}} their gift.} |
||
989 | }} |
||
990 | } |
||
991 | other {{{sender.name}} gave {{recipients.length}} people gifts.} |
||
992 | }}</p> |
||
993 | </file> |
||
994 | * <file name="script.js"> |
||
995 | * function Person(name, gender) { |
||
996 | * this.name = name; |
||
997 | * this.gender = gender; |
||
998 | * } |
||
999 | * |
||
1000 | * var alice = new Person('Alice', 'female'), |
||
1001 | * bob = new Person('Bob', 'male'), |
||
1002 | * harry = new Person('Harry Potter', 'male'), |
||
1003 | * ashley = new Person('Ashley', ''); |
||
1004 | * |
||
1005 | * angular.module('msgFmtExample', ['ngMessageFormat']) |
||
1006 | * .controller('AppController', ['$scope', function($scope) { |
||
1007 | * $scope.people = [alice, bob, ashley]; |
||
1008 | * $scope.recipients = [alice]; |
||
1009 | * $scope.sender = harry; |
||
1010 | * }]); |
||
1011 | * </file> |
||
1012 | </example> |
||
1013 | */ |
||
1014 | |||
1015 | var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat( |
||
1016 | $parse, $locale, $sce, $exceptionHandler) { |
||
1017 | |||
1018 | function getStringifier(trustedContext, allOrNothing, text) { |
||
1019 | return function stringifier(value) { |
||
1020 | try { |
||
1021 | value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value); |
||
1022 | return allOrNothing && (value === undefined) ? value : $$stringify(value); |
||
1023 | } catch (err) { |
||
1024 | $exceptionHandler($interpolateMinErr['interr'](text, err)); |
||
0 ignored issues
–
show
|
|||
1025 | } |
||
1026 | }; |
||
1027 | } |
||
1028 | |||
1029 | function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { |
||
1030 | var stringifier = getStringifier(trustedContext, allOrNothing, text); |
||
1031 | var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier, |
||
1032 | mustHaveExpression, trustedContext, allOrNothing); |
||
1033 | parser.run(parser.ruleInterpolate); |
||
1034 | return parser.parsedFn; |
||
1035 | } |
||
1036 | |||
1037 | return { |
||
1038 | 'interpolate': interpolate |
||
1039 | }; |
||
1040 | }]; |
||
1041 | |||
1042 | var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) { |
||
1043 | if ($interpolate['startSymbol']() !== '{{' || $interpolate['endSymbol']() !== '}}') { |
||
1044 | throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.'); |
||
1045 | } |
||
1046 | var interpolate = $$messageFormat['interpolate']; |
||
1047 | interpolate['startSymbol'] = $interpolate['startSymbol']; |
||
1048 | interpolate['endSymbol'] = $interpolate['endSymbol']; |
||
1049 | return interpolate; |
||
1050 | }]; |
||
1051 | |||
1052 | var $interpolateMinErr; |
||
1053 | var isFunction; |
||
1054 | var noop; |
||
1055 | var toJson; |
||
1056 | var $$stringify; |
||
1057 | |||
1058 | var ngModule = window['angular']['module']('ngMessageFormat', ['ng']); |
||
1059 | ngModule['info']({ 'angularVersion': '1.8.2' }); |
||
1060 | ngModule['factory']('$$messageFormat', $$MessageFormatFactory); |
||
1061 | ngModule['config'](['$provide', function($provide) { |
||
1062 | $interpolateMinErr = window['angular']['$interpolateMinErr']; |
||
1063 | isFunction = window['angular']['isFunction']; |
||
1064 | noop = window['angular']['noop']; |
||
1065 | toJson = window['angular']['toJson']; |
||
1066 | $$stringify = window['angular']['$$stringify']; |
||
1067 | |||
1068 | $provide['decorator']('$interpolate', $$interpolateDecorator); |
||
1069 | }]); |
||
1070 | |||
1071 | |||
1072 | })(window, window.angular); |
||
1073 |