master
Raw Download raw file
  1
  2  'use strict';
  3
  4  /***
  5   *
  6   * @package Inflections
  7   * @dependency string
  8   * @description Pluralization similar to ActiveSupport including uncountable words and acronyms. Humanized and URL-friendly strings.
  9   *
 10   ***/
 11
 12  /***
 13   * String module
 14   *
 15   ***/
 16
 17
 18  var plurals      = [],
 19      singulars    = [],
 20      uncountables = [],
 21      humans       = [],
 22      acronyms     = {},
 23      Downcased,
 24      Inflector;
 25
 26  function removeFromArray(arr, find) {
 27    var index = arr.indexOf(find);
 28    if(index > -1) {
 29      arr.splice(index, 1);
 30    }
 31  }
 32
 33  function removeFromUncountablesAndAddTo(arr, rule, replacement) {
 34    if(isString(rule)) {
 35      removeFromArray(uncountables, rule);
 36    }
 37    removeFromArray(uncountables, replacement);
 38    arr.unshift({ rule: rule, replacement: replacement })
 39  }
 40
 41  function paramMatchesType(param, type) {
 42    return param == type || param == 'all' || !param;
 43  }
 44
 45  function isUncountable(word) {
 46    return uncountables.some(function(uncountable) {
 47      return new regexp('\\b' + uncountable + '$', 'i').test(word);
 48    });
 49  }
 50
 51  function inflect(word, pluralize) {
 52    word = isString(word) ? word.toString() : '';
 53    if(word.isBlank() || isUncountable(word)) {
 54      return word;
 55    } else {
 56      return runReplacements(word, pluralize ? plurals : singulars);
 57    }
 58  }
 59
 60  function runReplacements(word, table) {
 61    iterateOverObject(table, function(i, inflection) {
 62      if(word.match(inflection.rule)) {
 63        word = word.replace(inflection.rule, inflection.replacement);
 64        return false;
 65      }
 66    });
 67    return word;
 68  }
 69
 70  function capitalize(word) {
 71    return word.replace(/^\W*[a-z]/, function(w){
 72      return w.toUpperCase();
 73    });
 74  }
 75
 76  Inflector = {
 77
 78    /*
 79     * Specifies a new acronym. An acronym must be specified as it will appear in a camelized string.  An underscore
 80     * string that contains the acronym will retain the acronym when passed to %camelize%, %humanize%, or %titleize%.
 81     * A camelized string that contains the acronym will maintain the acronym when titleized or humanized, and will
 82     * convert the acronym into a non-delimited single lowercase word when passed to String#underscore.
 83     *
 84     * Examples:
 85     *   String.Inflector.acronym('HTML')
 86     *   'html'.titleize()     -> 'HTML'
 87     *   'html'.camelize()     -> 'HTML'
 88     *   'MyHTML'.underscore() -> 'my_html'
 89     *
 90     * The acronym, however, must occur as a delimited unit and not be part of another word for conversions to recognize it:
 91     *
 92     *   String.Inflector.acronym('HTTP')
 93     *   'my_http_delimited'.camelize() -> 'MyHTTPDelimited'
 94     *   'https'.camelize()             -> 'Https', not 'HTTPs'
 95     *   'HTTPS'.underscore()           -> 'http_s', not 'https'
 96     *
 97     *   String.Inflector.acronym('HTTPS')
 98     *   'https'.camelize()   -> 'HTTPS'
 99     *   'HTTPS'.underscore() -> 'https'
100     *
101     * Note: Acronyms that are passed to %pluralize% will no longer be recognized, since the acronym will not occur as
102     * a delimited unit in the pluralized result. To work around this, you must specify the pluralized form as an
103     * acronym as well:
104     *
105     *    String.Inflector.acronym('API')
106     *    'api'.pluralize().camelize() -> 'Apis'
107     *
108     *    String.Inflector.acronym('APIs')
109     *    'api'.pluralize().camelize() -> 'APIs'
110     *
111     * %acronym% may be used to specify any word that contains an acronym or otherwise needs to maintain a non-standard
112     * capitalization. The only restriction is that the word must begin with a capital letter.
113     *
114     * Examples:
115     *   String.Inflector.acronym('RESTful')
116     *   'RESTful'.underscore()           -> 'restful'
117     *   'RESTfulController'.underscore() -> 'restful_controller'
118     *   'RESTfulController'.titleize()   -> 'RESTful Controller'
119     *   'restful'.camelize()             -> 'RESTful'
120     *   'restful_controller'.camelize()  -> 'RESTfulController'
121     *
122     *   String.Inflector.acronym('McDonald')
123     *   'McDonald'.underscore() -> 'mcdonald'
124     *   'mcdonald'.camelize()   -> 'McDonald'
125     */
126    'acronym': function(word) {
127      acronyms[word.toLowerCase()] = word;
128      var all = object.keys(acronyms).map(function(key) {
129        return acronyms[key];
130      });
131      Inflector.acronymRegExp = regexp(all.join('|'), 'g');
132    },
133
134    /*
135     * Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
136     * The replacement should always be a string that may include references to the matched data from the rule.
137     */
138    'plural': function(rule, replacement) {
139      removeFromUncountablesAndAddTo(plurals, rule, replacement);
140    },
141
142    /*
143     * Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
144     * The replacement should always be a string that may include references to the matched data from the rule.
145     */
146    'singular': function(rule, replacement) {
147      removeFromUncountablesAndAddTo(singulars, rule, replacement);
148    },
149
150    /*
151     * Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
152     * for strings, not regular expressions. You simply pass the irregular in singular and plural form.
153     *
154     * Examples:
155     *   String.Inflector.irregular('octopus', 'octopi')
156     *   String.Inflector.irregular('person', 'people')
157     */
158    'irregular': function(singular, plural) {
159      var singularFirst      = singular.first(),
160          singularRest       = singular.from(1),
161          pluralFirst        = plural.first(),
162          pluralRest         = plural.from(1),
163          pluralFirstUpper   = pluralFirst.toUpperCase(),
164          pluralFirstLower   = pluralFirst.toLowerCase(),
165          singularFirstUpper = singularFirst.toUpperCase(),
166          singularFirstLower = singularFirst.toLowerCase();
167      removeFromArray(uncountables, singular);
168      removeFromArray(uncountables, plural);
169      if(singularFirstUpper == pluralFirstUpper) {
170        Inflector.plural(new regexp('({1}){2}$'.assign(singularFirst, singularRest), 'i'), '$1' + pluralRest);
171        Inflector.plural(new regexp('({1}){2}$'.assign(pluralFirst, pluralRest), 'i'), '$1' + pluralRest);
172        Inflector.singular(new regexp('({1}){2}$'.assign(pluralFirst, pluralRest), 'i'), '$1' + singularRest);
173      } else {
174        Inflector.plural(new regexp('{1}{2}$'.assign(singularFirstUpper, singularRest)), pluralFirstUpper + pluralRest);
175        Inflector.plural(new regexp('{1}{2}$'.assign(singularFirstLower, singularRest)), pluralFirstLower + pluralRest);
176        Inflector.plural(new regexp('{1}{2}$'.assign(pluralFirstUpper, pluralRest)), pluralFirstUpper + pluralRest);
177        Inflector.plural(new regexp('{1}{2}$'.assign(pluralFirstLower, pluralRest)), pluralFirstLower + pluralRest);
178        Inflector.singular(new regexp('{1}{2}$'.assign(pluralFirstUpper, pluralRest)), singularFirstUpper + singularRest);
179        Inflector.singular(new regexp('{1}{2}$'.assign(pluralFirstLower, pluralRest)), singularFirstLower + singularRest);
180      }
181    },
182
183    /*
184     * Add uncountable words that shouldn't be attempted inflected.
185     *
186     * Examples:
187     *   String.Inflector.uncountable('money')
188     *   String.Inflector.uncountable('money', 'information')
189     *   String.Inflector.uncountable(['money', 'information', 'rice'])
190     */
191    'uncountable': function(first) {
192      var add = array.isArray(first) ? first : multiArgs(arguments);
193      uncountables = uncountables.concat(add);
194    },
195
196    /*
197     * Specifies a humanized form of a string by a regular expression rule or by a string mapping.
198     * When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
199     * When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
200     *
201     * Examples:
202     *   String.Inflector.human(/_cnt$/i, '_count')
203     *   String.Inflector.human('legacy_col_person_name', 'Name')
204     */
205    'human': function(rule, replacement) {
206      humans.unshift({ rule: rule, replacement: replacement })
207    },
208
209
210    /*
211     * Clears the loaded inflections within a given scope (default is 'all').
212     * Options are: 'all', 'plurals', 'singulars', 'uncountables', 'humans'.
213     *
214     * Examples:
215     *   String.Inflector.clear('all')
216     *   String.Inflector.clear('plurals')
217     */
218    'clear': function(type) {
219      if(paramMatchesType(type, 'singulars'))    singulars    = [];
220      if(paramMatchesType(type, 'plurals'))      plurals      = [];
221      if(paramMatchesType(type, 'uncountables')) uncountables = [];
222      if(paramMatchesType(type, 'humans'))       humans       = [];
223      if(paramMatchesType(type, 'acronyms'))     acronyms     = {};
224    }
225
226  };
227
228  Downcased = [
229    'and', 'or', 'nor', 'a', 'an', 'the', 'so', 'but', 'to', 'of', 'at',
230    'by', 'from', 'into', 'on', 'onto', 'off', 'out', 'in', 'over',
231    'with', 'for'
232  ];
233
234  Inflector.plural(/$/, 's');
235  Inflector.plural(/s$/gi, 's');
236  Inflector.plural(/(ax|test)is$/gi, '$1es');
237  Inflector.plural(/(octop|vir|fung|foc|radi|alumn)(i|us)$/gi, '$1i');
238  Inflector.plural(/(census|alias|status)$/gi, '$1es');
239  Inflector.plural(/(bu)s$/gi, '$1ses');
240  Inflector.plural(/(buffal|tomat)o$/gi, '$1oes');
241  Inflector.plural(/([ti])um$/gi, '$1a');
242  Inflector.plural(/([ti])a$/gi, '$1a');
243  Inflector.plural(/sis$/gi, 'ses');
244  Inflector.plural(/f+e?$/gi, 'ves');
245  Inflector.plural(/(cuff|roof)$/gi, '$1s');
246  Inflector.plural(/([ht]ive)$/gi, '$1s');
247  Inflector.plural(/([^aeiouy]o)$/gi, '$1es');
248  Inflector.plural(/([^aeiouy]|qu)y$/gi, '$1ies');
249  Inflector.plural(/(x|ch|ss|sh)$/gi, '$1es');
250  Inflector.plural(/(matr|vert|ind)(?:ix|ex)$/gi, '$1ices');
251  Inflector.plural(/([ml])ouse$/gi, '$1ice');
252  Inflector.plural(/([ml])ice$/gi, '$1ice');
253  Inflector.plural(/^(ox)$/gi, '$1en');
254  Inflector.plural(/^(oxen)$/gi, '$1');
255  Inflector.plural(/(quiz)$/gi, '$1zes');
256  Inflector.plural(/(phot|cant|hom|zer|pian|portic|pr|quart|kimon)o$/gi, '$1os');
257  Inflector.plural(/(craft)$/gi, '$1');
258  Inflector.plural(/([ft])[eo]{2}(th?)$/gi, '$1ee$2');
259
260  Inflector.singular(/s$/gi, '');
261  Inflector.singular(/([pst][aiu]s)$/gi, '$1');
262  Inflector.singular(/([aeiouy])ss$/gi, '$1ss');
263  Inflector.singular(/(n)ews$/gi, '$1ews');
264  Inflector.singular(/([ti])a$/gi, '$1um');
265  Inflector.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/gi, '$1$2sis');
266  Inflector.singular(/(^analy)ses$/gi, '$1sis');
267  Inflector.singular(/(i)(f|ves)$/i, '$1fe');
268  Inflector.singular(/([aeolr]f?)(f|ves)$/i, '$1f');
269  Inflector.singular(/([ht]ive)s$/gi, '$1');
270  Inflector.singular(/([^aeiouy]|qu)ies$/gi, '$1y');
271  Inflector.singular(/(s)eries$/gi, '$1eries');
272  Inflector.singular(/(m)ovies$/gi, '$1ovie');
273  Inflector.singular(/(x|ch|ss|sh)es$/gi, '$1');
274  Inflector.singular(/([ml])(ous|ic)e$/gi, '$1ouse');
275  Inflector.singular(/(bus)(es)?$/gi, '$1');
276  Inflector.singular(/(o)es$/gi, '$1');
277  Inflector.singular(/(shoe)s?$/gi, '$1');
278  Inflector.singular(/(cris|ax|test)[ie]s$/gi, '$1is');
279  Inflector.singular(/(octop|vir|fung|foc|radi|alumn)(i|us)$/gi, '$1us');
280  Inflector.singular(/(census|alias|status)(es)?$/gi, '$1');
281  Inflector.singular(/^(ox)(en)?/gi, '$1');
282  Inflector.singular(/(vert|ind)(ex|ices)$/gi, '$1ex');
283  Inflector.singular(/(matr)(ix|ices)$/gi, '$1ix');
284  Inflector.singular(/(quiz)(zes)?$/gi, '$1');
285  Inflector.singular(/(database)s?$/gi, '$1');
286  Inflector.singular(/ee(th?)$/gi, 'oo$1');
287
288  Inflector.irregular('person', 'people');
289  Inflector.irregular('man', 'men');
290  Inflector.irregular('child', 'children');
291  Inflector.irregular('sex', 'sexes');
292  Inflector.irregular('move', 'moves');
293  Inflector.irregular('save', 'saves');
294  Inflector.irregular('cow', 'kine');
295  Inflector.irregular('goose', 'geese');
296  Inflector.irregular('zombie', 'zombies');
297
298  Inflector.uncountable('equipment,information,rice,money,species,series,fish,sheep,jeans'.split(','));
299
300
301  extend(string, true, true, {
302
303    /***
304     * @method pluralize()
305     * @returns String
306     * @short Returns the plural form of the word in the string.
307     * @example
308     *
309     *   'post'.pluralize()         -> 'posts'
310     *   'octopus'.pluralize()      -> 'octopi'
311     *   'sheep'.pluralize()        -> 'sheep'
312     *   'words'.pluralize()        -> 'words'
313     *   'CamelOctopus'.pluralize() -> 'CamelOctopi'
314     *
315     ***/
316    'pluralize': function() {
317      return inflect(this, true);
318    },
319
320    /***
321     * @method singularize()
322     * @returns String
323     * @short The reverse of String#pluralize. Returns the singular form of a word in a string.
324     * @example
325     *
326     *   'posts'.singularize()       -> 'post'
327     *   'octopi'.singularize()      -> 'octopus'
328     *   'sheep'.singularize()       -> 'sheep'
329     *   'word'.singularize()        -> 'word'
330     *   'CamelOctopi'.singularize() -> 'CamelOctopus'
331     *
332     ***/
333    'singularize': function() {
334      return inflect(this, false);
335    },
336
337    /***
338     * @method humanize()
339     * @returns String
340     * @short Creates a human readable string.
341     * @extra Capitalizes the first word and turns underscores into spaces and strips a trailing '_id', if any. Like String#titleize, this is meant for creating pretty output.
342     * @example
343     *
344     *   'employee_salary'.humanize() -> 'Employee salary'
345     *   'author_id'.humanize()       -> 'Author'
346     *
347     ***/
348    'humanize': function() {
349      var str = runReplacements(this, humans), acronym;
350      str = str.replace(/_id$/g, '');
351      str = str.replace(/(_)?([a-z\d]*)/gi, function(match, _, word){
352        acronym = hasOwnProperty(acronyms, word) ? acronyms[word] : null;
353        return (_ ? ' ' : '') + (acronym || word.toLowerCase());
354      });
355      return capitalize(str);
356    },
357
358    /***
359     * @method titleize()
360     * @returns String
361     * @short Creates a title version of the string.
362     * @extra Capitalizes all the words and replaces some characters in the string to create a nicer looking title. String#titleize is meant for creating pretty output.
363     * @example
364     *
365     *   'man from the boondocks'.titleize() -> 'Man from the Boondocks'
366     *   'x-men: the last stand'.titleize() -> 'X Men: The Last Stand'
367     *   'TheManWithoutAPast'.titleize() -> 'The Man Without a Past'
368     *   'raiders_of_the_lost_ark'.titleize() -> 'Raiders of the Lost Ark'
369     *
370     ***/
371    'titleize': function() {
372      var fullStopPunctuation = /[.:;!]$/, hasPunctuation, lastHadPunctuation, isFirstOrLast;
373      return this.spacify().humanize().words(function(word, index, words) {
374        hasPunctuation = fullStopPunctuation.test(word);
375        isFirstOrLast = index == 0 || index == words.length - 1 || hasPunctuation || lastHadPunctuation;
376        lastHadPunctuation = hasPunctuation;
377        if(isFirstOrLast || Downcased.indexOf(word) === -1) {
378          return capitalize(word);
379        } else {
380          return word;
381        }
382      }).join(' ');
383    },
384
385    /***
386     * @method parameterize()
387     * @returns String
388     * @short Replaces special characters in a string so that it may be used as part of a pretty URL.
389     * @example
390     *
391     *   'hell, no!'.parameterize() -> 'hell-no'
392     *
393     ***/
394    'parameterize': function(separator) {
395      var str = this;
396      if(separator === undefined) separator = '-';
397      if(str.normalize) {
398        str = str.normalize();
399      }
400      str = str.replace(/[^a-z0-9\-_]+/gi, separator)
401      if(separator) {
402        str = str.replace(new regexp('^{sep}+|{sep}+$|({sep}){sep}+'.assign({ 'sep': escapeRegExp(separator) }), 'g'), '$1');
403      }
404      return encodeURI(str.toLowerCase());
405    }
406
407  });
408
409  string.Inflector = Inflector;
410  string.Inflector.acronyms = acronyms;
411