frontline 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|