aboutsummaryrefslogtreecommitdiffstats
path: root/src/service
diff options
context:
space:
mode:
Diffstat (limited to 'src/service')
-rw-r--r--src/service/filter/filters.js647
1 files changed, 647 insertions, 0 deletions
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
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl() {
+ this.amount = 1234.56;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ <input type="number" ng:model="amount"/> <br/>
+ default currency symbol ($): {{amount | currency}}<br/>
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
+ </div>
+ </doc:source>
+ <doc:scenario>
+ 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/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+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
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl() {
+ this.val = 1234.56789;
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Enter number: <input ng:model='val'><br/>
+ Default formatting: {{val | number}}<br/>
+ No fractions: {{val | number:0}}<br/>
+ Negative number: {{-val | number:4}}
+ </div>
+ </doc:source>
+ <doc:scenario>
+ 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');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+
+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
+ <doc:example>
+ <doc:source>
+ <span ng:non-bindable>{{1288323623006 | date:'medium'}}</span>:
+ {{1288323623006 | date:'medium'}}<br/>
+ <span ng:non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>:
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}<br/>
+ <span ng:non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
+ {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}<br/>
+ </doc:source>
+ <doc:scenario>
+ 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)/);
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+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:
+ <doc:example>
+ <doc:source>
+ <pre>{{ {'name':'value'} | json }}</pre>
+ </doc:source>
+ <doc:scenario>
+ it('should jsonify filtered objects', function() {
+ expect(binding('| json')).toBe('{\n "name":"value"}');
+ });
+ </doc:scenario>
+ </doc:example>
+ *
+ */
+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
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl() {
+ this.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Filter</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="html-filter">
+ <td>html filter</td>
+ <td>
+ <pre>&lt;div ng:bind="snippet | html"&gt;<br/>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng:bind="snippet | html"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet"></div></td>
+ </tr>
+ <tr id="html-unsafe-filter">
+ <td>unsafe html filter</td>
+ <td><pre>&lt;div ng:bind="snippet | html:'unsafe'"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet | html:'unsafe'"></div></td>
+ </tr>
+ </table>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should sanitize the html snippet ', function() {
+ expect(using('#html-filter').binding('snippet | html')).
+ toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
+ });
+
+ it('should escape snippet without any filter', function() {
+ expect(using('#escaped-html').binding('snippet')).
+ toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
+ "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
+ "snippet&lt;/p&gt;");
+ });
+
+ it('should inline raw snippet if filtered as unsafe', function() {
+ expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).
+ toBe("<p style=\"color:blue\">an html\n" +
+ "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
+ "snippet</p>");
+ });
+
+ it('should update', function() {
+ input('snippet').enter('new <b>text</b>');
+ expect(using('#html-filter').binding('snippet | html')).toBe('new <b>text</b>');
+ expect(using('#escaped-html').binding('snippet')).toBe("new &lt;b&gt;text&lt;/b&gt;");
+ expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new <b>text</b>');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+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
+ <doc:example>
+ <doc:source>
+ <script>
+ function Ctrl() {
+ this.snippet =
+ '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/.';
+ }
+ </script>
+ <div ng:controller="Ctrl">
+ Snippet: <textarea ng:model="snippet" cols="60" rows="3"></textarea>
+ <table>
+ <tr>
+ <td>Filter</td>
+ <td>Source</td>
+ <td>Rendered</td>
+ </tr>
+ <tr id="linky-filter">
+ <td>linky filter</td>
+ <td>
+ <pre>&lt;div ng:bind="snippet | linky"&gt;<br/>&lt;/div&gt;</pre>
+ </td>
+ <td>
+ <div ng:bind="snippet | linky"></div>
+ </td>
+ </tr>
+ <tr id="escaped-html">
+ <td>no filter</td>
+ <td><pre>&lt;div ng:bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
+ <td><div ng:bind="snippet"></div></td>
+ </tr>
+ </table>
+ </doc:source>
+ <doc:scenario>
+ it('should linkify the snippet with urls', function() {
+ expect(using('#linky-filter').binding('snippet | linky')).
+ toBe('Pretty text with some links:\n' +
+ '<a href="http://angularjs.org/">http://angularjs.org/</a>,\n' +
+ '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,\n' +
+ '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,\n' +
+ 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
+ });
+
+ 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 <a href="http://link">http://link</a>.');
+ expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
+ });
+ </doc:scenario>
+ </doc:example>
+ */
+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(''));
+};