2 * @license AngularJS v1.5.0
3 * (c) 2010-2016 Google, Inc. http://angularjs.org
6 (function(window, angular, undefined) {'use strict';
8 // NOTE: ADVANCED_OPTIMIZATIONS mode.
10 // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
11 // constructs incompatible with that mode.
13 var $interpolateMinErr = window['angular']['$interpolateMinErr'];
15 var noop = window['angular']['noop'],
16 isFunction = window['angular']['isFunction'],
17 toJson = window['angular']['toJson'];
19 function stringify(value) {
20 if (value == null /* null/undefined */) { return ''; }
21 switch (typeof value) {
22 case 'string': return value;
23 case 'number': return '' + value;
24 default: return toJson(value);
28 // Convert an index into the string into line/column for use in error messages
29 // As such, this doesn't have to be efficient.
30 function indexToLineAndColumn(text, index) {
31 var lines = text.split(/\n/g);
32 for (var i=0; i < lines.length; i++) {
34 if (index >= line.length) {
37 return { line: i + 1, column: index + 1 };
41 var PARSE_CACHE_FOR_TEXT_LITERALS = Object.create(null);
43 function parseTextLiteral(text) {
44 var cachedFn = PARSE_CACHE_FOR_TEXT_LITERALS[text];
45 if (cachedFn != null) {
48 function parsedFn(context) { return text; }
49 parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) {
50 var unwatch = scope['$watch'](noop,
51 function textLiteralWatcher() {
52 if (isFunction(listener)) { listener.call(null, text, text, scope); }
58 PARSE_CACHE_FOR_TEXT_LITERALS[text] = parsedFn;
59 parsedFn['exp'] = text; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
60 parsedFn['expressions'] = []; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding.
64 function subtractOffset(expressionFn, offset) {
68 function minusOffset(value) {
69 return (value == void 0) ? value : value - offset;
71 function parsedFn(context) { return minusOffset(expressionFn(context)); }
73 parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) {
74 unwatch = scope['$watch'](expressionFn,
75 function pluralExpressionWatchListener(newValue, oldValue) {
76 if (isFunction(listener)) { listener.call(null, minusOffset(newValue), minusOffset(oldValue), scope); }
84 // NOTE: ADVANCED_OPTIMIZATIONS mode.
86 // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
87 // constructs incompatible with that mode.
89 /* global $interpolateMinErr: false */
90 /* global isFunction: false */
91 /* global noop: false */
97 function MessageSelectorBase(expressionFn, choices) {
99 this.expressionFn = expressionFn;
100 this.choices = choices;
101 if (choices["other"] === void 0) {
102 throw $interpolateMinErr('reqother', '“other” is a required option.');
104 this.parsedFn = function(context) { return self.getResult(context); };
105 this.parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) {
106 return self.watchDelegate(scope, listener, objectEquality);
108 this.parsedFn['exp'] = expressionFn['exp'];
109 this.parsedFn['expressions'] = expressionFn['expressions'];
112 MessageSelectorBase.prototype.getMessageFn = function getMessageFn(value) {
113 return this.choices[this.categorizeValue(value)];
116 MessageSelectorBase.prototype.getResult = function getResult(context) {
117 return this.getMessageFn(this.expressionFn(context))(context);
120 MessageSelectorBase.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) {
121 var watchers = new MessageSelectorWatchers(this, scope, listener, objectEquality);
122 return function() { watchers.cancelWatch(); };
129 function MessageSelectorWatchers(msgSelector, scope, listener, objectEquality) {
132 this.msgSelector = msgSelector;
133 this.listener = listener;
134 this.objectEquality = objectEquality;
135 this.lastMessage = void 0;
136 this.messageFnWatcher = noop;
137 var expressionFnListener = function(newValue, oldValue) { return self.expressionFnListener(newValue, oldValue); };
138 this.expressionFnWatcher = scope['$watch'](msgSelector.expressionFn, expressionFnListener, objectEquality);
141 MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnListener(newValue, oldValue) {
143 this.messageFnWatcher();
144 var messageFnListener = function(newMessage, oldMessage) { return self.messageFnListener(newMessage, oldMessage); };
145 var messageFn = this.msgSelector.getMessageFn(newValue);
146 this.messageFnWatcher = this.scope['$watch'](messageFn, messageFnListener, this.objectEquality);
149 MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) {
150 if (isFunction(this.listener)) {
151 this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope);
153 this.lastMessage = newMessage;
156 MessageSelectorWatchers.prototype.cancelWatch = function cancelWatch() {
157 this.expressionFnWatcher();
158 this.messageFnWatcher();
163 * @extends MessageSelectorBase
166 function SelectMessage(expressionFn, choices) {
167 MessageSelectorBase.call(this, expressionFn, choices);
170 function SelectMessageProto() {}
171 SelectMessageProto.prototype = MessageSelectorBase.prototype;
173 SelectMessage.prototype = new SelectMessageProto();
174 SelectMessage.prototype.categorizeValue = function categorizeSelectValue(value) {
175 return (this.choices[value] !== void 0) ? value : "other";
180 * @extends MessageSelectorBase
183 function PluralMessage(expressionFn, choices, offset, pluralCat) {
184 MessageSelectorBase.call(this, expressionFn, choices);
185 this.offset = offset;
186 this.pluralCat = pluralCat;
189 function PluralMessageProto() {}
190 PluralMessageProto.prototype = MessageSelectorBase.prototype;
192 PluralMessage.prototype = new PluralMessageProto();
193 PluralMessage.prototype.categorizeValue = function categorizePluralValue(value) {
196 } else if (this.choices[value] !== void 0) {
199 var category = this.pluralCat(value - this.offset);
200 return (this.choices[category] !== void 0) ? category : "other";
204 // NOTE: ADVANCED_OPTIMIZATIONS mode.
206 // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
207 // constructs incompatible with that mode.
209 /* global $interpolateMinErr: false */
210 /* global isFunction: false */
211 /* global parseTextLiteral: false */
217 function InterpolationParts(trustedContext, allOrNothing) {
218 this.trustedContext = trustedContext;
219 this.allOrNothing = allOrNothing;
221 this.expressionFns = [];
222 this.expressionIndices = [];
223 this.partialText = '';
224 this.concatParts = null;
227 InterpolationParts.prototype.flushPartialText = function flushPartialText() {
228 if (this.partialText) {
229 if (this.concatParts == null) {
230 this.textParts.push(this.partialText);
232 this.textParts.push(this.concatParts.join(''));
233 this.concatParts = null;
235 this.partialText = '';
239 InterpolationParts.prototype.addText = function addText(text) {
241 if (!this.partialText) {
242 this.partialText = text;
243 } else if (this.concatParts) {
244 this.concatParts.push(text);
246 this.concatParts = [this.partialText, text];
251 InterpolationParts.prototype.addExpressionFn = function addExpressionFn(expressionFn) {
252 this.flushPartialText();
253 this.expressionIndices.push(this.textParts.length);
254 this.expressionFns.push(expressionFn);
255 this.textParts.push('');
258 InterpolationParts.prototype.getExpressionValues = function getExpressionValues(context) {
259 var expressionValues = new Array(this.expressionFns.length);
260 for (var i = 0; i < this.expressionFns.length; i++) {
261 expressionValues[i] = this.expressionFns[i](context);
263 return expressionValues;
266 InterpolationParts.prototype.getResult = function getResult(expressionValues) {
267 for (var i = 0; i < this.expressionIndices.length; i++) {
268 var expressionValue = expressionValues[i];
269 if (this.allOrNothing && expressionValue === void 0) return;
270 this.textParts[this.expressionIndices[i]] = expressionValue;
272 return this.textParts.join('');
276 InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression, originalText) {
278 this.flushPartialText();
279 if (mustHaveExpression && this.expressionFns.length === 0) {
282 if (this.textParts.length === 0) {
283 return parseTextLiteral('');
285 if (this.trustedContext && this.textParts.length > 1) {
286 $interpolateMinErr['throwNoconcat'](originalText);
288 if (this.expressionFns.length === 0) {
289 if (this.textParts.length != 1) { this.errorInParseLogic(); }
290 return parseTextLiteral(this.textParts[0]);
292 var parsedFn = function(context) {
293 return self.getResult(self.getExpressionValues(context));
295 parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) {
296 return self.watchDelegate(scope, listener, objectEquality);
299 parsedFn['exp'] = originalText; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
300 parsedFn['expressions'] = new Array(this.expressionFns.length); // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding.
301 for (var i = 0; i < this.expressionFns.length; i++) {
302 parsedFn['expressions'][i] = this.expressionFns[i]['exp'];
308 InterpolationParts.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) {
309 var watcher = new InterpolationPartsWatcher(this, scope, listener, objectEquality);
310 return function() { watcher.cancelWatch(); };
313 function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEquality) {
314 this.interpolationParts = interpolationParts;
316 this.previousResult = (void 0);
317 this.listener = listener;
319 this.expressionFnsWatcher = scope['$watchGroup'](interpolationParts.expressionFns, function(newExpressionValues, oldExpressionValues) {
320 self.watchListener(newExpressionValues, oldExpressionValues);
324 InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) {
325 var result = this.interpolationParts.getResult(newExpressionValues);
326 if (isFunction(this.listener)) {
327 this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope);
329 this.previousResult = result;
332 InterpolationPartsWatcher.prototype.cancelWatch = function cancelWatch() {
333 this.expressionFnsWatcher();
336 // NOTE: ADVANCED_OPTIMIZATIONS mode.
338 // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
339 // constructs incompatible with that mode.
341 /* global $interpolateMinErr: false */
342 /* global indexToLineAndColumn: false */
343 /* global InterpolationParts: false */
344 /* global PluralMessage: false */
345 /* global SelectMessage: false */
346 /* global subtractOffset: false */
348 // The params src and dst are exactly one of two types: NestedParserState or MessageFormatParser.
349 // This function is fully optimized by V8. (inspect via IRHydra or --trace-deopt.)
350 // The idea behind writing it this way is to avoid repeating oneself. This is the ONE place where
351 // the parser state that is saved/restored when parsing nested mustaches is specified.
352 function copyNestedParserState(src, dst) {
353 dst.expressionFn = src.expressionFn;
354 dst.expressionMinusOffsetFn = src.expressionMinusOffsetFn;
355 dst.pluralOffset = src.pluralOffset;
356 dst.choices = src.choices;
357 dst.choiceKey = src.choiceKey;
358 dst.interpolationParts = src.interpolationParts;
359 dst.ruleChoiceKeyword = src.ruleChoiceKeyword;
360 dst.msgStartIndex = src.msgStartIndex;
361 dst.expressionStartIndex = src.expressionStartIndex;
364 function NestedParserState(parser) {
365 copyNestedParserState(parser, this);
372 function MessageFormatParser(text, startIndex, $parse, pluralCat, stringifier,
373 mustHaveExpression, trustedContext, allOrNothing) {
375 this.index = startIndex || 0;
376 this.$parse = $parse;
377 this.pluralCat = pluralCat;
378 this.stringifier = stringifier;
379 this.mustHaveExpression = !!mustHaveExpression;
380 this.trustedContext = trustedContext;
381 this.allOrNothing = !!allOrNothing;
382 this.expressionFn = null;
383 this.expressionMinusOffsetFn = null;
384 this.pluralOffset = null;
386 this.choiceKey = null;
387 this.interpolationParts = null;
388 this.msgStartIndex = null;
389 this.nestedStateStack = [];
390 this.parsedFn = null;
392 this.ruleStack = null;
393 this.ruleChoiceKeyword = null;
394 this.interpNestLevel = null;
395 this.expressionStartIndex = null;
396 this.stringStartIndex = null;
397 this.stringQuote = null;
398 this.stringInterestsRe = null;
399 this.angularOperatorStack = null;
400 this.textPart = null;
403 // preserve v8 optimization.
404 var EMPTY_STATE = new NestedParserState(new MessageFormatParser(
405 /* text= */ '', /* startIndex= */ 0, /* $parse= */ null, /* pluralCat= */ null, /* stringifier= */ null,
406 /* mustHaveExpression= */ false, /* trustedContext= */ null, /* allOrNothing */ false));
408 MessageFormatParser.prototype.pushState = function pushState() {
409 this.nestedStateStack.push(new NestedParserState(this));
410 copyNestedParserState(EMPTY_STATE, this);
413 MessageFormatParser.prototype.popState = function popState() {
414 if (this.nestedStateStack.length === 0) {
415 this.errorInParseLogic();
417 var previousState = this.nestedStateStack.pop();
418 copyNestedParserState(previousState, this);
421 // Oh my JavaScript! Who knew you couldn't match a regex at a specific
422 // location in a string but will always search forward?!
423 // Apparently you'll be growing this ability via the sticky flag (y) in
424 // ES6. I'll just to work around you for now.
425 MessageFormatParser.prototype.matchRe = function matchRe(re, search) {
426 re.lastIndex = this.index;
427 var match = re.exec(this.text);
428 if (match != null && (search === true || (match.index == this.index))) {
429 this.index = re.lastIndex;
435 MessageFormatParser.prototype.searchRe = function searchRe(re) {
436 return this.matchRe(re, true);
440 MessageFormatParser.prototype.consumeRe = function consumeRe(re) {
441 // Without the sticky flag, we can't use the .test() method to consume a
442 // match at the current index. Instead, we'll use the slower .exec() method
443 // and verify match.index.
444 return !!this.matchRe(re);
447 // Run through our grammar avoiding deeply nested function call chains.
448 MessageFormatParser.prototype.run = function run(initialRule) {
449 this.ruleStack = [initialRule];
451 this.rule = this.ruleStack.pop();
455 this.assertRuleOrNull(this.rule);
456 } while (this.ruleStack.length > 0);
459 MessageFormatParser.prototype.errorInParseLogic = function errorInParseLogic() {
460 throw $interpolateMinErr('logicbug',
461 '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}”',
465 MessageFormatParser.prototype.assertRuleOrNull = function assertRuleOrNull(rule) {
466 if (rule === void 0) {
467 this.errorInParseLogic();
471 var NEXT_WORD_RE = /\s*(\w+)\s*/g;
472 MessageFormatParser.prototype.errorExpecting = function errorExpecting() {
473 // What was wrong with the syntax? Unsupported type, missing comma, or something else?
474 var match = this.matchRe(NEXT_WORD_RE), position;
476 position = indexToLineAndColumn(this.text, this.index);
477 throw $interpolateMinErr('reqarg',
478 'Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”',
479 position.line, position.column, this.text);
482 if (word == "select" || word == "plural") {
483 position = indexToLineAndColumn(this.text, this.index);
484 throw $interpolateMinErr('reqcomma',
485 'Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”',
486 word, position.line, position.column, this.text);
488 position = indexToLineAndColumn(this.text, this.index);
489 throw $interpolateMinErr('unknarg',
490 'Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”',
491 word, position.line, position.column, this.text);
495 var STRING_START_RE = /['"]/g;
496 MessageFormatParser.prototype.ruleString = function ruleString() {
497 var match = this.matchRe(STRING_START_RE);
499 var position = indexToLineAndColumn(this.text, this.index);
500 throw $interpolateMinErr('wantstring',
501 'Expected the beginning of a string at line {0}, column {1} in text “{2}”',
502 position.line, position.column, this.text);
504 this.startStringAtMatch(match);
507 MessageFormatParser.prototype.startStringAtMatch = function startStringAtMatch(match) {
508 this.stringStartIndex = match.index;
509 this.stringQuote = match[0];
510 this.stringInterestsRe = this.stringQuote == "'" ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE;
511 this.rule = this.ruleInsideString;
514 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;
515 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;
516 MessageFormatParser.prototype.ruleInsideString = function ruleInsideString() {
517 var match = this.searchRe(this.stringInterestsRe);
519 var position = indexToLineAndColumn(this.text, this.stringStartIndex);
520 throw $interpolateMinErr('untermstr',
521 'The string beginning at line {0}, column {1} is unterminated in text “{2}”',
522 position.line, position.column, this.text);
524 var chars = match[0];
525 if (match == this.stringQuote) {
530 var PLURAL_OR_SELECT_ARG_TYPE_RE = /\s*(plural|select)\s*,\s*/g;
531 MessageFormatParser.prototype.rulePluralOrSelect = function rulePluralOrSelect() {
532 var match = this.searchRe(PLURAL_OR_SELECT_ARG_TYPE_RE);
534 this.errorExpecting();
536 var argType = match[1];
538 case "plural": this.rule = this.rulePluralStyle; break;
539 case "select": this.rule = this.ruleSelectStyle; break;
540 default: this.errorInParseLogic();
544 MessageFormatParser.prototype.rulePluralStyle = function rulePluralStyle() {
545 this.choices = Object.create(null);
546 this.ruleChoiceKeyword = this.rulePluralValueOrKeyword;
547 this.rule = this.rulePluralOffset;
550 MessageFormatParser.prototype.ruleSelectStyle = function ruleSelectStyle() {
551 this.choices = Object.create(null);
552 this.ruleChoiceKeyword = this.ruleSelectKeyword;
553 this.rule = this.ruleSelectKeyword;
556 var NUMBER_RE = /[0]|(?:[1-9][0-9]*)/g;
557 var PLURAL_OFFSET_RE = new RegExp("\\s*offset\\s*:\\s*(" + NUMBER_RE.source + ")", "g");
559 MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() {
560 var match = this.matchRe(PLURAL_OFFSET_RE);
561 this.pluralOffset = (match == null) ? 0 : parseInt(match[1], 10);
562 this.expressionMinusOffsetFn = subtractOffset(this.expressionFn, this.pluralOffset);
563 this.rule = this.rulePluralValueOrKeyword;
566 MessageFormatParser.prototype.assertChoiceKeyIsNew = function assertChoiceKeyIsNew(choiceKey, index) {
567 if (this.choices[choiceKey] !== void 0) {
568 var position = indexToLineAndColumn(this.text, index);
569 throw $interpolateMinErr('dupvalue',
570 'The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”',
571 choiceKey, position.line, position.column, this.text);
575 var SELECT_KEYWORD = /\s*(\w+)/g;
576 MessageFormatParser.prototype.ruleSelectKeyword = function ruleSelectKeyword() {
577 var match = this.matchRe(SELECT_KEYWORD);
579 this.parsedFn = new SelectMessage(this.expressionFn, this.choices).parsedFn;
583 this.choiceKey = match[1];
584 this.assertChoiceKeyIsNew(this.choiceKey, match.index);
585 this.rule = this.ruleMessageText;
588 var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp("\\s*(?:(?:=(" + NUMBER_RE.source + "))|(\\w+))", "g");
589 MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValueOrKeyword() {
590 var match = this.matchRe(EXPLICIT_VALUE_OR_KEYWORD_RE);
592 this.parsedFn = new PluralMessage(this.expressionFn, this.choices, this.pluralOffset, this.pluralCat).parsedFn;
596 if (match[1] != null) {
597 this.choiceKey = parseInt(match[1], 10);
599 this.choiceKey = match[2];
601 this.assertChoiceKeyIsNew(this.choiceKey, match.index);
602 this.rule = this.ruleMessageText;
605 var BRACE_OPEN_RE = /\s*{/g;
606 var BRACE_CLOSE_RE = /}/g;
607 MessageFormatParser.prototype.ruleMessageText = function ruleMessageText() {
608 if (!this.consumeRe(BRACE_OPEN_RE)) {
609 var position = indexToLineAndColumn(this.text, this.index);
610 throw $interpolateMinErr('reqopenbrace',
611 'The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”',
612 this.choiceKey, position.line, position.column, this.text);
614 this.msgStartIndex = this.index;
615 this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing);
616 this.rule = this.ruleInInterpolationOrMessageText;
619 // Note: Since "\" is used as an escape character, don't allow it to be part of the
620 // startSymbol/endSymbol when I add the feature to allow them to be redefined.
621 var INTERP_OR_END_MESSAGE_RE = /\\.|{{|}/g;
622 var INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE = /\\.|{{|#|}/g;
623 var ESCAPE_OR_MUSTACHE_BEGIN_RE = /\\.|{{/g;
624 MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function advanceInInterpolationOrMessageText() {
625 var currentIndex = this.index, match, re;
626 if (this.ruleChoiceKeyword == null) { // interpolation
627 match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE);
628 if (match == null) { // End of interpolation text. Nothing more to process.
629 this.textPart = this.text.substring(currentIndex);
630 this.index = this.text.length;
634 match = this.searchRe(this.ruleChoiceKeyword == this.rulePluralValueOrKeyword ?
635 INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE : INTERP_OR_END_MESSAGE_RE);
637 var position = indexToLineAndColumn(this.text, this.msgStartIndex);
638 throw $interpolateMinErr('reqendbrace',
639 'The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”',
640 this.choiceKey, position.line, position.column, this.text);
643 // match is non-null.
644 var token = match[0];
645 this.textPart = this.text.substring(currentIndex, match.index);
649 MessageFormatParser.prototype.ruleInInterpolationOrMessageText = function ruleInInterpolationOrMessageText() {
650 var currentIndex = this.index;
651 var token = this.advanceInInterpolationOrMessageText();
653 // End of interpolation text. Nothing more to process.
654 this.index = this.text.length;
655 this.interpolationParts.addText(this.text.substring(currentIndex));
659 if (token[0] == "\\") {
660 // unescape next character and continue
661 this.interpolationParts.addText(this.textPart + token[1]);
664 this.interpolationParts.addText(this.textPart);
667 this.ruleStack.push(this.ruleEndMustacheInInterpolationOrMessage);
668 this.rule = this.ruleEnteredMustache;
669 } else if (token == "}") {
670 this.choices[this.choiceKey] = this.interpolationParts.toParsedFn(/*mustHaveExpression=*/false, this.text);
671 this.rule = this.ruleChoiceKeyword;
672 } else if (token == "#") {
673 this.interpolationParts.addExpressionFn(this.expressionMinusOffsetFn);
675 this.errorInParseLogic();
679 MessageFormatParser.prototype.ruleInterpolate = function ruleInterpolate() {
680 this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing);
681 this.rule = this.ruleInInterpolation;
684 MessageFormatParser.prototype.ruleInInterpolation = function ruleInInterpolation() {
685 var currentIndex = this.index;
686 var match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE);
688 // End of interpolation text. Nothing more to process.
689 this.index = this.text.length;
690 this.interpolationParts.addText(this.text.substring(currentIndex));
691 this.parsedFn = this.interpolationParts.toParsedFn(this.mustHaveExpression, this.text);
695 var token = match[0];
696 if (token[0] == "\\") {
697 // unescape next character and continue
698 this.interpolationParts.addText(this.text.substring(currentIndex, match.index) + token[1]);
701 this.interpolationParts.addText(this.text.substring(currentIndex, match.index));
703 this.ruleStack.push(this.ruleInterpolationEndMustache);
704 this.rule = this.ruleEnteredMustache;
707 MessageFormatParser.prototype.ruleInterpolationEndMustache = function ruleInterpolationEndMustache() {
708 var expressionFn = this.parsedFn;
710 this.interpolationParts.addExpressionFn(expressionFn);
711 this.rule = this.ruleInInterpolation;
714 MessageFormatParser.prototype.ruleEnteredMustache = function ruleEnteredMustache() {
715 this.parsedFn = null;
716 this.ruleStack.push(this.ruleEndMustache);
717 this.rule = this.ruleAngularExpression;
720 MessageFormatParser.prototype.ruleEndMustacheInInterpolationOrMessage = function ruleEndMustacheInInterpolationOrMessage() {
721 var expressionFn = this.parsedFn;
723 this.interpolationParts.addExpressionFn(expressionFn);
724 this.rule = this.ruleInInterpolationOrMessageText;
729 var INTERP_END_RE = /\s*}}/g;
730 MessageFormatParser.prototype.ruleEndMustache = function ruleEndMustache() {
731 var match = this.matchRe(INTERP_END_RE);
733 var position = indexToLineAndColumn(this.text, this.index);
734 throw $interpolateMinErr('reqendinterp',
735 'Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”',
736 '}}', position.line, position.column, this.text);
738 if (this.parsedFn == null) {
739 // If we parsed a MessageFormat extension, (e.g. select/plural today, maybe more some other
740 // day), then the result *has* to be a string and those rules would have already set
741 // this.parsedFn. If there was no MessageFormat extension, then there is no requirement to
742 // stringify the result and parsedFn isn't set. We set it here. While we could have set it
743 // unconditionally when exiting the Angular expression, I intend for us to not just replace
744 // $interpolate, but also to replace $parse in a future version (so ng-bind can work), and in
745 // such a case we do not want to unnecessarily stringify something if it's not going to be used
746 // in a string context.
747 this.parsedFn = this.$parse(this.expressionFn, this.stringifier);
748 this.parsedFn['exp'] = this.expressionFn['exp']; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
749 this.parsedFn['expressions'] = this.expressionFn['expressions']; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding.
754 MessageFormatParser.prototype.ruleAngularExpression = function ruleAngularExpression() {
755 this.angularOperatorStack = [];
756 this.expressionStartIndex = this.index;
757 this.rule = this.ruleInAngularExpression;
760 function getEndOperator(opBegin) {
762 case "{": return "}";
763 case "[": return "]";
764 case "(": return ")";
765 default: return null;
769 function getBeginOperator(opEnd) {
771 case "}": return "{";
772 case "]": return "[";
773 case ")": return "(";
774 default: return null;
778 // TODO(chirayu): The interpolation endSymbol must also be accounted for. It
779 // just so happens that "}" is an operator so it's in the list below. But we
780 // should support any other type of start/end interpolation symbol.
781 var INTERESTING_OPERATORS_RE = /[[\]{}()'",]/g;
782 MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularExpression() {
783 var startIndex = this.index;
784 var match = this.searchRe(INTERESTING_OPERATORS_RE);
787 if (this.angularOperatorStack.length === 0) {
788 // This is the end of the Angular expression so this is actually a
789 // success. Note that when inside an interpolation, this means we even
790 // consumed the closing interpolation symbols if they were curlies. This
791 // is NOT an error at this point but will become an error further up the
792 // stack when the part that saw the opening curlies is unable to find the
794 this.index = this.text.length;
795 this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index));
796 // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
797 this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index);
798 this.expressionFn['expressions'] = this.expressionFn['expressions'];
802 var innermostOperator = this.angularOperatorStack[0];
803 throw $interpolateMinErr('badexpr',
804 'Unexpected end of Angular expression. Expecting operator “{0}” at the end of the text “{1}”',
805 this.getEndOperator(innermostOperator), this.text);
807 var operator = match[0];
808 if (operator == "'" || operator == '"') {
809 this.ruleStack.push(this.ruleInAngularExpression);
810 this.startStringAtMatch(match);
813 if (operator == ",") {
814 if (this.trustedContext) {
815 position = indexToLineAndColumn(this.text, this.index);
816 throw $interpolateMinErr('unsafe',
817 'Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”',
818 this.trustedContext, position.line, position.column, this.text);
820 // only the top level comma has relevance.
821 if (this.angularOperatorStack.length === 0) {
822 // todo: does this need to be trimmed?
823 this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, match.index));
824 // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
825 this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, match.index);
826 this.expressionFn['expressions'] = this.expressionFn['expressions'];
828 this.rule = this.rulePluralOrSelect;
832 if (getEndOperator(operator) != null) {
833 this.angularOperatorStack.unshift(operator);
836 var beginOperator = getBeginOperator(operator);
837 if (beginOperator == null) {
838 this.errorInParseLogic();
840 if (this.angularOperatorStack.length > 0) {
841 if (beginOperator == this.angularOperatorStack[0]) {
842 this.angularOperatorStack.shift();
845 position = indexToLineAndColumn(this.text, this.index);
846 throw $interpolateMinErr('badexpr',
847 'Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”',
848 operator, position.line, position.column, getEndOperator(this.angularOperatorStack[0]), this.text);
850 // We are trying to pop off the operator stack but there really isn't anything to pop off.
851 this.index = match.index;
852 this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index));
853 // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js
854 this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index);
855 this.expressionFn['expressions'] = this.expressionFn['expressions'];
859 // NOTE: ADVANCED_OPTIMIZATIONS mode.
861 // This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using
862 // constructs incompatible with that mode.
864 /* global $interpolateMinErr: false */
865 /* global MessageFormatParser: false */
866 /* global stringify: false */
870 * @name $$messageFormat
873 * Angular internal service to recognize MessageFormat extensions in interpolation expressions.
874 * For more information, see:
875 * https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit
879 * <example name="ngMessageFormat-example" module="msgFmtExample" deps="angular-message-format.min.js">
880 * <file name="index.html">
881 * <div ng-controller="AppController">
882 * <button ng-click="decreaseRecipients()" id="decreaseRecipients">decreaseRecipients</button><br>
883 * <span>{{recipients.length, plural, offset:1
884 * =0 {{{sender.name}} gave no gifts (\#=#)}
885 * =1 {{{sender.name}} gave one gift to {{recipients[0].name}} (\#=#)}
886 * one {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)}
887 * other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)}
892 * <file name="script.js">
893 * function Person(name, gender) {
895 * this.gender = gender;
898 * var alice = new Person("Alice", "female"),
899 * bob = new Person("Bob", "male"),
900 * charlie = new Person("Charlie", "male"),
901 * harry = new Person("Harry Potter", "male");
903 * angular.module('msgFmtExample', ['ngMessageFormat'])
904 * .controller('AppController', ['$scope', function($scope) {
905 * $scope.recipients = [alice, bob, charlie];
906 * $scope.sender = harry;
907 * $scope.decreaseRecipients = function() {
908 * --$scope.recipients.length;
913 * <file name="protractor.js" type="protractor">
914 * describe('MessageFormat plural', function() {
915 * it('should pluralize initial values', function() {
916 * var messageElem = element(by.binding('recipients.length')), decreaseRecipientsBtn = element(by.id('decreaseRecipients'));
917 * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)');
918 * decreaseRecipientsBtn.click();
919 * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)');
920 * decreaseRecipientsBtn.click();
921 * expect(messageElem.getText()).toEqual('Harry Potter gave one gift to Alice (#=0)');
922 * decreaseRecipientsBtn.click();
923 * expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)');
929 var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat(
930 $parse, $locale, $sce, $exceptionHandler) {
932 function getStringifier(trustedContext, allOrNothing, text) {
933 return function stringifier(value) {
935 value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value);
936 return allOrNothing && (value === void 0) ? value : stringify(value);
938 $exceptionHandler($interpolateMinErr['interr'](text, err));
943 function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
944 var stringifier = getStringifier(trustedContext, allOrNothing, text);
945 var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier,
946 mustHaveExpression, trustedContext, allOrNothing);
947 parser.run(parser.ruleInterpolate);
948 return parser.parsedFn;
952 'interpolate': interpolate
956 var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) {
957 if ($interpolate['startSymbol']() != "{{" || $interpolate['endSymbol']() != "}}") {
958 throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.');
960 var interpolate = $$messageFormat['interpolate'];
961 interpolate['startSymbol'] = $interpolate['startSymbol'];
962 interpolate['endSymbol'] = $interpolate['endSymbol'];
969 * @name ngMessageFormat
970 * @packageName angular-message-format
973 var module = window['angular']['module']('ngMessageFormat', ['ng']);
974 module['factory']('$$messageFormat', $$MessageFormatFactory);
975 module['config'](['$provide', function($provide) {
976 $provide['decorator']('$interpolate', $$interpolateDecorator);
980 })(window, window.angular);