Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
@@ -0,0 +1,499 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides functions to parse and manipulate email addresses.
*
*/
goog.provide('goog.format.EmailAddress');
goog.require('goog.string');
/**
* Formats an email address string for display, and allows for extraction of
* the individual components of the address.
* @param {string=} opt_address The email address.
* @param {string=} opt_name The name associated with the email address.
* @constructor
*/
goog.format.EmailAddress = function(opt_address, opt_name) {
/**
* The name or personal string associated with the address.
* @type {string}
* @private
*/
this.name_ = opt_name || '';
/**
* The email address.
* @type {string}
* @protected
*/
this.address = opt_address || '';
};
/**
* Match string for opening tokens.
* @type {string}
* @private
*/
goog.format.EmailAddress.OPENERS_ = '"<([';
/**
* Match string for closing tokens.
* @type {string}
* @private
*/
goog.format.EmailAddress.CLOSERS_ = '">)]';
/**
* Match string for characters that require display names to be quoted and are
* not address separators.
* @type {string}
* @const
* @package
*/
goog.format.EmailAddress.SPECIAL_CHARS = '()<>@:\\\".[]';
/**
* Match string for address separators.
* @type {string}
* @const
* @private
*/
goog.format.EmailAddress.ADDRESS_SEPARATORS_ = ',;';
/**
* Match string for characters that, when in a display name, require it to be
* quoted.
* @type {string}
* @const
* @private
*/
goog.format.EmailAddress.CHARS_REQUIRE_QUOTES_ =
goog.format.EmailAddress.SPECIAL_CHARS +
goog.format.EmailAddress.ADDRESS_SEPARATORS_;
/**
* A RegExp to match all double quotes. Used in cleanAddress().
* @type {RegExp}
* @private
*/
goog.format.EmailAddress.ALL_DOUBLE_QUOTES_ = /\"/g;
/**
* A RegExp to match escaped double quotes. Used in parse().
* @type {RegExp}
* @private
*/
goog.format.EmailAddress.ESCAPED_DOUBLE_QUOTES_ = /\\\"/g;
/**
* A RegExp to match all backslashes. Used in cleanAddress().
* @type {RegExp}
* @private
*/
goog.format.EmailAddress.ALL_BACKSLASHES_ = /\\/g;
/**
* A RegExp to match escaped backslashes. Used in parse().
* @type {RegExp}
* @private
*/
goog.format.EmailAddress.ESCAPED_BACKSLASHES_ = /\\\\/g;
/**
* A string representing the RegExp for the local part of an email address.
* @private {string}
*/
goog.format.EmailAddress.LOCAL_PART_REGEXP_STR_ =
'[+a-zA-Z0-9_.!#$%&\'*\\/=?^`{|}~-]+';
/**
* A string representing the RegExp for the domain part of an email address.
* @private {string}
*/
goog.format.EmailAddress.DOMAIN_PART_REGEXP_STR_ =
'([a-zA-Z0-9-]+\\.)+[a-zA-Z0-9]{2,63}';
/**
* A RegExp to match the local part of an email address.
* @private {!RegExp}
*/
goog.format.EmailAddress.LOCAL_PART_ =
new RegExp('^' + goog.format.EmailAddress.LOCAL_PART_REGEXP_STR_ + '$');
/**
* A RegExp to match the domain part of an email address.
* @private {!RegExp}
*/
goog.format.EmailAddress.DOMAIN_PART_ =
new RegExp('^' + goog.format.EmailAddress.DOMAIN_PART_REGEXP_STR_ + '$');
/**
* A RegExp to match an email address.
* @private {!RegExp}
*/
goog.format.EmailAddress.EMAIL_ADDRESS_ =
new RegExp('^' + goog.format.EmailAddress.LOCAL_PART_REGEXP_STR_ + '@' +
goog.format.EmailAddress.DOMAIN_PART_REGEXP_STR_ + '$');
/**
* Get the name associated with the email address.
* @return {string} The name or personal portion of the address.
* @final
*/
goog.format.EmailAddress.prototype.getName = function() {
return this.name_;
};
/**
* Get the email address.
* @return {string} The email address.
* @final
*/
goog.format.EmailAddress.prototype.getAddress = function() {
return this.address;
};
/**
* Set the name associated with the email address.
* @param {string} name The name to associate.
* @final
*/
goog.format.EmailAddress.prototype.setName = function(name) {
this.name_ = name;
};
/**
* Set the email address.
* @param {string} address The email address.
* @final
*/
goog.format.EmailAddress.prototype.setAddress = function(address) {
this.address = address;
};
/**
* Return the address in a standard format:
* - remove extra spaces.
* - Surround name with quotes if it contains special characters.
* @return {string} The cleaned address.
* @override
*/
goog.format.EmailAddress.prototype.toString = function() {
return this.toStringInternal(
goog.format.EmailAddress.CHARS_REQUIRE_QUOTES_);
};
/**
* Check if a display name requires quoting.
* @param {string} name The display name
* @param {string} specialChars String that contains the characters that require
* the display name to be quoted. This may change based in whereas we are
* in EAI context or not.
* @return {boolean}
* @private
*/
goog.format.EmailAddress.isQuoteNeeded_ = function(name, specialChars) {
for (var i = 0; i < specialChars.length; i++) {
var specialChar = specialChars[i];
if (goog.string.contains(name, specialChar)) {
return true;
}
}
return false;
};
/**
* Return the address in a standard format:
* - remove extra spaces.
* - Surround name with quotes if it contains special characters.
* @param {string} specialChars String that contains the characters that require
* the display name to be quoted.
* @return {string} The cleaned address.
* @protected
*/
goog.format.EmailAddress.prototype.toStringInternal = function(specialChars) {
var name = this.getName();
// We intentionally remove double quotes in the name because escaping
// them to \" looks ugly.
name = name.replace(goog.format.EmailAddress.ALL_DOUBLE_QUOTES_, '');
// If the name has special characters, we need to quote it and escape \'s.
if (goog.format.EmailAddress.isQuoteNeeded_(name, specialChars)) {
name = '"' +
name.replace(goog.format.EmailAddress.ALL_BACKSLASHES_, '\\\\') + '"';
}
if (name == '') {
return this.address;
}
if (this.address == '') {
return name;
}
return name + ' <' + this.address + '>';
};
/**
* Determines is the current object is a valid email address.
* @return {boolean} Whether the email address is valid.
*/
goog.format.EmailAddress.prototype.isValid = function() {
return goog.format.EmailAddress.isValidAddrSpec(this.address);
};
/**
* Checks if the provided string is a valid email address. Supports both
* simple email addresses (address specs) and addresses that contain display
* names.
* @param {string} str The email address to check.
* @return {boolean} Whether the provided string is a valid address.
*/
goog.format.EmailAddress.isValidAddress = function(str) {
return goog.format.EmailAddress.parse(str).isValid();
};
/**
* Checks if the provided string is a valid address spec (local@domain.com).
* @param {string} str The email address to check.
* @return {boolean} Whether the provided string is a valid address spec.
*/
goog.format.EmailAddress.isValidAddrSpec = function(str) {
// This is a fairly naive implementation, but it covers 99% of use cases.
// For more details, see http://en.wikipedia.org/wiki/Email_address#Syntax
return goog.format.EmailAddress.EMAIL_ADDRESS_.test(str);
};
/**
* Checks if the provided string is a valid local part (part before the '@') of
* an email address.
* @param {string} str The local part to check.
* @return {boolean} Whether the provided string is a valid local part.
*/
goog.format.EmailAddress.isValidLocalPartSpec = function(str) {
return goog.format.EmailAddress.LOCAL_PART_.test(str);
};
/**
* Checks if the provided string is a valid domain part (part after the '@') of
* an email address.
* @param {string} str The domain part to check.
* @return {boolean} Whether the provided string is a valid domain part.
*/
goog.format.EmailAddress.isValidDomainPartSpec = function(str) {
return goog.format.EmailAddress.DOMAIN_PART_.test(str);
};
/**
* Parses an email address of the form "name" &lt;address&gt; ("name" is
* optional) into an email address.
* @param {string} addr The address string.
* @param {function(new: goog.format.EmailAddress, string=,string=)} ctor
* EmailAddress constructor to instantiate the output address.
* @return {!goog.format.EmailAddress} The parsed address.
* @protected
*/
goog.format.EmailAddress.parseInternal = function(addr, ctor) {
// TODO(ecattell): Strip bidi markers.
var name = '';
var address = '';
for (var i = 0; i < addr.length;) {
var token = goog.format.EmailAddress.getToken_(addr, i);
if (token.charAt(0) == '<' && token.indexOf('>') != -1) {
var end = token.indexOf('>');
address = token.substring(1, end);
} else if (address == '') {
name += token;
}
i += token.length;
}
// Check if it's a simple email address of the form "jlim@google.com".
if (address == '' && name.indexOf('@') != -1) {
address = name;
name = '';
}
name = goog.string.collapseWhitespace(name);
name = goog.string.stripQuotes(name, '\'');
name = goog.string.stripQuotes(name, '"');
// Replace escaped quotes and slashes.
name = name.replace(goog.format.EmailAddress.ESCAPED_DOUBLE_QUOTES_, '"');
name = name.replace(goog.format.EmailAddress.ESCAPED_BACKSLASHES_, '\\');
address = goog.string.collapseWhitespace(address);
return new ctor(address, name);
};
/**
* Parses an email address of the form "name" &lt;address&gt; into
* an email address.
* @param {string} addr The address string.
* @return {!goog.format.EmailAddress} The parsed address.
*/
goog.format.EmailAddress.parse = function(addr) {
return goog.format.EmailAddress.parseInternal(
addr, goog.format.EmailAddress);
};
/**
* Parse a string containing email addresses of the form
* "name" &lt;address&gt; into an array of email addresses.
* @param {string} str The address list.
* @param {function(string)} parser The parser to employ.
* @param {function(string):boolean} separatorChecker Accepts a character and
* returns whether it should be considered an address separator.
* @return {!Array<!goog.format.EmailAddress>} The parsed emails.
* @protected
*/
goog.format.EmailAddress.parseListInternal = function(
str, parser, separatorChecker) {
var result = [];
var email = '';
var token;
// Remove non-UNIX-style newlines that would otherwise cause getToken_ to
// choke. Remove multiple consecutive whitespace characters for the same
// reason.
str = goog.string.collapseWhitespace(str);
for (var i = 0; i < str.length; ) {
token = goog.format.EmailAddress.getToken_(str, i);
if (separatorChecker(token) ||
(token == ' ' && parser(email).isValid())) {
if (!goog.string.isEmptyOrWhitespace(email)) {
result.push(parser(email));
}
email = '';
i++;
continue;
}
email += token;
i += token.length;
}
// Add the final token.
if (!goog.string.isEmptyOrWhitespace(email)) {
result.push(parser(email));
}
return result;
};
/**
* Parses a string containing email addresses of the form
* "name" &lt;address&gt; into an array of email addresses.
* @param {string} str The address list.
* @return {!Array<!goog.format.EmailAddress>} The parsed emails.
*/
goog.format.EmailAddress.parseList = function(str) {
return goog.format.EmailAddress.parseListInternal(
str, goog.format.EmailAddress.parse,
goog.format.EmailAddress.isAddressSeparator);
};
/**
* Get the next token from a position in an address string.
* @param {string} str the string.
* @param {number} pos the position.
* @return {string} the token.
* @private
*/
goog.format.EmailAddress.getToken_ = function(str, pos) {
var ch = str.charAt(pos);
var p = goog.format.EmailAddress.OPENERS_.indexOf(ch);
if (p == -1) {
return ch;
}
if (goog.format.EmailAddress.isEscapedDlQuote_(str, pos)) {
// If an opener is an escaped quote we do not treat it as a real opener
// and keep accumulating the token.
return ch;
}
var closerChar = goog.format.EmailAddress.CLOSERS_.charAt(p);
var endPos = str.indexOf(closerChar, pos + 1);
// If the closer is a quote we go forward skipping escaped quotes until we
// hit the real closing one.
while (endPos >= 0 &&
goog.format.EmailAddress.isEscapedDlQuote_(str, endPos)) {
endPos = str.indexOf(closerChar, endPos + 1);
}
var token = (endPos >= 0) ? str.substring(pos, endPos + 1) : ch;
return token;
};
/**
* Checks if the character in the current position is an escaped double quote
* ( \" ).
* @param {string} str the string.
* @param {number} pos the position.
* @return {boolean} true if the char is escaped double quote.
* @private
*/
goog.format.EmailAddress.isEscapedDlQuote_ = function(str, pos) {
if (str.charAt(pos) != '"') {
return false;
}
var slashCount = 0;
for (var idx = pos - 1; idx >= 0 && str.charAt(idx) == '\\'; idx--) {
slashCount++;
}
return ((slashCount % 2) != 0);
};
/**
* @param {string} ch The character to test.
* @return {boolean} Whether the provided character is an address separator.
*/
goog.format.EmailAddress.isAddressSeparator = function(ch) {
return goog.string.contains(goog.format.EmailAddress.ADDRESS_SEPARATORS_, ch);
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.format.EmailAddress
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.format.EmailAddressTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,229 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.format.EmailAddressTest');
goog.setTestOnly('goog.format.EmailAddressTest');
goog.require('goog.array');
goog.require('goog.format.EmailAddress');
goog.require('goog.testing.jsunit');
function testparseList() {
assertParsedList('', [], 'Failed to parse empty stringy');
assertParsedList(',,', [], 'Failed to parse string with commas only');
assertParsedList('<foo@gmail.com>', ['foo@gmail.com']);
assertParsedList('<foo@gmail.com>,', ['foo@gmail.com'],
'Failed to parse 1 address with trailing comma');
assertParsedList('<foo@gmail.com>, ', ['foo@gmail.com'],
'Failed to parse 1 address with trailing whitespace and comma');
assertParsedList(',<foo@gmail.com>', ['foo@gmail.com'],
'Failed to parse 1 address with leading comma');
assertParsedList(' ,<foo@gmail.com>', ['foo@gmail.com'],
'Failed to parse 1 address with leading whitespace and comma');
assertParsedList('<foo@gmail.com>, <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses');
assertParsedList('<foo@gmail.com>, <bar@gmail.com>,',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses and trailing comma');
assertParsedList('<foo@gmail.com>, <bar@gmail.com>, ',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses, trailing comma and whitespace');
assertParsedList(
'John Doe <john@gmail.com>; Jane Doe <jane@gmail.com>, ' +
'<jerry@gmail.com>',
['john@gmail.com', 'jane@gmail.com', 'jerry@gmail.com'],
'Failed to parse addresses with semicolon separator');
}
function testparseListOpenersAndClosers() {
assertParsedList(
'aaa@gmail.com, "bbb@gmail.com", <ccc@gmail.com>, ' +
'(ddd@gmail.com), [eee@gmail.com]',
['aaa@gmail.com', '"bbb@gmail.com"', 'ccc@gmail.com',
'(ddd@gmail.com)', '[eee@gmail.com]'],
'Failed to handle all 5 opener/closer characters');
}
function testparseListIdn() {
var idnaddr = 'mailtest@\u4F8B\u3048.\u30C6\u30B9\u30C8';
assertParsedList(idnaddr, [idnaddr]);
}
function testparseListWithQuotedSpecialChars() {
var res = assertParsedList(
'a\\"b\\"c <d@e.f>,"g\\"h\\"i\\\\" <j@k.l>',
['d@e.f', 'j@k.l']);
assertEquals('Wrong name 0', 'a"b"c', res[0].getName());
assertEquals('Wrong name 1', 'g"h"i\\', res[1].getName());
}
function testparseListWithCommaInLocalPart() {
var res = assertParsedList(
'"Doe, John" <doe.john@gmail.com>, <someone@gmail.com>',
['doe.john@gmail.com', 'someone@gmail.com']);
assertEquals('Doe, John', res[0].getName());
assertEquals('', res[1].getName());
}
function testparseListWithWhitespaceSeparatedEmails() {
var res = assertParsedList(
'a@b.com <c@d.com> e@f.com "G H" <g@h.com> i@j.com',
['a@b.com', 'c@d.com', 'e@f.com', 'g@h.com', 'i@j.com']);
assertEquals('G H', res[3].getName());
}
function testparseListSystemNewlines() {
// These Windows newlines can be inserted in IE8, or copied-and-pasted from
// bad data on a Mac, as seen in bug 11081852.
assertParsedList('a@b.com\r\nc@d.com', ['a@b.com', 'c@d.com'],
'Failed to parse Windows newlines');
assertParsedList('a@b.com\nc@d.com', ['a@b.com', 'c@d.com'],
'Failed to parse *nix newlines');
assertParsedList('a@b.com\n\rc@d.com', ['a@b.com', 'c@d.com'],
'Failed to parse obsolete newlines');
assertParsedList('a@b.com\rc@d.com', ['a@b.com', 'c@d.com'],
'Failed to parse pre-OS X Mac newlines');
}
function testToString() {
var f = function(str) {
return goog.format.EmailAddress.parse(str).toString();
};
// No modification.
assertEquals('JOHN Doe <john@gmail.com>',
f('JOHN Doe <john@gmail.com>'));
// Extra spaces.
assertEquals('JOHN Doe <john@gmail.com>',
f(' JOHN Doe <john@gmail.com> '));
// No name.
assertEquals('john@gmail.com', f('<john@gmail.com>'));
assertEquals('john@gmail.com', f('john@gmail.com'));
// No address.
assertEquals('JOHN Doe', f('JOHN Doe <>'));
// Special chars in the name.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, Doe <john@gmail.com>'));
assertEquals('"JOHN(Johnny) Doe" <john@gmail.com>',
f('JOHN(Johnny) Doe <john@gmail.com>'));
assertEquals('"JOHN[Johnny] Doe" <john@gmail.com>',
f('JOHN[Johnny] Doe <john@gmail.com>'));
assertEquals('"JOHN@work Doe" <john@gmail.com>',
f('JOHN@work Doe <john@gmail.com>'));
assertEquals('"JOHN:theking Doe" <john@gmail.com>',
f('JOHN:theking Doe <john@gmail.com>'));
assertEquals('"JOHN\\\\ Doe" <john@gmail.com>',
f('JOHN\\ Doe <john@gmail.com>'));
assertEquals('"JOHN.com Doe" <john@gmail.com>',
f('JOHN.com Doe <john@gmail.com>'));
// Already quoted.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('"JOHN, Doe" <john@gmail.com>'));
// Needless quotes.
assertEquals('JOHN Doe <john@gmail.com>',
f('"JOHN Doe" <john@gmail.com>'));
// Not quoted-string, but has double quotes.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, "Doe" <john@gmail.com>'));
// No special characters other than quotes.
assertEquals('JOHN Doe <john@gmail.com>',
f('JOHN "Doe" <john@gmail.com>'));
// Escaped quotes are also removed.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, \\"Doe\\" <john@gmail.com>'));
}
function doIsValidTest(testFunc, valid, invalid) {
goog.array.forEach(valid, function(str) {
assertTrue('"' + str + '" should be valid.', testFunc(str));
});
goog.array.forEach(invalid, function(str) {
assertFalse('"' + str + '" should be invalid.', testFunc(str));
});
}
function testIsValid() {
var valid = [
'e@b.eu', '<a.b+foo@c.com>', 'eric <e@b.com>', '"e" <e@b.com>',
'a@FOO.MUSEUM', 'bla@b.co.ac.uk', 'bla@a.b.com', 'o\'hara@gm.com',
'plus+is+allowed@gmail.com', '!/#$%&\'*+-=~|`{}?^_@expample.com',
'confirm-bhk=modulo.org@yahoogroups.com'];
var invalid = [
'e', '', 'e @c.com', 'a@b', 'foo.com', 'foo@c..com', 'test@gma=il.com',
'aaa@gmail', 'has some spaces@gmail.com', 'has@three@at@signs.com',
'@no-local-part.com', 'み.ん-あ@みんあ.みんあ',
'みんあ@test.com', 'test@test.みんあ', 'test@みんあ.com',
'fullwidthfullstop@sld' + '\uff0e' + 'tld',
'ideographicfullstop@sld' + '\u3002' + 'tld',
'halfwidthideographicfullstop@sld' + '\uff61' + 'tld'];
doIsValidTest(goog.format.EmailAddress.isValidAddress, valid, invalid);
}
function testIsValidLocalPart() {
var valid = [
'e', 'a.b+foo', 'o\'hara', 'user+someone', '!/#$%&\'*+-=~|`{}?^_',
'confirm-bhk=modulo.org'];
var invalid = [
'A@b@c', 'a"b(c)d,e:f;g<h>i[j\\k]l', 'just"not"right',
'this is"not\\allowed', 'this\\ still\"not\\\\allowed', 'has some spaces'];
doIsValidTest(goog.format.EmailAddress.isValidLocalPartSpec, valid, invalid);
}
function testIsValidDomainPart() {
var valid = [
'example.com', 'dept.example.org', 'long.domain.with.lots.of.dots'];
var invalid = ['', '@has.an.at.sign', '..has.leading.dots', 'gma=il.com',
'DoesNotHaveADot', 'sld' + '\uff0e' + 'tld', 'sld' + '\u3002' + 'tld',
'sld' + '\uff61' + 'tld'];
doIsValidTest(goog.format.EmailAddress.isValidDomainPartSpec, valid, invalid);
}
/**
* Asserts that parsing the inputString produces a list of email addresses
* containing the specified address strings, irrespective of their order.
* @param {string} inputString A raw address list.
* @param {Array<string>} expectedList The expected results.
* @param {string=} opt_message An assertion message.
* @return {string} the resulting email address objects.
*/
function assertParsedList(inputString, expectedList, opt_message) {
var message = opt_message || 'Should parse address correctly';
var result = goog.format.EmailAddress.parseList(inputString);
assertEquals(
'Should have correct # of addresses', expectedList.length, result.length);
for (var i = 0; i < expectedList.length; ++i) {
assertEquals(message, expectedList[i], result[i].getAddress());
}
return result;
}
@@ -0,0 +1,502 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides utility functions for formatting strings, numbers etc.
*
*/
goog.provide('goog.format');
goog.require('goog.i18n.GraphemeBreak');
goog.require('goog.string');
goog.require('goog.userAgent');
/**
* Formats a number of bytes in human readable form.
* 54, 450K, 1.3M, 5G etc.
* @param {number} bytes The number of bytes to show.
* @param {number=} opt_decimals The number of decimals to use. Defaults to 2.
* @return {string} The human readable form of the byte size.
*/
goog.format.fileSize = function(bytes, opt_decimals) {
return goog.format.numBytesToString(bytes, opt_decimals, false);
};
/**
* Checks whether string value containing scaling units (K, M, G, T, P, m,
* u, n) can be converted to a number.
*
* Where there is a decimal, there must be a digit to the left of the
* decimal point.
*
* Negative numbers are valid.
*
* Examples:
* 0, 1, 1.0, 10.4K, 2.3M, -0.3P, 1.2m
*
* @param {string} val String value to check.
* @return {boolean} True if string could be converted to a numeric value.
*/
goog.format.isConvertableScaledNumber = function(val) {
return goog.format.SCALED_NUMERIC_RE_.test(val);
};
/**
* Converts a string to numeric value, taking into account the units.
* If string ends in 'B', use binary conversion.
* @param {string} stringValue String to be converted to numeric value.
* @return {number} Numeric value for string.
*/
goog.format.stringToNumericValue = function(stringValue) {
if (goog.string.endsWith(stringValue, 'B')) {
return goog.format.stringToNumericValue_(
stringValue, goog.format.NUMERIC_SCALES_BINARY_);
}
return goog.format.stringToNumericValue_(
stringValue, goog.format.NUMERIC_SCALES_SI_);
};
/**
* Converts a string to number of bytes, taking into account the units.
* Binary conversion.
* @param {string} stringValue String to be converted to numeric value.
* @return {number} Numeric value for string.
*/
goog.format.stringToNumBytes = function(stringValue) {
return goog.format.stringToNumericValue_(
stringValue, goog.format.NUMERIC_SCALES_BINARY_);
};
/**
* Converts a numeric value to string representation. SI conversion.
* @param {number} val Value to be converted.
* @param {number=} opt_decimals The number of decimals to use. Defaults to 2.
* @return {string} String representation of number.
*/
goog.format.numericValueToString = function(val, opt_decimals) {
return goog.format.numericValueToString_(
val, goog.format.NUMERIC_SCALES_SI_, opt_decimals);
};
/**
* Converts number of bytes to string representation. Binary conversion.
* Default is to return the additional 'B' suffix, e.g. '10.5KB' to minimize
* confusion with counts that are scaled by powers of 1000.
* @param {number} val Value to be converted.
* @param {number=} opt_decimals The number of decimals to use. Defaults to 2.
* @param {boolean=} opt_suffix If true, include trailing 'B' in returned
* string. Default is true.
* @param {boolean=} opt_useSeparator If true, number and scale will be
* separated by a no break space. Default is false.
* @return {string} String representation of number of bytes.
*/
goog.format.numBytesToString = function(val, opt_decimals, opt_suffix,
opt_useSeparator) {
var suffix = '';
if (!goog.isDef(opt_suffix) || opt_suffix) {
suffix = 'B';
}
return goog.format.numericValueToString_(
val, goog.format.NUMERIC_SCALES_BINARY_, opt_decimals, suffix,
opt_useSeparator);
};
/**
* Converts a string to numeric value, taking into account the units.
* @param {string} stringValue String to be converted to numeric value.
* @param {Object} conversion Dictionary of conversion scales.
* @return {number} Numeric value for string. If it cannot be converted,
* returns NaN.
* @private
*/
goog.format.stringToNumericValue_ = function(stringValue, conversion) {
var match = stringValue.match(goog.format.SCALED_NUMERIC_RE_);
if (!match) {
return NaN;
}
var val = match[1] * conversion[match[2]];
return val;
};
/**
* Converts a numeric value to string, using specified conversion
* scales.
* @param {number} val Value to be converted.
* @param {Object} conversion Dictionary of scaling factors.
* @param {number=} opt_decimals The number of decimals to use. Default is 2.
* @param {string=} opt_suffix Optional suffix to append.
* @param {boolean=} opt_useSeparator If true, number and scale will be
* separated by a space. Default is false.
* @return {string} The human readable form of the byte size.
* @private
*/
goog.format.numericValueToString_ = function(val, conversion,
opt_decimals, opt_suffix, opt_useSeparator) {
var prefixes = goog.format.NUMERIC_SCALE_PREFIXES_;
var orig_val = val;
var symbol = '';
var separator = '';
var scale = 1;
if (val < 0) {
val = -val;
}
for (var i = 0; i < prefixes.length; i++) {
var unit = prefixes[i];
scale = conversion[unit];
if (val >= scale || (scale <= 1 && val > 0.1 * scale)) {
// Treat values less than 1 differently, allowing 0.5 to be "0.5" rather
// than "500m"
symbol = unit;
break;
}
}
if (!symbol) {
scale = 1;
} else {
if (opt_suffix) {
symbol += opt_suffix;
}
if (opt_useSeparator) {
separator = ' ';
}
}
var ex = Math.pow(10, goog.isDef(opt_decimals) ? opt_decimals : 2);
return Math.round(orig_val / scale * ex) / ex + separator + symbol;
};
/**
* Regular expression for detecting scaling units, such as K, M, G, etc. for
* converting a string representation to a numeric value.
*
* Also allow 'k' to be aliased to 'K'. These could be used for SI (powers
* of 1000) or Binary (powers of 1024) conversions.
*
* Also allow final 'B' to be interpreted as byte-count, implicitly triggering
* binary conversion (e.g., '10.2MB').
*
* @type {RegExp}
* @private
*/
goog.format.SCALED_NUMERIC_RE_ = /^([-]?\d+\.?\d*)([K,M,G,T,P,k,m,u,n]?)[B]?$/;
/**
* Ordered list of scaling prefixes in decreasing order.
* @private {Array<string>}
*/
goog.format.NUMERIC_SCALE_PREFIXES_ = [
'P', 'T', 'G', 'M', 'K', '', 'm', 'u', 'n'
];
/**
* Scaling factors for conversion of numeric value to string. SI conversion.
* @type {Object}
* @private
*/
goog.format.NUMERIC_SCALES_SI_ = {
'': 1,
'n': 1e-9,
'u': 1e-6,
'm': 1e-3,
'k': 1e3,
'K': 1e3,
'M': 1e6,
'G': 1e9,
'T': 1e12,
'P': 1e15
};
/**
* Scaling factors for conversion of numeric value to string. Binary
* conversion.
* @type {Object}
* @private
*/
goog.format.NUMERIC_SCALES_BINARY_ = {
'': 1,
'n': Math.pow(1024, -3),
'u': Math.pow(1024, -2),
'm': 1.0 / 1024,
'k': 1024,
'K': 1024,
'M': Math.pow(1024, 2),
'G': Math.pow(1024, 3),
'T': Math.pow(1024, 4),
'P': Math.pow(1024, 5)
};
/**
* First Unicode code point that has the Mark property.
* @type {number}
* @private
*/
goog.format.FIRST_GRAPHEME_EXTEND_ = 0x300;
/**
* Returns true if and only if given character should be treated as a breaking
* space. All ASCII control characters, the main Unicode range of spacing
* characters (U+2000 to U+200B inclusive except for U+2007), and several other
* Unicode space characters are treated as breaking spaces.
* @param {number} charCode The character code under consideration.
* @return {boolean} True if the character is a breaking space.
* @private
*/
goog.format.isTreatedAsBreakingSpace_ = function(charCode) {
return (charCode <= goog.format.WbrToken_.SPACE) ||
(charCode >= 0x1000 &&
((charCode >= 0x2000 && charCode <= 0x2006) ||
(charCode >= 0x2008 && charCode <= 0x200B) ||
charCode == 0x1680 ||
charCode == 0x180E ||
charCode == 0x2028 ||
charCode == 0x2029 ||
charCode == 0x205f ||
charCode == 0x3000));
};
/**
* Returns true if and only if given character is an invisible formatting
* character.
* @param {number} charCode The character code under consideration.
* @return {boolean} True if the character is an invisible formatting character.
* @private
*/
goog.format.isInvisibleFormattingCharacter_ = function(charCode) {
// See: http://unicode.org/charts/PDF/U2000.pdf
return (charCode >= 0x200C && charCode <= 0x200F) ||
(charCode >= 0x202A && charCode <= 0x202E);
};
/**
* Inserts word breaks into an HTML string at a given interval. The counter is
* reset if a space or a character which behaves like a space is encountered,
* but it isn't incremented if an invisible formatting character is encountered.
* WBRs aren't inserted into HTML tags or entities. Entities count towards the
* character count, HTML tags do not.
*
* With common strings aliased, objects allocations are constant based on the
* length of the string: N + 3. This guarantee does not hold if the string
* contains an element >= U+0300 and hasGraphemeBreak is non-trivial.
*
* @param {string} str HTML to insert word breaks into.
* @param {function(number, number, boolean): boolean} hasGraphemeBreak A
* function determining if there is a grapheme break between two characters,
* in the same signature as goog.i18n.GraphemeBreak.hasGraphemeBreak.
* @param {number=} opt_maxlen Maximum length after which to ensure
* there is a break. Default is 10 characters.
* @return {string} The string including word breaks.
* @private
*/
goog.format.insertWordBreaksGeneric_ = function(str, hasGraphemeBreak,
opt_maxlen) {
var maxlen = opt_maxlen || 10;
if (maxlen > str.length) return str;
var rv = [];
var n = 0; // The length of the current token
// This will contain the ampersand or less-than character if one of the
// two has been seen; otherwise, the value is zero.
var nestingCharCode = 0;
// First character position from input string that has not been outputted.
var lastDumpPosition = 0;
var charCode = 0;
for (var i = 0; i < str.length; i++) {
// Using charCodeAt versus charAt avoids allocating new string objects.
var lastCharCode = charCode;
charCode = str.charCodeAt(i);
// Don't add a WBR before characters that might be grapheme extending.
var isPotentiallyGraphemeExtending =
charCode >= goog.format.FIRST_GRAPHEME_EXTEND_ &&
!hasGraphemeBreak(lastCharCode, charCode, true);
// Don't add a WBR at the end of a word. For the purposes of determining
// work breaks, all ASCII control characters and some commonly encountered
// Unicode spacing characters are treated as breaking spaces.
if (n >= maxlen &&
!goog.format.isTreatedAsBreakingSpace_(charCode) &&
!isPotentiallyGraphemeExtending) {
// Flush everything seen so far, and append a word break.
rv.push(str.substring(lastDumpPosition, i), goog.format.WORD_BREAK_HTML);
lastDumpPosition = i;
n = 0;
}
if (!nestingCharCode) {
// Not currently within an HTML tag or entity
if (charCode == goog.format.WbrToken_.LT ||
charCode == goog.format.WbrToken_.AMP) {
// Entering an HTML Entity '&' or open tag '<'
nestingCharCode = charCode;
} else if (goog.format.isTreatedAsBreakingSpace_(charCode)) {
// A space or control character -- reset the token length
n = 0;
} else if (!goog.format.isInvisibleFormattingCharacter_(charCode)) {
// A normal flow character - increment. For grapheme extending
// characters, this is not *technically* a new character. However,
// since the grapheme break detector might be overly conservative,
// we have to continue incrementing, or else we won't even be able
// to add breaks when we get to things like punctuation. For the
// case where we have a full grapheme break detector, it is okay if
// we occasionally break slightly early.
n++;
}
} else if (charCode == goog.format.WbrToken_.GT &&
nestingCharCode == goog.format.WbrToken_.LT) {
// Leaving an HTML tag, treat the tag as zero-length
nestingCharCode = 0;
} else if (charCode == goog.format.WbrToken_.SEMI_COLON &&
nestingCharCode == goog.format.WbrToken_.AMP) {
// Leaving an HTML entity, treat it as length one
nestingCharCode = 0;
n++;
}
}
// Take care of anything we haven't flushed so far.
rv.push(str.substr(lastDumpPosition));
return rv.join('');
};
/**
* Inserts word breaks into an HTML string at a given interval.
*
* This method is as aggressive as possible, using a full table of Unicode
* characters where it is legal to insert word breaks; however, this table
* comes at a 2.5k pre-gzip (~1k post-gzip) size cost. Consider using
* insertWordBreaksBasic to minimize the size impact.
*
* @param {string} str HTML to insert word breaks into.
* @param {number=} opt_maxlen Maximum length after which to ensure there is a
* break. Default is 10 characters.
* @return {string} The string including word breaks.
*/
goog.format.insertWordBreaks = function(str, opt_maxlen) {
return goog.format.insertWordBreaksGeneric_(str,
goog.i18n.GraphemeBreak.hasGraphemeBreak, opt_maxlen);
};
/**
* Determines conservatively if a character has a Grapheme break.
*
* Conforms to a similar signature as goog.i18n.GraphemeBreak, but is overly
* conservative, returning true only for characters in common scripts that
* are simple to account for.
*
* @param {number} lastCharCode The previous character code. Ignored.
* @param {number} charCode The character code under consideration. It must be
* at least \u0300 as a precondition -- this case is covered by
* insertWordBreaksGeneric_.
* @param {boolean=} opt_extended Ignored, to conform with the interface.
* @return {boolean} Whether it is one of the recognized subsets of characters
* with a grapheme break.
* @private
*/
goog.format.conservativelyHasGraphemeBreak_ = function(
lastCharCode, charCode, opt_extended) {
// Return false for everything except the most common Cyrillic characters.
// Don't worry about Latin characters, because insertWordBreaksGeneric_
// itself already handles those.
// TODO(gboyer): Also account for Greek, Armenian, and Georgian if it is
// simple to do so.
return charCode >= 0x400 && charCode < 0x523;
};
// TODO(gboyer): Consider using a compile-time flag to switch implementations
// rather than relying on the developers to toggle implementations.
/**
* Inserts word breaks into an HTML string at a given interval.
*
* This method is less aggressive than insertWordBreaks, only inserting
* breaks next to punctuation and between Latin or Cyrillic characters.
* However, this is good enough for the common case of URLs. It also
* works for all Latin and Cyrillic languages, plus CJK has no need for word
* breaks. When this method is used, goog.i18n.GraphemeBreak may be dead
* code eliminated.
*
* @param {string} str HTML to insert word breaks into.
* @param {number=} opt_maxlen Maximum length after which to ensure there is a
* break. Default is 10 characters.
* @return {string} The string including word breaks.
*/
goog.format.insertWordBreaksBasic = function(str, opt_maxlen) {
return goog.format.insertWordBreaksGeneric_(str,
goog.format.conservativelyHasGraphemeBreak_, opt_maxlen);
};
/**
* True iff the current userAgent is IE8 or above.
* @type {boolean}
* @private
*/
goog.format.IS_IE8_OR_ABOVE_ = goog.userAgent.IE &&
goog.userAgent.isVersionOrHigher(8);
/**
* Constant for the WBR replacement used by insertWordBreaks. Safari requires
* <wbr></wbr>, Opera needs the &shy; entity, though this will give a visible
* hyphen at breaks. IE8 uses a zero width space.
* Other browsers just use <wbr>.
* @type {string}
*/
goog.format.WORD_BREAK_HTML =
goog.userAgent.WEBKIT ?
'<wbr></wbr>' : goog.userAgent.OPERA ?
'&shy;' : goog.format.IS_IE8_OR_ABOVE_ ?
'&#8203;' : '<wbr>';
/**
* Tokens used within insertWordBreaks.
* @private
* @enum {number}
*/
goog.format.WbrToken_ = {
LT: 60, // '<'.charCodeAt(0)
GT: 62, // '>'.charCodeAt(0)
AMP: 38, // '&'.charCodeAt(0)
SEMI_COLON: 59, // ';'.charCodeAt(0)
SPACE: 32 // ' '.charCodeAt(0)
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.format
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.formatTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,303 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.formatTest');
goog.setTestOnly('goog.formatTest');
goog.require('goog.dom');
goog.require('goog.format');
goog.require('goog.string');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
var propertyReplacer = new goog.testing.PropertyReplacer();
function tearDown() {
// set wordBreakHtml back to the original value (some tests edit this member).
propertyReplacer.reset();
}
function testFormatFileSize() {
var fileSize = goog.format.fileSize;
assertEquals('45', fileSize(45));
assertEquals('45', fileSize(45, 0));
assertEquals('45', fileSize(45, 1));
assertEquals('45', fileSize(45, 3));
assertEquals('454', fileSize(454));
assertEquals('600', fileSize(600));
assertEquals('1K', fileSize(1024));
assertEquals('2K', fileSize(2 * 1024));
assertEquals('5K', fileSize(5 * 1024));
assertEquals('5.123K', fileSize(5.12345 * 1024, 3));
assertEquals('5.68K', fileSize(5.678 * 1024, 2));
assertEquals('1M', fileSize(1024 * 1024));
assertEquals('1.5M', fileSize(1.5 * 1024 * 1024));
assertEquals('2M', fileSize(1.5 * 1024 * 1024, 0));
assertEquals('1.5M', fileSize(1.51 * 1024 * 1024, 1));
assertEquals('1.56M', fileSize(1.56 * 1024 * 1024, 2));
assertEquals('1G', fileSize(1024 * 1024 * 1024));
assertEquals('6G', fileSize(6 * 1024 * 1024 * 1024));
assertEquals('12.06T', fileSize(12345.6789 * 1024 * 1024 * 1024));
}
function testIsConvertableScaledNumber() {
var isConvertableScaledNumber = goog.format.isConvertableScaledNumber;
assertTrue(isConvertableScaledNumber('0'));
assertTrue(isConvertableScaledNumber('45'));
assertTrue(isConvertableScaledNumber('45K'));
assertTrue(isConvertableScaledNumber('45MB'));
assertTrue(isConvertableScaledNumber('45GB'));
assertTrue(isConvertableScaledNumber('45T'));
assertTrue(isConvertableScaledNumber('2.33P'));
assertTrue(isConvertableScaledNumber('45m'));
assertTrue(isConvertableScaledNumber('45u'));
assertTrue(isConvertableScaledNumber('-5.0n'));
assertFalse(isConvertableScaledNumber('45x'));
assertFalse(isConvertableScaledNumber('ux'));
assertFalse(isConvertableScaledNumber('K'));
}
function testNumericValueToString() {
var numericValueToString = goog.format.numericValueToString;
assertEquals('0', numericValueToString(0.0));
assertEquals('45', numericValueToString(45));
assertEquals('454', numericValueToString(454));
assertEquals('600', numericValueToString(600));
assertEquals('1.02K', numericValueToString(1024));
assertEquals('2.05K', numericValueToString(2 * 1024));
assertEquals('5.12K', numericValueToString(5 * 1024));
assertEquals('5.246K', numericValueToString(5.12345 * 1024, 3));
assertEquals('5.81K', numericValueToString(5.678 * 1024, 2));
assertEquals('1.05M', numericValueToString(1024 * 1024));
assertEquals('1.57M', numericValueToString(1.5 * 1024 * 1024));
assertEquals('2M', numericValueToString(1.5 * 1024 * 1024, 0));
assertEquals('1.6M', numericValueToString(1.51 * 1024 * 1024, 1));
assertEquals('1.64M', numericValueToString(1.56 * 1024 * 1024, 2));
assertEquals('1.07G', numericValueToString(1024 * 1024 * 1024));
assertEquals('6.44G', numericValueToString(6 * 1024 * 1024 * 1024));
assertEquals('13.26T', numericValueToString(12345.6789 * 1024 * 1024 * 1024));
assertEquals('23.4m', numericValueToString(0.0234));
assertEquals('1.23u', numericValueToString(0.00000123));
assertEquals('15.78n', numericValueToString(0.000000015784));
assertEquals('0.58u', numericValueToString(0.0000005784));
assertEquals('0.5', numericValueToString(0.5));
assertEquals('-45', numericValueToString(-45.3, 0));
assertEquals('-45', numericValueToString(-45.5, 0));
assertEquals('-46', numericValueToString(-45.51, 0));
}
function testFormatNumBytes() {
var numBytesToString = goog.format.numBytesToString;
assertEquals('45', numBytesToString(45));
assertEquals('454', numBytesToString(454));
assertEquals('5KB', numBytesToString(5 * 1024));
assertEquals('1MB', numBytesToString(1024 * 1024));
assertEquals('6GB', numBytesToString(6 * 1024 * 1024 * 1024));
assertEquals('12.06TB', numBytesToString(12345.6789 * 1024 * 1024 * 1024));
assertEquals('454', numBytesToString(454, 2, true, true));
assertEquals('5 KB', numBytesToString(5 * 1024, 2, true, true));
}
function testStringToNumeric() {
var stringToNumericValue = goog.format.stringToNumericValue;
var epsilon = Math.pow(10, -10);
assertNaN(stringToNumericValue('foo'));
assertEquals(45, stringToNumericValue('45'));
assertEquals(-45, stringToNumericValue('-45'));
assertEquals(-45, stringToNumericValue('-45'));
assertEquals(454, stringToNumericValue('454'));
assertEquals(5 * 1024, stringToNumericValue('5KB'));
assertEquals(1024 * 1024, stringToNumericValue('1MB'));
assertEquals(6 * 1024 * 1024 * 1024, stringToNumericValue('6GB'));
assertEquals(13260110230978.56, stringToNumericValue('12.06TB'));
assertEquals(5010, stringToNumericValue('5.01K'));
assertEquals(5100000, stringToNumericValue('5.1M'));
assertTrue(Math.abs(0.051 - stringToNumericValue('51.0m')) < epsilon);
assertTrue(Math.abs(0.000051 - stringToNumericValue('51.0u')) < epsilon);
}
function testStringToNumBytes() {
var stringToNumBytes = goog.format.stringToNumBytes;
assertEquals(45, stringToNumBytes('45'));
assertEquals(454, stringToNumBytes('454'));
assertEquals(5 * 1024, stringToNumBytes('5K'));
assertEquals(1024 * 1024, stringToNumBytes('1M'));
assertEquals(6 * 1024 * 1024 * 1024, stringToNumBytes('6G'));
assertEquals(13260110230978.56, stringToNumBytes('12.06T'));
}
function testInsertWordBreaks() {
// HTML that gets inserted is browser dependent, ensure for the test it is
// a constant - browser dependent HTML is for display purposes only.
propertyReplacer.set(goog.format, 'WORD_BREAK_HTML', '<wbr>');
var insertWordBreaks = goog.format.insertWordBreaks;
assertEquals('abcdef', insertWordBreaks('abcdef', 10));
assertEquals('ab<wbr>cd<wbr>ef', insertWordBreaks('abcdef', 2));
assertEquals(
'a<wbr>b<wbr>c<wbr>d<wbr>e<wbr>f', insertWordBreaks('abcdef', 1));
assertEquals(
'a&amp;b=<wbr>=fal<wbr>se', insertWordBreaks('a&amp;b==false', 4));
assertEquals('&lt;&amp;&gt;&raquo;<wbr>&laquo;',
insertWordBreaks('&lt;&amp;&gt;&raquo;&laquo;', 4));
assertEquals('a<wbr>b<wbr>c d<wbr>e<wbr>f', insertWordBreaks('abc def', 1));
assertEquals('ab<wbr>c de<wbr>f', insertWordBreaks('abc def', 2));
assertEquals('abc def', insertWordBreaks('abc def', 3));
assertEquals('abc def', insertWordBreaks('abc def', 4));
assertEquals('a<b>cd</b>e<wbr>f', insertWordBreaks('a<b>cd</b>ef', 4));
assertEquals('Thi<wbr>s is a <a href="">lin<wbr>k</a>.',
insertWordBreaks('This is a <a href="">link</a>.', 3));
assertEquals('<abc a="&amp;&amp;&amp;&amp;&amp;">a<wbr>b',
insertWordBreaks('<abc a="&amp;&amp;&amp;&amp;&amp;">ab', 1));
assertEquals('ab\u0300<wbr>cd', insertWordBreaks('ab\u0300cd', 2));
assertEquals('ab\u036F<wbr>cd', insertWordBreaks('ab\u036Fcd', 2));
assertEquals('ab<wbr>\u0370c<wbr>d', insertWordBreaks('ab\u0370cd', 2));
assertEquals('ab<wbr>\uFE1Fc<wbr>d', insertWordBreaks('ab\uFE1Fcd', 2));
assertEquals('ab\u0300<wbr>c\u0301<wbr>de<wbr>f',
insertWordBreaks('ab\u0300c\u0301def', 2));
}
function testInsertWordBreaksWithFormattingCharacters() {
// HTML that gets inserted is browser dependent, ensure for the test it is
// a constant - browser dependent HTML is for display purposes only.
propertyReplacer.set(goog.format, 'WORD_BREAK_HTML', '<wbr>');
var insertWordBreaks = goog.format.insertWordBreaks;
// A date in Arabic-Indic digits with Right-to-Left Marks (U+200F).
// The date is "11<RLM>/01<RLM>/2012".
var textWithRLMs = 'This is a date - ' +
'\u0661\u0661\u200f/\u0660\u0661\u200f/\u0662\u0660\u0661\u0662';
// A string of 10 Xs with invisible formatting characters in between.
// These characters are in the ranges U+200C to U+200F and U+202A to
// U+202E, inclusive. See: http://unicode.org/charts/PDF/U2000.pdf
var stringWithInvisibleFormatting = 'X\u200cX\u200dX\u200eX\u200fX\u202a' +
'X\u202bX\u202cX\u202dX\u202eX';
// A string formed by concatenating copies of the previous string alternating
// with characters which behave like breaking spaces. Besides the space
// character itself, the other characters are in the range U+2000 to U+200B
// inclusive, except for the exclusion of U+2007 and inclusion of U+2029.
// See: http://unicode.org/charts/PDF/U2000.pdf
var stringWithInvisibleFormattingAndSpacelikeCharacters =
stringWithInvisibleFormatting + ' ' +
stringWithInvisibleFormatting + '\u2000' +
stringWithInvisibleFormatting + '\u2001' +
stringWithInvisibleFormatting + '\u2002' +
stringWithInvisibleFormatting + '\u2003' +
stringWithInvisibleFormatting + '\u2005' +
stringWithInvisibleFormatting + '\u2006' +
stringWithInvisibleFormatting + '\u2008' +
stringWithInvisibleFormatting + '\u2009' +
stringWithInvisibleFormatting + '\u200A' +
stringWithInvisibleFormatting + '\u200B' +
stringWithInvisibleFormatting + '\u2029' +
stringWithInvisibleFormatting;
// Test that the word break algorithm does not count RLMs towards word
// length, and therefore does not insert word breaks into a typical date
// written in Arabic-Indic digits with RTMs (b/5853915).
assertEquals(textWithRLMs, insertWordBreaks(textWithRLMs, 10));
// Test that invisible formatting characters are not counted towards word
// length, and that characters which are treated as breaking spaces behave as
// breaking spaces.
assertEquals(stringWithInvisibleFormattingAndSpacelikeCharacters,
insertWordBreaks(stringWithInvisibleFormattingAndSpacelikeCharacters,
10));
}
function testInsertWordBreaksBasic() {
// HTML that gets inserted is browser dependent, ensure for the test it is
// a constant - browser dependent HTML is for display purposes only.
propertyReplacer.set(goog.format, 'WORD_BREAK_HTML', '<wbr>');
var insertWordBreaksBasic = goog.format.insertWordBreaksBasic;
assertEquals('abcdef', insertWordBreaksBasic('abcdef', 10));
assertEquals('ab<wbr>cd<wbr>ef', insertWordBreaksBasic('abcdef', 2));
assertEquals(
'a<wbr>b<wbr>c<wbr>d<wbr>e<wbr>f', insertWordBreaksBasic('abcdef', 1));
assertEquals('ab\u0300<wbr>c\u0301<wbr>de<wbr>f',
insertWordBreaksBasic('ab\u0300c\u0301def', 2));
assertEquals(
'Inserting word breaks into the word "Russia" should work fine.',
'\u0420\u043E<wbr>\u0441\u0441<wbr>\u0438\u044F',
insertWordBreaksBasic('\u0420\u043E\u0441\u0441\u0438\u044F', 2));
// The word 'Internet' in Hindi.
var hindiInternet = '\u0907\u0902\u091F\u0930\u0928\u0947\u091F';
assertEquals('The basic algorithm is not good enough to insert word ' +
'breaks into Hindi.',
hindiInternet, insertWordBreaksBasic(hindiInternet, 2));
// The word 'Internet' in Hindi broken into slashes.
assertEquals('Hindi can have word breaks inserted between slashes',
hindiInternet + '<wbr>/' + hindiInternet + '<wbr>.' + hindiInternet,
insertWordBreaksBasic(hindiInternet + '/' + hindiInternet + '.' +
hindiInternet, 2));
}
function testWordBreaksWorking() {
var text = goog.string.repeat('test', 20);
var textWbr = goog.string.repeat('test' + goog.format.WORD_BREAK_HTML, 20);
var overflowEl = goog.dom.createDom('div',
{'style': 'width: 100px; overflow: hidden; margin 5px'});
var wbrEl = goog.dom.createDom('div',
{'style': 'width: 100px; overflow: hidden; margin-top: 15px'});
goog.dom.appendChild(goog.global.document.body, overflowEl);
goog.dom.appendChild(goog.global.document.body, wbrEl);
overflowEl.innerHTML = text;
wbrEl.innerHTML = textWbr;
assertTrue('Text should overflow', overflowEl.scrollWidth > 100);
assertTrue('Text should not overflow', wbrEl.scrollWidth <= 100);
}
function testWordBreaksRemovedFromTextContent() {
var expectedText = goog.string.repeat('test', 20);
var textWbr = goog.string.repeat('test' + goog.format.WORD_BREAK_HTML, 20);
var wbrEl = goog.dom.createDom('div', null);
wbrEl.innerHTML = textWbr;
assertEquals('text content should have wbr character removed', expectedText,
goog.dom.getTextContent(wbrEl));
}
@@ -0,0 +1,409 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides functions to parse and pretty-print HTML strings.
*
*/
goog.provide('goog.format.HtmlPrettyPrinter');
goog.provide('goog.format.HtmlPrettyPrinter.Buffer');
goog.require('goog.object');
goog.require('goog.string.StringBuffer');
/**
* This class formats HTML to be more human-readable.
* TODO(user): Add hierarchical indentation.
* @param {number=} opt_timeOutMillis Max # milliseconds to spend on #format. If
* this time is exceeded, return partially formatted. 0 or negative number
* indicates no timeout.
* @constructor
* @final
*/
goog.format.HtmlPrettyPrinter = function(opt_timeOutMillis) {
/**
* Max # milliseconds to spend on #format.
* @type {number}
* @private
*/
this.timeOutMillis_ = opt_timeOutMillis && opt_timeOutMillis > 0 ?
opt_timeOutMillis : 0;
};
/**
* Singleton.
* @type {goog.format.HtmlPrettyPrinter?}
* @private
*/
goog.format.HtmlPrettyPrinter.instance_ = null;
/**
* Singleton lazy initializer.
* @return {!goog.format.HtmlPrettyPrinter} Singleton.
* @private
*/
goog.format.HtmlPrettyPrinter.getInstance_ = function() {
if (!goog.format.HtmlPrettyPrinter.instance_) {
goog.format.HtmlPrettyPrinter.instance_ =
new goog.format.HtmlPrettyPrinter();
}
return goog.format.HtmlPrettyPrinter.instance_;
};
/**
* Static utility function. See prototype #format.
* @param {string} html The HTML text to pretty print.
* @return {string} Formatted result.
*/
goog.format.HtmlPrettyPrinter.format = function(html) {
return goog.format.HtmlPrettyPrinter.getInstance_().format(html);
};
/**
* List of patterns used to tokenize HTML for pretty printing. Cache
* subexpression for tag name.
* comment|meta-tag|tag|text|other-less-than-characters
* @type {RegExp}
* @private
*/
goog.format.HtmlPrettyPrinter.TOKEN_REGEX_ =
/(?:<!--.*?-->|<!.*?>|<(\/?)(\w+)[^>]*>|[^<]+|<)/g;
/**
* Tags whose contents we don't want pretty printed.
* @type {Object}
* @private
*/
goog.format.HtmlPrettyPrinter.NON_PRETTY_PRINTED_TAGS_ = goog.object.createSet(
'script',
'style',
'pre',
'xmp');
/**
* 'Block' tags. We should add newlines before and after these tags during
* pretty printing. Tags drawn mostly from HTML4 definitions for block and other
* non-online tags, excepting the ones in
* #goog.format.HtmlPrettyPrinter.NON_PRETTY_PRINTED_TAGS_.
*
* @type {Object}
* @private
*/
goog.format.HtmlPrettyPrinter.BLOCK_TAGS_ = goog.object.createSet(
'address',
'applet',
'area',
'base',
'basefont',
'blockquote',
'body',
'caption',
'center',
'col',
'colgroup',
'dir',
'div',
'dl',
'fieldset',
'form',
'frame',
'frameset',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'hr',
'html',
'iframe',
'isindex',
'legend',
'link',
'menu',
'meta',
'noframes',
'noscript',
'ol',
'optgroup',
'option',
'p',
'param',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'title',
'tr',
'ul');
/**
* Non-block tags that break flow. We insert a line break after, but not before
* these. Tags drawn from HTML4 definitions.
* @type {Object}
* @private
*/
goog.format.HtmlPrettyPrinter.BREAKS_FLOW_TAGS_ = goog.object.createSet(
'br',
'dd',
'dt',
'br',
'li',
'noframes');
/**
* Empty tags. These are treated as both start and end tags.
* @type {Object}
* @private
*/
goog.format.HtmlPrettyPrinter.EMPTY_TAGS_ = goog.object.createSet(
'br',
'hr',
'isindex');
/**
* Breaks up HTML so it's easily readable by the user.
* @param {string} html The HTML text to pretty print.
* @return {string} Formatted result.
* @throws {Error} Regex error, data loss, or endless loop detected.
*/
goog.format.HtmlPrettyPrinter.prototype.format = function(html) {
// Trim leading whitespace, but preserve first indent; in other words, keep
// any spaces immediately before the first non-whitespace character (that's
// what $1 is), but remove all other leading whitespace. This adjustment
// historically had been made in Docs. The motivation is that some
// browsers prepend several line breaks in designMode.
html = html.replace(/^\s*?( *\S)/, '$1');
// Trim trailing whitespace.
html = html.replace(/\s+$/, '');
// Keep track of how much time we've used.
var timeOutMillis = this.timeOutMillis_;
var startMillis = timeOutMillis ? goog.now() : 0;
// Handles concatenation of the result and required line breaks.
var buffer = new goog.format.HtmlPrettyPrinter.Buffer();
// Declare these for efficiency since we access them in a loop.
var tokenRegex = goog.format.HtmlPrettyPrinter.TOKEN_REGEX_;
var nonPpTags = goog.format.HtmlPrettyPrinter.NON_PRETTY_PRINTED_TAGS_;
var blockTags = goog.format.HtmlPrettyPrinter.BLOCK_TAGS_;
var breaksFlowTags = goog.format.HtmlPrettyPrinter.BREAKS_FLOW_TAGS_;
var emptyTags = goog.format.HtmlPrettyPrinter.EMPTY_TAGS_;
// Used to verify we're making progress through our regex tokenization.
var lastIndex = 0;
// Use this to track non-pretty-printed tags and childen.
var nonPpTagStack = [];
// Loop through each matched token.
var match;
while (match = tokenRegex.exec(html)) {
// Get token.
var token = match[0];
// Is this token a tag? match.length == 3 for tags, 1 for all others.
if (match.length == 3) {
var tagName = match[2];
if (tagName) {
tagName = tagName.toLowerCase();
}
// Non-pretty-printed tags?
if (nonPpTags.hasOwnProperty(tagName)) {
// End tag?
if (match[1] == '/') {
// Do we have a matching start tag?
var stackSize = nonPpTagStack.length;
var startTagName = stackSize ? nonPpTagStack[stackSize - 1] : null;
if (startTagName == tagName) {
// End of non-pretty-printed block. Line break after.
nonPpTagStack.pop();
buffer.pushToken(false, token, !nonPpTagStack.length);
} else {
// Malformed HTML. No line breaks.
buffer.pushToken(false, token, false);
}
} else {
// Start of non-pretty-printed block. Line break before.
buffer.pushToken(!nonPpTagStack.length, token, false);
nonPpTagStack.push(tagName);
}
} else if (nonPpTagStack.length) {
// Inside non-pretty-printed block, no new line breaks.
buffer.pushToken(false, token, false);
} else if (blockTags.hasOwnProperty(tagName)) {
// Put line break before start block and after end block tags.
var isEmpty = emptyTags.hasOwnProperty(tagName);
var isEndTag = match[1] == '/';
buffer.pushToken(isEmpty || !isEndTag, token, isEmpty || isEndTag);
} else if (breaksFlowTags.hasOwnProperty(tagName)) {
var isEmpty = emptyTags.hasOwnProperty(tagName);
var isEndTag = match[1] == '/';
// Put line break after end flow-breaking tags.
buffer.pushToken(false, token, isEndTag || isEmpty);
} else {
// All other tags, no line break.
buffer.pushToken(false, token, false);
}
} else {
// Non-tags, no line break.
buffer.pushToken(false, token, false);
}
// Double check that we're making progress.
var newLastIndex = tokenRegex.lastIndex;
if (!token || newLastIndex <= lastIndex) {
throw Error('Regex failed to make progress through source html.');
}
lastIndex = newLastIndex;
// Out of time?
if (timeOutMillis) {
if (goog.now() - startMillis > timeOutMillis) {
// Push unprocessed data as one big token and reset regex object.
buffer.pushToken(false, html.substring(tokenRegex.lastIndex), false);
tokenRegex.lastIndex = 0;
break;
}
}
}
// Ensure we end in a line break.
buffer.lineBreak();
// Construct result string.
var result = String(buffer);
// Length should be original length plus # line breaks added.
var expectedLength = html.length + buffer.breakCount;
if (result.length != expectedLength) {
throw Error('Lost data pretty printing html.');
}
return result;
};
/**
* This class is a buffer to which we push our output. It tracks line breaks to
* make sure we don't add unnecessary ones.
* @constructor
* @final
*/
goog.format.HtmlPrettyPrinter.Buffer = function() {
/**
* Tokens to be output in #toString.
* @type {goog.string.StringBuffer}
* @private
*/
this.out_ = new goog.string.StringBuffer();
};
/**
* Tracks number of line breaks added.
* @type {number}
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.breakCount = 0;
/**
* Tracks if we are at the start of a new line.
* @type {boolean}
* @private
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.isBeginningOfNewLine_ = true;
/**
* Tracks if we need a new line before the next token.
* @type {boolean}
* @private
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.needsNewLine_ = false;
/**
* Adds token and necessary line breaks to output buffer.
* @param {boolean} breakBefore If true, add line break before token if
* necessary.
* @param {string} token Token to push.
* @param {boolean} breakAfter If true, add line break after token if
* necessary.
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.pushToken = function(
breakBefore, token, breakAfter) {
// If this token needs a preceeding line break, and
// we haven't already added a line break, and
// this token does not start with a line break,
// then add line break.
// Due to FF3.0 bug with lists, we don't insert a /n
// right before </ul>. See bug 1520665.
if ((this.needsNewLine_ || breakBefore) &&
!/^\r?\n/.test(token) &&
!/\/ul/i.test(token)) {
this.lineBreak();
}
// Token.
this.out_.append(token);
// Remember if this string ended with a line break so we know we don't have to
// insert another one before the next token.
this.isBeginningOfNewLine_ = /\r?\n$/.test(token);
// Remember if this token requires a line break after it. We don't insert it
// here because we might not have to if the next token starts with a line
// break.
this.needsNewLine_ = breakAfter && !this.isBeginningOfNewLine_;
};
/**
* Append line break if we need one.
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.lineBreak = function() {
if (!this.isBeginningOfNewLine_) {
this.out_.append('\n');
++this.breakCount;
}
};
/**
* @return {string} String representation of tokens.
* @override
*/
goog.format.HtmlPrettyPrinter.Buffer.prototype.toString = function() {
return this.out_.toString();
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.format.HtmlPrettyPrinter
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.format.HtmlPrettyPrinterTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,205 @@
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.format.HtmlPrettyPrinterTest');
goog.setTestOnly('goog.format.HtmlPrettyPrinterTest');
goog.require('goog.format.HtmlPrettyPrinter');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
var COMPLEX_HTML = '<!DOCTYPE root-element [SYSTEM OR PUBLIC FPI] "uri" [' +
'<!-- internal declarations -->]>' +
'<html><head><title>My HTML</title><!-- my comment --></head>' +
'<body><h1>My Header</h1>My text.<br><b>My bold text.</b><hr>' +
'<pre>My\npreformatted <br> HTML.</pre>5 < 10</body>' +
'</html>';
var mockClock;
var mockClockTicks;
function setUp() {
mockClockTicks = 0;
mockClock = new goog.testing.MockClock();
mockClock.getCurrentTime = function() {
return mockClockTicks++;
};
mockClock.install();
}
function tearDown() {
if (mockClock) {
mockClock.uninstall();
}
}
function testSimpleHtml() {
var actual = goog.format.HtmlPrettyPrinter.format('<br><b>bold</b>');
assertEquals('<br>\n<b>bold</b>\n', actual);
assertEquals(actual, goog.format.HtmlPrettyPrinter.format(actual));
}
function testSimpleHtmlMixedCase() {
var actual = goog.format.HtmlPrettyPrinter.format('<BR><b>bold</b>');
assertEquals('<BR>\n<b>bold</b>\n', actual);
assertEquals(actual, goog.format.HtmlPrettyPrinter.format(actual));
}
function testComplexHtml() {
var actual = goog.format.HtmlPrettyPrinter.format(COMPLEX_HTML);
var expected = '<!DOCTYPE root-element [SYSTEM OR PUBLIC FPI] "uri" [' +
'<!-- internal declarations -->]>\n' +
'<html>\n' +
'<head>\n' +
'<title>My HTML</title>\n' +
'<!-- my comment -->' +
'</head>\n' +
'<body>\n' +
'<h1>My Header</h1>\n' +
'My text.<br>\n' +
'<b>My bold text.</b>\n' +
'<hr>\n' +
'<pre>My\npreformatted <br> HTML.</pre>\n' +
'5 < 10' +
'</body>\n' +
'</html>\n';
assertEquals(expected, actual);
assertEquals(actual, goog.format.HtmlPrettyPrinter.format(actual));
}
function testTimeout() {
var pp = new goog.format.HtmlPrettyPrinter(3);
var actual = pp.format(COMPLEX_HTML);
var expected = '<!DOCTYPE root-element [SYSTEM OR PUBLIC FPI] "uri" [' +
'<!-- internal declarations -->]>\n' +
'<html>\n' +
'<head><title>My HTML</title><!-- my comment --></head>' +
'<body><h1>My Header</h1>My text.<br><b>My bold text.</b><hr>' +
'<pre>My\npreformatted <br> HTML.</pre>5 < 10</body>' +
'</html>\n';
assertEquals(expected, actual);
}
function testKeepLeadingIndent() {
var original = ' <b>Bold</b> <i>Ital</i> ';
var expected = ' <b>Bold</b> <i>Ital</i>\n';
assertEquals(expected, goog.format.HtmlPrettyPrinter.format(original));
}
function testTrimLeadingLineBreaks() {
var original = '\n \t\r\n \n <b>Bold</b> <i>Ital</i> ';
var expected = ' <b>Bold</b> <i>Ital</i>\n';
assertEquals(expected, goog.format.HtmlPrettyPrinter.format(original));
}
function testExtraLines() {
var original = '<br>\ntombrat';
assertEquals(original + '\n', goog.format.HtmlPrettyPrinter.format(original));
}
function testCrlf() {
var original = '<br>\r\none\r\ntwo<br>';
assertEquals(original + '\n', goog.format.HtmlPrettyPrinter.format(original));
}
function testEndInLineBreak() {
assertEquals('foo\n', goog.format.HtmlPrettyPrinter.format('foo'));
assertEquals('foo\n', goog.format.HtmlPrettyPrinter.format('foo\n'));
assertEquals('foo\n', goog.format.HtmlPrettyPrinter.format('foo\n\n'));
assertEquals('foo<br>\n', goog.format.HtmlPrettyPrinter.format('foo<br>'));
assertEquals('foo<br>\n', goog.format.HtmlPrettyPrinter.format('foo<br>\n'));
}
function testTable() {
var original = '<table>' +
'<tr><td>one.one</td><td>one.two</td></tr>' +
'<tr><td>two.one</td><td>two.two</td></tr>' +
'</table>';
var expected = '<table>\n' +
'<tr>\n<td>one.one</td>\n<td>one.two</td>\n</tr>\n' +
'<tr>\n<td>two.one</td>\n<td>two.two</td>\n</tr>\n' +
'</table>\n';
assertEquals(expected, goog.format.HtmlPrettyPrinter.format(original));
}
/**
* We have a sanity check in HtmlPrettyPrinter to make sure the regex index
* advances after every match. We should never hit this, but we include it on
* the chance there is some corner case where the pattern would match but not
* process a new token. It's not generally a good idea to break the
* implementation to test behavior, but this is the easiest way to mimic a
* bad internal state.
*/
function testRegexMakesProgress() {
var original = goog.format.HtmlPrettyPrinter.TOKEN_REGEX_;
try {
// This regex matches \B, an index between 2 word characters, so the regex
// index does not advance when matching this.
goog.format.HtmlPrettyPrinter.TOKEN_REGEX_ =
/(?:\B|<!--.*?-->|<!.*?>|<(\/?)(\w+)[^>]*>|[^<]+|<)/g;
// It would work on this string.
assertEquals('f o o\n', goog.format.HtmlPrettyPrinter.format('f o o'));
// But not this one.
var ex = assertThrows('should have failed for invalid regex - endless loop',
goog.partial(goog.format.HtmlPrettyPrinter.format, COMPLEX_HTML));
assertEquals('Regex failed to make progress through source html.',
ex.message);
} finally {
goog.format.HtmlPrettyPrinter.TOKEN_REGEX_ = original;
}
}
/**
* FF3.0 doesn't like \n between </li> and </ul>. See bug 1520665.
*/
function testLists() {
var original = '<ul><li>one</li><ul><li>two</li></UL><li>three</li></ul>';
var expected =
'<ul><li>one</li>\n<ul><li>two</li></UL>\n<li>three</li></ul>\n';
assertEquals(expected, goog.format.HtmlPrettyPrinter.format(original));
}
/**
* We have a sanity check in HtmlPrettyPrinter to make sure the regex fully
* tokenizes the string. We should never hit this, but we include it on the
* chance there is some corner case where the pattern would miss a section of
* original string. It's not generally a good idea to break the
* implementation to test behavior, but this is the easiest way to mimic a
* bad internal state.
*/
function testAvoidDataLoss() {
var original = goog.format.HtmlPrettyPrinter.TOKEN_REGEX_;
try {
// This regex does not match stranded '<' characters, so does not fully
// tokenize the string.
goog.format.HtmlPrettyPrinter.TOKEN_REGEX_ =
/(?:<!--.*?-->|<!.*?>|<(\/?)(\w+)[^>]*>|[^<]+)/g;
// It would work on this string.
assertEquals('foo\n', goog.format.HtmlPrettyPrinter.format('foo'));
// But not this one.
var ex = assertThrows('should have failed for invalid regex - data loss',
goog.partial(goog.format.HtmlPrettyPrinter.format, COMPLEX_HTML));
assertEquals('Lost data pretty printing html.', ex.message);
} finally {
goog.format.HtmlPrettyPrinter.TOKEN_REGEX_ = original;
}
}
@@ -0,0 +1,256 @@
// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides functions to parse and manipulate internationalized
* email addresses. This is useful in the context of Email Address
* Internationalization (EAI) as defined by RFC6530.
*
*/
goog.provide('goog.format.InternationalizedEmailAddress');
goog.require('goog.format.EmailAddress');
goog.require('goog.string');
/**
* Formats an email address string for display, and allows for extraction of
* the individual components of the address.
* @param {string=} opt_address The email address.
* @param {string=} opt_name The name associated with the email address.
* @constructor
* @extends {goog.format.EmailAddress}
*/
goog.format.InternationalizedEmailAddress = function(opt_address, opt_name) {
goog.format.InternationalizedEmailAddress.base(
this, 'constructor', opt_address, opt_name);
};
goog.inherits(
goog.format.InternationalizedEmailAddress, goog.format.EmailAddress);
/**
* A string representing the RegExp for the local part of an EAI email address.
* @private
*/
goog.format.InternationalizedEmailAddress.EAI_LOCAL_PART_REGEXP_STR_ =
'((?!\\s)[+a-zA-Z0-9_.!#$%&\'*\\/=?^`{|}~\u0080-\uFFFFFF-])+';
/**
* A string representing the RegExp for a label in the domain part of an EAI
* email address.
* @private
*/
goog.format.InternationalizedEmailAddress.EAI_LABEL_CHAR_REGEXP_STR_ =
'(?!\\s)[a-zA-Z0-9\u0080-\u3001\u3003-\uFF0D\uFF0F-\uFF60\uFF62-\uFFFFFF-]';
/**
* A string representing the RegExp for the domain part of an EAI email address.
* @private
*/
goog.format.InternationalizedEmailAddress.EAI_DOMAIN_PART_REGEXP_STR_ =
// A unicode character (ASCII or Unicode excluding periods)
'(' + goog.format.InternationalizedEmailAddress.EAI_LABEL_CHAR_REGEXP_STR_ +
// Such character 1+ times, followed by a Unicode period. All 1+ times.
'+[\\.\\uFF0E\\u3002\\uFF61])+' +
// And same thing but without a period in the end
goog.format.InternationalizedEmailAddress.EAI_LABEL_CHAR_REGEXP_STR_ +
'{2,63}';
/**
* Match string for address separators. This list is the result of the
* discussion in b/16241003.
* @type {string}
* @private
*/
goog.format.InternationalizedEmailAddress.ADDRESS_SEPARATORS_ =
',' + // U+002C ( , ) COMMA
';' + // U+003B ( ; ) SEMICOLON
'\u055D' + // ( ՝ ) ARMENIAN COMMA
'\u060C' + // ( ، ) ARABIC COMMA
'\u1363' + // ( ፣ ) ETHIOPIC COMMA
'\u1802' + // ( ᠂ ) MONGOLIAN COMMA
'\u1808' + // ( ᠈ ) MONGOLIAN MANCHU COMMA
'\u2E41' + // ( ⹁ ) REVERSED COMMA
'\u3001' + // ( 、 ) IDEOGRAPHIC COMMA
'\uFF0C' + // ( ) FULLWIDTH COMMA
'\u061B' + // ( ‎؛‎ ) ARABIC SEMICOLON
'\u1364' + // ( ፤ ) ETHIOPIC SEMICOLON
'\uFF1B' + // ( ) FULLWIDTH SEMICOLON
'\uFF64' + // ( 、 ) HALFWIDTH IDEOGRAPHIC COMMA
'\u104A'; // ( ၊ ) MYANMAR SIGN LITTLE SECTION
/**
* Match string for characters that, when in a display name, require it to be
* quoted.
* @type {string}
* @private
*/
goog.format.InternationalizedEmailAddress.CHARS_REQUIRE_QUOTES_ =
goog.format.EmailAddress.SPECIAL_CHARS +
goog.format.InternationalizedEmailAddress.ADDRESS_SEPARATORS_;
/**
* A RegExp to match the local part of an EAI email address.
* @private {!RegExp}
*/
goog.format.InternationalizedEmailAddress.EAI_LOCAL_PART_ =
new RegExp('^' +
goog.format.InternationalizedEmailAddress.EAI_LOCAL_PART_REGEXP_STR_ +
'$');
/**
* A RegExp to match the domain part of an EAI email address.
* @private {!RegExp}
*/
goog.format.InternationalizedEmailAddress.EAI_DOMAIN_PART_ =
new RegExp('^' +
goog.format.InternationalizedEmailAddress.EAI_DOMAIN_PART_REGEXP_STR_ +
'$');
/**
* A RegExp to match an EAI email address.
* @private {!RegExp}
*/
goog.format.InternationalizedEmailAddress.EAI_EMAIL_ADDRESS_ =
new RegExp('^' +
goog.format.InternationalizedEmailAddress.EAI_LOCAL_PART_REGEXP_STR_ +
'@' +
goog.format.InternationalizedEmailAddress.EAI_DOMAIN_PART_REGEXP_STR_ +
'$');
/**
* Checks if the provided string is a valid local part (part before the '@') of
* an EAI email address.
* @param {string} str The local part to check.
* @return {boolean} Whether the provided string is a valid local part.
*/
goog.format.InternationalizedEmailAddress.isValidLocalPartSpec = function(str) {
if (!goog.isDefAndNotNull(str)) {
return false;
}
return goog.format.InternationalizedEmailAddress.EAI_LOCAL_PART_.test(str);
};
/**
* Checks if the provided string is a valid domain part (part after the '@') of
* an EAI email address.
* @param {string} str The domain part to check.
* @return {boolean} Whether the provided string is a valid domain part.
*/
goog.format.InternationalizedEmailAddress.isValidDomainPartSpec =
function(str) {
if (!goog.isDefAndNotNull(str)) {
return false;
}
return goog.format.InternationalizedEmailAddress.EAI_DOMAIN_PART_.test(str);
};
/** @override */
goog.format.InternationalizedEmailAddress.prototype.isValid = function() {
return goog.format.InternationalizedEmailAddress.isValidAddrSpec(
this.address);
};
/**
* Checks if the provided string is a valid email address. Supports both
* simple email addresses (address specs) and addresses that contain display
* names.
* @param {string} str The email address to check.
* @return {boolean} Whether the provided string is a valid address.
*/
goog.format.InternationalizedEmailAddress.isValidAddress = function(str) {
if (!goog.isDefAndNotNull(str)) {
return false;
}
return goog.format.InternationalizedEmailAddress.parse(str).isValid();
};
/**
* Checks if the provided string is a valid address spec (local@domain.com).
* @param {string} str The email address to check.
* @return {boolean} Whether the provided string is a valid address spec.
*/
goog.format.InternationalizedEmailAddress.isValidAddrSpec = function(str) {
if (!goog.isDefAndNotNull(str)) {
return false;
}
// This is a fairly naive implementation, but it covers 99% of use cases.
// For more details, see http://en.wikipedia.org/wiki/Email_address#Syntax
return goog.format.InternationalizedEmailAddress.EAI_EMAIL_ADDRESS_.test(str);
};
/**
* Parses a string containing email addresses of the form
* "name" &lt;address&gt; into an array of email addresses.
* @param {string} str The address list.
* @return {!Array<!goog.format.EmailAddress>} The parsed emails.
*/
goog.format.InternationalizedEmailAddress.parseList = function(str) {
return goog.format.EmailAddress.parseListInternal(
str, goog.format.InternationalizedEmailAddress.parse,
goog.format.InternationalizedEmailAddress.isAddressSeparator);
};
/**
* Parses an email address of the form "name" &lt;address&gt; into
* an email address.
* @param {string} addr The address string.
* @return {!goog.format.EmailAddress} The parsed address.
*/
goog.format.InternationalizedEmailAddress.parse = function(addr) {
return goog.format.EmailAddress.parseInternal(
addr, goog.format.InternationalizedEmailAddress);
};
/**
* @param {string} ch The character to test.
* @return {boolean} Whether the provided character is an address separator.
*/
goog.format.InternationalizedEmailAddress.isAddressSeparator = function(ch) {
return goog.string.contains(
goog.format.InternationalizedEmailAddress.ADDRESS_SEPARATORS_, ch);
};
/**
* Return the address in a standard format:
* - remove extra spaces.
* - Surround name with quotes if it contains special characters.
* @return {string} The cleaned address.
* @override
*/
goog.format.InternationalizedEmailAddress.prototype.toString = function() {
return this.toStringInternal(
goog.format.InternationalizedEmailAddress.CHARS_REQUIRE_QUOTES_);
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2014 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.format.InternationalizedEmailAddress
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.format.InternationalizedEmailAddressTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,335 @@
// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.format.InternationalizedEmailAddressTest');
goog.setTestOnly('goog.format.InternationalizedEmailAddressTest');
goog.require('goog.array');
goog.require('goog.format.InternationalizedEmailAddress');
goog.require('goog.testing.jsunit');
/**
* Asserts that the given validation function generates the expected outcome for
* a set of expected valid and a second set of expected invalid addresses.
* containing the specified address strings, irrespective of their order.
* @param {function(string):boolean} testFunc Validation function to be tested.
* @param {!Array<string>} valid List of addresses that should be valid.
* @param {!Array<string>} invalid List of addresses that should be invalid.
* @private
*/
function doIsValidTest(testFunc, valid, invalid) {
goog.array.forEach(valid, function(str) {
assertTrue('"' + str + '" should be valid.', testFunc(str));
});
goog.array.forEach(invalid, function(str) {
assertFalse('"' + str + '" should be invalid.', testFunc(str));
});
}
/**
* Asserts that parsing the inputString produces a list of email addresses
* containing the specified address strings, irrespective of their order.
* @param {string} inputString A raw address list.
* @param {!Array<string>} expectedList The expected results.
* @param {string=} opt_message An assertion message.
* @return {string} the resulting email address objects.
*/
function assertParsedList(inputString, expectedList, opt_message) {
var message = opt_message || 'Should parse address correctly';
var result = goog.format.InternationalizedEmailAddress.parseList(inputString);
assertEquals(
'Should have correct # of addresses', expectedList.length, result.length);
for (var i = 0; i < expectedList.length; ++i) {
assertEquals(message, expectedList[i], result[i].getAddress());
}
return result;
}
function testParseList() {
// Test only the new cases added by EAI (other cases covered in parent
// class test)
assertParsedList('<me.みんあ@me.xn--l8jtg9b>', ['me.みんあ@me.xn--l8jtg9b']);
}
function testIsEaiValid() {
var valid = [
'e@b.eu',
'<a.b+foo@c.com>',
'eric <e@b.com>',
'"e" <e@b.com>',
'a@FOO.MUSEUM',
'bla@b.co.ac.uk',
'bla@a.b.com',
'o\'hara@gm.com',
'plus+is+allowed@gmail.com',
'!/#$%&\'*+-=~|`{}?^_@expample.com',
'confirm-bhk=modulo.org@yahoogroups.com',
'み.ん-あ@みんあ.みんあ',
'みんあ@test.com',
'test@test.みんあ',
'test@みんあ.com',
'me.みんあ@me.xn--l8jtg9b',
'みんあ@me.xn--l8jtg9b',
'fullwidthfullstop@sld' + '\uff0e' + 'tld',
'ideographicfullstop@sld' + '\u3002' + 'tld',
'halfwidthideographicfullstop@sld' + '\uff61' + 'tld'
];
var invalid = [
null,
undefined,
'e',
'',
'e @c.com',
'a@b',
'foo.com',
'foo@c..com',
'test@gma=il.com',
'aaa@gmail',
'has some spaces@gmail.com',
'has@three@at@signs.com',
'@no-local-part.com'
];
doIsValidTest(
goog.format.InternationalizedEmailAddress.isValidAddress, valid, invalid);
}
function testIsValidLocalPart() {
var valid = [
'e',
'a.b+foo',
'o\'hara',
'user+someone',
'!/#$%&\'*+-=~|`{}?^_',
'confirm-bhk=modulo.org',
'me.みんあ',
'みんあ'
];
var invalid = [
null,
undefined,
'A@b@c',
'a"b(c)d,e:f;g<h>i[j\\k]l',
'just"not"right',
'this is"not\\allowed',
'this\\ still\"not\\\\allowed',
'has some spaces'
];
doIsValidTest(goog.format.InternationalizedEmailAddress.isValidLocalPartSpec,
valid, invalid);
}
function testIsValidDomainPart() {
var valid = [
'example.com',
'dept.example.org',
'long.domain.with.lots.of.dots',
'me.xn--l8jtg9b',
'me.みんあ',
'sld.looooooongtld',
'sld' + '\uff0e' + 'tld',
'sld' + '\u3002' + 'tld',
'sld' + '\uff61' + 'tld'
];
var invalid = [
null,
undefined,
'',
'@has.an.at.sign',
'..has.leading.dots',
'gma=il.com',
'DoesNotHaveADot',
'aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeffffffffffgggggggggg'
];
doIsValidTest(goog.format.InternationalizedEmailAddress.isValidDomainPartSpec,
valid, invalid);
}
function testparseListWithAdditionalSeparators() {
assertParsedList('<foo@gmail.com>\u055D <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+055D');
assertParsedList('<foo@gmail.com>\u055D <bar@gmail.com>\u055D',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+055D');
assertParsedList('<foo@gmail.com>\u060C <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+060C');
assertParsedList('<foo@gmail.com>\u060C <bar@gmail.com>\u060C',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+060C');
assertParsedList('<foo@gmail.com>\u1363 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+1363');
assertParsedList('<foo@gmail.com>\u1363 <bar@gmail.com>\u1363',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+1363');
assertParsedList('<foo@gmail.com>\u1802 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+1802');
assertParsedList('<foo@gmail.com>\u1802 <bar@gmail.com>\u1802',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+1802');
assertParsedList('<foo@gmail.com>\u1808 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+1808');
assertParsedList('<foo@gmail.com>\u1808 <bar@gmail.com>\u1808',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+1808');
assertParsedList('<foo@gmail.com>\u2E41 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+2E41');
assertParsedList('<foo@gmail.com>\u2E41 <bar@gmail.com>\u2E41',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+2E41');
assertParsedList('<foo@gmail.com>\u3001 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+3001');
assertParsedList('<foo@gmail.com>\u3001 <bar@gmail.com>\u3001',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+3001');
assertParsedList('<foo@gmail.com>\uFF0C <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+FF0C');
assertParsedList('<foo@gmail.com>\uFF0C <bar@gmail.com>\uFF0C',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+FF0C');
assertParsedList('<foo@gmail.com>\u0613 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+0613');
assertParsedList('<foo@gmail.com>\u0613 <bar@gmail.com>\u0613',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+0613');
assertParsedList('<foo@gmail.com>\u1364 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+1364');
assertParsedList('<foo@gmail.com>\u1364 <bar@gmail.com>\u1364',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+1364');
assertParsedList('<foo@gmail.com>\uFF1B <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+FF1B');
assertParsedList('<foo@gmail.com>\uFF1B <bar@gmail.com>\uFF1B',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+FF1B');
assertParsedList('<foo@gmail.com>\uFF64 <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+FF64');
assertParsedList('<foo@gmail.com>\uFF64 <bar@gmail.com>\uFF64',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+FF64');
assertParsedList('<foo@gmail.com>\u104A <bar@gmail.com>',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with U+104A');
assertParsedList('<foo@gmail.com>\u104A <bar@gmail.com>\u104A',
['foo@gmail.com', 'bar@gmail.com'],
'Failed to parse 2 email addresses with trailing U+104A');
}
function testToString() {
var f = function(str) {
return goog.format.InternationalizedEmailAddress.parse(str).toString();
};
// No modification.
assertEquals('JOHN Doe <john@gmail.com>',
f('JOHN Doe <john@gmail.com>'));
// Extra spaces.
assertEquals('JOHN Doe <john@gmail.com>',
f(' JOHN Doe <john@gmail.com> '));
// No name.
assertEquals('john@gmail.com', f('<john@gmail.com>'));
assertEquals('john@gmail.com', f('john@gmail.com'));
// No address.
assertEquals('JOHN Doe', f('JOHN Doe <>'));
// Already quoted.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('"JOHN, Doe" <john@gmail.com>'));
// Needless quotes.
assertEquals('JOHN Doe <john@gmail.com>',
f('"JOHN Doe" <john@gmail.com>'));
// Not quoted-string, but has double quotes.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, "Doe" <john@gmail.com>'));
// No special characters other than quotes.
assertEquals('JOHN Doe <john@gmail.com>',
f('JOHN "Doe" <john@gmail.com>'));
// Escaped quotes are also removed.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, \\"Doe\\" <john@gmail.com>'));
// Characters that require quoting for the display name.
assertEquals('"JOHN, Doe" <john@gmail.com>',
f('JOHN, Doe <john@gmail.com>'));
assertEquals('"JOHN; Doe" <john@gmail.com>',
f('JOHN; Doe <john@gmail.com>'));
assertEquals('"JOHN\u055D Doe" <john@gmail.com>',
f('JOHN\u055D Doe <john@gmail.com>'));
assertEquals('"JOHN\u060C Doe" <john@gmail.com>',
f('JOHN\u060C Doe <john@gmail.com>'));
assertEquals('"JOHN\u1363 Doe" <john@gmail.com>',
f('JOHN\u1363 Doe <john@gmail.com>'));
assertEquals('"JOHN\u1802 Doe" <john@gmail.com>',
f('JOHN\u1802 Doe <john@gmail.com>'));
assertEquals('"JOHN\u1808 Doe" <john@gmail.com>',
f('JOHN\u1808 Doe <john@gmail.com>'));
assertEquals('"JOHN\u2E41 Doe" <john@gmail.com>',
f('JOHN\u2E41 Doe <john@gmail.com>'));
assertEquals('"JOHN\u3001 Doe" <john@gmail.com>',
f('JOHN\u3001 Doe <john@gmail.com>'));
assertEquals('"JOHN\uFF0C Doe" <john@gmail.com>',
f('JOHN\uFF0C Doe <john@gmail.com>'));
assertEquals('"JOHN\u061B Doe" <john@gmail.com>',
f('JOHN\u061B Doe <john@gmail.com>'));
assertEquals('"JOHN\u1364 Doe" <john@gmail.com>',
f('JOHN\u1364 Doe <john@gmail.com>'));
assertEquals('"JOHN\uFF1B Doe" <john@gmail.com>',
f('JOHN\uFF1B Doe <john@gmail.com>'));
assertEquals('"JOHN\uFF64 Doe" <john@gmail.com>',
f('JOHN\uFF64 Doe <john@gmail.com>'));
assertEquals('"JOHN(Johnny) Doe" <john@gmail.com>',
f('JOHN(Johnny) Doe <john@gmail.com>'));
assertEquals('"JOHN[Johnny] Doe" <john@gmail.com>',
f('JOHN[Johnny] Doe <john@gmail.com>'));
assertEquals('"JOHN@work Doe" <john@gmail.com>',
f('JOHN@work Doe <john@gmail.com>'));
assertEquals('"JOHN:theking Doe" <john@gmail.com>',
f('JOHN:theking Doe <john@gmail.com>'));
assertEquals('"JOHN\\\\ Doe" <john@gmail.com>',
f('JOHN\\ Doe <john@gmail.com>'));
assertEquals('"JOHN.com Doe" <john@gmail.com>',
f('JOHN.com Doe <john@gmail.com>'));
}
@@ -0,0 +1,414 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Creates a string of a JSON object, properly indented for
* display.
*
*/
goog.provide('goog.format.JsonPrettyPrinter');
goog.provide('goog.format.JsonPrettyPrinter.HtmlDelimiters');
goog.provide('goog.format.JsonPrettyPrinter.TextDelimiters');
goog.require('goog.json');
goog.require('goog.json.Serializer');
goog.require('goog.string');
goog.require('goog.string.StringBuffer');
goog.require('goog.string.format');
/**
* Formats a JSON object as a string, properly indented for display. Supports
* displaying the string as text or html. Users can also specify their own
* set of delimiters for different environments. For example, the JSON object:
*
* <code>{"a": 1, "b": {"c": null, "d": true, "e": [1, 2]}}</code>
*
* Will be displayed like this:
*
* <code>{
* "a": 1,
* "b": {
* "c": null,
* "d": true,
* "e": [
* 1,
* 2
* ]
* }
* }</code>
* @param {goog.format.JsonPrettyPrinter.TextDelimiters} delimiters Container
* for the various strings to use to delimit objects, arrays, newlines, and
* other pieces of the output.
* @constructor
*/
goog.format.JsonPrettyPrinter = function(delimiters) {
/**
* The set of characters to use as delimiters.
* @type {goog.format.JsonPrettyPrinter.TextDelimiters}
* @private
*/
this.delimiters_ = delimiters ||
new goog.format.JsonPrettyPrinter.TextDelimiters();
/**
* Used to serialize property names and values.
* @type {goog.json.Serializer}
* @private
*/
this.jsonSerializer_ = new goog.json.Serializer();
};
/**
* Formats a JSON object as a string, properly indented for display.
* @param {*} json The object to pretty print. It could be a JSON object, a
* string representing a JSON object, or any other type.
* @return {string} Returns a string of the JSON object, properly indented for
* display.
*/
goog.format.JsonPrettyPrinter.prototype.format = function(json) {
// If input is undefined, null, or empty, return an empty string.
if (!goog.isDefAndNotNull(json)) {
return '';
}
if (goog.isString(json)) {
if (goog.string.isEmptyOrWhitespace(json)) {
return '';
}
// Try to coerce a string into a JSON object.
json = goog.json.parse(json);
}
var outputBuffer = new goog.string.StringBuffer();
this.printObject_(json, outputBuffer, 0);
return outputBuffer.toString();
};
/**
* Formats a property value based on the type of the propery.
* @param {*} val The object to format.
* @param {goog.string.StringBuffer} outputBuffer The buffer to write the
* response to.
* @param {number} indent The number of spaces to indent each line of the
* output.
* @private
*/
goog.format.JsonPrettyPrinter.prototype.printObject_ = function(val,
outputBuffer, indent) {
var typeOf = goog.typeOf(val);
switch (typeOf) {
case 'null':
case 'boolean':
case 'number':
case 'string':
// "null", "boolean", "number" and "string" properties are printed
// directly to the output.
this.printValue_(
/** @type {null|string|boolean|number} */ (val),
typeOf, outputBuffer);
break;
case 'array':
// Example of how an array looks when formatted
// (using the default delimiters):
// [
// 1,
// 2,
// 3
// ]
outputBuffer.append(this.delimiters_.arrayStart);
var i = 0;
// Iterate through the array and format each element.
for (i = 0; i < val.length; i++) {
if (i > 0) {
// There are multiple elements, add a comma to separate them.
outputBuffer.append(this.delimiters_.propertySeparator);
}
outputBuffer.append(this.delimiters_.lineBreak);
this.printSpaces_(indent + this.delimiters_.indent, outputBuffer);
this.printObject_(val[i], outputBuffer,
indent + this.delimiters_.indent);
}
// If there are no properties in this object, don't put a line break
// between the beginning "[" and ending "]", so the output of an empty
// array looks like <code>[]</code>.
if (i > 0) {
outputBuffer.append(this.delimiters_.lineBreak);
this.printSpaces_(indent, outputBuffer);
}
outputBuffer.append(this.delimiters_.arrayEnd);
break;
case 'object':
// Example of how an object looks when formatted
// (using the default delimiters):
// {
// "a": 1,
// "b": 2,
// "c": "3"
// }
outputBuffer.append(this.delimiters_.objectStart);
var propertyCount = 0;
// Iterate through the object and display each property.
for (var name in val) {
if (!val.hasOwnProperty(name)) {
continue;
}
if (propertyCount > 0) {
// There are multiple properties, add a comma to separate them.
outputBuffer.append(this.delimiters_.propertySeparator);
}
outputBuffer.append(this.delimiters_.lineBreak);
this.printSpaces_(indent + this.delimiters_.indent, outputBuffer);
this.printName_(name, outputBuffer);
outputBuffer.append(this.delimiters_.nameValueSeparator,
this.delimiters_.space);
this.printObject_(val[name], outputBuffer,
indent + this.delimiters_.indent);
propertyCount++;
}
// If there are no properties in this object, don't put a line break
// between the beginning "{" and ending "}", so the output of an empty
// object looks like <code>{}</code>.
if (propertyCount > 0) {
outputBuffer.append(this.delimiters_.lineBreak);
this.printSpaces_(indent, outputBuffer);
}
outputBuffer.append(this.delimiters_.objectEnd);
break;
// Other types, such as "function", aren't expected in JSON, and their
// behavior is undefined. In these cases, just print an empty string to the
// output buffer. This allows the pretty printer to continue while still
// outputing well-formed JSON.
default:
this.printValue_('', 'unknown', outputBuffer);
}
};
/**
* Prints a property name to the output.
* @param {string} name The property name.
* @param {goog.string.StringBuffer} outputBuffer The buffer to write the
* response to.
* @private
*/
goog.format.JsonPrettyPrinter.prototype.printName_ = function(name,
outputBuffer) {
outputBuffer.append(this.delimiters_.preName,
this.jsonSerializer_.serialize(name), this.delimiters_.postName);
};
/**
* Prints a property name to the output.
* @param {string|boolean|number|null} val The property value.
* @param {string} typeOf The type of the value. Used to customize
* value-specific css in the display. This allows clients to distinguish
* between different types in css. For example, the client may define two
* classes: "goog-jsonprettyprinter-propertyvalue-string" and
* "goog-jsonprettyprinter-propertyvalue-number" to assign a different color
* to string and number values.
* @param {goog.string.StringBuffer} outputBuffer The buffer to write the
* response to.
* @private
*/
goog.format.JsonPrettyPrinter.prototype.printValue_ = function(val,
typeOf, outputBuffer) {
outputBuffer.append(goog.string.format(this.delimiters_.preValue, typeOf),
this.jsonSerializer_.serialize(val),
goog.string.format(this.delimiters_.postValue, typeOf));
};
/**
* Print a number of space characters to the output.
* @param {number} indent The number of spaces to indent the line.
* @param {goog.string.StringBuffer} outputBuffer The buffer to write the
* response to.
* @private
*/
goog.format.JsonPrettyPrinter.prototype.printSpaces_ = function(indent,
outputBuffer) {
outputBuffer.append(goog.string.repeat(this.delimiters_.space, indent));
};
/**
* A container for the delimiting characters used to display the JSON string
* to a text display. Each delimiter is a publicly accessible property of
* the object, which makes it easy to tweak delimiters to specific environments.
* @constructor
*/
goog.format.JsonPrettyPrinter.TextDelimiters = function() {
};
/**
* Represents a space character in the output. Used to indent properties a
* certain number of spaces, and to separate property names from property
* values.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.space = ' ';
/**
* Represents a newline character in the output. Used to begin a new line.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.lineBreak = '\n';
/**
* Represents the start of an object in the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.objectStart = '{';
/**
* Represents the end of an object in the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.objectEnd = '}';
/**
* Represents the start of an array in the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.arrayStart = '[';
/**
* Represents the end of an array in the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.arrayEnd = ']';
/**
* Represents the string used to separate properties in the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.propertySeparator = ',';
/**
* Represents the string used to separate property names from property values in
* the output.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.nameValueSeparator = ':';
/**
* A string that's placed before a property name in the output. Useful for
* wrapping a property name in an html tag.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.preName = '';
/**
* A string that's placed after a property name in the output. Useful for
* wrapping a property name in an html tag.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.postName = '';
/**
* A string that's placed before a property value in the output. Useful for
* wrapping a property value in an html tag.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.preValue = '';
/**
* A string that's placed after a property value in the output. Useful for
* wrapping a property value in an html tag.
* @type {string}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.postValue = '';
/**
* Represents the number of spaces to indent each sub-property of the JSON.
* @type {number}
*/
goog.format.JsonPrettyPrinter.TextDelimiters.prototype.indent = 2;
/**
* A container for the delimiting characters used to display the JSON string
* to an HTML <code>&lt;pre&gt;</code> or <code>&lt;code&gt;</code> element.
* @constructor
* @extends {goog.format.JsonPrettyPrinter.TextDelimiters}
* @final
*/
goog.format.JsonPrettyPrinter.HtmlDelimiters = function() {
goog.format.JsonPrettyPrinter.TextDelimiters.call(this);
};
goog.inherits(goog.format.JsonPrettyPrinter.HtmlDelimiters,
goog.format.JsonPrettyPrinter.TextDelimiters);
/**
* A <code>span</code> tag thats placed before a property name. Used to style
* property names with CSS.
* @type {string}
* @override
*/
goog.format.JsonPrettyPrinter.HtmlDelimiters.prototype.preName =
'<span class="' +
goog.getCssName('goog-jsonprettyprinter-propertyname') +
'">';
/**
* A closing <code>span</code> tag that's placed after a property name.
* @type {string}
* @override
*/
goog.format.JsonPrettyPrinter.HtmlDelimiters.prototype.postName = '</span>';
/**
* A <code>span</code> tag thats placed before a property value. Used to style
* property value with CSS. The span tag's class is in the format
* goog-jsonprettyprinter-propertyvalue-{TYPE}, where {TYPE} is the JavaScript
* type of the object (the {TYPE} parameter is obtained from goog.typeOf). This
* can be used to style different value types.
* @type {string}
* @override
*/
goog.format.JsonPrettyPrinter.HtmlDelimiters.prototype.preValue =
'<span class="' +
goog.getCssName('goog-jsonprettyprinter-propertyvalue') +
'-%s">';
/**
* A closing <code>span</code> tag that's placed after a property value.
* @type {string}
* @override
*/
goog.format.JsonPrettyPrinter.HtmlDelimiters.prototype.postValue = '</span>';
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.format.JsonPrettyPrinter
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.format.JsonPrettyPrinterTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,109 @@
// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
goog.provide('goog.format.JsonPrettyPrinterTest');
goog.setTestOnly('goog.format.JsonPrettyPrinterTest');
goog.require('goog.format.JsonPrettyPrinter');
goog.require('goog.testing.jsunit');
var formatter;
function setUp() {
formatter = new goog.format.JsonPrettyPrinter();
}
function testUndefined() {
assertEquals('', formatter.format());
}
function testNull() {
assertEquals('', formatter.format(null));
}
function testBoolean() {
assertEquals('true', formatter.format(true));
}
function testNumber() {
assertEquals('1', formatter.format(1));
}
function testEmptyString() {
assertEquals('', formatter.format(''));
}
function testWhitespaceString() {
assertEquals('', formatter.format(' '));
}
function testString() {
assertEquals('{}', formatter.format('{}'));
}
function testEmptyArray() {
assertEquals('[]', formatter.format([]));
}
function testArrayOneElement() {
assertEquals('[\n 1\n]', formatter.format([1]));
}
function testArrayMultipleElements() {
assertEquals('[\n 1,\n 2,\n 3\n]', formatter.format([1, 2, 3]));
}
function testFunction() {
assertEquals('{\n "a": "1",\n "b": ""\n}',
formatter.format({'a': '1', 'b': function() { return null; }}));
}
function testObject() {
assertEquals('{}', formatter.format({}));
}
function testObjectMultipleProperties() {
assertEquals('{\n "a": null,\n "b": true,\n "c": 1,\n "d": "d",\n "e":' +
' [\n 1,\n 2,\n 3\n ],\n "f": {\n "g": 1,\n "h": "h"\n' +
' }\n}',
formatter.format({'a': null, 'b': true, 'c': 1, 'd': 'd', 'e': [1, 2, 3],
'f': {'g': 1, 'h': 'h'}}));
}
function testHtmlDelimiters() {
var htmlFormatter = new goog.format.JsonPrettyPrinter(
new goog.format.JsonPrettyPrinter.HtmlDelimiters());
assertEquals('{\n <span class="goog-jsonprettyprinter-propertyname">"a"</s' +
'pan>: <span class="goog-jsonprettyprinter-propertyvalue-number">1</spa' +
'n>,\n <span class="goog-jsonprettyprinter-propertyname">"b"</span>: <' +
'span class="goog-jsonprettyprinter-propertyvalue-string">"2"</span>,\n' +
' <span class="goog-jsonprettyprinter-propertyname">"c"</span>: <span ' +
'class="goog-jsonprettyprinter-propertyvalue-unknown">""</span>\n}',
htmlFormatter.format({'a': 1, 'b': '2', 'c': function() {}}));
}