text: 
  
  (function() {
  
  /**
  * Config
  */
  var moduleName = 'angularUtils.directives.dirPagination';
  var DEFAULT_ID = '__default';
  
  /**
  * Module
  */
  angular.module(moduleName, [])
  .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
  .directive('dirPaginateNoCompile', noCompileDirective)
  .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
  .filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
  .service('paginationService', paginationService)
  .provider('paginationTemplate', paginationTemplateProvider)
  .run(['$templateCache',dirPaginationControlsTemplateInstaller]);
  
  function dirPaginateDirective($compile, $parse, paginationService) {
  
  return  {
  terminal: true,
  multiElement: true,
  priority: 100,
  compile: dirPaginationCompileFn
  };
  
  function dirPaginationCompileFn(tElement, tAttrs){
  
  var expression = tAttrs.dirPaginate;
  // regex taken directly from https://github.com/angular/angular.js/blob/v1.4.x/src/ng/directive/ngRepeat.js#L339
  var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
  
  var filterPattern = /\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\s+))|[^\)]*)/;
  if (match[2].match(filterPattern) === null) {
  throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
  }
  var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
  var collectionGetter = $parse(itemsPerPageFilterRemoved);
  
  addNoCompileAttributes(tElement);
  
  // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
  // dir-pagination-controls directives that may be looking for this ID.
  var rawId = tAttrs.paginationId || DEFAULT_ID;
  paginationService.registerInstance(rawId);
  
  return function dirPaginationLinkFn(scope, element, attrs){
  
  // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
  // potentially register a new ID if it evaluates to a different value than the rawId.
  var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
  
  // (TODO: this seems sound, but I'm reverting as many bug reports followed it's introduction in 0.11.0.
  // Needs more investigation.)
  // In case rawId != paginationId we deregister using rawId for the sake of general cleanliness
  // before registering using paginationId
  // paginationService.deregisterInstance(rawId);
  paginationService.registerInstance(paginationId);
  
  var repeatExpression = getRepeatExpression(expression, paginationId);
  addNgRepeatToElement(element, attrs, repeatExpression);
  
  removeTemporaryAttributes(element);
  var compiled =  $compile(element);
  
  var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
  paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
  
  if (typeof attrs.totalItems !== 'undefined') {
  paginationService.setAsyncModeTrue(paginationId);
  scope.$watch(function() {
  return $parse(attrs.totalItems)(scope);
  }, function (result) {
  if (0 <= result) {
  paginationService.setCollectionLength(paginationId, result);
  }
  });
  } else {
  paginationService.setAsyncModeFalse(paginationId);
  scope.$watchCollection(function() {
  return collectionGetter(scope);
  }, function(collection) {
  if (collection) {
  var collectionLength = (collection instanceof Array) ? collection.length : Object.keys(collection).length;
  paginationService.setCollectionLength(paginationId, collectionLength);
  }
  });
  }
  
  // Delegate to the link function returned by the new compilation of the ng-repeat
  compiled(scope);
  
  // (TODO: Reverting this due to many bug reports in v 0.11.0. Needs investigation as the
  // principle is sound)
  // When the scope is destroyed, we make sure to remove the reference to it in paginationService
  // so that it can be properly garbage collected
  // scope.$on('$destroy', function destroyDirPagination() {
  //     paginationService.deregisterInstance(paginationId);
  // });
  };
  }
  
  /**
  * If a pagination id has been specified, we need to check that it is present as the second argument passed to
  * the itemsPerPage filter. If it is not there, we add it and return the modified expression.
  *
  * @param expression
  * @param paginationId
  * @returns {*}
  */
  function getRepeatExpression(expression, paginationId) {
  var repeatExpression,
  idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);
  
  if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
  repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/, "$1 : '" + paginationId + "'");
  } else {
  repeatExpression = expression;
  }
  
  return repeatExpression;
  }
  
  /**
  * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
  * appropriate multi-element ng-repeat to the first and last element in the range.
  * @param element
  * @param attrs
  * @param repeatExpression
  */
  function addNgRepeatToElement(element, attrs, repeatExpression) {
  if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
  // using multiElement mode (dir-paginate-start, dir-paginate-end)
  attrs.$set('ngRepeatStart', repeatExpression);
  element.eq(element.length - 1).attr('ng-repeat-end', true);
  } else {
  attrs.$set('ngRepeat', repeatExpression);
  }
  }
  
  /**
  * Adds the dir-paginate-no-compile directive to each element in the tElement range.
  * @param tElement
  */
  function addNoCompileAttributes(tElement) {
  angular.forEach(tElement, function(el) {
  if (el.nodeType === 1) {
  angular.element(el).attr('dir-paginate-no-compile', true);
  }
  });
  }
  
  /**
  * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
  * @param element
  */
  function removeTemporaryAttributes(element) {
  angular.forEach(element, function(el) {
  if (el.nodeType === 1) {
  angular.element(el).removeAttr('dir-paginate-no-compile');
  }
  });
  element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
  element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
  }
  
  /**
  * Creates a getter function for the current-page attribute, using the expression provided or a default value if
  * no current-page expression was specified.
  *
  * @param scope
  * @param attrs
  * @param paginationId
  * @returns {*}
  */
  function makeCurrentPageGetterFn(scope, attrs, paginationId) {
  var currentPageGetter;
  if (attrs.currentPage) {
  currentPageGetter = $parse(attrs.currentPage);
  } else {
  // If the current-page attribute was not set, we'll make our own.
  // Replace any non-alphanumeric characters which might confuse
  // the $parse service and give unexpected results.
  // See https://github.com/michaelbromley/angularUtils/issues/233
  // Adding the '_' as a prefix resolves an issue where paginationId might be have a digit as its first char
  // See https://github.com/michaelbromley/angularUtils/issues/400
  var defaultCurrentPage = '_' + (paginationId + '__currentPage').replace(/\W/g, '_');
  scope[defaultCurrentPage] = 1;
  currentPageGetter = $parse(defaultCurrentPage);
  }
  return currentPageGetter;
  }
  }
  
  /**
  * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
  * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
  * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
  */
  function noCompileDirective() {
  return {
  priority: 5000,
  terminal: true
  };
  }
  
  function dirPaginationControlsTemplateInstaller($templateCache) {
  $templateCache.put('angularUtils.directives.dirPagination.template',
      '<ul class="pagination" ' +
      ' ng-if="1 < pages.length || !autoHide">' +
      '<li  style=" color: black;\n' +
      '  float: left;\n' +
      '  padding: 8px 16px;\n' +
      '  text-decoration: none;\n' +
      '  transition: background-color .3s;\n' +
      '  border: 1px solid #ddd;" ng-if="boundaryLinks"' +
      ' ng-class="{ disabled : pagination.current == 1 }">' +
      '<a href="" ng-click="setCurrent(1)">&laquo;</a></li>' +
      '' +
      '<li style=" color: black;\n' +
      '  float: left;\n' +
      '  padding: 8px 16px;\n' +
      '  text-decoration: none;\n' +
      '  transition: background-color .3s;\n' +
      '  border: 1px solid #ddd;" ng-if="directionLinks"' +
      ' ng-class="{ disabled : pagination.current == 1 }">' +
      '<a href="" ng-click="setCurrent(pagination.current - 1)">&lsaquo;</a></li>' +
      '<li style=" color: black;\n' +
      '  float: left;\n' +
      '  padding: 8px 16px;\n' +
      '  text-decoration: none;\n' +
      '  transition: background-color .3s;\n' +
      '  border: 1px solid #ddd;" ' +
      'ng-repeat="pageNumber in pages track by tracker(pageNumber, $index)" ' +
      'ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == \'...\' || ( ! autoHide && pages.length === 1 ) }"><a href="" ng-click="setCurrent(pageNumber)">' +
      '{{ pageNumber }}</a></li>' +
      '<li style="color: black;\n' +
      '  float: left;\n' +
      '  padding: 8px 16px;\n' +
      '  text-decoration: none;\n' +
      '  transition: background-color .3s;\n' +
      '  border: 1px solid #ddd;" ng-if="directionLinks"' +
      ' ng-class="{ disabled : pagination.current == pagination.last }">' +
      '<a href="" ng-click="setCurrent(pagination.current + 1)">&rsaquo;</a></li>' +
      '<li style="color: black;\n' +
      '  float: left;\n' +
      '  padding: 8px 16px;\n' +
      '  text-decoration: none;\n' +
      '  transition: background-color .3s;\n' +
      '  border: 1px solid #ddd;" ng-if="boundaryLinks"  ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.last)">&raquo;</a></li></ul>');
  }
  
  function dirPaginationControlsDirective(paginationService, paginationTemplate) {
  
  var numberRegex = /^\d+$/;
  
  var DDO = {
  restrict: 'AE',
  scope: {
  maxSize: '=?',
  onPageChange: '&?',
  paginationId: '=?',
  autoHide: '=?'
  },
  link: dirPaginationControlsLinkFn
  };
  
  // We need to check the paginationTemplate service to see whether a template path or
  // string has been specified, and add the `template` or `templateUrl` property to
  // the DDO as appropriate. The order of priority to decide which template to use is
  // (highest priority first):
  // 1. paginationTemplate.getString()
  // 2. attrs.templateUrl
  // 3. paginationTemplate.getPath()
  var templateString = paginationTemplate.getString();
  if (templateString !== undefined) {
  DDO.template = templateString;
  } else {
  DDO.templateUrl = function(elem, attrs) {
  return attrs.templateUrl || paginationTemplate.getPath();
  };
  }
  return DDO;
  
  function dirPaginationControlsLinkFn(scope, element, attrs) {
  
  // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
  // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is
  // no corresponding dir-paginate directive and wrongly throwing an exception.
  var rawId = attrs.paginationId ||  DEFAULT_ID;
  var paginationId = scope.paginationId || attrs.paginationId ||  DEFAULT_ID;
  
  if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
  var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
  if (window.console) {
  console.warn('Pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive, which was not found at link time.');
  }
  }
  
  if (!scope.maxSize) { scope.maxSize = 9; }
  scope.autoHide = scope.autoHide === undefined ? true : scope.autoHide;
  scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
  scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
  
  var paginationRange = Math.max(scope.maxSize, 5);
  scope.pages = [];
  scope.pagination = {
  last: 1,
  current: 1
  };
  scope.range = {
  lower: 1,
  upper: 1,
  total: 1
  };
  
  scope.$watch('maxSize', function(val) {
  if (val) {
  paginationRange = Math.max(scope.maxSize, 5);
  generatePagination();
  }
  });
  
  scope.$watch(function() {
  if (paginationService.isRegistered(paginationId)) {
  return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
  }
  }, function(length) {
  if (0 < length) {
  generatePagination();
  }
  });
  
  scope.$watch(function() {
  if (paginationService.isRegistered(paginationId)) {
  return (paginationService.getItemsPerPage(paginationId));
  }
  }, function(current, previous) {
  if (current != previous && typeof previous !== 'undefined') {
  goToPage(scope.pagination.current);
  }
  });
  
  scope.$watch(function() {
  if (paginationService.isRegistered(paginationId)) {
  return paginationService.getCurrentPage(paginationId);
  }
  }, function(currentPage, previousPage) {
  if (currentPage != previousPage) {
  goToPage(currentPage);
  }
  });
  
  scope.setCurrent = function(num) {
  if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
  num = parseInt(num, 10);
  paginationService.setCurrentPage(paginationId, num);
  }
  };
  
  /**
  * Custom "track by" function which allows for duplicate "..." entries on long lists,
  * yet fixes the problem of wrongly-highlighted links which happens when using
  * "track by $index" - see https://github.com/michaelbromley/angularUtils/issues/153
  * @param id
  * @param index
  * @returns {string}
  */
  scope.tracker = function(id, index) {
  return id + '_' + index;
  };
  
  function goToPage(num) {
  if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
  var oldPageNumber = scope.pagination.current;
  
  scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
  scope.pagination.current = num;
  updateRangeValues();
  
  // if a callback has been set, then call it with the page number as the first argument
  // and the previous page number as a second argument
  if (scope.onPageChange) {
  scope.onPageChange({
  newPageNumber : num,
  oldPageNumber : oldPageNumber
  });
  }
  }
  }
  
  function generatePagination() {
  if (paginationService.isRegistered(paginationId)) {
  var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;
  scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
  scope.pagination.current = page;
  scope.pagination.last = scope.pages[scope.pages.length - 1];
  if (scope.pagination.last < scope.pagination.current) {
  scope.setCurrent(scope.pagination.last);
  } else {
  updateRangeValues();
  }
  }
  }
  
  /**
  * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
  * template to display the current page range, e.g. "showing 21 - 40 of 144 results";
  */
  function updateRangeValues() {
  if (paginationService.isRegistered(paginationId)) {
  var currentPage = paginationService.getCurrentPage(paginationId),
  itemsPerPage = paginationService.getItemsPerPage(paginationId),
  totalItems = paginationService.getCollectionLength(paginationId);
  
  scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
  scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
  scope.range.total = totalItems;
  }
  }
  function isValidPageNumber(num) {
  return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
  }
  }
  
  /**
  * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
  * links used in pagination
  *
  * @param currentPage
  * @param rowsPerPage
  * @param paginationRange
  * @param collectionLength
  * @returns {Array}
  */
  function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) {
  var pages = [];
  var totalPages = Math.ceil(collectionLength / rowsPerPage);
  var halfWay = Math.ceil(paginationRange / 2);
  var position;
  
  if (currentPage <= halfWay) {
  position = 'start';
  } else if (totalPages - halfWay < currentPage) {
  position = 'end';
  } else {
  position = 'middle';
  }
  
  var ellipsesNeeded = paginationRange < totalPages;
  var i = 1;
  while (i <= totalPages && i <= paginationRange) {
  var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
  
  var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end'));
  var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start'));
  if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
  pages.push('...');
  } else {
  pages.push(pageNumber);
  }
  i ++;
  }
  return pages;
  }
  
  /**
  * Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position.
  *
  * @param i
  * @param currentPage
  * @param paginationRange
  * @param totalPages
  * @returns {*}
  */
  function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
  var halfWay = Math.ceil(paginationRange/2);
  if (i === paginationRange) {
  return totalPages;
  } else if (i === 1) {
  return i;
  } else if (paginationRange < totalPages) {
  if (totalPages - halfWay < currentPage) {
  return totalPages - paginationRange + i;
  } else if (halfWay < currentPage) {
  return currentPage - halfWay + i;
  } else {
  return i;
  }
  } else {
  return i;
  }
  }
  }
  
  /**
  * This filter slices the collection into pages based on the current page number and number of items per page.
  * @param paginationService
  * @returns {Function}
  */
  function itemsPerPageFilter(paginationService) {
  
  return function(collection, itemsPerPage, paginationId) {
  if (typeof (paginationId) === 'undefined') {
  paginationId = DEFAULT_ID;
  }
  if (!paginationService.isRegistered(paginationId)) {
  throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
  }
  var end;
  var start;
  if (angular.isObject(collection)) {
  itemsPerPage = parseInt(itemsPerPage) || 9999999999;
  if (paginationService.isAsyncMode(paginationId)) {
  start = 0;
  } else {
  start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage;
  }
  end = start + itemsPerPage;
  paginationService.setItemsPerPage(paginationId, itemsPerPage);
  
  if (collection instanceof Array) {
  // the array just needs to be sliced
  return collection.slice(start, end);
  } else {
  // in the case of an object, we need to get an array of keys, slice that, then map back to
  // the original object.
  var slicedObject = {};
  angular.forEach(keys(collection).slice(start, end), function(key) {
  slicedObject[key] = collection[key];
  });
  return slicedObject;
  }
  } else {
  return collection;
  }
  };
  }
  
  /**
  * Shim for the Object.keys() method which does not exist in IE < 9
  * @param obj
  * @returns {Array}
  */
  function keys(obj) {
  if (!Object.keys) {
  var objKeys = [];
  for (var i in obj) {
  if (obj.hasOwnProperty(i)) {
  objKeys.push(i);
  }
  }
  return objKeys;
  } else {
  return Object.keys(obj);
  }
  }
  
  /**
  * This service allows the various parts of the module to communicate and stay in sync.
  */
  function paginationService() {
  
  var instances = {};
  var lastRegisteredInstance;
  
  this.registerInstance = function(instanceId) {
  if (typeof instances[instanceId] === 'undefined') {
  instances[instanceId] = {
  asyncMode: false
  };
  lastRegisteredInstance = instanceId;
  }
  };
  
  this.deregisterInstance = function(instanceId) {
  delete instances[instanceId];
  };
  
  this.isRegistered = function(instanceId) {
  return (typeof instances[instanceId] !== 'undefined');
  };
  
  this.getLastInstanceId = function() {
  return lastRegisteredInstance;
  };
  
  this.setCurrentPageParser = function(instanceId, val, scope) {
  instances[instanceId].currentPageParser = val;
  instances[instanceId].context = scope;
  };
  this.setCurrentPage = function(instanceId, val) {
  instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
  };
  this.getCurrentPage = function(instanceId) {
  var parser = instances[instanceId].currentPageParser;
  return parser ? parser(instances[instanceId].context) : 1;
  };
  
  this.setItemsPerPage = function(instanceId, val) {
  instances[instanceId].itemsPerPage = val;
  };
  this.getItemsPerPage = function(instanceId) {
  return instances[instanceId].itemsPerPage;
  };
  
  this.setCollectionLength = function(instanceId, val) {
  instances[instanceId].collectionLength = val;
  };
  this.getCollectionLength = function(instanceId) {
  return instances[instanceId].collectionLength;
  };
  
  this.setAsyncModeTrue = function(instanceId) {
  instances[instanceId].asyncMode = true;
  };
  
  this.setAsyncModeFalse = function(instanceId) {
  instances[instanceId].asyncMode = false;
  };
  
  this.isAsyncMode = function(instanceId) {
  return instances[instanceId].asyncMode;
  };
  }
  
  /**
  * This provider allows global configuration of the template path used by the dir-pagination-controls directive.
  */
  function paginationTemplateProvider() {
  
  var templatePath = 'angularUtils.directives.dirPagination.template';
  var templateString;
  
  /**
  * Set a templateUrl to be used by all instances of <dir-pagination-controls>
      * @param {String} path
      */
      this.setPath = function(path) {
      templatePath = path;
      };
  
      /**
      * Set a string of HTML to be used as a template by all instances
      * of <dir-pagination-controls>. If both a path *and* a string have been set,
          * the string takes precedence.
          * @param {String} str
          */
          this.setString = function(str) {
          templateString = str;
          };
  
          this.$get = function() {
          return {
          getPath: function() {
          return templatePath;
          },
          getString: function() {
          return templateString;
          }
          };
          };
          }
          })();