cybertrain 0.1.1 → 0.2.1
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.
- checksums.yaml +4 -4
- data/README.md +233 -67
- data/cybertrain/app_name.rb +36 -0
- data/cybertrain/cli/build.rb +93 -16
- data/cybertrain/cli/new_app.rb +22 -12
- data/cybertrain/cli/scaffold.rb +78 -11
- data/cybertrain/cli/templates.rb +16 -20
- data/cybertrain/cli/toolchain.rb +787 -0
- data/cybertrain/cli.rb +114 -25
- data/cybertrain/generator/inflector.rb +154 -88
- data/cybertrain/ident.rb +171 -0
- data/cybertrain/version.rb +5 -1
- metadata +8 -3
data/cybertrain/cli.rb
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
# The `cybertrain` command: exe/cybertrain under CRuby (the gem), or
|
|
2
2
|
# bin/cybertrain.rb built by spin (`spin install` from a checkout).
|
|
3
3
|
#
|
|
4
|
-
# cybertrain new NAME [--path DIR | --version V] [--skip-spin]
|
|
4
|
+
# cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]
|
|
5
5
|
# cybertrain generate scaffold NAME field:type ... [parent:references]
|
|
6
|
-
# cybertrain
|
|
7
|
-
# cybertrain version | help
|
|
6
|
+
# cybertrain db migrate | db COMMAND... | server [PORT] | build | spin ARGS...
|
|
7
|
+
# cybertrain setup [--force] | doctor | version | help
|
|
8
|
+
#
|
|
9
|
+
# Every command that runs spin first puts a Spinel of the pinned release on
|
|
10
|
+
# PATH (Toolchain.ensure!), installing it under ~/.cybertrain the first time.
|
|
8
11
|
require "cybertrain/version"
|
|
9
12
|
require "cybertrain/cli/templates"
|
|
10
13
|
require "cybertrain/cli/new_app"
|
|
11
14
|
require "cybertrain/cli/scaffold"
|
|
12
15
|
require "cybertrain/cli/build"
|
|
16
|
+
require "cybertrain/cli/toolchain"
|
|
13
17
|
|
|
14
18
|
module Cybertrain
|
|
15
19
|
module CLI
|
|
16
20
|
USAGE = <<~TEXT
|
|
17
21
|
Usage:
|
|
18
|
-
cybertrain new NAME [--path DIR | --version V] [--skip-spin]
|
|
22
|
+
cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]
|
|
19
23
|
Create an application in NAME, then run `spin lock` and
|
|
20
24
|
`spin run gen` in it (skipped with --skip-spin). Its spin.toml
|
|
21
25
|
depends on this cybertrain's release by default:
|
|
@@ -23,20 +27,33 @@ module Cybertrain
|
|
|
23
27
|
--path DIR: the framework checkout at DIR (relative to the
|
|
24
28
|
current directory; written as an absolute path).
|
|
25
29
|
--version V: the index version constraint V.
|
|
30
|
+
--git URL [--ref R]: the framework at URL, at branch or tag R.
|
|
26
31
|
cybertrain generate scaffold NAME field:type ... [parent:references]
|
|
27
32
|
Add a resource to the application in the current directory.
|
|
28
33
|
Types: #{Field::TYPES.join(", ")} (default string).
|
|
29
|
-
cybertrain
|
|
34
|
+
cybertrain db migrate
|
|
30
35
|
Generate, apply pending migrations, generate again
|
|
31
36
|
(spin run gen; spin run db -- migrate; spin run gen).
|
|
32
37
|
cybertrain db COMMAND...
|
|
33
|
-
Any database command
|
|
38
|
+
Any other database command, after `spin run gen`: status,
|
|
39
|
+
rollback [N] (generates again afterwards), schema:dump, create.
|
|
40
|
+
`cybertrain migration` is the same as `cybertrain db migrate`.
|
|
34
41
|
cybertrain server [PORT]
|
|
35
42
|
Generate, then start the development server
|
|
36
43
|
(spin run gen; spin run NAME [-- PORT]; default port 3000).
|
|
37
44
|
cybertrain build
|
|
38
45
|
Build NAME with app/views embedded and assemble dist/
|
|
39
46
|
(the binary, public/, storage/, tmp/).
|
|
47
|
+
cybertrain spin ARGS...
|
|
48
|
+
Run spin with the pinned Spinel, from any directory
|
|
49
|
+
(cybertrain spin test; cybertrain spin run gen -- --check).
|
|
50
|
+
cybertrain setup [--force]
|
|
51
|
+
Install Spinel #{Cybertrain::SPINEL_TAG} into ~/.cybertrain (CYBERTRAIN_HOME)
|
|
52
|
+
unless a spinel of that release is already on PATH, and print
|
|
53
|
+
the PATH line for ~/.cybertrain/bin. The commands above do the
|
|
54
|
+
install by themselves the first time they need spin.
|
|
55
|
+
cybertrain doctor
|
|
56
|
+
Check the C toolchain, the SQLite headers and the Spinel install.
|
|
40
57
|
cybertrain version
|
|
41
58
|
cybertrain help
|
|
42
59
|
TEXT
|
|
@@ -47,10 +64,13 @@ module Cybertrain
|
|
|
47
64
|
case command
|
|
48
65
|
when "new" then run_new(argv)
|
|
49
66
|
when "generate", "g" then run_generate(argv)
|
|
50
|
-
when "migration" then run_in_app { |_name|
|
|
51
|
-
when "db" then run_in_app { |_name|
|
|
67
|
+
when "migration" then run_in_app { |_name| run_spin(Build.db_commands(["migrate"])) }
|
|
68
|
+
when "db" then run_in_app { |_name| run_db(argv) }
|
|
52
69
|
when "server" then run_in_app { |name| run_server(name, argv) }
|
|
53
|
-
when "build" then run_in_app { |name|
|
|
70
|
+
when "build" then run_in_app { |name| build_app(name) }
|
|
71
|
+
when "spin" then run_spin_passthrough(argv)
|
|
72
|
+
when "setup" then Toolchain.setup(argv[1, argv.size - 1])
|
|
73
|
+
when "doctor" then run_doctor(argv)
|
|
54
74
|
when "version", "--version", "-v"
|
|
55
75
|
puts "cybertrain #{Cybertrain::VERSION}"
|
|
56
76
|
0
|
|
@@ -68,7 +88,7 @@ module Cybertrain
|
|
|
68
88
|
end
|
|
69
89
|
|
|
70
90
|
def self.run_new(argv)
|
|
71
|
-
raise InvalidArgument, "usage: cybertrain new NAME [--path DIR | --version V] [--skip-spin]" if argv.size < 2
|
|
91
|
+
raise InvalidArgument, "usage: cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin]" if argv.size < 2
|
|
72
92
|
|
|
73
93
|
dir = argv[1]
|
|
74
94
|
raise InvalidArgument, "'#{File.basename(dir)}' is not a valid app name (lowercase letters, digits and _)" unless Templates.identifier?(File.basename(dir))
|
|
@@ -76,8 +96,15 @@ module Cybertrain
|
|
|
76
96
|
|
|
77
97
|
NewApp.create(dir, framework_dep(argv))
|
|
78
98
|
return 0 if argv.include?("--skip-spin")
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
return 1 unless NewApp.bootstrap(dir)
|
|
100
|
+
|
|
101
|
+
puts ""
|
|
102
|
+
puts "next:"
|
|
103
|
+
puts " cd #{Build.quote_arg(dir)}"
|
|
104
|
+
puts " cybertrain generate scaffold article title:string body:text"
|
|
105
|
+
puts " cybertrain db migrate"
|
|
106
|
+
puts " cybertrain server"
|
|
107
|
+
0
|
|
81
108
|
end
|
|
82
109
|
|
|
83
110
|
# The spin.toml value of the `cybertrain =` dependency. By default the
|
|
@@ -89,20 +116,41 @@ module Cybertrain
|
|
|
89
116
|
def self.framework_dep(argv)
|
|
90
117
|
path = option(argv, "--path")
|
|
91
118
|
version = option(argv, "--version")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
git = option(argv, "--git")
|
|
120
|
+
ref = option(argv, "--ref")
|
|
121
|
+
given = 0
|
|
122
|
+
given += 1 unless path == ""
|
|
123
|
+
given += 1 unless version == ""
|
|
124
|
+
given += 1 unless git == ""
|
|
125
|
+
raise InvalidArgument, "pass only one of --path, --version and --git" if given > 1
|
|
126
|
+
raise InvalidArgument, "--ref needs --git" if ref != "" && git == ""
|
|
127
|
+
return toml_string(version) unless version == ""
|
|
128
|
+
return "{ path = #{toml_string(File.expand_path(path, Dir.pwd))} }" unless path == ""
|
|
129
|
+
return "{ git = #{toml_string(git)} }" if git != "" && ref == ""
|
|
130
|
+
return "{ git = #{toml_string(git)}, ref = #{toml_string(ref)} }" unless git == ""
|
|
95
131
|
|
|
96
132
|
"{ git = \"#{Cybertrain::REPOSITORY}\", ref = \"v#{Cybertrain::VERSION}\" }"
|
|
97
133
|
end
|
|
98
134
|
|
|
99
|
-
#
|
|
135
|
+
# `value` as a TOML basic string: `\` and `"` escaped. Control
|
|
136
|
+
# characters are refused rather than encoded: no path, URL or git ref
|
|
137
|
+
# needs one, and spin.toml stays readable.
|
|
138
|
+
def self.toml_string(value)
|
|
139
|
+
raise InvalidArgument, "control characters are not allowed in --path, --version, --git or --ref" unless value.bytes.all? { |b| b >= 32 && b != 127 }
|
|
140
|
+
|
|
141
|
+
"\"#{value.gsub("\\", "\\\\\\\\").gsub("\"", "\\\"")}\""
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The value after `--flag`, or "" when the flag is absent. An empty
|
|
145
|
+
# value, or another option in the value's place, means it is missing.
|
|
100
146
|
def self.option(argv, flag)
|
|
101
147
|
i = argv.index(flag)
|
|
102
148
|
return "" if i.nil?
|
|
103
|
-
raise InvalidArgument, "#{flag} needs a value" if i + 1 >= argv.size
|
|
104
149
|
|
|
105
|
-
argv[i + 1]
|
|
150
|
+
value = i + 1 < argv.size ? argv[i + 1] : ""
|
|
151
|
+
raise InvalidArgument, "#{flag} needs a value" if value == "" || value.start_with?("--")
|
|
152
|
+
|
|
153
|
+
value
|
|
106
154
|
end
|
|
107
155
|
|
|
108
156
|
# Commands that need the app: its name comes from ./spin.toml.
|
|
@@ -115,7 +163,22 @@ module Cybertrain
|
|
|
115
163
|
yield name
|
|
116
164
|
end
|
|
117
165
|
|
|
118
|
-
#
|
|
166
|
+
# `cybertrain build`: the one dist/ name check, made before the toolchain
|
|
167
|
+
# is fetched or any spin command runs; Build.run assumes a name that got
|
|
168
|
+
# past it.
|
|
169
|
+
def self.build_app(name)
|
|
170
|
+
Build.require_dist_name!(name)
|
|
171
|
+
Toolchain.ensure! ? Build.run(".", name) : 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Runs spin commands in turn once a Spinel of the pinned release is on
|
|
175
|
+
# PATH; stops at the first failure.
|
|
176
|
+
def self.run_spin(commands)
|
|
177
|
+
return 1 unless Toolchain.ensure!
|
|
178
|
+
|
|
179
|
+
run_all(commands)
|
|
180
|
+
end
|
|
181
|
+
|
|
119
182
|
def self.run_all(commands)
|
|
120
183
|
status = 0
|
|
121
184
|
commands.each do |command|
|
|
@@ -128,6 +191,37 @@ module Cybertrain
|
|
|
128
191
|
status
|
|
129
192
|
end
|
|
130
193
|
|
|
194
|
+
# `cybertrain spin ARGS...`: spin from the pinned toolchain, wherever
|
|
195
|
+
# the command runs (an application directory or not).
|
|
196
|
+
def self.run_spin_passthrough(argv)
|
|
197
|
+
args = argv[1, argv.size - 1]
|
|
198
|
+
if args.empty?
|
|
199
|
+
puts "usage: cybertrain spin ARGS... (for example: cybertrain spin test)"
|
|
200
|
+
return 1
|
|
201
|
+
end
|
|
202
|
+
return 1 unless Toolchain.ensure!
|
|
203
|
+
|
|
204
|
+
system((["spin"] + args.map { |arg| Build.quote_arg(arg) }).join(" ")) ? 0 : 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def self.run_doctor(argv)
|
|
208
|
+
if argv.size > 1
|
|
209
|
+
puts "usage: cybertrain doctor"
|
|
210
|
+
return 1
|
|
211
|
+
end
|
|
212
|
+
Toolchain.doctor
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# `cybertrain db COMMAND...`.
|
|
216
|
+
def self.run_db(argv)
|
|
217
|
+
args = argv[1, argv.size - 1]
|
|
218
|
+
if args.empty?
|
|
219
|
+
puts "usage: cybertrain db (migrate|rollback [N]|status|schema:dump|create)"
|
|
220
|
+
return 1
|
|
221
|
+
end
|
|
222
|
+
run_spin(Build.db_commands(args))
|
|
223
|
+
end
|
|
224
|
+
|
|
131
225
|
# `cybertrain server [PORT]`.
|
|
132
226
|
def self.run_server(name, argv)
|
|
133
227
|
port = argv.size > 1 ? argv[1] : ""
|
|
@@ -139,12 +233,7 @@ module Cybertrain
|
|
|
139
233
|
puts "error: unexpected argument '#{argv[2]}'"
|
|
140
234
|
return 1
|
|
141
235
|
end
|
|
142
|
-
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# The words after `db`, each shell-quoted.
|
|
146
|
-
def self.db_args(argv)
|
|
147
|
-
argv[1, argv.size - 1].map { |arg| Build.shell_quote(arg) }.join(" ")
|
|
236
|
+
run_spin(Build.server_commands(name, port))
|
|
148
237
|
end
|
|
149
238
|
|
|
150
239
|
def self.run_generate(argv)
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
module Cybertrain
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# ActiveSupport::Inflector's camelize, underscore, pluralize and
|
|
3
|
+
# singularize with Rails' default English inflections
|
|
4
|
+
# (active_support/inflections.rb), checked rule for rule against
|
|
5
|
+
# ActiveSupport 8.1.4.
|
|
6
|
+
#
|
|
7
|
+
# Rails keeps its rules in tables that inflect.plural/singular/irregular/
|
|
8
|
+
# uncountable prepend to and tries them newest first, the first match
|
|
9
|
+
# winning. Spinel cannot keep Regexp objects in a collection, so each table
|
|
10
|
+
# is written out below as literal regexes in that application order: the
|
|
11
|
+
# expansion of every inflect.irregular (newest first), then the plain rules
|
|
12
|
+
# (newest first). An uncountable word -- or a compound ending in one after a
|
|
13
|
+
# word boundary, so "my money" but not "blog_fish" -- and "" come back
|
|
14
|
+
# unchanged.
|
|
15
|
+
#
|
|
16
|
+
# Application-defined inflections (config/initializers/inflections.rb:
|
|
17
|
+
# inflect.irregular, inflect.uncountable, inflect.acronym, ...) and locales
|
|
18
|
+
# other than English are not supported; camelize and underscore behave as
|
|
19
|
+
# Rails' do with no acronyms defined.
|
|
5
20
|
module Inflector
|
|
6
|
-
IRREGULARS = {
|
|
7
|
-
"person" => "people", "man" => "men", "woman" => "women",
|
|
8
|
-
"child" => "children", "mouse" => "mice", "ox" => "oxen"
|
|
9
|
-
}
|
|
10
|
-
UNCOUNTABLE = %w[equipment information rice money species series fish sheep news jeans]
|
|
11
|
-
# Singulars ending in "s" whose plural adds "es" (Rails' alias/status/bus rules).
|
|
12
|
-
ES_WORDS = %w[status alias bus]
|
|
13
|
-
|
|
14
21
|
# "posts_controller" -> "PostsController"; "admin/posts" -> "Admin::Posts".
|
|
15
|
-
|
|
22
|
+
# As Rails: the leading lowercase/digit run is capitalized, and each "_"
|
|
23
|
+
# or "/" is dropped and the alphanumeric run after it capitalized (the
|
|
24
|
+
# rest of that run downcased: "foo_BAR" -> "FooBar"); "/" becomes "::".
|
|
25
|
+
def self.camelize(term)
|
|
16
26
|
out = +""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
n = term.size
|
|
28
|
+
i = 0
|
|
29
|
+
while i < n && lower_or_digit?(term[i])
|
|
30
|
+
out << (i == 0 ? term[i].upcase : term[i])
|
|
31
|
+
i += 1
|
|
32
|
+
end
|
|
33
|
+
while i < n
|
|
34
|
+
ch = term[i]
|
|
35
|
+
i += 1
|
|
36
|
+
if ch == "_" || ch == "/"
|
|
37
|
+
out << "::" if ch == "/"
|
|
38
|
+
start = i
|
|
39
|
+
while i < n && alnum?(term[i])
|
|
40
|
+
out << (i == start ? term[i].upcase : term[i].downcase)
|
|
41
|
+
i += 1
|
|
42
|
+
end
|
|
27
43
|
else
|
|
28
44
|
out << ch
|
|
29
45
|
end
|
|
@@ -31,96 +47,146 @@ module Cybertrain
|
|
|
31
47
|
out
|
|
32
48
|
end
|
|
33
49
|
|
|
34
|
-
# "PostsController" -> "posts_controller"; "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
# "PostsController" -> "posts_controller"; "HTMLTidyGenerator" ->
|
|
51
|
+
# "html_tidy_generator"; "Admin::Posts" -> "admin/posts". As Rails: "_"
|
|
52
|
+
# goes before an uppercase letter that follows a lowercase letter or digit,
|
|
53
|
+
# or that follows an uppercase letter and precedes a lowercase one; "-"
|
|
54
|
+
# becomes "_" and the result is downcased.
|
|
55
|
+
def self.underscore(camel_cased_word)
|
|
56
|
+
return camel_cased_word.dup unless camel_cased_word.match?(/[A-Z-]|::/)
|
|
57
|
+
|
|
58
|
+
# Two steps, the gsub result held in a local: in a threaded program a
|
|
59
|
+
# collection inside `chars` frees an unnamed gsub result it is
|
|
60
|
+
# splitting ("Post" comes back as "").
|
|
61
|
+
path = camel_cased_word.gsub("::", "/")
|
|
39
62
|
chars = path.chars
|
|
40
63
|
out = +""
|
|
41
64
|
chars.each_with_index do |ch, i|
|
|
42
|
-
if upper?(ch)
|
|
43
|
-
prev =
|
|
65
|
+
if i > 0 && upper?(ch)
|
|
66
|
+
prev = chars[i - 1]
|
|
44
67
|
nxt = i + 1 < chars.size ? chars[i + 1] : ""
|
|
45
|
-
|
|
46
|
-
out << "_"
|
|
47
|
-
end
|
|
48
|
-
out << ch.downcase
|
|
49
|
-
else
|
|
50
|
-
out << ch
|
|
68
|
+
out << "_" if lower_or_digit?(prev) || (upper?(prev) && lower?(nxt))
|
|
51
69
|
end
|
|
70
|
+
out << (ch == "-" ? "_" : ch)
|
|
52
71
|
end
|
|
53
|
-
out
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# "posts" -> "post"; only the last "_" word is inflected ("blog_posts").
|
|
57
|
-
def self.singularize(str)
|
|
58
|
-
parts = split_last_word(str)
|
|
59
|
-
parts[0] + singular_word(parts[1])
|
|
72
|
+
out.downcase
|
|
60
73
|
end
|
|
61
74
|
|
|
62
|
-
# "post" -> "posts"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def self.singular_word(word)
|
|
69
|
-
return word if UNCOUNTABLE.include?(word)
|
|
75
|
+
# "post" -> "posts", "person" -> "people", "sales_person" ->
|
|
76
|
+
# "sales_people", "Category" -> "Categories", "sheep" -> "sheep".
|
|
77
|
+
def self.pluralize(word)
|
|
78
|
+
return word.dup if word.empty? || uncountable?(word)
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
return word if
|
|
80
|
+
# inflect.irregular, newest first: zombie, move, sex, child, man, person.
|
|
81
|
+
return word.sub(/(z)ombies$/i, '\1ombies') if word.match?(/(z)ombies$/i)
|
|
82
|
+
return word.sub(/(z)ombie$/i, '\1ombies') if word.match?(/(z)ombie$/i)
|
|
83
|
+
return word.sub(/(m)oves$/i, '\1oves') if word.match?(/(m)oves$/i)
|
|
84
|
+
return word.sub(/(m)ove$/i, '\1oves') if word.match?(/(m)ove$/i)
|
|
85
|
+
return word.sub(/(s)exes$/i, '\1exes') if word.match?(/(s)exes$/i)
|
|
86
|
+
return word.sub(/(s)ex$/i, '\1exes') if word.match?(/(s)ex$/i)
|
|
87
|
+
return word.sub(/(c)hildren$/i, '\1hildren') if word.match?(/(c)hildren$/i)
|
|
88
|
+
return word.sub(/(c)hild$/i, '\1hildren') if word.match?(/(c)hild$/i)
|
|
89
|
+
return word.sub(/(m)en$/i, '\1en') if word.match?(/(m)en$/i)
|
|
90
|
+
return word.sub(/(m)an$/i, '\1en') if word.match?(/(m)an$/i)
|
|
91
|
+
return word.sub(/(p)eople$/i, '\1eople') if word.match?(/(p)eople$/i)
|
|
92
|
+
return word.sub(/(p)erson$/i, '\1eople') if word.match?(/(p)erson$/i)
|
|
93
|
+
# inflect.plural, newest first.
|
|
94
|
+
return word.sub(/(quiz)$/i, '\1zes') if word.match?(/(quiz)$/i)
|
|
95
|
+
return word.sub(/^(oxen)$/i, '\1') if word.match?(/^(oxen)$/i)
|
|
96
|
+
return word.sub(/^(ox)$/i, '\1en') if word.match?(/^(ox)$/i)
|
|
97
|
+
return word.sub(/^(m|l)ice$/i, '\1ice') if word.match?(/^(m|l)ice$/i)
|
|
98
|
+
return word.sub(/^(m|l)ouse$/i, '\1ice') if word.match?(/^(m|l)ouse$/i)
|
|
99
|
+
return word.sub(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') if word.match?(/(matr|vert|ind)(?:ix|ex)$/i)
|
|
100
|
+
return word.sub(/(x|ch|ss|sh)$/i, '\1es') if word.match?(/(x|ch|ss|sh)$/i)
|
|
101
|
+
return word.sub(/([^aeiouy]|qu)y$/i, '\1ies') if word.match?(/([^aeiouy]|qu)y$/i)
|
|
102
|
+
return word.sub(/(hive)$/i, '\1s') if word.match?(/(hive)$/i)
|
|
103
|
+
return word.sub(/(?:([^f])fe|([lr])f)$/i, '\1\2ves') if word.match?(/(?:([^f])fe|([lr])f)$/i)
|
|
104
|
+
return word.sub(/sis$/i, "ses") if word.match?(/sis$/i)
|
|
105
|
+
return word.sub(/([ti])a$/i, '\1a') if word.match?(/([ti])a$/i)
|
|
106
|
+
return word.sub(/([ti])um$/i, '\1a') if word.match?(/([ti])um$/i)
|
|
107
|
+
return word.sub(/(buffal|tomat)o$/i, '\1oes') if word.match?(/(buffal|tomat)o$/i)
|
|
108
|
+
return word.sub(/(bu)s$/i, '\1ses') if word.match?(/(bu)s$/i)
|
|
109
|
+
return word.sub(/(alias|status)$/i, '\1es') if word.match?(/(alias|status)$/i)
|
|
110
|
+
return word.sub(/(octop|vir)i$/i, '\1i') if word.match?(/(octop|vir)i$/i)
|
|
111
|
+
return word.sub(/(octop|vir)us$/i, '\1i') if word.match?(/(octop|vir)us$/i)
|
|
112
|
+
return word.sub(/^(ax|test)is$/i, '\1es') if word.match?(/^(ax|test)is$/i)
|
|
113
|
+
return word.sub(/s$/i, "s") if word.match?(/s$/i)
|
|
73
114
|
|
|
74
|
-
|
|
75
|
-
word[0, word.size - 3] + "y"
|
|
76
|
-
elsif ES_WORDS.include?(word[0, word.size - 2]) || word.end_with?("sses") ||
|
|
77
|
-
word.end_with?("xes") || word.end_with?("ches") || word.end_with?("shes")
|
|
78
|
-
word[0, word.size - 2]
|
|
79
|
-
elsif word.end_with?("ss") || ES_WORDS.include?(word)
|
|
80
|
-
word
|
|
81
|
-
elsif word.end_with?("s")
|
|
82
|
-
word[0, word.size - 1]
|
|
83
|
-
else
|
|
84
|
-
word
|
|
85
|
-
end
|
|
115
|
+
word.sub(/$/, "s")
|
|
86
116
|
end
|
|
87
117
|
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
# "posts" -> "post", "people" -> "person", "sales_people" ->
|
|
119
|
+
# "sales_person", "Categories" -> "Category", "news" -> "news".
|
|
120
|
+
def self.singularize(word)
|
|
121
|
+
return word.dup if word.empty? || uncountable?(word)
|
|
90
122
|
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
# inflect.irregular, newest first: zombie, move, sex, child, man, person.
|
|
124
|
+
return word.sub(/(z)ombies$/i, '\1ombie') if word.match?(/(z)ombies$/i)
|
|
125
|
+
return word.sub(/(z)ombie$/i, '\1ombie') if word.match?(/(z)ombie$/i)
|
|
126
|
+
return word.sub(/(m)oves$/i, '\1ove') if word.match?(/(m)oves$/i)
|
|
127
|
+
return word.sub(/(m)ove$/i, '\1ove') if word.match?(/(m)ove$/i)
|
|
128
|
+
return word.sub(/(s)exes$/i, '\1ex') if word.match?(/(s)exes$/i)
|
|
129
|
+
return word.sub(/(s)ex$/i, '\1ex') if word.match?(/(s)ex$/i)
|
|
130
|
+
return word.sub(/(c)hildren$/i, '\1hild') if word.match?(/(c)hildren$/i)
|
|
131
|
+
return word.sub(/(c)hild$/i, '\1hild') if word.match?(/(c)hild$/i)
|
|
132
|
+
return word.sub(/(m)en$/i, '\1an') if word.match?(/(m)en$/i)
|
|
133
|
+
return word.sub(/(m)an$/i, '\1an') if word.match?(/(m)an$/i)
|
|
134
|
+
return word.sub(/(p)eople$/i, '\1erson') if word.match?(/(p)eople$/i)
|
|
135
|
+
return word.sub(/(p)erson$/i, '\1erson') if word.match?(/(p)erson$/i)
|
|
136
|
+
# inflect.singular, newest first.
|
|
137
|
+
return word.sub(/(database)s$/i, '\1') if word.match?(/(database)s$/i)
|
|
138
|
+
return word.sub(/(quiz)zes$/i, '\1') if word.match?(/(quiz)zes$/i)
|
|
139
|
+
return word.sub(/(matr)ices$/i, '\1ix') if word.match?(/(matr)ices$/i)
|
|
140
|
+
return word.sub(/(vert|ind)ices$/i, '\1ex') if word.match?(/(vert|ind)ices$/i)
|
|
141
|
+
return word.sub(/^(ox)en/i, '\1') if word.match?(/^(ox)en/i)
|
|
142
|
+
return word.sub(/(alias|status)(es)?$/i, '\1') if word.match?(/(alias|status)(es)?$/i)
|
|
143
|
+
return word.sub(/(octop|vir)(us|i)$/i, '\1us') if word.match?(/(octop|vir)(us|i)$/i)
|
|
144
|
+
return word.sub(/^(a)x[ie]s$/i, '\1xis') if word.match?(/^(a)x[ie]s$/i)
|
|
145
|
+
return word.sub(/(cris|test)(is|es)$/i, '\1is') if word.match?(/(cris|test)(is|es)$/i)
|
|
146
|
+
return word.sub(/(shoe)s$/i, '\1') if word.match?(/(shoe)s$/i)
|
|
147
|
+
return word.sub(/(o)es$/i, '\1') if word.match?(/(o)es$/i)
|
|
148
|
+
return word.sub(/(bus)(es)?$/i, '\1') if word.match?(/(bus)(es)?$/i)
|
|
149
|
+
return word.sub(/^(m|l)ice$/i, '\1ouse') if word.match?(/^(m|l)ice$/i)
|
|
150
|
+
return word.sub(/(x|ch|ss|sh)es$/i, '\1') if word.match?(/(x|ch|ss|sh)es$/i)
|
|
151
|
+
return word.sub(/(m)ovies$/i, '\1ovie') if word.match?(/(m)ovies$/i)
|
|
152
|
+
return word.sub(/(s)eries$/i, '\1eries') if word.match?(/(s)eries$/i)
|
|
153
|
+
return word.sub(/([^aeiouy]|qu)ies$/i, '\1y') if word.match?(/([^aeiouy]|qu)ies$/i)
|
|
154
|
+
return word.sub(/([lr])ves$/i, '\1f') if word.match?(/([lr])ves$/i)
|
|
155
|
+
return word.sub(/(tive)s$/i, '\1') if word.match?(/(tive)s$/i)
|
|
156
|
+
return word.sub(/(hive)s$/i, '\1') if word.match?(/(hive)s$/i)
|
|
157
|
+
return word.sub(/([^f])ves$/i, '\1fe') if word.match?(/([^f])ves$/i)
|
|
158
|
+
return word.sub(/(^analy)(sis|ses)$/i, '\1sis') if word.match?(/(^analy)(sis|ses)$/i)
|
|
159
|
+
if word.match?(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i)
|
|
160
|
+
return word.sub(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
|
|
103
161
|
end
|
|
104
|
-
|
|
162
|
+
return word.sub(/([ti])a$/i, '\1um') if word.match?(/([ti])a$/i)
|
|
163
|
+
return word.sub(/(n)ews$/i, '\1ews') if word.match?(/(n)ews$/i)
|
|
164
|
+
return word.sub(/(ss)$/i, '\1') if word.match?(/(ss)$/i)
|
|
165
|
+
return word.sub(/s$/i, "") if word.match?(/s$/i)
|
|
105
166
|
|
|
106
|
-
|
|
107
|
-
def self.split_last_word(str)
|
|
108
|
-
i = str.rindex("_")
|
|
109
|
-
return ["", str] if i.nil?
|
|
110
|
-
|
|
111
|
-
[str[0, i + 1], str[i + 1, str.size - i - 1]]
|
|
167
|
+
word.dup
|
|
112
168
|
end
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
|
|
170
|
+
# inflect.uncountable: Rails matches /\b<word>\Z/i, so "my money" and
|
|
171
|
+
# "Sheep" are uncountable but "blog_fish" ("_" is a word character) is not.
|
|
172
|
+
def self.uncountable?(word)
|
|
173
|
+
word.match?(/\b(?:equipment|information|rice|money|species|series|fish|sheep|jeans|police)\Z/i)
|
|
116
174
|
end
|
|
117
175
|
|
|
118
176
|
def self.upper?(ch)
|
|
119
177
|
ch >= "A" && ch <= "Z"
|
|
120
178
|
end
|
|
121
179
|
|
|
180
|
+
def self.lower?(ch)
|
|
181
|
+
ch >= "a" && ch <= "z"
|
|
182
|
+
end
|
|
183
|
+
|
|
122
184
|
def self.lower_or_digit?(ch)
|
|
123
|
-
(ch
|
|
185
|
+
lower?(ch) || (ch >= "0" && ch <= "9")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.alnum?(ch)
|
|
189
|
+
upper?(ch) || lower_or_digit?(ch)
|
|
124
190
|
end
|
|
125
191
|
end
|
|
126
192
|
end
|