text: 'use strict';
  
  /**
   * @ngdoc directive
   * @name ngRepeat
   *
   * @description
   * The `ngRepeat` directive instantiates a template once per item from a collection. Each template
   * instance gets its own scope, where the given loop variable is set to the current collection item,
   * and `$index` is set to the item index or key.
   *
   * Special properties are exposed on the local scope of each template instance, including:
   *
   * | Variable  | Type            | Details                                                                     |
   * |-----------|-----------------|-----------------------------------------------------------------------------|
   * | `$index`  | {@type number}  | iterator offset of the repeated element (0..length-1)                       |
   * | `$first`  | {@type boolean} | true if the repeated element is first in the iterator.                      |
   * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. |
   * | `$last`   | {@type boolean} | true if the repeated element is last in the iterator.                       |
   * | `$even`   | {@type boolean} | true if the iterator position `$index` is even (otherwise false).           |
   * | `$odd`    | {@type boolean} | true if the iterator position `$index` is odd (otherwise false).            |
   *
   * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
   * This may be useful when, for instance, nesting ngRepeats.
   *
   * # Special repeat start and end points
   * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
   * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
   * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
   * up to and including the ending HTML tag where **ng-repeat-end** is placed.
   *
   * The example below makes use of this feature:
   * ```html
   *   <header ng-repeat-start="item in items">
   *     Header {{ item }}
   *   </header>
   *   <div class="body">
   *     Body {{ item }}
   *   </div>
   *   <footer ng-repeat-end>
   *     Footer {{ item }}
   *   </footer>
   * ```
   *
   * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to:
   * ```html
   *   <header>
   *     Header A
   *   </header>
   *   <div class="body">
   *     Body A
   *   </div>
   *   <footer>
   *     Footer A
   *   </footer>
   *   <header>
   *     Header B
   *   </header>
   *   <div class="body">
   *     Body B
   *   </div>
   *   <footer>
   *     Footer B
   *   </footer>
   * ```
   *
   * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such
   * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
   *
   * @animations
   * **.enter** - when a new item is added to the list or when an item is revealed after a filter
   *
   * **.leave** - when an item is removed from the list or when an item is filtered out
   *
   * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
   *
   * @element ANY
   * @scope
   * @priority 1000
   * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
   *   formats are currently supported:
   *
   *   * `variable in expression` – where variable is the user defined loop variable and `expression`
   *     is a scope expression giving the collection to enumerate.
   *
   *     For example: `album in artist.albums`.
   *
   *   * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
   *     and `expression` is the scope expression giving the collection to enumerate.
   *
   *     For example: `(name, age) in {'adam':10, 'amalie':12}`.
   *
   *   * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
   *     which can be used to associate the objects in the collection with the DOM elements. If no tracking function
   *     is specified the ng-repeat associates elements by identity in the collection. It is an error to have
   *     more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
   *     mapped to the same DOM element, which is not possible.)  Filters should be applied to the expression,
   *     before specifying a tracking expression.
   *
   *     For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
   *     will be associated by item identity in the array.
   *
   *     For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
   *     `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
   *     with the corresponding item in the array by identity. Moving the same object in array would move the DOM
   *     element in the same way in the DOM.
   *
   *     For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
   *     case the object identity does not matter. Two objects are considered equivalent as long as their `id`
   *     property is same.
   *
   *     For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
   *     to items in conjunction with a tracking expression.
   *
   * @example
   * This example initializes the scope to a list of names and
   * then uses `ngRepeat` to display every person:
    <example module="ngAnimate" deps="angular-animate.js" animations="true">
      <file name="index.html">
        <div ng-init="friends = [
          {name:'John', age:25, gender:'boy'},
          {name:'Jessie', age:30, gender:'girl'},
          {name:'Johanna', age:28, gender:'girl'},
          {name:'Joy', age:15, gender:'girl'},
          {name:'Mary', age:28, gender:'girl'},
          {name:'Peter', age:95, gender:'boy'},
          {name:'Sebastian', age:50, gender:'boy'},
          {name:'Erika', age:27, gender:'girl'},
          {name:'Patrick', age:40, gender:'boy'},
          {name:'Samantha', age:60, gender:'girl'}
        ]">
          I have {{friends.length}} friends. They are:
          <input type="search" ng-model="q" placeholder="filter friends..." />
          <ul class="example-animate-container">
            <li class="animate-repeat" ng-repeat="friend in friends | filter:q">
              [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
            </li>
          </ul>
        </div>
      </file>
      <file name="animations.css">
        .example-animate-container {
          background:white;
          border:1px solid black;
          list-style:none;
          margin:0;
          padding:0 10px;
        }
  
        .animate-repeat {
          line-height:40px;
          list-style:none;
          box-sizing:border-box;
        }
  
        .animate-repeat.ng-move,
        .animate-repeat.ng-enter,
        .animate-repeat.ng-leave {
          -webkit-transition:all linear 0.5s;
          transition:all linear 0.5s;
        }
  
        .animate-repeat.ng-leave.ng-leave-active,
        .animate-repeat.ng-move,
        .animate-repeat.ng-enter {
          opacity:0;
          max-height:0;
        }
  
        .animate-repeat.ng-leave,
        .animate-repeat.ng-move.ng-move-active,
        .animate-repeat.ng-enter.ng-enter-active {
          opacity:1;
          max-height:40px;
        }
      </file>
      <file name="protractor.js" type="protractor">
        var friends = element.all(by.repeater('friend in friends'));
  
        it('should render initial data set', function() {
          expect(friends.count()).toBe(10);
          expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.');
          expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.');
          expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.');
          expect(element(by.binding('friends.length')).getText())
              .toMatch("I have 10 friends. They are:");
        });
  
         it('should update repeater when filter predicate changes', function() {
           expect(friends.count()).toBe(10);
  
           element(by.model('q')).sendKeys('ma');
  
           expect(friends.count()).toBe(2);
           expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.');
           expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.');
         });
        </file>
      </example>
   */
  var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
    var NG_REMOVED = '$$NG_REMOVED';
    var ngRepeatMinErr = minErr('ngRepeat');
    return {
      transclude: 'element',
      priority: 1000,
      terminal: true,
      $$tlb: true,
      link: function($scope, $element, $attr, ctrl, $transclude){
          var expression = $attr.ngRepeat;
          var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
            trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
            lhs, rhs, valueIdentifier, keyIdentifier,
            hashFnLocals = {$id: hashKey};
  
          if (!match) {
            throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
              expression);
          }
  
          lhs = match[1];
          rhs = match[2];
          trackByExp = match[3];
  
          if (trackByExp) {
            trackByExpGetter = $parse(trackByExp);
            trackByIdExpFn = function(key, value, index) {
              // assign key, value, and $index to the locals so that they can be used in hash functions
              if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
              hashFnLocals[valueIdentifier] = value;
              hashFnLocals.$index = index;
              return trackByExpGetter($scope, hashFnLocals);
            };
          } else {
            trackByIdArrayFn = function(key, value) {
              return hashKey(value);
            };
            trackByIdObjFn = function(key) {
              return key;
            };
          }
  
          match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
          if (!match) {
            throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
                                                                      lhs);
          }
          valueIdentifier = match[3] || match[1];
          keyIdentifier = match[2];
  
          // Store a list of elements from previous run. This is a hash where key is the item from the
          // iterator, and the value is objects with following properties.
          //   - scope: bound scope
          //   - element: previous element.
          //   - index: position
          var lastBlockMap = {};
  
          //watch props
          $scope.$watchCollection(rhs, function ngRepeatAction(collection){
            var index, length,
                previousNode = $element[0],     // current position of the node
                nextNode,
                // Same as lastBlockMap but it has the current state. It will become the
                // lastBlockMap on the next iteration.
                nextBlockMap = {},
                arrayLength,
                childScope,
                key, value, // key/value of iteration
                trackById,
                trackByIdFn,
                collectionKeys,
                block,       // last object information {scope, element, id}
                nextBlockOrder = [],
                elementsToRemove;
  
  
            if (isArrayLike(collection)) {
              collectionKeys = collection;
              trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
            } else {
              trackByIdFn = trackByIdExpFn || trackByIdObjFn;
              // if object, extract keys, sort them and use to determine order of iteration over obj props
              collectionKeys = [];
              for (key in collection) {
                if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
                  collectionKeys.push(key);
                }
              }
              collectionKeys.sort();
            }
  
            arrayLength = collectionKeys.length;
  
            // locate existing items
            length = nextBlockOrder.length = collectionKeys.length;
            for(index = 0; index < length; index++) {
             key = (collection === collectionKeys) ? index : collectionKeys[index];
             value = collection[key];
             trackById = trackByIdFn(key, value, index);
             assertNotHasOwnProperty(trackById, '`track by` id');
             if(lastBlockMap.hasOwnProperty(trackById)) {
               block = lastBlockMap[trackById];
               delete lastBlockMap[trackById];
               nextBlockMap[trackById] = block;
               nextBlockOrder[index] = block;
             } else if (nextBlockMap.hasOwnProperty(trackById)) {
               // restore lastBlockMap
               forEach(nextBlockOrder, function(block) {
                 if (block && block.scope) lastBlockMap[block.id] = block;
               });
               // This is a duplicate and we need to throw an error
               throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
                                                                                                                                                      expression,       trackById);
             } else {
               // new never before seen block
               nextBlockOrder[index] = { id: trackById };
               nextBlockMap[trackById] = false;
             }
           }
  
            // remove existing items
            for (key in lastBlockMap) {
              // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
              if (lastBlockMap.hasOwnProperty(key)) {
                block = lastBlockMap[key];
                elementsToRemove = getBlockElements(block.clone);
                $animate.leave(elementsToRemove);
                forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; });
                block.scope.$destroy();
              }
            }
  
            // we are not using forEach for perf reasons (trying to avoid #call)
            for (index = 0, length = collectionKeys.length; index < length; index++) {
              key = (collection === collectionKeys) ? index : collectionKeys[index];
              value = collection[key];
              block = nextBlockOrder[index];
              if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]);
  
              if (block.scope) {
                // if we have already seen this object, then we need to reuse the
                // associated scope/element
                childScope = block.scope;
  
                nextNode = previousNode;
                do {
                  nextNode = nextNode.nextSibling;
                } while(nextNode && nextNode[NG_REMOVED]);
  
                if (getBlockStart(block) != nextNode) {
                  // existing item which got moved
                  $animate.move(getBlockElements(block.clone), null, jqLite(previousNode));
                }
                previousNode = getBlockEnd(block);
              } else {
                // new item which we don't know about
                childScope = $scope.$new();
              }
  
              childScope[valueIdentifier] = value;
              if (keyIdentifier) childScope[keyIdentifier] = key;
              childScope.$index = index;
              childScope.$first = (index === 0);
              childScope.$last = (index === (arrayLength - 1));
              childScope.$middle = !(childScope.$first || childScope.$last);
              // jshint bitwise: false
              childScope.$odd = !(childScope.$even = (index&1) === 0);
              // jshint bitwise: true
  
              if (!block.scope) {
                $transclude(childScope, function(clone) {
                  clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' ');
                  $animate.enter(clone, null, jqLite(previousNode));
                  previousNode = clone;
                  block.scope = childScope;
                  // Note: We only need the first/last node of the cloned nodes.
                  // However, we need to keep the reference to the jqlite wrapper as it might be changed later
                  // by a directive with templateUrl when it's template arrives.
                  block.clone = clone;
                  nextBlockMap[block.id] = block;
                });
              }
            }
            lastBlockMap = nextBlockMap;
          });
      }
    };
  
    function getBlockStart(block) {
      return block.clone[0];
    }
  
    function getBlockEnd(block) {
      return block.clone[block.clone.length - 1];
    }
  }];
  
  