From 6022f3df399a5d98830dfe7904f0ad2baaa308c7 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 3 Nov 2011 14:36:22 -0700 Subject: move(filters): appease the History God --- angularFiles.js | 4 +- src/filters.js | 647 ------------------------------------- src/service/filter/filters.js | 647 +++++++++++++++++++++++++++++++++++++ test/FiltersSpec.js | 300 ----------------- test/service/filter/filtersSpec.js | 300 +++++++++++++++++ 5 files changed, 950 insertions(+), 948 deletions(-) delete mode 100644 src/filters.js create mode 100644 src/service/filter/filters.js delete mode 100644 test/FiltersSpec.js create mode 100644 test/service/filter/filtersSpec.js diff --git a/angularFiles.js b/angularFiles.js index 269f52fa..f2563094 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -8,13 +8,14 @@ angularFiles = { 'src/sanitizer.js', 'src/jqLite.js', 'src/apis.js', - 'src/filters.js', 'src/service/compiler.js', 'src/service/cookieStore.js', 'src/service/cookies.js', 'src/service/defer.js', 'src/service/document.js', 'src/service/exceptionHandler.js', + 'src/service/filter.js', + 'src/service/filter/filters.js', 'src/service/formFactory.js', 'src/service/location.js', 'src/service/log.js', @@ -75,6 +76,7 @@ angularFiles = { 'test/jstd-scenario-adapter/*.js', 'test/*.js', 'test/service/*.js', + 'test/service/filter/*.js', 'test/widget/*.js', 'example/personalLog/test/*.js' ], diff --git a/src/filters.js b/src/filters.js deleted file mode 100644 index b9c3d2ef..00000000 --- a/src/filters.js +++ /dev/null @@ -1,647 +0,0 @@ -'use strict'; - -/** - * @ngdoc overview - * @name angular.filter - * @description - * - * Filters are used for formatting data displayed to the user. - * - * The general syntax in templates is as follows: - * - * {{ expression | [ filter_name ] }} - * - * Following is the list of built-in angular filters: - * - * * {@link angular.filter.currency currency} - * * {@link angular.filter.date date} - * * {@link angular.filter.html html} - * * {@link angular.filter.json json} - * * {@link angular.filter.linky linky} - * * {@link angular.filter.lowercase lowercase} - * * {@link angular.filter.number number} - * * {@link angular.filter.uppercase uppercase} - * - * For more information about how angular filters work, and how to create your own filters, see - * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer - * Guide. - */ - -/** - * @ngdoc filter - * @name angular.filter.currency - * @function - * - * @description - * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default - * symbol for current locale is used. - * - * @param {number} amount Input to filter. - * @param {string=} symbol Currency symbol or identifier to be displayed. - * @returns {string} Formatted number. - * - * @css ng-format-negative - * When the value is negative, this css class is applied to the binding making it (by default) red. - * - * @example - - - -
-
- default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}} -
-
- - it('should init with 1234.56', function() { - expect(binding('amount | currency')).toBe('$1,234.56'); - expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); - }); - it('should update', function() { - input('amount').enter('-1234'); - expect(binding('amount | currency')).toBe('($1,234.00)'); - expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); - expect(element('.doc-example-live .ng-binding').prop('className')). - toMatch(/ng-format-negative/); - }); - -
- */ -angularFilter.currency = function(amount, currencySymbol){ - var formats = this.$service('$locale').NUMBER_FORMATS; - this.$element.toggleClass('ng-format-negative', amount < 0); - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); -}; - -/** - * @ngdoc filter - * @name angular.filter.number - * @function - * - * @description - * Formats a number as text. - * - * If the input is not a number an empty string is returned. - * - * @param {number|string} number Number to format. - * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. - * - * @example - - - -
- Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}} -
-
- - it('should format numbers', function() { - expect(binding('val | number')).toBe('1,234.568'); - expect(binding('val | number:0')).toBe('1,235'); - expect(binding('-val | number:4')).toBe('-1,234.5679'); - }); - - it('should update', function() { - input('val').enter('3374.333'); - expect(binding('val | number')).toBe('3,374.333'); - expect(binding('val | number:0')).toBe('3,374'); - expect(binding('-val | number:4')).toBe('-3,374.3330'); - }); - -
- */ - -var DECIMAL_SEP = '.'; - -angularFilter.number = function(number, fractionSize) { - var formats = this.$service('$locale').NUMBER_FORMATS; - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, - formats.DECIMAL_SEP, fractionSize); -}; - -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isNaN(number) || !isFinite(number)) return ''; - - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; - - if (numStr.indexOf('e') !== -1) { - formatedText = numStr; - } else { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; - - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } - - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; - - var pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; - - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (var i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - } - - for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } - - // format fraction part. - while(fraction.length < fractionSize) { - fraction += '0'; - } - - if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize); - } - - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); -} - -function padNumber(num, digits, trim) { - var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; - } - num = '' + num; - while(num.length < digits) num = '0' + num; - if (trim) - num = num.substr(num.length - digits); - return neg + num; -} - - -function dateGetter(name, size, offset, trim) { - return function(date) { - var value = date['get' + name](); - if (offset > 0 || value > -offset) - value += offset; - if (value === 0 && offset == -12 ) value = 12; - return padNumber(value, size, trim); - }; -} - -function dateStrGetter(name, shortForm) { - return function(date, formats) { - var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); - - return formats[get][value]; - }; -} - -function timeZoneGetter(date) { - var offset = date.getTimezoneOffset(); - return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); -} - -function ampmGetter(date, formats) { - return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; -} - -var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), - MMMM: dateStrGetter('Month'), - MMM: dateStrGetter('Month', true), - MM: dateGetter('Month', 2, 1), - M: dateGetter('Month', 1, 1), - dd: dateGetter('Date', 2), - d: dateGetter('Date', 1), - HH: dateGetter('Hours', 2), - H: dateGetter('Hours', 1), - hh: dateGetter('Hours', 2, -12), - h: dateGetter('Hours', 1, -12), - mm: dateGetter('Minutes', 2), - m: dateGetter('Minutes', 1), - ss: dateGetter('Seconds', 2), - s: dateGetter('Seconds', 1), - EEEE: dateStrGetter('Day'), - EEE: dateStrGetter('Day', true), - a: ampmGetter, - Z: timeZoneGetter -}; - -var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/, - DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, - OPERA_TOSTRING_PATTERN = /^[\d].*Z$/, - NUMBER_STRING = /^\d+$/; - -/** - * @ngdoc filter - * @name angular.filter.date - * @function - * - * @description - * Formats `date` to a string based on the requested `format`. - * - * `format` string can be composed of the following elements: - * - * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) - * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) - * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) - * * `'MMMM'`: Month in year (January-December) - * * `'MMM'`: Month in year (Jan-Dec) - * * `'MM'`: Month in year, padded (01-12) - * * `'M'`: Month in year (1-12) - * * `'dd'`: Day in month, padded (01-31) - * * `'d'`: Day in month (1-31) - * * `'EEEE'`: Day in Week,(Sunday-Saturday) - * * `'EEE'`: Day in Week, (Sun-Sat) - * * `'HH'`: Hour in day, padded (00-23) - * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) - * * `'mm'`: Minute in hour, padded (00-59) - * * `'m'`: Minute in hour (0-59) - * * `'ss'`: Second in minute, padded (00-59) - * * `'s'`: Second in minute (0-59) - * * `'a'`: am/pm marker - * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200) - * - * `format` string can also be one of the following predefined - * {@link guide/dev_guide.i18n localizable formats}: - * - * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale - * (e.g. Friday, September 3, 2010) - * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 - * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) - * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) - * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence - * (e.g. `"h o''clock"`). - * - * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ). - * @param {string=} format Formatting rules (see Description). If not specified, - * `mediumDate` is used. - * @returns {string} Formatted string or the input if input is not recognized as date/millis. - * - * @example - - - {{1288323623006 | date:'medium'}}: - {{1288323623006 | date:'medium'}}
- {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: - {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
- {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: - {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
- - it('should format date', function() { - expect(binding("1288323623006 | date:'medium'")). - toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); - expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/); - expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). - toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); - }); - -
- */ -angularFilter.date = function(date, format) { - var $locale = this.$service('$locale'), - text = '', - parts = [], - fn, match; - - format = format || 'mediumDate' - format = $locale.DATETIME_FORMATS[format] || format; - if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = parseInt(date, 10); - } else { - date = angularString.toDate(date); - } - } - - if (isNumber(date)) { - date = new Date(date); - } - - if (!isDate(date)) { - return date; - } - - while(format) { - match = DATE_FORMATS_SPLIT.exec(format); - if (match) { - parts = concat(parts, match, 1); - format = parts.pop(); - } else { - parts.push(format); - format = null; - } - } - - forEach(parts, function(value){ - fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); - }); - - return text; -}; - - -/** - * @ngdoc filter - * @name angular.filter.json - * @function - * - * @description - * Allows you to convert a JavaScript object into JSON string. - * - * This filter is mostly useful for debugging. When using the double curly {{value}} notation - * the binding is automatically converted to JSON. - * - * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. - * @returns {string} JSON string. - * - * @css ng-monospace Always applied to the encapsulating element. - * - * @example: - - -
{{ {'name':'value'} | json }}
-
- - it('should jsonify filtered objects', function() { - expect(binding('| json')).toBe('{\n "name":"value"}'); - }); - -
- * - */ -angularFilter.json = function(object) { - this.$element.addClass("ng-monospace"); - return toJson(object, true, /^(\$|this$)/); -}; - - -/** - * @ngdoc filter - * @name angular.filter.lowercase - * @function - * - * @see angular.lowercase - */ -angularFilter.lowercase = lowercase; - - -/** - * @ngdoc filter - * @name angular.filter.uppercase - * @function - * - * @see angular.uppercase - */ -angularFilter.uppercase = uppercase; - - -/** - * @ngdoc filter - * @name angular.filter.html - * @function - * - * @description - * Prevents the input from getting escaped by angular. By default the input is sanitized and - * inserted into the DOM as is. - * - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. - * - * If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses - * the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this - * option is strongly discouraged and should be used only if you absolutely trust the input being - * filtered and you can't get the content through the sanitizer. - * - * @param {string} html Html input. - * @param {string=} option If 'unsafe' then do not sanitize the HTML input. - * @returns {string} Sanitized or raw html. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
html filter -
<div ng:bind="snippet | html">
</div>
-
-
-
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
-
-
- - it('should sanitize the html snippet ', function() { - expect(using('#html-filter').binding('snippet | html')). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should escape snippet without any filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should inline raw snippet if filtered as unsafe', function() { - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should update', function() { - input('snippet').enter('new text'); - expect(using('#html-filter').binding('snippet | html')).toBe('new text'); - expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); - expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); - }); -
-
- */ -angularFilter.html = function(html, option){ - return new HTML(html, option); -}; - - -/** - * @ngdoc filter - * @name angular.filter.linky - * @function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * @param {string} text Input text. - * @returns {string} Html-linkified text. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng:bind="snippet | linky">
</div>
-
-
-
no filter
<div ng:bind="snippet">
</div>
- - - it('should linkify the snippet with urls', function() { - expect(using('#linky-filter').binding('snippet | linky')). - toBe('Pretty text with some links:\n' + - 'http://angularjs.org/,\n' + - 'us@somewhere.org,\n' + - 'another@somewhere.org,\n' + - 'and one more: ftp://127.0.0.1/.'); - }); - - it ('should not linkify snippet without the linky filter', function() { - expect(using('#escaped-html').binding('snippet')). - toBe("Pretty text with some links:\n" + - "http://angularjs.org/,\n" + - "mailto:us@somewhere.org,\n" + - "another@somewhere.org,\n" + - "and one more: ftp://127.0.0.1/."); - }); - - it('should update', function() { - input('snippet').enter('new http://link.'); - expect(using('#linky-filter').binding('snippet | linky')). - toBe('new http://link.'); - expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); - }); - - - */ -var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, - MAILTO_REGEXP = /^mailto:/; - -angularFilter.linky = function(text) { - if (!text) return text; - var match; - var raw = text; - var html = []; - var writer = htmlSanitizeWriter(html); - var url; - var i; - while ((match = raw.match(LINKY_URL_REGEXP))) { - // We can not end in these as they are sometimes found at the end of the sentence - url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; - i = match.index; - writer.chars(raw.substr(0, i)); - writer.start('a', {href:url}); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); - raw = raw.substring(i + match[0].length); - } - writer.chars(raw); - return new HTML(html.join('')); -}; diff --git a/src/service/filter/filters.js b/src/service/filter/filters.js new file mode 100644 index 00000000..b9c3d2ef --- /dev/null +++ b/src/service/filter/filters.js @@ -0,0 +1,647 @@ +'use strict'; + +/** + * @ngdoc overview + * @name angular.filter + * @description + * + * Filters are used for formatting data displayed to the user. + * + * The general syntax in templates is as follows: + * + * {{ expression | [ filter_name ] }} + * + * Following is the list of built-in angular filters: + * + * * {@link angular.filter.currency currency} + * * {@link angular.filter.date date} + * * {@link angular.filter.html html} + * * {@link angular.filter.json json} + * * {@link angular.filter.linky linky} + * * {@link angular.filter.lowercase lowercase} + * * {@link angular.filter.number number} + * * {@link angular.filter.uppercase uppercase} + * + * For more information about how angular filters work, and how to create your own filters, see + * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer + * Guide. + */ + +/** + * @ngdoc filter + * @name angular.filter.currency + * @function + * + * @description + * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default + * symbol for current locale is used. + * + * @param {number} amount Input to filter. + * @param {string=} symbol Currency symbol or identifier to be displayed. + * @returns {string} Formatted number. + * + * @css ng-format-negative + * When the value is negative, this css class is applied to the binding making it (by default) red. + * + * @example + + + +
+
+ default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}} +
+
+ + it('should init with 1234.56', function() { + expect(binding('amount | currency')).toBe('$1,234.56'); + expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56'); + }); + it('should update', function() { + input('amount').enter('-1234'); + expect(binding('amount | currency')).toBe('($1,234.00)'); + expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)'); + expect(element('.doc-example-live .ng-binding').prop('className')). + toMatch(/ng-format-negative/); + }); + +
+ */ +angularFilter.currency = function(amount, currencySymbol){ + var formats = this.$service('$locale').NUMBER_FORMATS; + this.$element.toggleClass('ng-format-negative', amount < 0); + if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; + return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). + replace(/\u00A4/g, currencySymbol); +}; + +/** + * @ngdoc filter + * @name angular.filter.number + * @function + * + * @description + * Formats a number as text. + * + * If the input is not a number an empty string is returned. + * + * @param {number|string} number Number to format. + * @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to. + * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * + * @example + + + +
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}} +
+
+ + it('should format numbers', function() { + expect(binding('val | number')).toBe('1,234.568'); + expect(binding('val | number:0')).toBe('1,235'); + expect(binding('-val | number:4')).toBe('-1,234.5679'); + }); + + it('should update', function() { + input('val').enter('3374.333'); + expect(binding('val | number')).toBe('3,374.333'); + expect(binding('val | number:0')).toBe('3,374'); + expect(binding('-val | number:4')).toBe('-3,374.3330'); + }); + +
+ */ + +var DECIMAL_SEP = '.'; + +angularFilter.number = function(number, fractionSize) { + var formats = this.$service('$locale').NUMBER_FORMATS; + return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, + formats.DECIMAL_SEP, fractionSize); +}; + +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + if (isNaN(number) || !isFinite(number)) return ''; + + var isNegative = number < 0; + number = Math.abs(number); + var numStr = number + '', + formatedText = '', + parts = []; + + if (numStr.indexOf('e') !== -1) { + formatedText = numStr; + } else { + var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + + // determine fractionSize if it is not specified + if (isUndefined(fractionSize)) { + fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); + } + + var pow = Math.pow(10, fractionSize); + number = Math.round(number * pow) / pow; + var fraction = ('' + number).split(DECIMAL_SEP); + var whole = fraction[0]; + fraction = fraction[1] || ''; + + var pos = 0, + lgroup = pattern.lgSize, + group = pattern.gSize; + + if (whole.length >= (lgroup + group)) { + pos = whole.length - lgroup; + for (var i = 0; i < pos; i++) { + if ((pos - i)%group === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } + } + + for (i = pos; i < whole.length; i++) { + if ((whole.length - i)%lgroup === 0 && i !== 0) { + formatedText += groupSep; + } + formatedText += whole.charAt(i); + } + + // format fraction part. + while(fraction.length < fractionSize) { + fraction += '0'; + } + + if (fractionSize) formatedText += decimalSep + fraction.substr(0, fractionSize); + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre); + parts.push(formatedText); + parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + return parts.join(''); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while(num.length < digits) num = '0' + num; + if (trim) + num = num.substr(num.length - digits); + return neg + num; +} + + +function dateGetter(name, size, offset, trim) { + return function(date) { + var value = date['get' + name](); + if (offset > 0 || value > -offset) + value += offset; + if (value === 0 && offset == -12 ) value = 12; + return padNumber(value, size, trim); + }; +} + +function dateStrGetter(name, shortForm) { + return function(date, formats) { + var value = date['get' + name](); + var get = uppercase(shortForm ? ('SHORT' + name) : name); + + return formats[get][value]; + }; +} + +function timeZoneGetter(date) { + var offset = date.getTimezoneOffset(); + return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2); +} + +function ampmGetter(date, formats) { + return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; +} + +var DATE_FORMATS = { + yyyy: dateGetter('FullYear', 4), + yy: dateGetter('FullYear', 2, 0, true), + y: dateGetter('FullYear', 1), + MMMM: dateStrGetter('Month'), + MMM: dateStrGetter('Month', true), + MM: dateGetter('Month', 2, 1), + M: dateGetter('Month', 1, 1), + dd: dateGetter('Date', 2), + d: dateGetter('Date', 1), + HH: dateGetter('Hours', 2), + H: dateGetter('Hours', 1), + hh: dateGetter('Hours', 2, -12), + h: dateGetter('Hours', 1, -12), + mm: dateGetter('Minutes', 2), + m: dateGetter('Minutes', 1), + ss: dateGetter('Seconds', 2), + s: dateGetter('Seconds', 1), + EEEE: dateStrGetter('Day'), + EEE: dateStrGetter('Day', true), + a: ampmGetter, + Z: timeZoneGetter +}; + +var GET_TIME_ZONE = /[A-Z]{3}(?![+\-])/, + DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, + OPERA_TOSTRING_PATTERN = /^[\d].*Z$/, + NUMBER_STRING = /^\d+$/; + +/** + * @ngdoc filter + * @name angular.filter.date + * @function + * + * @description + * Formats `date` to a string based on the requested `format`. + * + * `format` string can be composed of the following elements: + * + * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010) + * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10) + * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199) + * * `'MMMM'`: Month in year (January-December) + * * `'MMM'`: Month in year (Jan-Dec) + * * `'MM'`: Month in year, padded (01-12) + * * `'M'`: Month in year (1-12) + * * `'dd'`: Day in month, padded (01-31) + * * `'d'`: Day in month (1-31) + * * `'EEEE'`: Day in Week,(Sunday-Saturday) + * * `'EEE'`: Day in Week, (Sun-Sat) + * * `'HH'`: Hour in day, padded (00-23) + * * `'H'`: Hour in day (0-23) + * * `'hh'`: Hour in am/pm, padded (01-12) + * * `'h'`: Hour in am/pm, (1-12) + * * `'mm'`: Minute in hour, padded (00-59) + * * `'m'`: Minute in hour (0-59) + * * `'ss'`: Second in minute, padded (00-59) + * * `'s'`: Second in minute (0-59) + * * `'a'`: am/pm marker + * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-1200) + * + * `format` string can also be one of the following predefined + * {@link guide/dev_guide.i18n localizable formats}: + * + * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 pm) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Friday, September 3, 2010) + * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010 + * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) + * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * + * `format` string can contain literal values. These need to be quoted with single quotes (e.g. + * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * (e.g. `"h o''clock"`). + * + * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or + * number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ). + * @param {string=} format Formatting rules (see Description). If not specified, + * `mediumDate` is used. + * @returns {string} Formatted string or the input if input is not recognized as date/millis. + * + * @example + + + {{1288323623006 | date:'medium'}}: + {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}: + {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}: + {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+
+ + it('should format date', function() { + expect(binding("1288323623006 | date:'medium'")). + toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); + expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")). + toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/); + expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")). + toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + }); + +
+ */ +angularFilter.date = function(date, format) { + var $locale = this.$service('$locale'), + text = '', + parts = [], + fn, match; + + format = format || 'mediumDate' + format = $locale.DATETIME_FORMATS[format] || format; + if (isString(date)) { + if (NUMBER_STRING.test(date)) { + date = parseInt(date, 10); + } else { + date = angularString.toDate(date); + } + } + + if (isNumber(date)) { + date = new Date(date); + } + + if (!isDate(date)) { + return date; + } + + while(format) { + match = DATE_FORMATS_SPLIT.exec(format); + if (match) { + parts = concat(parts, match, 1); + format = parts.pop(); + } else { + parts.push(format); + format = null; + } + } + + forEach(parts, function(value){ + fn = DATE_FORMATS[value]; + text += fn ? fn(date, $locale.DATETIME_FORMATS) + : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + }); + + return text; +}; + + +/** + * @ngdoc filter + * @name angular.filter.json + * @function + * + * @description + * Allows you to convert a JavaScript object into JSON string. + * + * This filter is mostly useful for debugging. When using the double curly {{value}} notation + * the binding is automatically converted to JSON. + * + * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @returns {string} JSON string. + * + * @css ng-monospace Always applied to the encapsulating element. + * + * @example: + + +
{{ {'name':'value'} | json }}
+
+ + it('should jsonify filtered objects', function() { + expect(binding('| json')).toBe('{\n "name":"value"}'); + }); + +
+ * + */ +angularFilter.json = function(object) { + this.$element.addClass("ng-monospace"); + return toJson(object, true, /^(\$|this$)/); +}; + + +/** + * @ngdoc filter + * @name angular.filter.lowercase + * @function + * + * @see angular.lowercase + */ +angularFilter.lowercase = lowercase; + + +/** + * @ngdoc filter + * @name angular.filter.uppercase + * @function + * + * @see angular.uppercase + */ +angularFilter.uppercase = uppercase; + + +/** + * @ngdoc filter + * @name angular.filter.html + * @function + * + * @description + * Prevents the input from getting escaped by angular. By default the input is sanitized and + * inserted into the DOM as is. + * + * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string, however, since our parser is more strict than a typical browser + * parser, it's possible that some obscure input, which would be recognized as valid HTML by a + * browser, won't make it through the sanitizer. + * + * If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses + * the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this + * option is strongly discouraged and should be used only if you absolutely trust the input being + * filtered and you can't get the content through the sanitizer. + * + * @param {string} html Html input. + * @param {string=} option If 'unsafe' then do not sanitize the HTML input. + * @returns {string} Sanitized or raw html. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
html filter +
<div ng:bind="snippet | html">
</div>
+
+
+
no filter
<div ng:bind="snippet">
</div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'">
</div>
+
+
+ + it('should sanitize the html snippet ', function() { + expect(using('#html-filter').binding('snippet | html')). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should escape snippet without any filter', function() { + expect(using('#escaped-html').binding('snippet')). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should inline raw snippet if filtered as unsafe', function() { + expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should update', function() { + input('snippet').enter('new text'); + expect(using('#html-filter').binding('snippet | html')).toBe('new text'); + expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>"); + expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text'); + }); +
+
+ */ +angularFilter.html = function(html, option){ + return new HTML(html, option); +}; + + +/** + * @ngdoc filter + * @name angular.filter.linky + * @function + * + * @description + * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and + * plain email address links. + * + * @param {string} text Input text. + * @returns {string} Html-linkified text. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng:bind="snippet | linky">
</div>
+
+
+
no filter
<div ng:bind="snippet">
</div>
+ + + it('should linkify the snippet with urls', function() { + expect(using('#linky-filter').binding('snippet | linky')). + toBe('Pretty text with some links:\n' + + 'http://angularjs.org/,\n' + + 'us@somewhere.org,\n' + + 'another@somewhere.org,\n' + + 'and one more: ftp://127.0.0.1/.'); + }); + + it ('should not linkify snippet without the linky filter', function() { + expect(using('#escaped-html').binding('snippet')). + toBe("Pretty text with some links:\n" + + "http://angularjs.org/,\n" + + "mailto:us@somewhere.org,\n" + + "another@somewhere.org,\n" + + "and one more: ftp://127.0.0.1/."); + }); + + it('should update', function() { + input('snippet').enter('new http://link.'); + expect(using('#linky-filter').binding('snippet | linky')). + toBe('new http://link.'); + expect(using('#escaped-html').binding('snippet')).toBe('new http://link.'); + }); + + + */ +var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/, + MAILTO_REGEXP = /^mailto:/; + +angularFilter.linky = function(text) { + if (!text) return text; + var match; + var raw = text; + var html = []; + var writer = htmlSanitizeWriter(html); + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/mailto then assume mailto + if (match[2] == match[3]) url = 'mailto:' + url; + i = match.index; + writer.chars(raw.substr(0, i)); + writer.start('a', {href:url}); + writer.chars(match[0].replace(MAILTO_REGEXP, '')); + writer.end('a'); + raw = raw.substring(i + match[0].length); + } + writer.chars(raw); + return new HTML(html.join('')); +}; diff --git a/test/FiltersSpec.js b/test/FiltersSpec.js deleted file mode 100644 index 8c567441..00000000 --- a/test/FiltersSpec.js +++ /dev/null @@ -1,300 +0,0 @@ -'use strict'; - -describe('filter', function() { - - var filter = angular.filter; - - it('should called the filter when evaluating expression', inject(function($rootScope) { - filter.fakeFilter = function() {}; - spyOn(filter, 'fakeFilter'); - - $rootScope.$eval('10|fakeFilter'); - expect(filter.fakeFilter).toHaveBeenCalledWith(10); - delete filter['fakeFilter']; - })); - - it('should call filter on scope context', inject(function($rootScope) { - $rootScope.name = 'misko'; - filter.fakeFilter = function() { - expect(this.name).toEqual('misko'); - }; - spyOn(filter, 'fakeFilter').andCallThrough(); - - $rootScope.$eval('10|fakeFilter'); - expect(filter.fakeFilter).toHaveBeenCalled(); - delete filter['fakeFilter']; - })); - - describe('formatNumber', function() { - var pattern; - - beforeEach(function() { - pattern = { minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 }; - }); - - it('should format according to different patterns', function() { - pattern.gSize = 2; - var num = formatNumber(1234567.89, pattern, ',', '.'); - expect(num).toBe('12,34,567.89'); - num = formatNumber(1234.56, pattern, ',', '.'); - expect(num).toBe('1,234.56'); - - pattern.negPre = '('; - pattern.negSuf = '-)'; - num = formatNumber(-1234, pattern, ',', '.'); - expect(num).toBe('(1,234-)'); - pattern.posPre = '+'; - pattern.posSuf = '+'; - num = formatNumber(1234, pattern, ',', '.'); - expect(num).toBe('+1,234+'); - pattern.posPre = pattern.posSuf = ''; - - pattern.minFrac = 2; - num = formatNumber(1, pattern, ',', '.'); - expect(num).toBe('1.00'); - pattern.maxFrac = 4; - num = formatNumber(1.11119, pattern, ',', '.'); - expect(num).toBe('1.1112'); - }); - - it('should format according different seperators', function() { - var num = formatNumber(1234567.1, pattern, '.', ',', 2); - expect(num).toBe('1.234.567,10'); - }); - - it('should format with or without fractionSize', function() { - var num = formatNumber(123.1, pattern, ',', '.', 3); - expect(num).toBe('123.100'); - num = formatNumber(123.12, pattern, ',', '.'); - expect(num).toBe('123.12'); - var num = formatNumber(123.1116, pattern, ',', '.'); - expect(num).toBe('123.112'); - }); - }); - - describe('currency', function() { - var currency, html, context; - - beforeEach(inject(function($rootScope) { - html = jqLite(''); - context = $rootScope; - context.$element = html; - currency = bind(context, filter.currency); - })); - - it('should do basic currency filtering', function() { - expect(currency(0)).toEqual('$0.00'); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); - expect(currency(-999)).toEqual('($999.00)'); - expect(html.hasClass('ng-format-negative')).toBeTruthy(); - expect(currency(1234.5678, "USD$")).toEqual('USD$1,234.57'); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); - }); - - - it('should return empty string for non-numbers', function() { - expect(currency()).toBe(''); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); - expect(currency('abc')).toBe(''); - expect(html.hasClass('ng-format-negative')).toBeFalsy(); - }); - }); - - - describe('number', function() { - var context, number; - - beforeEach(inject(function($rootScope) { - context = $rootScope; - number = bind(context, filter.number); - })); - - - it('should do basic filter', function() { - expect(number(0, 0)).toEqual('0'); - expect(number(-999)).toEqual('-999'); - expect(number(123)).toEqual('123'); - expect(number(1234567)).toEqual('1,234,567'); - expect(number(1234)).toEqual('1,234'); - expect(number(1234.5678)).toEqual('1,234.568'); - expect(number(Number.NaN)).toEqual(''); - expect(number("1234.5678")).toEqual('1,234.568'); - expect(number(1/0)).toEqual(""); - expect(number(1, 2)).toEqual("1.00"); - expect(number(.1, 2)).toEqual("0.10"); - expect(number(.01, 2)).toEqual("0.01"); - expect(number(.001, 3)).toEqual("0.001"); - expect(number(.0001, 3)).toEqual("0.000"); - expect(number(9, 2)).toEqual("9.00"); - expect(number(.9, 2)).toEqual("0.90"); - expect(number(.99, 2)).toEqual("0.99"); - expect(number(.999, 3)).toEqual("0.999"); - expect(number(.9999, 3)).toEqual("1.000"); - expect(number(1234.567, 0)).toEqual("1,235"); - expect(number(1234.567, 1)).toEqual("1,234.6"); - expect(number(1234.567, 2)).toEqual("1,234.57"); - }); - - it('should filter exponential numbers', function() { - expect(number(1e50, 0)).toEqual('1e+50'); - expect(number(-2e50, 2)).toEqual('-2e+50'); - }); - }); - - describe('json', function () { - it('should do basic filter', function() { - expect(filter.json.call({$element:jqLite('
')}, {a:"b"})).toEqual(toJson({a:"b"}, true)); - }); - }); - - describe('lowercase', function() { - it('should do basic filter', function() { - expect(filter.lowercase('AbC')).toEqual('abc'); - expect(filter.lowercase(null)).toBeNull(); - }); - }); - - describe('uppercase', function() { - it('should do basic filter', function() { - expect(filter.uppercase('AbC')).toEqual('ABC'); - expect(filter.uppercase(null)).toBeNull(); - }); - }); - - describe('html', function() { - it('should do basic filter', function() { - var html = filter.html("acd"); - expect(html instanceof HTML).toBeTruthy(); - expect(html.html).toEqual("acd"); - }); - }); - - describe('linky', function() { - var linky = filter.linky; - it('should do basic filter', function() { - expect(linky("http://ab/ (http://a/) http://1.2/v:~-123. c").html). - toEqual('http://ab/ ' + - '(http://a/) ' + - '<http://a/> ' + - 'http://1.2/v:~-123. c'); - expect(linky(undefined)).not.toBeDefined(); - }); - - it('should handle mailto:', function() { - expect(linky("mailto:me@example.com").html). - toEqual('me@example.com'); - expect(linky("me@example.com").html). - toEqual('me@example.com'); - expect(linky("send email to me@example.com, but").html). - toEqual('send email to me@example.com, but'); - }); - }); - - describe('date', function() { - - var morning = new angular.mock.TzDate(+5, '2010-09-03T12:05:08.000Z'); //7am - var noon = new angular.mock.TzDate(+5, '2010-09-03T17:05:08.000Z'); //12pm - var midnight = new angular.mock.TzDate(+5, '2010-09-03T05:05:08.000Z'); //12am - var earlyDate = new angular.mock.TzDate(+5, '0001-09-03T05:05:08.000Z'); - - var context, date; - - beforeEach(inject(function($rootScope) { - context = $rootScope; - date = bind(context, filter.date); - })); - - it('should ignore falsy inputs', function() { - expect(date(null)).toBeNull(); - expect(date('')).toEqual(''); - }); - - it('should do basic filter', function() { - expect(date(noon)).toEqual(date(noon, 'mediumDate')); - expect(date(noon, '')).toEqual(date(noon, 'mediumDate')); - }); - - it('should accept number or number string representing milliseconds as input', function() { - expect(date(noon.getTime())).toEqual(date(noon.getTime(), 'mediumDate')); - expect(date(noon.getTime() + "")).toEqual(date(noon.getTime() + "", 'mediumDate')); - }); - - it('should accept various format strings', function() { - expect(date(morning, "yy-MM-dd HH:mm:ss")). - toEqual('10-09-03 07:05:08'); - - expect(date(midnight, "yyyy-M-d h=H:m:saZ")). - toEqual('2010-9-3 12=0:5:8AM0500'); - - expect(date(midnight, "yyyy-MM-dd hh=HH:mm:ssaZ")). - toEqual('2010-09-03 12=00:05:08AM0500'); - - expect(date(noon, "yyyy-MM-dd hh=HH:mm:ssaZ")). - toEqual('2010-09-03 12=12:05:08PM0500'); - - expect(date(noon, "EEE, MMM d, yyyy")). - toEqual('Fri, Sep 3, 2010'); - - expect(date(noon, "EEEE, MMMM dd, yyyy")). - toEqual('Friday, September 03, 2010'); - - expect(date(earlyDate, "MMMM dd, y")). - toEqual('September 03, 1'); - }); - - it('should treat single quoted strings as string literals', function() { - expect(date(midnight, "yyyy'de' 'a'x'dd' 'adZ' h=H:m:saZ")). - toEqual('2010de axdd adZ 12=0:5:8AM0500'); - }); - - it('should treat a sequence of two single quotes as a literal single quote', function() { - expect(date(midnight, "yyyy'de' 'a''dd' 'adZ' h=H:m:saZ")). - toEqual("2010de a'dd adZ 12=0:5:8AM0500"); - }); - - it('should accept default formats', function() { - - expect(date(noon, "medium")). - toEqual('Sep 3, 2010 12:05:08 PM'); - - expect(date(noon, "short")). - toEqual('9/3/10 12:05 PM'); - - expect(date(noon, "fullDate")). - toEqual('Friday, September 3, 2010'); - - expect(date(noon, "longDate")). - toEqual('September 3, 2010'); - - expect(date(noon, "mediumDate")). - toEqual('Sep 3, 2010'); - - expect(date(noon, "shortDate")). - toEqual('9/3/10'); - - expect(date(noon, "mediumTime")). - toEqual('12:05:08 PM'); - - expect(date(noon, "shortTime")). - toEqual('12:05 PM'); - }); - - it('should be able to parse ISO 8601 dates/times using', function() { - var isoString = '2010-09-03T05:05:08.872Z'; - expect(date(isoString)). - toEqual(date(isoString, 'mediumDate')); - }); - - it('should parse format ending with non-replaced string', function() { - expect(date(morning, 'yy/xxx')).toEqual('10/xxx'); - }); - }); -}); diff --git a/test/service/filter/filtersSpec.js b/test/service/filter/filtersSpec.js new file mode 100644 index 00000000..8c567441 --- /dev/null +++ b/test/service/filter/filtersSpec.js @@ -0,0 +1,300 @@ +'use strict'; + +describe('filter', function() { + + var filter = angular.filter; + + it('should called the filter when evaluating expression', inject(function($rootScope) { + filter.fakeFilter = function() {}; + spyOn(filter, 'fakeFilter'); + + $rootScope.$eval('10|fakeFilter'); + expect(filter.fakeFilter).toHaveBeenCalledWith(10); + delete filter['fakeFilter']; + })); + + it('should call filter on scope context', inject(function($rootScope) { + $rootScope.name = 'misko'; + filter.fakeFilter = function() { + expect(this.name).toEqual('misko'); + }; + spyOn(filter, 'fakeFilter').andCallThrough(); + + $rootScope.$eval('10|fakeFilter'); + expect(filter.fakeFilter).toHaveBeenCalled(); + delete filter['fakeFilter']; + })); + + describe('formatNumber', function() { + var pattern; + + beforeEach(function() { + pattern = { minInt: 1, + minFrac: 0, + maxFrac: 3, + posPre: '', + posSuf: '', + negPre: '-', + negSuf: '', + gSize: 3, + lgSize: 3 }; + }); + + it('should format according to different patterns', function() { + pattern.gSize = 2; + var num = formatNumber(1234567.89, pattern, ',', '.'); + expect(num).toBe('12,34,567.89'); + num = formatNumber(1234.56, pattern, ',', '.'); + expect(num).toBe('1,234.56'); + + pattern.negPre = '('; + pattern.negSuf = '-)'; + num = formatNumber(-1234, pattern, ',', '.'); + expect(num).toBe('(1,234-)'); + pattern.posPre = '+'; + pattern.posSuf = '+'; + num = formatNumber(1234, pattern, ',', '.'); + expect(num).toBe('+1,234+'); + pattern.posPre = pattern.posSuf = ''; + + pattern.minFrac = 2; + num = formatNumber(1, pattern, ',', '.'); + expect(num).toBe('1.00'); + pattern.maxFrac = 4; + num = formatNumber(1.11119, pattern, ',', '.'); + expect(num).toBe('1.1112'); + }); + + it('should format according different seperators', function() { + var num = formatNumber(1234567.1, pattern, '.', ',', 2); + expect(num).toBe('1.234.567,10'); + }); + + it('should format with or without fractionSize', function() { + var num = formatNumber(123.1, pattern, ',', '.', 3); + expect(num).toBe('123.100'); + num = formatNumber(123.12, pattern, ',', '.'); + expect(num).toBe('123.12'); + var num = formatNumber(123.1116, pattern, ',', '.'); + expect(num).toBe('123.112'); + }); + }); + + describe('currency', function() { + var currency, html, context; + + beforeEach(inject(function($rootScope) { + html = jqLite(''); + context = $rootScope; + context.$element = html; + currency = bind(context, filter.currency); + })); + + it('should do basic currency filtering', function() { + expect(currency(0)).toEqual('$0.00'); + expect(html.hasClass('ng-format-negative')).toBeFalsy(); + expect(currency(-999)).toEqual('($999.00)'); + expect(html.hasClass('ng-format-negative')).toBeTruthy(); + expect(currency(1234.5678, "USD$")).toEqual('USD$1,234.57'); + expect(html.hasClass('ng-format-negative')).toBeFalsy(); + }); + + + it('should return empty string for non-numbers', function() { + expect(currency()).toBe(''); + expect(html.hasClass('ng-format-negative')).toBeFalsy(); + expect(currency('abc')).toBe(''); + expect(html.hasClass('ng-format-negative')).toBeFalsy(); + }); + }); + + + describe('number', function() { + var context, number; + + beforeEach(inject(function($rootScope) { + context = $rootScope; + number = bind(context, filter.number); + })); + + + it('should do basic filter', function() { + expect(number(0, 0)).toEqual('0'); + expect(number(-999)).toEqual('-999'); + expect(number(123)).toEqual('123'); + expect(number(1234567)).toEqual('1,234,567'); + expect(number(1234)).toEqual('1,234'); + expect(number(1234.5678)).toEqual('1,234.568'); + expect(number(Number.NaN)).toEqual(''); + expect(number("1234.5678")).toEqual('1,234.568'); + expect(number(1/0)).toEqual(""); + expect(number(1, 2)).toEqual("1.00"); + expect(number(.1, 2)).toEqual("0.10"); + expect(number(.01, 2)).toEqual("0.01"); + expect(number(.001, 3)).toEqual("0.001"); + expect(number(.0001, 3)).toEqual("0.000"); + expect(number(9, 2)).toEqual("9.00"); + expect(number(.9, 2)).toEqual("0.90"); + expect(number(.99, 2)).toEqual("0.99"); + expect(number(.999, 3)).toEqual("0.999"); + expect(number(.9999, 3)).toEqual("1.000"); + expect(number(1234.567, 0)).toEqual("1,235"); + expect(number(1234.567, 1)).toEqual("1,234.6"); + expect(number(1234.567, 2)).toEqual("1,234.57"); + }); + + it('should filter exponential numbers', function() { + expect(number(1e50, 0)).toEqual('1e+50'); + expect(number(-2e50, 2)).toEqual('-2e+50'); + }); + }); + + describe('json', function () { + it('should do basic filter', function() { + expect(filter.json.call({$element:jqLite('
')}, {a:"b"})).toEqual(toJson({a:"b"}, true)); + }); + }); + + describe('lowercase', function() { + it('should do basic filter', function() { + expect(filter.lowercase('AbC')).toEqual('abc'); + expect(filter.lowercase(null)).toBeNull(); + }); + }); + + describe('uppercase', function() { + it('should do basic filter', function() { + expect(filter.uppercase('AbC')).toEqual('ABC'); + expect(filter.uppercase(null)).toBeNull(); + }); + }); + + describe('html', function() { + it('should do basic filter', function() { + var html = filter.html("acd"); + expect(html instanceof HTML).toBeTruthy(); + expect(html.html).toEqual("acd"); + }); + }); + + describe('linky', function() { + var linky = filter.linky; + it('should do basic filter', function() { + expect(linky("http://ab/ (http://a/) http://1.2/v:~-123. c").html). + toEqual('http://ab/ ' + + '(http://a/) ' + + '<http://a/> ' + + 'http://1.2/v:~-123. c'); + expect(linky(undefined)).not.toBeDefined(); + }); + + it('should handle mailto:', function() { + expect(linky("mailto:me@example.com").html). + toEqual('me@example.com'); + expect(linky("me@example.com").html). + toEqual('me@example.com'); + expect(linky("send email to me@example.com, but").html). + toEqual('send email to me@example.com, but'); + }); + }); + + describe('date', function() { + + var morning = new angular.mock.TzDate(+5, '2010-09-03T12:05:08.000Z'); //7am + var noon = new angular.mock.TzDate(+5, '2010-09-03T17:05:08.000Z'); //12pm + var midnight = new angular.mock.TzDate(+5, '2010-09-03T05:05:08.000Z'); //12am + var earlyDate = new angular.mock.TzDate(+5, '0001-09-03T05:05:08.000Z'); + + var context, date; + + beforeEach(inject(function($rootScope) { + context = $rootScope; + date = bind(context, filter.date); + })); + + it('should ignore falsy inputs', function() { + expect(date(null)).toBeNull(); + expect(date('')).toEqual(''); + }); + + it('should do basic filter', function() { + expect(date(noon)).toEqual(date(noon, 'mediumDate')); + expect(date(noon, '')).toEqual(date(noon, 'mediumDate')); + }); + + it('should accept number or number string representing milliseconds as input', function() { + expect(date(noon.getTime())).toEqual(date(noon.getTime(), 'mediumDate')); + expect(date(noon.getTime() + "")).toEqual(date(noon.getTime() + "", 'mediumDate')); + }); + + it('should accept various format strings', function() { + expect(date(morning, "yy-MM-dd HH:mm:ss")). + toEqual('10-09-03 07:05:08'); + + expect(date(midnight, "yyyy-M-d h=H:m:saZ")). + toEqual('2010-9-3 12=0:5:8AM0500'); + + expect(date(midnight, "yyyy-MM-dd hh=HH:mm:ssaZ")). + toEqual('2010-09-03 12=00:05:08AM0500'); + + expect(date(noon, "yyyy-MM-dd hh=HH:mm:ssaZ")). + toEqual('2010-09-03 12=12:05:08PM0500'); + + expect(date(noon, "EEE, MMM d, yyyy")). + toEqual('Fri, Sep 3, 2010'); + + expect(date(noon, "EEEE, MMMM dd, yyyy")). + toEqual('Friday, September 03, 2010'); + + expect(date(earlyDate, "MMMM dd, y")). + toEqual('September 03, 1'); + }); + + it('should treat single quoted strings as string literals', function() { + expect(date(midnight, "yyyy'de' 'a'x'dd' 'adZ' h=H:m:saZ")). + toEqual('2010de axdd adZ 12=0:5:8AM0500'); + }); + + it('should treat a sequence of two single quotes as a literal single quote', function() { + expect(date(midnight, "yyyy'de' 'a''dd' 'adZ' h=H:m:saZ")). + toEqual("2010de a'dd adZ 12=0:5:8AM0500"); + }); + + it('should accept default formats', function() { + + expect(date(noon, "medium")). + toEqual('Sep 3, 2010 12:05:08 PM'); + + expect(date(noon, "short")). + toEqual('9/3/10 12:05 PM'); + + expect(date(noon, "fullDate")). + toEqual('Friday, September 3, 2010'); + + expect(date(noon, "longDate")). + toEqual('September 3, 2010'); + + expect(date(noon, "mediumDate")). + toEqual('Sep 3, 2010'); + + expect(date(noon, "shortDate")). + toEqual('9/3/10'); + + expect(date(noon, "mediumTime")). + toEqual('12:05:08 PM'); + + expect(date(noon, "shortTime")). + toEqual('12:05 PM'); + }); + + it('should be able to parse ISO 8601 dates/times using', function() { + var isoString = '2010-09-03T05:05:08.872Z'; + expect(date(isoString)). + toEqual(date(isoString, 'mediumDate')); + }); + + it('should parse format ending with non-replaced string', function() { + expect(date(morning, 'yy/xxx')).toEqual('10/xxx'); + }); + }); +}); -- cgit v1.2.3