Created
April 21, 2014 11:59
-
-
Save mieko/11140735 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Dirt | |
# This is the simplest inflector that would be useful. It doesn't have a list | |
# of any preloaded irregulars (e.g., Person -> People). | |
# | |
# Use Dirt::Inflector::irregular! to add your special cases. | |
# | |
# You can create an Inflector with #new, as expected, but the system has | |
# a default inflector, which is accessed via .instance. Calling methods | |
# on the class delegates to the default instance. | |
# | |
# We don't add methods to string, as everything in the world does that, and | |
# we don't want to clash. | |
class Inflector | |
# Regular expressions that convert plural forms into single forms, in | |
# priority order. | |
SINGULARIZE_REGEXPS = { | |
/(ss)es$/ => '\1', | |
/(ss)$/ => '\1', | |
/(s)$/ => '', | |
} | |
# Regular expressions that convert singular forms into plural forms, in | |
# priority order | |
PLURALIZE_REGEXPS = { | |
/(es)$/ => '\1', | |
/(s)$/ => '\1es', | |
/(.)$/ => '\1s', | |
} | |
def initialize | |
# We keep two inverted copies so we don't have to linearly | |
# search by value in one case or the other. | |
@irr_singular = {} | |
@irr_plural = {} | |
# @acronym entries have values equal to their keys. This is a trick | |
# stolen from Rails to avoid a lot of `@acronyms.include?` tests. | |
@acronyms = {} | |
# {/regexp/ => sub-pattern} replacement map for humanized words | |
@humans = {} | |
@singularize_regexps = SINGULARIZE_REGEXPS.dup | |
@pluralize_regexps = PLURALIZE_REGEXPS.dup | |
end | |
# Accepts: | |
# irregular!(singular, plural) | |
# or | |
# irregular!(singular1 => plural1, | |
# singularN => PluralN, ...) | |
def irregular!(singular, plural = nil) | |
# Were we passed the hash syntax? | |
if plural.nil? && singular.is_a?(Hash) | |
singular.each do |k,v| | |
irregular!(k, v) | |
end | |
else | |
@irr_singular[singular] = plural | |
@irr_plural[plural] = singular | |
end | |
end | |
def plural!(pattern, replacement) | |
@uncountable.delete(pattern) if pattern.is_a?(String) | |
@uncountable.delete(replacement) | |
@pluralize_regexps[pattern] = replacement | |
end | |
def singular!(pattern, replacement) | |
@uncountable.delete(pattern) if pattern.is_a?(String) | |
@uncountable.delete(replacement) | |
@singularize_regexps[pattern] = replacement | |
end | |
# When humanizing, if a word matches `regexp`, it'll be processed with | |
# `replacement`, a regexp replacement pattern. | |
def human!(regexp, replacement = nil) | |
if replacement.nil? && regexp.is_a?(Hash) | |
regexp.each do |k, v| | |
human!(k, v) | |
end | |
else | |
@humans[regexp] = replacement | |
end | |
end | |
# `term` is considered its own plural and single. For example: | |
# | |
# Inflector.uncountable!('fish') | |
# Inflector.pluralize('fish') # => 'fish' | |
# Inflector.singularize('fish') # => 'fish' | |
# | |
# # Note: the pseudo-pluralized form still gets the default behavior | |
# Inflector.singularize('fishes') # => 'fish' | |
def uncountable!(term) | |
irregular!(term => term) | |
end | |
# `term` is considered an acronym, and shouldnt be lowercased when | |
# camelized | |
def acronym!(term) | |
@acronyms[term] = term | |
@acronym_regexp = nil | |
end | |
# Applys the inflector methods in `args' to word, in-order. For example, | |
# the following two are identical: | |
# | |
# Inflector.pluralize(Inflector.underscore('User')) | |
# Inflector.chain('User', :pluralize, :underscore) | |
# | |
# This is useful as we don't mix in our methods into String. | |
def chain(word, *args) | |
args.each do |sym| | |
word = send(sym, *Array(word)) | |
end | |
word | |
end | |
# Returns the pluralized form of `word`, respecting exceptions specified | |
# with `irregurlar!` | |
def pluralize(word) | |
apply_inflections(word, @pluralize_regexps, @irr_plural) | |
end | |
# Returns the singlular form of `word`, respecting exceptions specified | |
# with `irregular!` | |
def singularize(word) | |
apply_inflections(word, @singularize_regexps, @irr_singular) | |
end | |
# Turns CamelCase into under_score_case. | |
def underscore(word) | |
# RE's ripped straight from Rails and made compatible with Opal. | |
word.gsub('::', '/') \ | |
.gsub(/(?:([A-Za-z\d])|^)(#{acronym_regexp})(?=\b|[^a-z])/) \ | |
{ "#{$1}#{$1 && '_'}#{$2.downcase}" } \ | |
.gsub(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') \ | |
.gsub(/([a-z\d])([A-Z])/,'\1_\2') \ | |
.gsub('-', '_') \ | |
.downcase | |
end | |
# Returns the "humanized english" version of an underscored word. | |
# Regexps that were added with 'human!' are taken into account, and | |
# the first one that matches is applied. | |
def humanize(word) | |
@humans.each do |re, rep| | |
if word.match(re) | |
word = word.gsub(re, rep) | |
break | |
end | |
end | |
word.gsub(/_id$/, "") \ | |
.gsub('_', ' ') \ | |
.gsub(/([a-z\d]*)/) \ | |
{ |match| @acronyms[match] || match.downcase } \ | |
.gsub(/^\w/) { $&.upcase } | |
end | |
# The inverse of underscore. | |
# some_thing -> SomeThing | |
# app/some_model -> App::SomeModel | |
def camelize(word) | |
word.sub(/^[a-z\d]*/) \ | |
{ @acronyms[$&] || humanize($&) } \ | |
.gsub(/([_\/][a-z\d]*)/) { |v| | |
o, t = v[0], v[1..-1] | |
o = '' if o == '_' | |
"#{o}#{@acronyms[t] || humanize(t)}" | |
}.gsub('/', '::') | |
end | |
alias_method :classify, :camelize | |
private | |
# Finds the first entry in regexp_map that matches word, and returns the | |
# substitution. Transformations in irregular_map are attempted first. | |
# Failing any applicable transformation, word itself is returned. | |
def apply_inflections(word, regexp_map, irregular_map) | |
return irreg if (irreg = irregular_map[word]) | |
regexp_map.each do |k, v| | |
if k.is_a?(Regexp) | |
return word.gsub(k, v) if word.match(k) | |
else | |
return v if word == k | |
end | |
end | |
return word | |
end | |
# Returns an OR-separated, escaped regexp that'll match any entry in | |
# the acronym list | |
def acronym_regexp | |
if @acronym_regexp.nil? | |
escaped = @acronyms.keys.map {|w| RegExp.escape(w)} | |
joined = escaped.join('|') | |
@acronym_regexp = /#{joined}/ | |
end | |
@acronym_regexp | |
end | |
public | |
# This is the trick that makes Inflector itself act as an instance. | |
# Method calls on Inflector are delegated to Inflector.instance, which | |
# is a normal instance, with its own state, etc. | |
class << self | |
def instance | |
@instance ||= new | |
end | |
def respond_to?(method) | |
super || instance.respond_to?(method) | |
end | |
def method_missing(method, *args) | |
if instance.respond_to?(method) | |
return instance.send(method, *args) | |
end | |
super | |
end | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment