frontline 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +0 -0
- data/LICENSE +19 -0
- data/README.md +92 -0
- data/Rakefile +21 -0
- data/assets/ansi_up.js +143 -0
- data/assets/api.js +533 -0
- data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/assets/bootstrap/css/bootstrap.min.css +9 -0
- data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
- data/assets/bootstrap/js/bootstrap.min.js +6 -0
- data/assets/jquery.cookie.js +95 -0
- data/assets/jquery.js +6 -0
- data/assets/noty/jquery.noty.js +520 -0
- data/assets/noty/layouts/top.js +34 -0
- data/assets/noty/layouts/topRight.js +43 -0
- data/assets/noty/promise.js +432 -0
- data/assets/noty/themes/default.js +156 -0
- data/assets/select2-bootstrap.css +86 -0
- data/assets/select2/select2-spinner.gif +0 -0
- data/assets/select2/select2.css +615 -0
- data/assets/select2/select2.min.js +22 -0
- data/assets/select2/select2.png +0 -0
- data/assets/select2/select2x2.png +0 -0
- data/assets/typeahead.js-bootstrap.css +49 -0
- data/assets/typeahead.min.js +7 -0
- data/assets/ui.css +28 -0
- data/assets/xhr.js +19 -0
- data/bin/frontline +19 -0
- data/frontline.gemspec +31 -0
- data/images/0.png +0 -0
- data/lib/frontline.rb +23 -0
- data/lib/frontline/actions.rb +15 -0
- data/lib/frontline/app.rb +86 -0
- data/lib/frontline/controllers/controllers.rb +71 -0
- data/lib/frontline/controllers/index.rb +167 -0
- data/lib/frontline/controllers/models.rb +104 -0
- data/lib/frontline/controllers/sources.rb +11 -0
- data/lib/frontline/frontline.rb +11 -0
- data/lib/frontline/helpers.rb +179 -0
- data/lib/frontline/inflect.rb +183 -0
- data/lib/frontline/templates/controllers/controller.slim +27 -0
- data/lib/frontline/templates/controllers/index.slim +56 -0
- data/lib/frontline/templates/controllers/route.slim +17 -0
- data/lib/frontline/templates/controllers/route_editor.slim +45 -0
- data/lib/frontline/templates/controllers/route_layout.slim +88 -0
- data/lib/frontline/templates/editor.slim +17 -0
- data/lib/frontline/templates/error.slim +36 -0
- data/lib/frontline/templates/index/applications.slim +123 -0
- data/lib/frontline/templates/layout.slim +182 -0
- data/lib/frontline/templates/models/index.slim +31 -0
- data/lib/frontline/templates/models/migration.slim +46 -0
- data/lib/frontline/templates/models/migration_layout.slim +159 -0
- data/lib/frontline/templates/models/model.slim +34 -0
- metadata +245 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
class Frontline
|
2
|
+
class Models < E
|
3
|
+
reject_automount!
|
4
|
+
|
5
|
+
# runs before new model generated.
|
6
|
+
# converts throughFor Hash, if any, into string options suitable for Enginery.
|
7
|
+
before :post_model do
|
8
|
+
(params.delete('throughFor') || {}).each_pair do |a,t|
|
9
|
+
next if (assoc = params[a].to_s.strip).empty? || (through = t.strip).empty?
|
10
|
+
params[a] = [assoc, 'through', through]*':'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# return a command that will generate a new model.
|
15
|
+
# `@name` and `@setups` variables are set by the triggered hooks(see frontline/app.rb for hooks)
|
16
|
+
def post_model
|
17
|
+
'generate:model %s %s' % [@name, @setups]
|
18
|
+
end
|
19
|
+
|
20
|
+
# return a command that will delete the given model.
|
21
|
+
# `@name` variable are set by the triggered hooks(see frontline/app.rb for hooks)
|
22
|
+
def delete_model
|
23
|
+
'delete:model:yes %s' % @name
|
24
|
+
end
|
25
|
+
|
26
|
+
# return a command that will generate a new migration for given model.
|
27
|
+
# `@name` and `@setups` variables are set by the triggered hooks(see frontline/app.rb for hooks)
|
28
|
+
def post_migration model
|
29
|
+
'm %s model:%s %s' % [@name, model, @setups]
|
30
|
+
end
|
31
|
+
|
32
|
+
# return a command that will delete the given migration of given model.
|
33
|
+
# `@name` variable are set by the triggered hooks(see frontline/app.rb for hooks)
|
34
|
+
def delete_migration model
|
35
|
+
'delete:migration:yes %s' % @name
|
36
|
+
end
|
37
|
+
|
38
|
+
# run given migration or outstanding ones if no migrations given.
|
39
|
+
# see `Helpers#pty_stream` for running/streaming details.
|
40
|
+
#
|
41
|
+
# it requires `:vector` param to be set to "up" or "down".
|
42
|
+
# by default it will skip already performed migrations
|
43
|
+
# if `:force_run` is set, it will run migrations regardless performed state.
|
44
|
+
# if `:force_run` NOT set, `:force_yes` option should be set to true
|
45
|
+
# to avoid any confirmation prompts generated by Enginery.
|
46
|
+
def post_run_migrations
|
47
|
+
@uuid = params[:uuid]
|
48
|
+
vector = params[:vector]
|
49
|
+
extra = params[:force_run] ? ':f' : (params[:force_yes] ? ':y' : '')
|
50
|
+
migrations = (params[:migrations]||[])*' '
|
51
|
+
cmd = 'bundle exec enginery m:%s%s %s' % [vector, extra, migrations]
|
52
|
+
stream do
|
53
|
+
pty_stream 'modal', 'show'
|
54
|
+
passed, failure_id = pty_spawn(cmd)
|
55
|
+
if passed
|
56
|
+
pty_stream 'modal', 'hide'
|
57
|
+
else
|
58
|
+
pty_stream 'failures', failure_id
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# run given DataMapper migrate task and stream result to browser.
|
64
|
+
# see `Helpers#pty_stream` for running/streaming details.
|
65
|
+
def post_run_datamapper_task task, model = nil
|
66
|
+
|
67
|
+
%w[auto_migrate auto_upgrade].include?(task) ||
|
68
|
+
halt(400, 'Unknown DataMapper task "%s"' % escape_html(task))
|
69
|
+
|
70
|
+
@uuid = params.delete('uuid')
|
71
|
+
cmd = 'rake dm:' + task
|
72
|
+
model && cmd << ':' << model
|
73
|
+
|
74
|
+
stream do
|
75
|
+
pty_stream 'modal', 'show'
|
76
|
+
passed, failure_id = pty_spawn(cmd)
|
77
|
+
if passed
|
78
|
+
pty_stream 'modal', 'hide'
|
79
|
+
else
|
80
|
+
pty_stream 'failures', failure_id
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# boot effective application and get last track for given migration
|
86
|
+
def last_run
|
87
|
+
Dir.chdir dst_path.root do
|
88
|
+
migrator = Enginery::Migrator.new(dst_path.root)
|
89
|
+
migrator.boot_app
|
90
|
+
if track = migrator.last_run(params[:file])
|
91
|
+
'%s on %s' % [track.first.upcase, track.last]
|
92
|
+
else
|
93
|
+
'Never'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def migration_layout model
|
99
|
+
model = models[model] || halt(400, '"%s" model does not exists' % escape_html(model))
|
100
|
+
render_p model: model
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Frontline
|
2
|
+
ROOT = (File.expand_path('../../..', __FILE__) + '/').freeze
|
3
|
+
TEMPLATES = (ROOT + 'lib/frontline/templates/').freeze
|
4
|
+
ASSETS = (ROOT + 'assets/').freeze
|
5
|
+
ASSETS_SUFFIX = '-frontline'.freeze
|
6
|
+
ASSETS_REGEXP = /#{Regexp.escape ASSETS_SUFFIX}/.freeze
|
7
|
+
STREAMS = {}
|
8
|
+
CONTROLLERS = {}
|
9
|
+
MODELS = {}
|
10
|
+
PORT = 5000
|
11
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
class Frontline
|
2
|
+
module Helpers
|
3
|
+
include Inflector
|
4
|
+
|
5
|
+
# send data to browser via EventSource socket.
|
6
|
+
# socket should be earlier set via `Index#get_streamer`.
|
7
|
+
# actions calling this helper should firstly set `@uuid` variable.
|
8
|
+
#
|
9
|
+
# @param [String] event
|
10
|
+
# event to be sent to browser
|
11
|
+
# @param [String] data
|
12
|
+
# data to be sent to browser
|
13
|
+
def pty_stream event, data = ''
|
14
|
+
return unless stream = STREAMS[@uuid]
|
15
|
+
stream.event event
|
16
|
+
stream.data data
|
17
|
+
end
|
18
|
+
|
19
|
+
# return the list of managed applications.
|
20
|
+
def applications
|
21
|
+
(a = cookies['Applications']) ? JSON.load(a) : []
|
22
|
+
end
|
23
|
+
|
24
|
+
# return the list of controllers for effective application
|
25
|
+
def controllers
|
26
|
+
return CONTROLLERS[dst_path.root] if CONTROLLERS[dst_path.root]
|
27
|
+
result = enginery_registry(:c)
|
28
|
+
return CONTROLLERS[dst_path.root] = result if result.is_a?(Hash)
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
# return the list of models for effective application
|
33
|
+
def models
|
34
|
+
return MODELS[dst_path.root] if MODELS[dst_path.root]
|
35
|
+
result = enginery_registry(:m)
|
36
|
+
return MODELS[dst_path.root] = result if result.is_a?(Hash)
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
# execute a enginery registry command.
|
41
|
+
# data are extracted via `$ enginery -c` command so it comes in YAML format.
|
42
|
+
# if call was successful, parse the YAML and return the result as a Hash.
|
43
|
+
# otherwise return the stdout+stderr as a String.
|
44
|
+
# if YAML parser failing, a exception will be raised.
|
45
|
+
def enginery_registry unit
|
46
|
+
Dir.chdir dst_path.root do
|
47
|
+
cmd = '%s -%s' % [Enginery::EXECUTABLE, unit]
|
48
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
49
|
+
if status && status.exitstatus == 0
|
50
|
+
YAML.load(stdout)
|
51
|
+
else
|
52
|
+
[stdout, stderr].join("\n")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def stringify_application name, path, url
|
58
|
+
JSON.dump([name, path, url])
|
59
|
+
end
|
60
|
+
|
61
|
+
# running given command via `PTY.spawn` and streaming each line to browser.
|
62
|
+
# actions calling this helper should firstly set `@uuid` variable for streaming to work.
|
63
|
+
#
|
64
|
+
# before executing given cmd it will change working directory
|
65
|
+
# to effective application root(see `Index#get_application`).
|
66
|
+
#
|
67
|
+
# if some exception raised it will be rescued
|
68
|
+
# and error message alongside backtrace will be streamed to browser.
|
69
|
+
#
|
70
|
+
# it returns an Array first element of which is the status
|
71
|
+
# and the second is a unique ID used to identify lines generated by given cmd.
|
72
|
+
# if status is negative, actions calling this helper will can identify
|
73
|
+
# lines and mark them as errored.
|
74
|
+
#
|
75
|
+
def pty_spawn cmd, opts = {}
|
76
|
+
failure_id = 'pty_' << cmd.__id__.to_s
|
77
|
+
root = opts[:root] || dst_path.root
|
78
|
+
pty_stream 'content', div_tag!('$ cd ' << root, class: 'muted')
|
79
|
+
pty_stream 'content', div_tag!(b_tag('$ ' << cmd, class: 'text-info'))
|
80
|
+
Dir.chdir root do
|
81
|
+
PTY.spawn cmd do |r, w, pid|
|
82
|
+
begin
|
83
|
+
r.sync
|
84
|
+
r.each_line do |line|
|
85
|
+
line.rstrip!
|
86
|
+
|
87
|
+
# div_tag will escape line before emitting it
|
88
|
+
html = line.empty? ? br_tag : div_tag(line, class: failure_id)
|
89
|
+
|
90
|
+
pty_stream 'content', html
|
91
|
+
|
92
|
+
end
|
93
|
+
rescue Errno::EIO # simply ignoring this
|
94
|
+
ensure
|
95
|
+
::Process.wait pid
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
[$? && $?.exitstatus == 0, failure_id]
|
100
|
+
rescue => e
|
101
|
+
pty_stream 'content', p_tag(e.message)
|
102
|
+
e.backtrace.each {|l| pty_stream('content', div_tag(l))}
|
103
|
+
[false, failure_id]
|
104
|
+
end
|
105
|
+
|
106
|
+
def maintenance_files
|
107
|
+
files = %w[
|
108
|
+
Gemfile
|
109
|
+
Rakefile
|
110
|
+
base/boot.rb
|
111
|
+
config/config.yml
|
112
|
+
config/database.yml
|
113
|
+
base/helpers/application_helpers.rb
|
114
|
+
]
|
115
|
+
Dir[dst_path(:views, 'layout*')].each do |e|
|
116
|
+
File.file?(e) && files.unshift('base/views/' + File.basename(e))
|
117
|
+
end
|
118
|
+
files
|
119
|
+
end
|
120
|
+
|
121
|
+
# determine whether effective application is using DataMapper ORM
|
122
|
+
def datamapper?
|
123
|
+
(m = models) && m.is_a?(Hash) && (m = models.values.first) && m[:orm].to_s =~ /\Ad/i
|
124
|
+
end
|
125
|
+
|
126
|
+
@@association_hints = {}
|
127
|
+
def association_hints
|
128
|
+
@@association_hints[models.__id__] ||= models.keys.map {|x| underscore(x.to_s)}
|
129
|
+
end
|
130
|
+
|
131
|
+
def singularized_association_hints
|
132
|
+
association_hints.map {|m| singularize(m)}
|
133
|
+
end
|
134
|
+
|
135
|
+
def pluralized_association_hints
|
136
|
+
association_hints.map {|m| pluralize(m)}
|
137
|
+
end
|
138
|
+
|
139
|
+
def association_hint_class assoc
|
140
|
+
assoc == :belongs_to || assoc == :has_one ?
|
141
|
+
'singularized_association_hints' :
|
142
|
+
'pluralized_association_hints'
|
143
|
+
end
|
144
|
+
|
145
|
+
def clear_registry_cache!
|
146
|
+
CONTROLLERS.clear
|
147
|
+
MODELS.clear
|
148
|
+
end
|
149
|
+
|
150
|
+
def postcrud_handlers
|
151
|
+
cache = if action_name[0] == 'm'
|
152
|
+
MODELS.delete(dst_path.root)
|
153
|
+
models()
|
154
|
+
else
|
155
|
+
CONTROLLERS.delete(dst_path.root)
|
156
|
+
controllers()
|
157
|
+
end
|
158
|
+
|
159
|
+
errors = []
|
160
|
+
if cache.is_a?(Hash)
|
161
|
+
begin
|
162
|
+
pty_stream 'render', render_p(action_name, action_params)
|
163
|
+
pty_stream 'modal', 'hide'
|
164
|
+
rescue => e
|
165
|
+
errors = [e.message] + e.backtrace
|
166
|
+
end
|
167
|
+
else
|
168
|
+
errors = cache.to_s.split("\n")
|
169
|
+
end
|
170
|
+
if errors.any?
|
171
|
+
html = hr_tag << div_tag(class: 'alert alert-error lead') {
|
172
|
+
'Error loading %ss' % action_name
|
173
|
+
} << errors.map {|e| div_tag(e, class: 'text-error')}.join
|
174
|
+
pty_stream('content', html)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
class Frontline
|
2
|
+
module Inflector
|
3
|
+
extend self
|
4
|
+
|
5
|
+
class Inflections
|
6
|
+
@__instance__ = {}
|
7
|
+
|
8
|
+
def self.instance(locale = :en)
|
9
|
+
@__instance__[locale] ||= new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :plurals, :singulars, :uncountables
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@plurals, @singulars, @uncountables = [], [], []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Specifies a new pluralization rule and its replacement. The rule can
|
19
|
+
# either be a string or a regular expression. The replacement should
|
20
|
+
# always be a string that may include references to the matched data from
|
21
|
+
# the rule.
|
22
|
+
def plural(rule, replacement)
|
23
|
+
@uncountables.delete(rule) if rule.is_a?(String)
|
24
|
+
@uncountables.delete(replacement)
|
25
|
+
@plurals.unshift([rule, replacement])
|
26
|
+
end
|
27
|
+
|
28
|
+
# Specifies a new singularization rule and its replacement. The rule can
|
29
|
+
# either be a string or a regular expression. The replacement should
|
30
|
+
# always be a string that may include references to the matched data from
|
31
|
+
# the rule.
|
32
|
+
def singular(rule, replacement)
|
33
|
+
@uncountables.delete(rule) if rule.is_a?(String)
|
34
|
+
@uncountables.delete(replacement)
|
35
|
+
@singulars.unshift([rule, replacement])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add uncountable words that shouldn't be attempted inflected.
|
39
|
+
#
|
40
|
+
# uncountable 'money'
|
41
|
+
# uncountable 'money', 'information'
|
42
|
+
# uncountable %w( money information rice )
|
43
|
+
def uncountable(*words)
|
44
|
+
(@uncountables << words).flatten!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Yields a singleton instance of Inflector::Inflections so you can specify
|
49
|
+
# additional inflector rules. If passed an optional locale, rules for other
|
50
|
+
# languages can be specified. If not specified, defaults to <tt>:en</tt>.
|
51
|
+
# Only rules for English are provided.
|
52
|
+
#
|
53
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
54
|
+
# inflect.uncountable 'rails'
|
55
|
+
# end
|
56
|
+
def inflections(locale = :en)
|
57
|
+
if block_given?
|
58
|
+
yield Inflections.instance(locale)
|
59
|
+
else
|
60
|
+
Inflections.instance(locale)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Inflector.inflections(:en) do |inflect|
|
66
|
+
inflect.plural(/$/, 's')
|
67
|
+
inflect.plural(/s$/i, 's')
|
68
|
+
inflect.plural(/^(ax|test)is$/i, '\1es')
|
69
|
+
inflect.plural(/(octop|vir)us$/i, '\1i')
|
70
|
+
inflect.plural(/(octop|vir)i$/i, '\1i')
|
71
|
+
inflect.plural(/(alias|status)$/i, '\1es')
|
72
|
+
inflect.plural(/(bu)s$/i, '\1ses')
|
73
|
+
inflect.plural(/(buffal|tomat)o$/i, '\1oes')
|
74
|
+
inflect.plural(/([ti])um$/i, '\1a')
|
75
|
+
inflect.plural(/([ti])a$/i, '\1a')
|
76
|
+
inflect.plural(/sis$/i, 'ses')
|
77
|
+
inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
|
78
|
+
inflect.plural(/(hive)$/i, '\1s')
|
79
|
+
inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
|
80
|
+
inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
|
81
|
+
inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
|
82
|
+
inflect.plural(/^(m|l)ouse$/i, '\1ice')
|
83
|
+
inflect.plural(/^(m|l)ice$/i, '\1ice')
|
84
|
+
inflect.plural(/^(ox)$/i, '\1en')
|
85
|
+
inflect.plural(/^(oxen)$/i, '\1')
|
86
|
+
inflect.plural(/(quiz)$/i, '\1zes')
|
87
|
+
|
88
|
+
inflect.singular(/s$/i, '')
|
89
|
+
inflect.singular(/(ss)$/i, '\1')
|
90
|
+
inflect.singular(/(n)ews$/i, '\1ews')
|
91
|
+
inflect.singular(/([ti])a$/i, '\1um')
|
92
|
+
inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
|
93
|
+
inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
|
94
|
+
inflect.singular(/([^f])ves$/i, '\1fe')
|
95
|
+
inflect.singular(/(hive)s$/i, '\1')
|
96
|
+
inflect.singular(/(tive)s$/i, '\1')
|
97
|
+
inflect.singular(/([lr])ves$/i, '\1f')
|
98
|
+
inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
99
|
+
inflect.singular(/(s)eries$/i, '\1eries')
|
100
|
+
inflect.singular(/(m)ovies$/i, '\1ovie')
|
101
|
+
inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
|
102
|
+
inflect.singular(/^(m|l)ice$/i, '\1ouse')
|
103
|
+
inflect.singular(/(bus)(es)?$/i, '\1')
|
104
|
+
inflect.singular(/(o)es$/i, '\1')
|
105
|
+
inflect.singular(/(shoe)s$/i, '\1')
|
106
|
+
inflect.singular(/(cris|test)(is|es)$/i, '\1is')
|
107
|
+
inflect.singular(/^(a)x[ie]s$/i, '\1xis')
|
108
|
+
inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
|
109
|
+
inflect.singular(/(alias|status)(es)?$/i, '\1')
|
110
|
+
inflect.singular(/^(ox)en/i, '\1')
|
111
|
+
inflect.singular(/(vert|ind)ices$/i, '\1ex')
|
112
|
+
inflect.singular(/(matr)ices$/i, '\1ix')
|
113
|
+
inflect.singular(/(quiz)zes$/i, '\1')
|
114
|
+
inflect.singular(/(database)s$/i, '\1')
|
115
|
+
|
116
|
+
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
|
117
|
+
end
|
118
|
+
|
119
|
+
module Inflector
|
120
|
+
extend self
|
121
|
+
|
122
|
+
# Returns the plural form of the word in the string.
|
123
|
+
#
|
124
|
+
# If passed an optional +locale+ parameter, the word will be
|
125
|
+
# pluralized using rules defined for that language. By default,
|
126
|
+
# this parameter is set to <tt>:en</tt>.
|
127
|
+
#
|
128
|
+
# 'post'.pluralize # => "posts"
|
129
|
+
# 'octopus'.pluralize # => "octopi"
|
130
|
+
# 'sheep'.pluralize # => "sheep"
|
131
|
+
# 'words'.pluralize # => "words"
|
132
|
+
# 'CamelOctopus'.pluralize # => "CamelOctopi"
|
133
|
+
# 'ley'.pluralize(:es) # => "leyes"
|
134
|
+
def pluralize(word, locale = :en)
|
135
|
+
apply_inflections(word, inflections(locale).plurals)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The reverse of +pluralize+, returns the singular form of a word in a
|
139
|
+
# string.
|
140
|
+
#
|
141
|
+
# If passed an optional +locale+ parameter, the word will be
|
142
|
+
# pluralized using rules defined for that language. By default,
|
143
|
+
# this parameter is set to <tt>:en</tt>.
|
144
|
+
#
|
145
|
+
# 'posts'.singularize # => "post"
|
146
|
+
# 'octopi'.singularize # => "octopus"
|
147
|
+
# 'sheep'.singularize # => "sheep"
|
148
|
+
# 'word'.singularize # => "word"
|
149
|
+
# 'CamelOctopi'.singularize # => "CamelOctopus"
|
150
|
+
# 'leyes'.singularize(:es) # => "ley"
|
151
|
+
def singularize(word, locale = :en)
|
152
|
+
apply_inflections(word, inflections(locale).singulars)
|
153
|
+
end
|
154
|
+
|
155
|
+
def underscore(camel_cased_word)
|
156
|
+
word = camel_cased_word.to_s.dup
|
157
|
+
word.gsub!('::', '/')
|
158
|
+
word.gsub!(/(?:([A-Za-z\d])|^)((?=a)b)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
|
159
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
160
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
161
|
+
word.tr!("-", "_")
|
162
|
+
word.downcase!
|
163
|
+
word
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# Applies inflection rules for +singularize+ and +pluralize+.
|
169
|
+
#
|
170
|
+
# apply_inflections('post', inflections.plurals) # => "posts"
|
171
|
+
# apply_inflections('posts', inflections.singulars) # => "post"
|
172
|
+
def apply_inflections(word, rules)
|
173
|
+
result = word.to_s.dup
|
174
|
+
if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
|
175
|
+
result
|
176
|
+
else
|
177
|
+
rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
|
178
|
+
result
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|