master
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