ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
711 lines (624 loc) • 21.4 kB
JavaScript
/*$
* @name name.js
* @fileOverview This file has the implementation of the Name object
*
*/
/*globals G11n */
//* @protected
enyo.g11n.NamePriv = {
/*$ private
Returns an array of the auxiliary words found at the beginning of the
name string.
*/
_findPrefix: function _findPrefix(parts, hash, isAsian) {
var prefix, prefixLower, prefixArray, aux = [], i;
if (parts.length > 0 && hash) {
//enyo.log("_findPrefix: finding prefixes");
for ( i = parts.length; i > 0; i-- ) {
prefixArray = parts.slice(0, i);
prefix = prefixArray.join(isAsian ? '' : ' ');
prefixLower = prefix.toLowerCase();
prefixLower = prefixLower.replace(/[,\.]/g, ''); // ignore commas and periods
//enyo.log("_findPrefix: checking prefix: '" + prefixLower + "'");
if ( prefixLower in hash ) {
aux = aux.concat(isAsian ? prefix : prefixArray);
parts = parts.slice(i);
i = parts.length + 1;
//enyo.log("_findPrefix: Found prefix '" + prefix + "' New parts list is " + JSON.stringify(parts));
}
}
}
return aux;
},
/*$ private
* Returns true if any Latin letters are found in the string. Returns false
* if all the characters are non-Latin.
*/
_isEuroName: function _isEuroName(name) {
var c, i;
for (i = 0; i < name.length; i++) {
c = name.charAt(i);
if (!enyo.g11n.Char.isIdeo(c) &&
!enyo.g11n.Char.isPunct(c) &&
!enyo.g11n.Char.isSpace(c)) {
return true;
}
}
return false;
},
/*$ private
* find the last instance of 'and' in the name
* @param {Array} parts
* @returns {integer}
*/
_findLastConjunction: function _findLastConjunction(parts, locale) {
var conjunctionIndex = -1, index, part, rb;
rb = new enyo.g11n.Resources({
locale: locale,
root: enyo.g11n.Utils._getEnyoRoot() + "/name",
});
for (index = 0; index < parts.length; index++) {
part = parts[index];
if (typeof(part) === 'string') {
if ("and" === part.toLowerCase() || "or" === part.toLowerCase() || "&" === part || "+" === part) {
conjunctionIndex = index;
}
if ((rb.$L({key: "and1", value: "and"}).toLowerCase() === part.toLowerCase()) ||
(rb.$L({key: "and2", value: "and"}).toLowerCase() === part.toLowerCase()) ||
(rb.$L({key: "or1", value: "or"}).toLowerCase() === part.toLowerCase()) ||
(rb.$L({key: "or2", value: "or"}).toLowerCase() === part.toLowerCase()) ||
("&" === part) ||
("+" === part)) {
conjunctionIndex = index;
//enyo.log("_findLastConjunction: found conjunction " + parts[index] + " at index " + index);
}
}
}
return conjunctionIndex;
},
_joinArrayOrString: function _joinArrayOrString(part) {
var i;
if (typeof(part) === 'object') {
for (i = 0; i < part.length; i++) {
part[i] = enyo.g11n.NamePriv._joinArrayOrString(part[i]);
}
return part.join(' ');
}
return part;
},
_shallowCopy: function _shallowCopy(from, to) {
var prop;
for ( prop in from ) {
if ( prop !== undefined && from[prop] ) {
to[prop] = from[prop];
}
}
}
};
//* @public
/**
Parses a personal name written in a free-form string. Returns a JavaScript
object with extracted name data in its properties.
* name (String): Name of a person to parse
* params (Object): Parameters controlling the parsing of the name
The returned object may contain the following properties:
* prefix: Any titles, such as "President" or "Dr.", or honorifics, such as
"Don" in Spanish or "Mister" in English, that precede the name.
* givenName: The given name(s) of the person, which is often unique to that
person within a family or group.
* familyName: The family name(s) of a person, which is shared with other
family members
* middleName: Auxiliary given name(s)
* suffix: Any suffixes that are attached to a name, such as "Jr." or "M.D."
in English, or honorifics like "-san" in Japanese.
Some properties (e.g., "middleName") may contain multiple names. The value
of a property with multiple names will be returned as a single string with
the original punctuation preserved. For example, if the name is "John Jacob
Jingleheimer Schmidt", there are two middle names and they will be returned
as the string: "Jacob Jingleheimer".
Suffixes may be optionally appended to names using a comma. If commas appear
in the original name, they will be preserved in the output of the suffix
property so that they can be reassembled again later by NameFmt.format().
For any titles or honorifics that are considered as whole, the name is
returned as a single string. For example, if the name to parse is "The Right
Honourable James Clawitter", the honorific would be returned as a prefix
with the whole string "The Right Honourable".
When a compound name is found in the name string, the conjunction is placed
in the givenName property. For example, "John and Mary Smith" gets parsed
into the following output:
{
givenName: "John and Mary",
familyName: "Smith"
}
This can be considered to be two names: "John Smith and Mary Smith".
Without the compound name rule, the words "and Mary" would be considered
middle names of the person "John Smith".
There are a few special cases in which a name will be parsed differently
from what the rules of the given locale would imply. If the name is composed
entirely of Asian characters, it is parsed as an Asian name, even in
non-Asian locales. If the locale is an Asian locale, and the name is
composed entirely of Latin alphabet characters, the name is parsed as a
generic Western name (using US/English rules). This way, Asian and western
names can be mixed in the same list, and they will all be parsed reasonably
correctly.
When a name cannot be parsed properly, the entire name will be placed
into the givenName property.
*/
enyo.g11n.Name = function(name, params) {
var locale, langInfo, nameInfo, langInfoEn, parts = [],
i, prefixArray, prefix, prefixLower,
suffixArray, suffix, suffixLower,
asianName, hpSuffix, conjunctionIndex;
if (!name || name === "") {
return this;
}
if (!params || !params.locale) {
locale = enyo.g11n.currentLocale();
} else if (typeof(params.locale) === 'string') {
locale = new enyo.g11n.Locale(params.locale);
} else {
locale = params.locale;
}
this.locale = locale; // save for later
// enyo.log("new Name: locale was determined to be " + locale.toString());
// construct a name object out of a json object that contains
// the parts already broken down
if (typeof(name) === 'object') {
this.prefix = name.prefix;
this.givenName = name.givenName;
this.middleName = name.middleName;
this.familyName = name.familyName;
this.suffix = name.suffix;
return this;
}
// else the name is a string that needs to be parsed.
// First break the name into parts, and extract all the parts
// that are not really part of the name
langInfoEn = enyo.g11n.Utils.getJsonFile({
root: enyo.g11n.Utils._getEnyoRoot(),
path: "name/data",
locale: new enyo.g11n.Locale("en")
});
langInfo = enyo.g11n.Utils.getJsonFile({
root: enyo.g11n.Utils._getEnyoRoot(),
path: "name/data",
locale: locale
});
// enyo.log("parsePersonalName: parsing name '" + name + "'");
// for DFISH-12905, pick off the part that the LDAP server automatically adds to our names in HP emails
i = name.search(/\s*[,\(\[\{<]/);
if (i !== -1) {
hpSuffix = name.substring(i);
hpSuffix = hpSuffix.replace(/\s+/g, ' '); // compress multiple whitespaces
suffixArray = hpSuffix.split(" ");
conjunctionIndex = enyo.g11n.NamePriv._findLastConjunction(suffixArray, locale);
if (conjunctionIndex > -1) {
// it's got conjunctions in it, so this is not really a suffix
hpSuffix = undefined;
} else {
name = name.substring(0,i);
}
}
if ( !langInfo || !langInfo.name || (langInfo.name.isAsianLocale && enyo.g11n.NamePriv._isEuroName(name))) {
// default to English if there is no info on the particular language, or if
// we are parsing a euro name in an asian locale
langInfo = langInfoEn;
}
nameInfo = langInfo.name;
if (!nameInfo.isAsianLocale || enyo.g11n.NamePriv._isEuroName(name)) {
name = name.replace(/\s+/g, ' '); // compress multiple whitespaces
parts = name.trim().split(' ');
asianName = false;
} else {
// all-asian names
name = name.replace(/\s+/g, ''); // eliminate all whitespaces
parts = name.trim().split('');
asianName = true;
}
// next, we are left with only name parts. Parse these
// according to the locale. Search for the various lengths of
// prefix in the name in the various tables. The tables are
// much longer than the name prefixes, so there are less
// iterations if we do it this way.
//enyo.log("parsePersonalName: parsing name '" + name + "'");
// check for prefixes
if (parts.length > 1) {
//enyo.log("parsePersonalName: finding prefixes");
for ( i = parts.length; i > 0; i-- ) {
prefixArray = parts.slice(0, i);
prefix = prefixArray.join(asianName ? '' : ' ');
prefixLower = prefix.toLowerCase();
prefixLower = prefixLower.replace(/[,\.]/g, ''); // ignore commas and periods
//enyo.log("checking prefix: '" + prefixLower + "'");
if ( (nameInfo.titles && enyo.indexOf(prefixLower, nameInfo.titles) > -1) ||
(nameInfo.honorifics && enyo.indexOf(prefixLower, nameInfo.honorifics) > -1) ) {
if ( this.prefix ) {
if ( !asianName ) {
this.prefix += ' ';
}
this.prefix += prefix;
} else {
this.prefix = prefix;
}
parts = parts.slice(i);
i = parts.length;
//enyo.log("Found prefix '" + prefix + "' New parts list is " + JSON.stringify(parts));
}
}
}
// check for suffixes
if (parts.length > 1) {
//enyo.log("parsePersonalName: finding suffixes");
for ( i = parts.length; i > 0; i-- ) {
suffixArray = parts.slice(-i);
suffix = suffixArray.join(asianName ? '' : ' ');
suffixLower = suffix.toLowerCase();
suffixLower = suffixLower.replace(/[,\.]/g, ''); // ignore commas and periods
//enyo.log("checking suffix: '" + suffixLower + "'");
if ( nameInfo.suffixes && enyo.indexOf(suffixLower, nameInfo.suffixes) > -1 ) {
if ( this.suffix ) {
if ( !asianName ) {
this.suffix = ' ' + this.suffix;
}
this.suffix = suffix + this.suffix;
} else {
this.suffix = suffix;
}
parts = parts.slice(0, parts.length-i);
//enyo.log("Found suffix '" + suffix + "' New parts list is " + JSON.stringify(parts));
i = parts.length;
}
}
}
if (hpSuffix) {
this.suffix = (this.suffix && this.suffix + hpSuffix) || hpSuffix;
}
// adjoin auxillary words to their headwords
if (parts.length > 1 && !asianName ) {
parts = this._adjoinAuxillaries(parts, nameInfo);
//enyo.log("parsePersonalName: parts is now " + JSON.stringify(parts));
}
if ( asianName ) {
this._parseAsianName(parts, nameInfo);
} else if (locale.language === "es") {
// in spain and mexico, we parse names differently than in the rest of the world because of the double family names
this._parseSpanishName(parts, locale);
} else {
this._parseNameDefaultLocale(parts, locale);
}
this._joinNameArrays();
// clean up
enyo.g11n.Utils.releaseAllJsonFiles();
return this;
};
//* @protected
enyo.g11n.Name.prototype = {
/*
* This is how names are parsed for Spanish names:
* 1
* F
*
* 1 2
* F L
*
* 1 2 3
* F L L
*
* 1 2 3 4
* F M L L
*
* 1 2 3 4 5
* F M M L L
*
* Unless there's one of { 'and', 'or', '&', '+' }, in which case it's:
* 1
* F
*
* 1 2
* F L
*
* 1 2 3
* F A F
* A L L
* F L L
*
* 1 2 3 4
* F A F L
* F F A F
*
* 1 2 3 4 5
* F A F L L
* F F A F L
* F F F A F
*/
_parseSpanishName: function (parts, locale) {
var conjunctionIndex;
if (parts.length === 1) {
if ( this.prefix || typeof(parts[0]) === 'object' ) {
this.familyName = parts[0];
} else {
this.givenName = parts[0];
}
} else if (parts.length === 2) {
//we do FL
this.givenName = parts[0];
this.familyName = parts[1];
} else if (parts.length === 3) {
conjunctionIndex = enyo.g11n.NamePriv._findLastConjunction(parts, locale);
//if there's an 'and' in the middle spot, put everything in the first name
if (conjunctionIndex === 1) {
this.givenName = parts;
} else {
//else, do FLL
this.givenName = parts[0];
this.familyName = parts.slice(1);
}
} else if (parts.length > 3) {
//there are at least 4 parts to this name
conjunctionIndex = enyo.g11n.NamePriv._findLastConjunction(parts, locale);
if (conjunctionIndex > 0) {
// if there's a conjunction that's not the first token, put everything up to and
// including the token after it into the first name, the last 2 tokens into
// the family name (if they exist) and everything else in to the middle name
// 0 1 2 3 4 5
// F A F
// F A F L
// F F A F
// F A F L L
// F F A F L
// F F F A F
// F A F M L L
// F F A F L L
// F F F A F L
// F F F F A F
this.givenName = parts.splice(0,conjunctionIndex+2);
if ( parts.length > 1 ) {
this.familyName = parts.splice(parts.length-2, 2);
if ( parts.length > 0 ) {
this.middleName = parts;
}
} else if ( parts.length === 1 ) {
this.familyName = parts[0];
}
} else {
this.givenName = parts.splice(0,1);
this.familyName = parts.splice(parts.length-2, 2);
this.middleName = parts;
}
}
},
/*
* This is how names are parsed for names by default (the English case):
*
* F stands for a first name
* M is a middle name
* L is a last name
* numbers are position
*
* 1
* F
*
* 1 2
* F L
*
* 1 2 3
* F M L
*
* 1 2 3 4
* F M M L
*
* 1 2 3 4 5
* F M M M L
*
* Unless there's one of { 'and', 'or', '&', '+' }, in which case it's:
* 1
* F
*
* 1 2
* F L
*
* 1 2 3
* F A F
*
* 1 2 3 4
* F A F L
* F F A F
*
* 1 2 3 4 5
* F A F M L
* F F A F L
* F F F A F
*/
/*$ private
* Helper function for enyo.g11n.Name constructor
*/
_parseNameDefaultLocale: function (parts, locale) {
var conjunctionIndex;
if (parts.length === 1) {
if ( this.prefix || typeof(parts[0]) === 'object' ) {
// already has a prefix, so assume it goes with the family name like "Dr. Roberts" or
// it is a name with auxillaries, which is almost always a family name
this.familyName = parts[0];
} else {
this.givenName = parts[0];
}
} else if (parts.length === 2) {
//we do FL
this.givenName = parts[0];
this.familyName = parts[1];
} else if (parts.length >= 3) {
//find the first instance of 'and' in the name
conjunctionIndex = enyo.g11n.NamePriv._findLastConjunction(parts, locale);
if (conjunctionIndex > 0) {
// if there's a conjunction that's not the first token, put everything up to and
// including the token after it into the first name, the last token into
// the family name (if it exists) and everything else in to the middle name
// 0 1 2 3 4 5
// F A F M M L
// F F A F M L
// F F F A F L
// F F F F A F
this.givenName = parts.slice(0,conjunctionIndex+2);
if ( conjunctionIndex + 1 < parts.length - 1 ) {
this.familyName = parts.splice(parts.length-1, 1);
if ( conjunctionIndex + 2 < parts.length - 1 ) {
this.middleName = parts.slice(conjunctionIndex + 2, parts.length - conjunctionIndex - 3);
}
}
} else {
this.givenName = parts[0];
this.middleName = parts.slice(1, parts.length-1);
this.familyName = parts[parts.length-1];
}
}
},
/*$ private
* Helper function for enyo.g11n.Name constructor
*/
_parseAsianName: function(parts, nameInfo) {
var familyNameArray = enyo.g11n.NamePriv._findPrefix(parts, nameInfo.knownFamilyNames, true);
if ( familyNameArray && familyNameArray.length > 0 ) {
this.familyName = familyNameArray.join('');
this.givenName = parts.slice(this.familyName.length).join('');
} else if ( this.suffix || this.prefix ) {
this.familyName = parts.join('');
} else {
this.givenName = parts.join('');
}
},
/*$ private
* Helper function for enyo.g11n.Name constructor
*/
_joinNameArrays: function _joinNameArrays() {
var prop;
for (prop in this) {
if (this[prop] !== undefined && typeof(this[prop]) === 'object' && this[prop] instanceof Array) {
this[prop] = enyo.g11n.NamePriv._joinArrayOrString(this[prop]);
}
}
},
/*$ private
* Helper function for enyo.g11n.Name constructor
* adjoin auxillary words to their head words
*/
_adjoinAuxillaries: function (parts, nameInfo) {
var start, i, prefixArray, prefix, prefixLower;
//enyo.log("_adjoinAuxillaries: finding and adjoining aux words in " + parts.join(' '));
if ( nameInfo.auxillaries && (parts.length > 2 || this.prefix) ) {
for ( start = 0; start < parts.length-1; start++ ) {
for ( i = parts.length; i > start; i-- ) {
prefixArray = parts.slice(start, i);
prefix = prefixArray.join(' ');
prefixLower = prefix.toLowerCase();
prefixLower = prefixLower.replace(/[,\.]/g, ''); // ignore commas and periods
//enyo.log("_adjoinAuxillaries: checking aux prefix: '" + prefixLower + "' which is " + start + " to " + i);
if ( prefixLower in nameInfo.auxillaries ) {
//enyo.log("Found! Old parts list is " + JSON.stringify(parts));
parts.splice(start, i+1-start, prefixArray.concat(parts[i]));
//enyo.log("_adjoinAuxillaries: Found! New parts list is " + JSON.stringify(parts));
i = start;
}
}
}
}
//enyo.log("_adjoinAuxillaries: done. Result is " + JSON.stringify(parts));
return parts;
},
//* @public
/**
Returns the portion of a person's family name that should be used for
sorting.
* locale (String): locale to use to decide the part to use
In English, we almost always sort by the first letter of the last
name--for example, the name "van der Heyden" would be sorted under "V".
Thus, if this function is called on a Name instance for someone with
that surname, the original string ("van der Heyden") will be returned
unmodified.
In other cultures, it is common to sort by the head word of a family
name containing auxiliaries like "van der". Returning to our example,
this function would return "Heyden" for Dutch, German, and other
Germanic languages.
If no value is specified, the locale defaults to the locale of the
current Name instance.
*/
getSortName: function (locale) {
var loc,
name,
auxillaries,
langInfo,
auxString,
nameInfo,
parts,
i;
// no name to sort by
if (!this.familyName) {
return undefined;
}
if (!locale) {
// default to the locale used to parse the name in the first place
loc = this.locale;
} else if (typeof(locale) === 'string') {
loc = new enyo.g11n.Locale(locale);
} else {
loc = locale;
}
// first break the name into parts
langInfo = enyo.g11n.Utils.getJsonFile({
root: enyo.g11n.Utils._getEnyoRoot(),
path: "name/data",
locale: loc
});
if ( !langInfo ) {
// default to English if there is no info on the particular locale
// this should never happen because you can't pick a language in the
// language picker that we don't already support
langInfo = enyo.g11n.Utils.getJsonFile({
root: enyo.g11n.Utils._getEnyoRoot(),
path: "name/data",
locale: new enyo.g11n.Locale("en")
});
}
nameInfo = langInfo.name;
if (nameInfo) {
if (nameInfo.sortByHeadWord) {
if (typeof(this.familyName) === 'string') {
name = this.familyName.replace(/\s+/g, ' '); // compress multiple whitespaces
parts = name.trim().split(' ');
} else {
// already split
parts = this.familyName;
}
auxillaries = enyo.g11n.NamePriv._findPrefix(parts, nameInfo.auxillaries, false);
if ( auxillaries && auxillaries.length > 0 ) {
if (typeof(this.familyName) === 'string') {
auxString = auxillaries.join(' ');
name = this.familyName.substring(auxString.length+1) + ', ' + auxString;
} else {
name = this.familyName.slice(auxillaries.length).join(' ') +
', ' +
this.familyName.slice(0,auxillaries.length).join(' ');
}
}
} else if (nameInfo.knownFamilyNames && this.familyName) {
parts = this.familyName.split('');
var familyNameArray = enyo.g11n.NamePriv._findPrefix(parts, nameInfo.knownFamilyNames, true);
name = "";
for (i = 0; i < familyNameArray.length; i++) {
name += (nameInfo.knownFamilyNames[familyNameArray[i]] || "");
}
}
}
// clean up
enyo.g11n.Utils.releaseAllJsonFiles();
return name || this.familyName;
},
//* @protected
/**
Returns a shallow copy of the current instance.
*/
clone: function () {
var other = new enyo.g11n.Name();
enyo.g11n.NamePriv._shallowCopy(this, other);
return other;
}
};